Alex Martin Obfuscation
How Control-Flow Flattening Works, and What It Costs
A method that reads from top to bottom is easy to follow in a decompiler. Control-flow flattening removes that shape: the flatten transformer turns the control flow of a method into a switch-dispatched state machine, so every block sits at the same level and a state value decides which one runs next.
This post walks through what changes in a method, which other passes work with it, and what you pay for it.
From a straight line to a dispatcher
flatten is loop-aware control-flow flattening. It rewrites a method so that its basic blocks are driven by a state variable inside a switch: the dispatcher reads the state, jumps to the matching block, and the block sets the next state. The original order of the blocks is no longer visible in the structure of the method.
// beforeint total = 0;for (int index = 0; index < count; index++) { total += weights[index];}return total;// after flattening, conceptuallyint state = 0;while (true) { switch (state) { case 0: total = 0; index = 0; state = 1; break; case 1: state = index < count ? 2 : 3; break; case 2: total += weights[index]; index++; state = 1; break; case 3: return total; }}Hiding the state
A plain state counter is easy to follow, so HashGuard can mask it. With cffStateMasking the state value is XOR-masked with ThreadLocalRandom on every dispatch, which means the state is no longer a plain counter.
The passes that work with it
flowShufflereorders basic blocks within a method.gotoObfuscationreplacesGOTOwith opaque conditional jumps across 13IF_xxxvariants.controlFlowadds opaque predicates, either Light (cheap arithmetic) or Heavy (LDIV-based).trapEdgeturnsGOTOinto a faketry/catchwith dead exception edges.
- 01 RENAME Renaming
- 02 PROTECT_EARLY Early protections
- 03 STRING String encryption
- 04 CONTROL_FLOW Control flow
- 05 STRUCTURE Structure
- 06 DYNAMIC_LAST Dynamic, runs last
What it costs
Flattening is one of the heavy passes. Every block now goes through the dispatcher, and the output holds more instructions than the input. HashGuard puts a ceiling on that growth with maxOutputRatio: a value of 2.0 means at most twice the instruction count of the input, and heavy transformers such as flattening stop automatically once the budget is spent.
Loops are the other cost. flattenLoops also flattens loop bodies, and its description is direct about the price: higher overhead on hot loops. The example configuration leaves it false.
Flattening hides the shape of a method, not its behaviour.
Keeping hot paths cheap
You do not have to choose between protecting a class and keeping one method fast. Exclude the method from specific transformers with an :api annotation, or exclude whole packages by rule in the config.
import fr.polargames.api.annotation.*;@ExcludeTransformers({ Transformers.CONTROL_FLOW, Transformers.FLATTEN })void hotPath() { }"exclude": "flatten:net.minecraft.**\nrename:Start**"