USER GUIDE

June 29, 2026 · View on GitHub

This guide explains how to enable and tune the obfuscation pipeline, how to generate JSON/HTML reports, and how to troubleshoot correctness and performance issues.

Table of contents


Quick mental model

  1. You annotate functions with "obf: ..." specs.
  2. A module analysis parses all annotations into a cached map: Function → ObfuscationConfig.
  3. The module entry pass obfuscation runs:
    • module-only work (strenc) if enabled anywhere in the module
    • a function driver that executes the topologically sorted function pipeline
  4. Optional:
    • verification (-obf-verify)
    • metrics and reports

Configuration via annotations

Grammar

In C/C++ the common pattern is:

__attribute__((annotate("obf: <spec>")))

Where <spec> is a comma-separated list of pass specifications:

<spec>     := <passSpec> ( "," <passSpec> )*
<passSpec> := <passName> [ "(" <params> ")" ]
<params>   := <kv> ( "," <kv> )*
<kv>       := <key> "=" <value>

Example:

__attribute__((annotate("obf: mba(prob=70,maxDepth=3), bcf(prob=40,loop=1), flattening(minBlocks=3,maxBlocks=120)")))

In C++ you can also use the attribute syntax:

[[clang::annotate("obf: mba(prob=60), bcf(prob=25)")]]

Pass IDs and aliases

CategoryCanonical IDAccepted aliases
Expression / data-flowmba
Expression / data-flowsubstitutionsub
Expression / data-flowsdiff
Call hardeningvcall
CFGsplit
CFGbcf
CFGflatteningfla
Post-hardeningshieldantiopt, anti-opt
Post-hardeningadecanti-decompiler, antidecompiler
Virtualisationvmvirtualize, virt
Module-onlystrenc

Aliases are case-insensitive. Canonical IDs in reports and manifests are always the canonical form.

Note

The pipeline driver enforces a stable ordering between passes via topological sort, regardless of annotation order. See Choosing an obfuscation strategy for the effective execution order.

Note

An internal aes_stub module pass also exists. It is not user-callable via annotations — it is auto-linked when strenc or vm (with useAES=1) is enabled, and embeds the shared __obf_aes_ctr_decrypt runtime into the module. You do not need to mention it in obf: specs.

Parameter parsing rules

  • Keys are [A-Za-z0-9_] (no dashes).
  • Values can be:
    • unquoted: maxSites=200
    • quoted: tag="hello world"
  • Whitespace around tokens is ignored.
  • For most boolean knobs, use 0 / 1. Some passes also accept true/false/yes/no/on/off.

Multiple annotations and merging

If the same function carries multiple obf: annotations, configs are merged:

  • Pass enablement is additive.
  • Parameters are overridden by later annotations (last wins).
  • Pass ordering is resolved after merging, so stacking annotations is safe.
// Enable mba from one annotation, bcf from another — both will run.
__attribute__((annotate("obf: mba(prob=60)")))
__attribute__((annotate("obf: bcf(prob=30)")))
int fn(int x) { return x; }

Running the obfuscator

Using opt

# 1) Compile to IR
clang -S -emit-llvm -O0 -g test.c -o test.ll

# 2) Run the module entry pass
opt -passes=obfuscation -S test.ll -o test.obf.ll \
  -obf-seed=1 -obf-deterministic -obf-verify

# 3) Compile obfuscated IR to native
clang test.obf.ll -O2 -o test.obf

Using clang

Because this is in-tree, you can pass the pipeline directly to clang via -mllvm:

clang test.c -O2 \
  -mllvm -passes=obfuscation \
  -mllvm -obf-seed=1 \
  -mllvm -obf-deterministic \
  -o test.obf

If your toolchain does not forward -passes reliably through -mllvm, use the opt flow instead.

Diagnostic passes

These are useful when debugging config resolution or measuring impact:

# Print resolved per-function config and computed seeds
opt -passes=obf-dump-config -S test.ll -o /dev/null -obf-verbose -obf-seed=1

# Emit JSONL metrics per function (instruction counts, cyclomatic complexity, etc.)
opt -passes=obf-metrics -S test.ll -o /dev/null > metrics.jsonl

Global command-line options

Core

OptionDefaultMeaning
-obf-seed=<N>0Base seed. Non-zero makes all runs reproducible.
-obf-deterministicoffWhen seed is 0: derive module seed from module identifier hash (otherwise uses random_device).
-obf-verifyoffRun IR verification before/after each obfuscation stage.
-obf-verboseoffPrint extra info (parsing, skips, budgets, pipeline order, etc.).
-obf-max-function-insts=<N>0 (off)Skip functions larger than N instructions.
-obf-max-function-blocks=<N>0 (off)Skip functions larger than N basic blocks.
-obf-max-loop-depth=<N>0 (off)Skip functions whose loop nesting depth exceeds N.

IR budget

OptionDefaultMeaning
-obf-ir-budget-multiplier=<N>50Budget limit = insts_before × N (clamped by -obf-ir-budget-max). 0 = unlimited.
-obf-ir-budget-max=<N>0 (off)Absolute IR instruction ceiling per function. 0 = no hard cap.

Note

Budget knobs are global — they cannot currently be expressed as per-function annotation tokens. Use the command-line options above to tune budgets globally.

Pipeline ordering overrides

OptionDefaultMeaning
-obf-pipeline-ordering=<csv>""Explicit comma-separated pipeline order (e.g. mba,split,bcf,flattening). Listed passes run first in this order; remaining enabled passes are appended in topological order. Unknown names are fatal.
-obf-pipeline-ordering-annoffUse the per-function annotation order verbatim instead of topological sort. Ignored when -obf-pipeline-ordering is set.

The default is topological sort with conflict enforcement (e.g. vm + flattening rejected). Both override modes still run conflict checks.

Seed manifest

OptionDefaultMeaning
-obf-seed-manifest=<path>""Write a JSON seed manifest (base/module/function/pass seeds) to stderr if set to -, else to the given path.
-obf-seed-manifest-mdoffAlso emit per-pass seed manifest into LLVM IR metadata (obf.seed.manifest.<passId>).

Debug info

OptionDefaultMeaning
-obf-strip-debugoffStrip debug metadata from obfuscated functions only.
-obf-debug-syntheticonAssign synthetic line-0 debug locations to obfuscation-inserted instructions so source steppers do not jump erratically.

Shield auto-enable

OptionDefaultMeaning
-obf-shield-autooffAuto-enable shield with default knobs for any function that has any obfuscation pass annotated but no explicit shield(...) token. Explicit shield(...) annotations are always honored regardless of this flag.

Reports

OptionDefaultMeaning
-obf-report-dir=<dir>""Emit report artifacts (CFG DOT files, JSON) into this directory.
-obf-report-json=<path>""Write report JSON to this path (- = stdout).

Anti-decompiler (adec) tuning

OptionDefaultMeaning
-adec-gadgets-file=<paths>""Comma-separated JSON gadget files merged into the global pool.
-adec-disable-builtin-gadgetsoffDrop compile-time built-in gadget tables; use only user files / annotations.
-adec-clobbers-x86=<csv>(built-in)Override default inline-asm clobber list for x86_64. Comma-separated short register names.
-adec-clobbers-aarch64=<csv>(empty)Override default inline-asm clobber list for aarch64.
-adec-techniques=<csv>"" (all)Whitelist of technique names (asmGadgets,indirectBr,deadDecoy,stackPollution,callTrampoline,aliasConfusion,fakeLoop,rdtscStretch,constLaunder).
-adec-categories=<csv>"" (all)Gadget category filter (anti-disasm,anti-trace,desync,...).
-adec-budget-split=<key:pct,...>(defaults)Per-technique budget percent split. Keys: asm,ibr,decoy,call,alias,loop,rdtsc,clndr.
-adec-prefix=<name>adecIR-name prefix for adec artifacts. Override with per-build random value to defeat signature scans against canonical adec.* names.
-adec-randomize-constsoffReplace hard-coded decoy payload constants with RNG values.

Note

Per-function annotation parameters (adec(gadgets="path",techniques="...",categories="...",asmInline="A;B;C")) override the CLI flags above when both are present.


Obfuscation reports

The report system is designed for debugging and troubleshooting: it helps you understand what changed, where it changed, and which pass caused it.

Enable report generation

Run opt with a report directory and a fixed seed:

opt -passes=obfuscation -S test.ll -o test.obf.ll \
  -obf-seed=1 -obf-deterministic \
  -obf-report-dir=obf_report

This produces:

  • obf_report/obf_report.json — machine-readable obfuscation map
  • obf_report/cfg/<fn>/before.dot — CFG before any pass
  • obf_report/cfg/<fn>/after.dot — CFG after all passes
  • obf_report/cfg/<fn>/per_pass/<passId>/after.dot — CFG after each pass
  • obf_report/cfg/<fn>/per_pass/<passId>/diff.dot — optional diff overlay

Generate the HTML viewer

python llvm/utils/obfuscator/obf_report_html.py \
  --json obf_report/obf_report.json \
  --out  obf_report/obf_report.html \
  --renderer dot

Open obf_report.html in your browser. The viewer provides per-function views with:

  • pass-by-pass instruction count deltas
  • CFG before/after
  • per-pass diff overlays
  • difficulty score (cyclomatic complexity, opaque predicate count, MBA depth, etc.)

Troubleshooting report rendering

  • Prefer --renderer dot and install Graphviz so dot / dot.exe is in PATH. This renders graphs offline as inline SVG without any CDN or WASM dependencies.
  • --renderer wasm requires the HTML to be served via HTTP:
python -m http.server 8000 --directory obf_report
# Then open http://localhost:8000/obf_report.html
  • --renderer text produces metadata tables only (no graphs) and works anywhere.

Pass reference

All parameters are optional; unspecified parameters use their defaults.


mba

Mixed Boolean/Arithmetic obfuscation for integer expressions.

Rewrites integer add/sub/and/or/xor and similar instructions as semantically equivalent MBA expressions consisting of linear zero-sum terms and optional nonlinear components.

KeyDefaultRangeMeaning
prob400–100Probability (%) to transform a candidate site.
maxDepth31–10Maximum recursive depth for MBA expansion.
maxSites1201–5000Cap on number of transformed sites per function.
linearTermsMin61–64Minimum number of linear "zero-sum" terms to inject.
linearTermsMax101–96Maximum number of linear terms (must be ≥ linearTermsMin).
enableNonLinear10/1Enable nonlinear zero-addends (mul/urem based).
nonLinearWeight / nonLinearProb200–100Weight/probability for nonlinear components per site.
enableLayered10/1Enable layered MBA windowing.
layeredWindow480–256Sliding window size for layered MBA.
layeredBudget10–32Max layered expansions per transformed site.

Example:

__attribute__((annotate("obf: mba(prob=70,maxDepth=3,maxSites=80)")))

Before: No transform

After: MBA under HexRays


substitution

Instruction substitution and diversification — replaces individual instructions with semantically equivalent (but structurally different) idiom sequences.

KeyDefaultRangeMeaning
prob500–100Probability (%) to substitute at a candidate site.
loop11–10Number of substitution rounds.
maxSites / max / sites00–100000Cap on number of substituted sites (0 = auto).

Example:

__attribute__((annotate("obf: substitution(loop=2,maxSites=250)")))

vcall

Virtualises selected direct calls via synthetic vtables and indirection layers.

Each transformed call site looks up the target through a generated table, with optional decoy entries and per-callsite index-expression variation.

KeyDefaultRangeMeaning
prob300–100Probability (%) to virtualise an eligible call site.
maxSites / max / sites00–100000Cap on number of call sites (0 = auto).
opaqueVTableNames10/1Use hash-based naming for generated globals.
addDecoyEntries10/1Add decoy vtable entries pointing to safe stubs.
decoyMin20–64Minimum decoy entries per table.
decoyMax40–64Maximum decoy entries per table (must be ≥ decoyMin).
varyIndexPerCallsite10/1Vary the index expression per call site.
indexStrength20–3Index obfuscation strength level.
mergeVTables00/1Opt-in vtable merging across compatible callees (experimental).

Example:

__attribute__((annotate("obf: vcall(prob=35,indexStrength=2,decoyMin=2,decoyMax=6)")))

split

Basic-block splitting — increases CFG complexity by splitting each block into multiple sub-blocks connected with unconditional branches.

KeyDefaultRangeMeaning
num / n52–10Split factor. Higher values create more blocks.

Example:

__attribute__((annotate("obf: split(num=6)")))

sdiff

Semantic diffusion — volatile-slot masking that resists local value propagation and constant folding by the optimizer.

KeyDefaultRangeMeaning
prob451–100Probability (%) to apply at a candidate site.
slots31–8Number of volatile slots used.
maxSites / max / sites801–2000Maximum transformed sites per function.

Example:

__attribute__((annotate("obf: sdiff(prob=35,slots=2,maxSites=60)")))

bcf

Bogus control flow — inserts opaque predicates (always-true/always-false conditions) and injects fake edges into the CFG to confuse decompilers and CFG analysis tools.

KeyDefaultRangeMeaning
prob300–100Site probability (%).
loop11–10Number of application rounds.
maxBlocks / max50000–100000Hard maximum block count (safety valve).

Example:

__attribute__((annotate("obf: bcf(prob=40,loop=1,maxBlocks=4000)")))

BCF CFG example


flattening

CFG flattening — replaces the original control flow graph with a dispatcher/state machine. All original blocks become cases of a central switch; control flow is encoded as state variable updates.

KeyDefaultRangeMeaning
minBlocks32–100000Minimum original blocks required to flatten.
maxBlocks2002–200000Maximum blocks considered (must be ≥ minBlocks).
allowIndirect / indirect00/1Allow indirect dispatch forms (higher risk, more confusion).
hybrid10/1Enable hybrid/structured flattening strategies.
opaqueState10/1Store state updates as opaque expressions instead of constants.
fakeTransitions / fake00/1Inject hard-false transitions + optional fake cases.
fakeCases00–64Extra fake cases per dispatcher (requires fakeTransitions=1).
perDispatcherDomain / domain10/1Per-dispatcher switch domain encoding.
obfuscateStatePtr / ptr10/1Pointer games around state storage.
opaqueAliasStatePtr / alias10/1Add hard-false alias pointers to confuse alias analysis.

Warning

flattening conflicts with vm — both restructure the entire CFG. Use one or the other, not both, on the same function.

Example:

__attribute__((annotate("obf: flattening(minBlocks=3,maxBlocks=160,fakeTransitions=1,fakeCases=2)")))

Flattening CFG example


shield

Anti-optimization shield — post-obfuscation hardening that inserts volatile barriers, opaque identity operations, dead-store protection patterns, and CFG guards to resist the LLVM optimizer from simplifying obfuscated code in a subsequent optimization pass.

KeyDefaultRangeMeaning
maxSites2000–10000Maximum insertion sites.
volatileBarriers / volatile10/1Insert volatile load/store barriers.
opaqueIdentities / identity10/1Insert opaque identity operations.
deadStoreProtect / dse10/1Add dead-store protection patterns.
cfgGuards / cfg10/1Add CFG guard patterns.

Example:

__attribute__((annotate("obf: shield(maxSites=250,volatile=1,identity=1,dse=1,cfg=1)")))

adec

Anti-decompiler hardening — inserts patterns specifically designed to confuse decompilers and disassemblers: indirectbr trampolines, inline-asm junk bytes, fake stack frame entries, opaque predicate dead blocks, indirect calls via volatile slots, and pointer aliasing via ptrtoint chains.

KeyDefaultRangeMeaning
prob501–100Site probability (%).
maxSites401–500Maximum insertion sites.
strength20–3Complexity level (0 = minimal, 3 = maximum).
enableIndirectBr / indirectbr10/1indirectbr trampolines.
enableAsmAntiDisasm / asm10/1Inline-asm junk bytes (target-dependent).
enableStackPollution / stack10/1Fake stack frame entries and noise.
enableDeadCodeDecoys / decoy10/1Opaque predicate + dead-code decoy blocks.
enableCallObfuscation / call10/1Indirect call via volatile slots.
enableAliasConfusion / alias10/1Pointer aliasing via ptrtoint chains.

Example:

__attribute__((annotate("obf: adec(prob=70,strength=2,maxSites=25,asm=0)")))

vm

Code virtualisation — the strongest available transformation. Compiles the entire function body into a private bytecode stream stored in a read-only global, then replaces the function body with a minimal interpreter (fetch–decode–execute loop). No original basic blocks or instruction patterns survive in the emitted IR.

See VM.md for the full reference (ISA, bytecode layout, hardening layers, interaction with other passes, debugging tips).

KeyDefaultRangeMeaning
minBlocks11–∞Skip virtualisation if the function has fewer than N basic blocks.
maxBlocks4000–∞Skip virtualisation if the function has more than N blocks (0 = no limit).
useAES10/1Replace the LCG layer-2 stream with AES-128-CTR bytecode encryption.
obfRegIdx10/1XOR every register-index byte in the bytecode with a compile-time salt.
encBytecode10/1.init_array constructor encrypts the bytecode stream at process load time.
hardened00/1MBA expressions + opaque predicates on handler blocks; enables anti-debug traps.
regEncrypt00/1XOR-encrypt virtual register values at rest in the register file.
antiDebug10/1Anti-debug timing traps in the interpreter (effective only when hardened=1).
adDispatchThreshold5000RDTSC delta (cycles) for the dispatch-level timing gate.
adHandlerThreshold500RDTSC delta (cycles) for handler-level spot checks.
adDispatchInterval64Check every N fetch iterations (must be a power of 2).
adHandlerProb100–100Percentage of handlers to equip with timing traps.

Warning

vm conflicts with flattening — both completely restructure the CFG. Combining them on the same function is not supported and will be rejected by the pipeline.

Note

vm does not support functions containing EH pads, invoke, callbr, indirectbr, or naked attributes. The pass will silently skip ineligible functions and record a skip reason in the obfuscation report.

Examples:

// Minimal — virtualise with default hardening
__attribute__((annotate("obf: vm")))
int minimal(int x) { return x + 1; }

// Standard — AES encryption + register-index obfuscation (defaults)
__attribute__((annotate("obf: vm(useAES=1,obfRegIdx=1,encBytecode=1)")))
int standard(int x, int y) { return x * y; }

// Maximum hardening
__attribute__((annotate("obf: vm(hardened=1,useAES=1,regEncrypt=1,antiDebug=1,adDispatchThreshold=3000)")))
int protected_fn(int key, int data) { return key ^ data; }

strenc

String encryption — module-only pass. Finds string literal globals whose length meets the minimum threshold and encrypts them at compile time. A .init_array constructor decrypts them at process load time using AES-128-CTR (shared runtime with the vm pass).

Enabled when at least one annotated function includes strenc(...) anywhere in the module.

KeyDefaultRangeMeaning
minlen / minLength / min41–100Minimum string length to encrypt.

Example:

// Any function — just needs to trigger the pass
__attribute__((annotate("obf: strenc(minlen=6)")))
void init(void) { puts("confidential string"); }

Choosing an obfuscation strategy

Start small and scale up: verify correctness at each step before adding more passes.

Pipeline execution order

The driver enforces the following partial order (earlier = runs first):

mba, substitution, sdiff, vcall → split → bcf → flattening → shield → adec
                                                        ↕ conflict ↕
                                                            vm

vm runs after all other function passes if combined with them. Because vm virtualises the entire function, pre-passes that transform the IR before it (mba, bcf, etc.) make the resulting bytecode harder to reverse since the virtual ISA now encodes pre-obfuscated logic.

Light — low risk, fast compile/run:

__attribute__((annotate("obf: mba(prob=40,maxSites=60), substitution(loop=1), sdiff(prob=25,maxSites=40), split(num=4)")))

Medium — balanced:

__attribute__((annotate("obf: mba(prob=70,maxSites=120,maxDepth=3), substitution(loop=2), vcall(prob=25,indexStrength=2), split(num=6), bcf(prob=25,loop=1,maxBlocks=4000)")))

Heavy — structural:

// Medium + CFG flattening + post-hardening
__attribute__((annotate("obf: mba(prob=70), substitution(loop=2), split(num=6), bcf(prob=25), flattening(minBlocks=3,maxBlocks=160,fakeTransitions=1,fakeCases=2), shield(maxSites=300), adec(prob=60,strength=2,maxSites=50)")))

Maximum — full virtualisation:

// Pre-obfuscate then virtualise the result
__attribute__((annotate("obf: mba(prob=70), bcf(prob=30), vm(hardened=1,useAES=1,regEncrypt=1)")))

Trade-offs and pitfalls

  • CFG transforms explode IR size: bcf + flattening is powerful but can significantly impact compile time and binary size. Use the IR budget knobs to cap growth.
  • vm is the heaviest option: it replaces the entire function. Runtime overhead is real — use it only for the most sensitive functions. See VM.md for performance guidance.
  • Budgets matter: if the report shows many "skipped due to budget" entries, lower probabilities or raise global budget knobs (-obf-ir-budget-multiplier, -obf-ir-budget-hardcap).
  • Start deterministic: fix -obf-seed during development so issues are reproducible.
  • Don't combine vm and flattening on the same function — they conflict.

Troubleshooting

Invalid IR / miscompile:

  • Use -obf-verify to catch invalid IR as close to the introducing transform as possible.
  • Use -obf-report-dir to identify which pass changed the CFG and how.
  • Bisect: start with a single pass, confirm it works, add passes one at a time.

Wrong config being applied:

  • Use -passes=obf-dump-config to print the resolved per-function config.
  • Confirm annotation syntax — check parenthesis balance and key names.

Function unexpectedly skipped:

  • Check -obf-verbose output for skip reasons.
  • Common caps: -obf-max-function-insts, -obf-max-function-blocks, -obf-max-loop-depth.
  • For vm: check eligibility (no EH, no invoke, no callbr, block count within range).
  • Check IR budget utilization in the report JSON.

Bloated binary / slow compile:

  • Lower probabilities (prob=20 instead of prob=70).
  • Reduce maxDepth, loop, maxSites.
  • Limit which functions are annotated.
  • Raise budget limit or reduce multiplier to allow earlier budget-based skips.

O2 optimization undoes obfuscation:

  • Use shield to harden volatile barriers and opaque identities against the optimizer.
  • Confirm inserted volatile loads/stores are truly optimization-resistant.
  • Use --o2-gate in the test suite to verify survival.
  • For the strongest resistance, use vm — the interpreter loop is opaque to the optimizer.