Configuration¶
Kagura accepts a JSON policy file via -kagura-config=<path> to select a
profile, switch individual passes on and off, set the numeric tuning
parameters and scope everything to a set of symbols. This is the recommended
way to drive obfuscation in real projects.
It does not cover every -kagura-* flag: the build-system switches
(-kagura-lto-safe, -kagura-o0-protect, -kagura-build-id), the reporting
switches (-kagura-metrics, -kagura-symmap, -kagura-symmap-out,
-kagura-audit), -kagura-autoselect and -kagura-vtp have no policy-file
key and must be passed on the command line. See
Tuning Parameters. Writing one
of them into "passes" or "tuning" now produces a warning that names the
flag to use instead.
JSON DSL¶
{
"profile": "BALANCED",
"passes": {
"str": true,
"fla": true,
"bcf": true,
"honey": true,
"mvo": false
},
"tuning": {
"bcf_prob": 40,
"seed": 12345
}
}
Key spelling¶
A pass key is its -kagura- flag name with the prefix removed. Both spellings
are accepted, so "str-aes" and "str_aes" mean the same thing; the
underscore form is canonical and is what the generated files in
integration/profiles/ use.
Anything the loader does not recognise is reported:
[kagura] warning: unknown key "str_ase" in the "passes" object of the policy
file — it is ignored, so this setting has no effect; did you mean "str_aes"?
Pass -kagura-config-strict to turn that warning into a build failure. Do
this in CI: an ignored key is a silent downgrade of the binary you ship, and
the failure mode is invisible in the output.
The policy file is read before the pass pipeline is constructed, not by a
pass within it. That distinction matters: pass selection is decided while the
pipeline is being built, so a loader running as a pipeline pass would always
arrive too late to influence it — which is exactly why -kagura-config was a
no-op prior to the current release.
Precedence, highest first:
- An explicit
-kagura-*command-line flag.-kagura-config=p.json -kagura-str=falsedisables string encryption even ifp.jsonasks for it. - A
flavorsblock matching$KAGURA_FLAVOR. - The
passes/tuning/stringobjects. - The
profilepreset. - The built-in default (every pass off).
The symbol filter lists are the one deliberate exception: "allowlist",
"denylist" and "protect" merge with an explicit -kagura-allow /
-kagura-deny / -kagura-protect rather than losing to it. Every entry in one
of those lists is a symbol somebody singled out on purpose, so dropping either
source would silently obfuscate something that was meant to be left alone. The
result is the union: -kagura-deny=vendor_* plus "denylist": ["test_*"]
excludes both. (Until recently the JSON simply overwrote the flag.)
Per-function
__attribute__((annotate("kagura_*")))
overrides still win on a function-by-function basis.
Strength profiles¶
Built-in profiles selected by the "profile" key:
| Profile | Passes enabled | Tuning | Intended use |
|---|---|---|---|
FAST |
str sv anti_debug |
— | Debug / CI builds with minimal overhead |
BALANCED |
str wstr bcf bbr bbs dci genc mvo sv anti_debug tamper |
bcf_prob 20, bcf_iter 1 |
Standard release builds |
STRONG |
str str_aes wstr fla bcf sub co ibr lt bbr bbs dci genc mvo sv honey anti_debug tamper |
bcf_prob 50, bcf_iter 2, sub_iter 2 |
Security-critical shipping builds |
Every profile enables anti_debug, so a profile build must link
kagura_runtime.
STRONG is not "every pass". It deliberately leaves off the passes that change
the ABI, need a specific target, or carry an outsized cost: ci, pac, vm,
fsplit, pe, elt, bbcheck, telemetry, cse_break and string_split,
plus the two platform-gated passes objc and jni (the Apple and Android
integrations add those). Enable them explicitly in the passes object when you
want them.
vtp is not in that list because it is not a passes key at all —
-kagura-vtp is a command-line-only flag.
The ready-made files in
integration/profiles/
restate exactly these presets — both they and the compiled presets are
generated from lib/Transforms/Profiles.def, so a profile name means the same
thing whichever way you select it. (Until recently it did not: the JSON files
enabled sv, anti-debug and tamper that the compiled preset skipped.)
A profile sets defaults; anything in "passes" or "tuning" overrides the
profile's choices for that specific key.
Broader string protection¶
The default string passes keep their conservative collection and lazy-use
behavior. To also encrypt short strings (including one-character literals),
printf/logging formats containing %, and literals referenced by global
arrays or nested structs, opt into startup decryption:
{
"profile": "CUSTOM",
"passes": { "str_aes": true, "wstr": true },
"string": { "aggressive": true }
}
The CLI equivalent is -kagura-string-aggressive, alongside the string pass
flags. This changes the policy of str, str_aes and wstr; it does not enable
them by itself. An explicit -kagura-string-aggressive=false overrides JSON,
and flavors may override the string object. Unknown keys in that object
also participate in -kagura-config-strict validation.
Eligible application strings use writable backing storage with a permanent
address. Initializers keep their pointers and GEP offsets; a priority-0 module
constructor decrypts the data before normal application constructors and
main. AES additionally stores a separate constant ciphertext blob and
requires kagura_runtime. This increases startup work and keeps plaintext in
memory for the lifetime of the module. Code in preinit hooks or other
priority-0 constructors must not rely on this initialization order.
Coverage remains deliberately limited to private/internal storage outside special sections. TLS, externally visible string storage, LLVM annotations, loader metadata, aliases and Kagura's own keys/helpers are excluded.
str runs before str_aes in the automatic pipeline. Enabling both does not
upgrade XOR-encrypted strings to AES: the earlier pass consumes those targets.
For AES protection of eligible narrow strings, set "str": false and
"str_aes": true, including when overriding a strength profile.
Limit plaintext lifetime¶
Enable -kagura-string-scoped or "string": {"scoped": true} alongside
str or str_aes. This option does not enable a pass itself and does not
apply to wstr. Eligible literals decrypt into a stack buffer immediately
before each consuming call; a volatile zeroing operation wipes it after the
call returns. Each call site and invocation has its own buffer, including
recursive and concurrent invocations.
Eligibility requires private/internal unnamed_addr storage (or dso-local
Windows linkonce_odr pooled literals) of at most 4096 bytes and only direct call-argument uses, optionally through constant GEPs or
casts. Each affected argument must be readonly and noncapturing, and the call
must be nonthrowing. Address-significant storage, escapes, returned pointers,
comparisons, instruction-valued GEPs, EH functions, operand bundles and
musttail calls keep the existing persistent behavior or collection exclusion.
Ordinary tail-call hints are cleared to permit the wipe. Function attributes
must truthfully describe the consumer; nonlocal exits and process termination
cannot guarantee cleanup.
scoped and aggressive can be combined: eligible literals use scoped buffers,
while other collected strings retain startup decryption and persistent storage.
Without aggressive, the conservative string selection still applies. AES
requires kagura_runtime. Scoped buffers add decryption work on every use and
do not wipe registers, copies made by consumers, or external I/O buffers.
Worked example — bank / FinTech release¶
Strong profile with per-build AES key rotation so a key extracted from one version is useless against the next:
{
"profile": "STRONG",
"passes": {
"str_aes": true,
"mvo": true,
"pe": true,
"bbcheck": true,
"tamper": true
},
"tuning": {
"bcf_prob": 60,
"seed": 0
}
}
clang -fpass-plugin=KaguraObfuscator.dylib \
-mllvm -kagura-config=kagura-bank.json \
-mllvm -kagura-config-strict \
-mllvm -kagura-build-id=$(git rev-parse HEAD) \
-O2 -c bank_core.c -o bank_core.o
bbcheck only inserts the call sites; it needs a kagura_bb_check you supply.
See Anti-Analysis Passes.
Root keys¶
| Key | Type | Meaning |
|---|---|---|
profile |
string | FAST / BALANCED / STRONG / CUSTOM. CUSTOM means "I am listing the passes myself" and applies no preset. |
passes |
object | Per-pass enables. Keys are -kagura- flag names without the prefix. "dwarf" additionally takes "keep" / "strip" / "obfuscate". |
tuning |
object | The five numeric parameters: seed, bcf_prob, bcf_iter, sub_iter, dci_prob. |
string |
object | "aggressive": true broadens collection; "scoped": true limits eligible XOR/AES plaintext lifetime. Both default to false. |
allowlist |
array of string | Same as -kagura-allow: when non-empty, only matching symbols are obfuscated. |
denylist |
array of string | Same as -kagura-deny: matching symbols are left alone. |
protect |
array of string | Same as -kagura-protect: force-protect matching symbols. Checked first, so it wins over denylist, allowlist, kagura_hotpath and per-function kagura_no* annotations alike. |
audit_out |
string | Same as -kagura-audit-out: where the audit log is written. Only has an effect together with -kagura-audit; the default is kagura_audit.json. |
flavors |
object | Per-flavor override blocks, keyed by $KAGURA_FLAVOR. |
All three list keys are matched against the symbol name and accept one trailing
* as a prefix glob (vendor_*). No other wildcard syntax is supported.
Flavors¶
Set KAGURA_FLAVOR in the environment and the matching block of "flavors" is
applied on top of the base config, so one file can cover several build
variants:
{
"profile": "BALANCED",
"denylist": ["test_*"],
"flavors": {
"staging": { "profile": "FAST" },
"production": { "profile": "STRONG", "passes": { "vm": true } }
}
}
KAGURA_FLAVOR=production clang -fpass-plugin=KaguraObfuscator.dylib \
-mllvm -kagura-config=kagura.json -O2 -c app.c -o app.o
A flavor block accepts profile, passes, tuning, string, allowlist, denylist
and protect — the same keys as the root, minus flavors and audit_out.
See also¶
- Tuning Parameters — every CLI flag, including symbol filters
and the
-kagura-build-idper-build key seed. - Pass Order — the deterministic order in which the plugin applies these passes.
- Game Protection —
Protected<T>for run-time value protection (complementary tomvo/pe).