Overview
The Dead Code Injection Pass inserts unreachableif (false) { ... } branches throughout your procedure bodies. These branches contain dummy operations that decompilers must analyze, making reverse engineering slower and more confusing.
Dead code has zero runtime cost — the Dart VM optimizer eliminates
if (false) branches during JIT/AOT compilation.How It Works
The pass walks all procedures and injects dummy conditional branches (lib/src/engine/passes/dead_code/dead_code_pass.dart:12):Find Target Procedures
Walk the kernel Component and identify all procedures with non-empty block bodies.
Inject Dead Branches
For each procedure, insert up to
max_insertions_per_procedure dummy if (false) branches before existing statements.Why Dead Code?
Decompilers and static analyzers must process all branches, even unreachable ones:- Control flow analysis: Must track all possible execution paths
- Data flow analysis: Must analyze variable usage in dead branches
- Type inference: Must verify type correctness of dead code
Configuration
Enable with Defaults
Full Configuration
max_insertions_per_procedure
max_insertions_per_procedure
Type:
Default:
intDefault:
1Maximum number of dead branches to insert per procedure. Higher values increase obfuscation strength but also binary size.Recommended values:1-2: Balanced (moderate obfuscation, small size increase)3-5: Strong (significant obfuscation, noticeable size increase)6+: Extreme (very strong obfuscation, large binaries)
What Gets Injected
The pass injects branches before each existing statement in a procedure body (lib/src/engine/passes/dead_code/dead_code_transformer.dart:14).Injection Pattern
Before:max_insertions_per_procedure: 2):
- A dummy variable declaration:
var _d = 0 - A dummy assignment:
_d = 1
The assignment is pointless (assigning to a local variable that’s never read), but decompilers must analyze it anyway.
Dead Branch Structure
Each injected branch follows this pattern (lib/src/engine/passes/dead_code/dead_code_transformer.dart:56):Why This Pattern?
Simple but effective
Simple but effective
The pattern is trivial for the VM optimizer to eliminate but forces decompilers to:
- Track the
_dvariable’s scope - Analyze the assignment’s data flow
- Verify type correctness
Doesn't interfere with real code
Doesn't interfere with real code
The dummy variable
_d is local to the dead branch, so it never conflicts with your actual variables.Looks realistic in decompiler output
Looks realistic in decompiler output
Decompilers show:This looks like leftover debug code or dead variable initialization, not obvious obfuscation.
Before and After Examples
Example 1: Simple Function
Before:max_insertions_per_procedure: 1):
Example 2: Multi-Statement Function
Before:max_insertions_per_procedure: 2):
Insertions stop after
max_insertions_per_procedure is reached, even if more statements remain.Example 3: Empty Function (Skipped)
Before:Implementation Details
Transformer Logic
TheDeadCodeTransformer extends PassTransformer and overrides visitProcedure (lib/src/engine/passes/dead_code/dead_code_transformer.dart:15):
- Iterates through existing statements
- Inserts a dead branch before each statement (up to the limit)
- Replaces the procedure body with the new statement list
- Sets the parent correctly for kernel AST consistency
Building Dead Branches
The_buildDeadBranch() method constructs the kernel AST (lib/src/engine/passes/dead_code/dead_code_transformer.dart:56):
The entire branch is built using kernel AST nodes, not source strings. This ensures type correctness and VM compatibility.
Performance Considerations
Runtime Cost
Zero cost. The Dart VM optimizer eliminatesif (false) branches during compilation:
false conditions and remove the entire branch from the generated machine code.
The VM optimizer runs after obfuscation, so dead branches never impact runtime performance.
Binary Size Impact
Increases binary size because dead branches are preserved in the.dill file:
- 1 insertion per procedure: +0.5% to +2% binary size
- 2 insertions per procedure: +1% to +4%
- 5 insertions per procedure: +3% to +10%
- Number of procedures in your code
- Average statements per procedure
max_insertions_per_proceduresetting
Decompilation Impact
Dead branches significantly increase decompiler output complexity: Without dead code:max_insertions_per_procedure: 2):
- Identify which branches are dead
- Manually remove them to understand logic
- Deal with broken control flow graphs
Advanced Patterns
Combining with Rename
Run Rename before Dead Code to obfuscate the dummy variable name:In practice, the dummy variable
_d is local to each dead branch, so renaming has minimal impact. The variable name is less important than the branch’s existence.Randomizing Insertions (Future Enhancement)
Currently, dead branches are inserted before each statement in order. A future enhancement could:- Insert at random positions (before, after, or between statements)
- Vary the dummy code pattern (different operations, multiple variables)
- Use different condition patterns (
if (1 == 2),if (null != null))
Nested Dead Branches (Not Implemented)
Another potential enhancement:Limitations
Binary Size Growth
Highmax_insertions_per_procedure values can significantly increase binary size:
Pattern Recognition
The current implementation uses a single pattern:Expression Bodies
Dead code cannot be injected into expression bodies:{ ... }).
Workaround: The Dart compiler often converts expression bodies to blocks during kernel compilation, so this limitation rarely matters in practice.
Best Practices
Start Conservative
Begin withmax_insertions_per_procedure: 1 and increase if needed:
Combine with Other Passes
Dead Code Injection is most effective when combined with Rename and String Encryption:Profile Binary Size
Always check the impact on your app’s binary:Monitor Decompiler Output
Test your obfuscation by decompiling your own binary:Troubleshooting
Binary size increased dramatically
Binary size increased dramatically
Reduce
max_insertions_per_procedure:Dead branches don't appear in decompiler output
Dead branches don't appear in decompiler output
Some decompilers perform dead code elimination. Try a different decompiler or check the raw
.dill file with refractor inspect:Compilation fails after enabling dead_code
Compilation fails after enabling dead_code
This should never happen — dead branches are syntactically valid. File a bug report with your configuration and code sample.
Some functions have no dead branches
Some functions have no dead branches
The pass skips:
- Empty functions
- Expression body functions (
=>syntax) - Functions in libraries outside obfuscation scope
Next Steps
Rename Pass
Learn about identifier obfuscation
String Encryption
Hide string literals with XOR encoding