Back to blogs

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.

Illustration in Java source form, not actual transformer output java
// 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

  • flowShuffle reorders basic blocks within a method.
  • gotoObfuscation replaces GOTO with opaque conditional jumps across 13 IF_xxx variants.
  • controlFlow adds opaque predicates, either Light (cheap arithmetic) or Heavy (LDIV-based).
  • trapEdge turns GOTO into a fake try/catch with dead exception edges.
  1. 01 RENAME Renaming
  2. 02 PROTECT_EARLY Early protections
  3. 03 STRING String encryption
  4. 04 CONTROL_FLOW Control flow
  5. 05 STRUCTURE Structure
  6. 06 DYNAMIC_LAST Dynamic, runs last
Transformers run in phase order, from RENAME to DYNAMIC_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.

Per-method exclusion java
import fr.polargames.api.annotation.*;@ExcludeTransformers({ Transformers.CONTROL_FLOW, Transformers.FLATTEN })void hotPath() { }
Per-package exclusion, one rule per line json
"exclude": "flatten:net.minecraft.**\nrename:Start**"