llvm-obfus is an out-of-tree LLVM 21+ pass plugin for policy-driven IR obfuscation.
The project applies native LLVM IR transforms to selected functions. The main production entry point is obf-safe-pipeline, which composes virtualization, structural rewrites, string and constant protection, late indirect dispatch, and final artifact cleanup.
The design goal is simple. The passes make static recovery much harder and stay inside normal LLVM semantics. The project does not rely on malformed objects, inline-asm traps, EH spoofing, or target-specific parser breaks.
- Protection levels are
none,light,strong,vm, andstrong_vm. vmandstrong_vmlower selected functions into VM-backed execution paths.- Later hardening stages also process
strong_vmimplementation bodies, not just the public wrapper. - MBA rewriting diversifies arithmetic identities across
add,sub,xor, andmul. It also rewritesudivanduremby power-of-two constant divisors. It works directly and as part of other transforms such as constant reconstruction and opaque predicates. - Shape families include linear identities (
x ^ y = (x | y) - (x & y)), affine wrappers (Encode(x) = a*x + bwith odd modular multiplier), polynomial zero terms (depth 3+), and constant-multiplication decomposition. - Entropy thunk interfaces use one of two forms per function: aggregate pair or out-parameter. First-hop entropy mixing uses per-site
xor,mul_add,rotate_xor, orbit_splitselection before MBA shape builders run. - A private
BudgetTrackerenforces a per-expression IR-instruction cap derived frommba.depth. When the budget is exhausted mid-expansion, the engine emits the plain LLVM binary operation instead. instruction_substitutionrewrites logicaland,or, andxoroperations into equivalent identities, such asx & y = (x | y) - (x ^ y). Each site selects one of two variants and can pad the result with an MBA opaque zero.
indirect_dispatchis a late pass in the safe pipeline.- It rewrites supported conditional branches and switch dispatch sites into per-site masked
blockaddressplus arithmetic plusindirectbrsequences. - Each dispatch site derives its masking material from the protected function seed and site index.
- The implementation reconstructs targets from same-function deltas in SSA instead of emitting absolute dispatch tables in globals.
- This pass does not use the authenticated BLAKE2s runtime that strings and constant pools use.
- The pass skips unsupported shapes conservatively: EH personalities, EH pads,
invoke,callbr, existingindirectbr,catchswitch,catchreturn,cleanupreturn,resume,musttail, and non-integral program address spaces.
- The
string_encodingsection configures string encoding. authenticated_modeenables the keyed and integrity-checked runtime decode path.- The runtime support lives in
runtime/string_auth_runtime.cand handles keyed string and constant-pool recovery. - The transform handles lazy decode, eager decode, constructor fallback, and forwarded-pointer cases.
- Short compare-only, non-escaping authenticated strings decode through
rt_core_sd3into per-use stack scratch. The decode path volatile-zeroes the scratch after the compare. Escaping, shared, forwarded, or weakly proven uses keep lazy or constructor stable storage.
- Constant encoding modes are
off,mba_inline,keyed_pool,auto, andall. mba_inlinereconstructs constants directly in IR.keyed_poolmoves constants into keyed, integrity-checked pools that the runtime recovers at use sites.autochooses a strategy per use site.
- The top-level
seedis the root build input. Function-selective passes such asindirect_dispatchderive per-site seeds from the top-level seed, the function name, and the site index. The keyed string and keyed-pool runtime currently uses the top-level seed directly. authenticated_modeandkeyed_pooluse a domain-separated BLAKE2s schedule. The fileinclude/obf/support/auth_encoding.himplements this schedule.- The schedule is
build_key(seed)->function_key(module_id, function_id)-> per-site or per-pool key -> labeledencandmacsubkeys. - Authenticated strings derive distinct keys from descriptor metadata including
module_id, a derivedfunction_id, andsite_id. Keyed constant pools derive distinct keys frommodule_idandpool_id. - Authentication uses a keyed BLAKE2s tag over descriptor metadata plus ciphertext. Encryption uses a BLAKE2s-derived XOR keystream with a derived nonce. The scheme does not use AES, ChaCha20, HMAC, or SipHash.
- The emitted artifacts store the 32-byte
build_keyin internal globals and reconstruct derived keys at runtime from descriptor metadata. This is an embedded-key, self-contained runtime. It does not use a hardware token, remote service, white-box key split, or entropy-anchor binding. - Integrity verification is fail-closed. Descriptor mismatches, tag mismatches, and length mismatches trap in the runtime. The runtime does not return tampered plaintext.
runtime/entropy_anchor.csupports opaque arithmetic and MBA-style transforms. It is separate from the keyed string and constant-pool key schedule. It exposes five deterministic accessor variants:direct,stack_roundtrip,split_recombine,xor_neutral, andadd_sub_neutral. The function and salt determine the variant.
- The build generates public runtime ABI names in
build/include/obf/support/runtime_abi_generated.h. - The default public prefix is
rt_core_. - Final cleanup strips marker attributes, removes annotation metadata, anonymizes local and internal obfuscation artifacts, and strips local SSA names.
- Security gates can fail the build on leaked public
obfsymbols. Note: The plugin config preflight fails the build on weakened security configs. To allow a weakened config, setsecurity.allow_unsafe_config: true. It rejectsstrong_vmand high-security profiles (fortress,lab) withoutsecurity.fail_on_public_obf_symbol: true. When VM obfuscation is active, it also rejectsdebug_preserve_generated_names: true.
- YAML loading and config parsing live in
lib/frontend/. - Profiles are
fast,standard,guarded,fortress, andlab. - The loader applies profile defaults first. Explicit top-level YAML sections override these defaults.
--obf-seedoverrides the final seed after config loading.
- Per-function feature extraction lives in
lib/analysis/. - Policy selection lives in
lib/policy/. - The pipeline is function-selective rather than blanket-on for the whole module.
- Core transforms live in
lib/transforms/. - VM lowering lives in
lib/vm/. - Pass registration and safe-pipeline orchestration live in
lib/plugin/.
runtime/entropy_anchor.cprovides the entropy anchor support object that builds and tests use.runtime/string_auth_runtime.cprovides keyed and integrity-checked decode support for strings and constant pools.
obf-safe-pipeline is the integrated pipeline that the benchmarks and lit coverage use. Its current high-level order is:
- entropy initialization
- VM lowering and call rewriting for
vm - VM lowering and call rewriting for
strong_vm - post-VM string encoding
- constant encoding
- opaque GEP
- instruction substitution for logical and boolean rewrites
- opaque predicates
- control flattening
- function outlining
- bogus control flow
- block splitting
- additional hardening on
strong_vmimplementation functions - CFG state cleanup
- indirect dispatch
- security gate enforcement
- artifact cleanup
The late ordering matters. Indirect dispatch runs after the major structural passes so it can rewrite the final dispatch-heavy CFG shapes, including VM implementation functions.
The loader currently supports these top-level sections:
profileseeddefault_leveloverridestargetsblock_splitstring_encodingconstant_encodingmbavmindirect_dispatchsecuritydebug_preserve_generated_namesemit_progress_warnings
overrides entries match exact function names. targets entries support glob-style wildcard patterns (e.g., "verify_*").
emit_progress_warnings: true emits stderr progress messages around long strong_vm lowering and hardening phases. The default is silent.
| Setting | fast |
standard |
guarded |
fortress |
lab |
|---|---|---|---|---|---|
mba.depth |
1 | 1 | 2 | 3 | 4 |
mba.enable_polynomial |
derived | derived | derived | derived | true |
mba.enable_multiplication |
derived | derived | derived | derived | true |
mba.max_ir_instructions |
derived | derived | derived | derived | 320 |
block_split.max_splits_per_function |
1 | 1 | 2 | 4 | 8 |
string_encoding.min_string_length |
3 | 2 | 2 | 1 | 1 |
string_encoding.max_strings_per_module |
32 | 128 | 256 | 512 | 1024 |
string_encoding.prefer_lazy_decode |
true | true | true | false | false |
string_encoding.allow_ctor_fallback |
true | true | false | false | false |
constant_encoding.max_constants_per_function |
2 | 4 | 8 | 16 | 32 |
security.fail_on_public_obf_symbol |
false | true | true | true | true |
All profiles default to authenticated_mode: false, indirect_dispatch.enabled: false, min_instructions_per_block: 2 (fortress and lab use 1), min_bit_width: 8, default_level: none, and constant_encoding.mode: mba_inline. MBA override fields (enable_polynomial, enable_multiplication, max_ir_instructions) are absent by default. Their values come from mba.depth. Polynomial and multiplication families enable at depth 3+. The IR-instruction budget scales with depth: 64 at depth 1, 128 at depth 2, 192 at depth 3, and 256 at depth 4. Explicit top-level YAML keys override profile defaults.
vm and strong_vm lowering intentionally emit large IR.
A function with about 20 instructions expands to about 100k IR lines at mba.depth: 0.
Depth 0 to depth 1 is the steepest step, about 5x, and reaches about 500k IR lines.
Higher levels add less: about 1.0M IR lines at depth 2 and about 1.6M at depth 3.
The obfuscation pass itself is fast.
The native backend that compiles the expanded IR causes the cost.
clang -O3 aggressively inlines and unrolls this IR.
That build mode is not practical for VM-obfuscated functions.
For VM-heavy builds, obfuscate IR built with -O1 -fno-inline.
The benchmark targets use this mode.
Target small hot functions.
Lower mba.depth for VM code when you need shorter build times.
Two settings bound VM cost.
vm.max_mba_depth clamps the MBA depth used inside VM lowering.
If you do not set it, the VM uses no clamp.
vm.max_virtual_instructions sets a per-function virtual-instruction budget.
Its default is 512.
If a strong_vm function exceeds the budget, the build fails with an actionable diagnostic.
You can set protection levels directly in source with LLVM's annotate attribute. The canonical annotation value is "obf:<level>" where <level> is one of none, light, strong, vm, or strong_vm. The parser also accepts a bare level string such as "strong". The level match ignores case.
__attribute__((annotate("obf:strong_vm")))
void sensitive_routine(void) { ... }Annotations take precedence below explicit overrides entries but above targets rule matching. The automatic security floor applies independently and may raise the level further.
Minimal example:
profile: fortress
seed: 20260601
default_level: none
targets:
- match: "verify_*"
level: strong_vm
- match: "license_*"
level: strong_vm
string_encoding:
authenticated_mode: true
prefer_lazy_decode: true
allow_ctor_fallback: false
constant_encoding:
mode: auto
max_constants_per_function: 8
min_bit_width: 8
mba:
depth: 3
enable_polynomial: true
enable_multiplication: true
max_ir_instructions: 320
indirect_dispatch:
enabled: true
max_sites_per_function: 4
max_switch_targets: 8
target_vm_dispatchers: true
target_flattened_headers: true
security:
fail_on_public_obf_symbol: true
strip_release_markers: trueRequirements:
- CMake 3.24+
- C++23 compiler
- LLVM 21+
- Python 3
lit- LLVM tools:
opt,clang,clang++,llvm-link,llc,llvm-strip,llvm-nm,llvm-objdump - Optional:
stringsfor benchmark string audits
Configure and build:
cmake -S . -B build -DLLVM_DIR="$(llvm-config --cmakedir)"
cmake --build buildUseful cache variables:
OBF_BENCHMARK_SEEDOBF_RUNTIME_ABI_PREFIXOBF_BENCHMARK_CLEAN_IROBF_BENCHMARK_CLEANUP_PASSES
The plugin integrates into the standard LLVM New Pass Manager (NPM) optimization pipeline. The plugin runs passively and applies no obfuscation by default. It applies obfuscation only when you enable it with the OBF_CONFIG or OBF_ENABLE environment variables. It safely handles both unoptimized (-O0) and optimized (-O1 through -O3, -flto) builds. The build tree provides build/obf-clang and build/obf-clang++ wrappers that inject the pass plugin and append the runtime archive for link actions:
OBF_CONFIG=config.yaml build/obf-clang++ -O3 input.cpp -o outputTo enable annotation-driven obfuscation without a config file, set the explicit enable environment variable:
OBF_ENABLE=1 build/obf-clang++ -O3 input.cpp -o outputManual clang fallback:
OBF_CONFIG=config.yaml clang++ -O3 \
-fpass-plugin=build/obf_plugin.so \
input.cpp build/libobf_runtime.a -o outputbuild/libobf_runtime.a contains the entropy anchor and string/constant authentication runtime support. When you invoke raw clang or clang++, place the archive after transformed inputs and objects on the linker command line.
For debugging, testing, or isolated transforms, you can run the passes manually over LLVM IR with opt.
Feature report:
obf-feature-reportis read-only. It emitsobf.feature_report.v3JSON with per-function policy decisions and per-transform strategy details. For functions that use MBA rewrites, it also adds MBA shape counters under thembapayload.
opt -load-pass-plugin build/obf_plugin.so \
--obf-config=config.yaml \
-passes=obf-feature-report \
-disable-output input.llPolicy audit:
obf-auditprints a policy-resolution table and can also writeobf.audit.v1JSON with--obf-audit-out.
opt -load-pass-plugin build/obf_plugin.so \
--obf-config=config.yaml \
--obf-audit-out=audit.json \
-passes=obf-audit \
-disable-output input.llFull safe pipeline:
opt -load-pass-plugin build/obf_plugin.so \
--obf-config=config.yaml \
-passes=obf-safe-pipeline \
-S input.ll -o output.llIsolated indirect dispatch:
opt -load-pass-plugin build/obf_plugin.so \
--obf-config=config.yaml \
-passes=obf-indirect-dispatch \
-S input.ll -o indirect.llOther standalone passes:
- Read-only/reporting:
obf-feature-report,obf-audit. - Transform stages:
obf-entropy-init,obf-vm,obf-block-split,obf-string-encode,obf-constant-encode,obf-opaque-gep,obf-instruction-substitute,obf-control-flatten,obf-function-outline,obf-opaque-preds,obf-bogus-cf,obf-indirect-dispatch,obf-cfg-state-cleanup, andobf-artifact-cleanup.
obf-driver remains a config-summary and debugging utility. build/obf-clang and build/obf-clang++ are the compile wrappers.
Expand visual examples and analysis
These screenshots compare one baseline function with one obfuscated function from the license_demo benchmark.
- Baseline function:
FUN_004008f0fromlicense_demo.baseline - Obfuscated function:
FUN_00400510fromlicense_demo.obfuscated
What the baseline image shows:
- A compact, readable verification-style routine.
- Clear control flow (simple bounds and loop structure).
- Data-dependent operations that remain semantically recoverable in the decompiler.
What the obfuscated image shows:
- Large opaque arithmetic chains with mixed rotates/xors/add-masks.
- Decompiler warnings around jump-table recovery and indirect control transfer.
- Reduced semantic readability despite valid executable behavior.
Pseudocode comparison:
Baseline (license_demo.baseline / FUN_004008f0) |
Obfuscated (license_demo.obfuscated / FUN_00400510) |
|---|---|
![]() |
![]() |
Benchmark targets build paired baseline and obfuscated artifacts under build/benchmarks/<name>/. The benchmark build passes --obf-seed=${OBF_EFFECTIVE_BENCHMARK_SEED} to opt. So OBF_BENCHMARK_SEED controls the effective benchmark seed for the whole build tree. This holds even when a sample benchmark config contains its own seed: entry.
Build benchmark pairs:
cmake --build build --target obf-benchmarksPer-benchmark artifacts:
<name>.baseline.ll<name>.obfuscated.ll<name>.obfuscated.cleaned.llwhenOBF_BENCHMARK_CLEAN_IR=ON<name>.baseline<name>.obfuscated
Benchmark and analysis targets:
obf-benchmarksbuilds stripped baseline and obfuscated pairs for the full corpus.obf-benchmarks-miremits MIR snapshots for linked benchmark targets such aswpo_demo.obf-audit-benchmarksaudits stripped obfuscated benchmark binaries for leaked symbols and, whenstringsis available, residual strings.obf-re-harnessscores how much VM structure is recoverable from obfuscated benchmark IR and writesbuild/re-harness/vm_recovery.json.obf-seed-diversityverifies seed-driven IR diversity and writesbuild/diversity/diversity.json.
Current benchmark corpus:
license_democonfig_demovm_workflow_demowpo_demo
Measure keyed string decode overhead:
python tools/obf-bench/measure_string_auth_overhead.py --build-dir buildThe helper writes temporary inputs under build/string-auth-bench/ and reports lazy first-decode cost, lazy steady-state helper cost, and constructor startup impact.
Requested release sweep:
cmake --build build --target obf-benchmarks obf-seed-diversity obf-unit-tests obf-driver obf-runtime
ctest --test-dir build --output-on-failure -R "obf-lit|obf-unit-tests"The lit suite covers 175 tests across MBA engine shapes, opaque predicates, bogus control flow, control flattening, opaque GEP, constant encoding (inline and keyed-pool), string encoding (lazy, eager, auth), indirect dispatch, VM lowering, VM handler and dispatcher polymorphism, seed determinism, safe pipeline ordering, security gates, and artifact cleanup. The tests check IR with opt, match expected output with FileCheck, and validate runtime behavior with lli. Runtime entropy anchors are external. The suite precompiles the runtime sources into obf_entropy_anchor.o and obf_string_auth_runtime.o. It links these objects into lli with --extra-object.
include/obf/ public headers
lib/analysis/ feature extraction
lib/frontend/ config loading and annotations
lib/plugin/ pass registration and pipeline wiring
lib/policy/ function-level policy selection
lib/report/ reporting
lib/transforms/ IR transforms
lib/vm/ VM lowering and dispatch
runtime/ runtime support objects
tests/lit/ lit coverage
tests/unit/ unit tests
benchmarks/ corpus, configs, and build targets
tools/ helper tools and scripts

