Control Flow Flattening: Turning Structure into a State Machine

Code has a shape, and the shape talks. A function that checks a condition, loops over items, and returns early reads like a story precisely because its layout mirrors its logic. Control flow flattening is the deliberate destruction of that shape. It is the most structural transform short of full virtualization, and understanding it makes virtualization itself click, because a flattened program is already halfway to being an interpreter.

Think of a story told in numbered scenes, but printed out of order and shuffled like a deck of cards. Scene one might sit in the middle of the pile; the only way to read the story is to follow a little note at the end of each scene saying which numbered scene comes next. The events never changed, and the ending is the same, but you can no longer just read top to bottom. Flattening shuffles a program's scenes exactly like that, and hides the reading order in a single number it keeps updating.

The transform

Every basic block of the original function (each straight-line run of statements) becomes a numbered case in a switch. A state variable decides which case runs next, a loop drives the switch forever, and each case ends by assigning the next state. The original order of execution stops living in the layout and moves into data:

function steps() {
  a();
  b();
  c();
}
function steps() {
  let state = 0;
  while (true) {
    switch (state) {
      case 0: a(); state = 2; break;   // real order lives in these
      case 1: c(); state = -1; break;  // numbers, not in the layout
      case 2: b(); state = 1; break;
      default: return;                 // state < 0 ends the loop
    }
  }
}

Read the cases top to bottom and you learn nothing: the visible order is 0, 1, 2 but the executed order is 0, 2, 1, reconstructing a-b-c only if you follow the state assignments. Real implementations go further: state values are large opaque numbers rather than small integers, the next state is computed rather than assigned as a literal, cases from different original functions get merged into one dispatcher, and opaque predicates guard fake transitions. Each addition makes the state graph harder to rebuild by eye.

Step the dispatcher yourself

Below is the same idea as a runnable state machine in the academy's teaching language: a loop, a state variable, and one case per step. Step through it and watch the state number jump around while the printed output comes out in the intended order. Notice that reading the cases in the file tells you nothing about the sequence; only tracing the state does. That gap between layout and execution is the entire point of the transform.

Why it raises cost

Skimming dies here. There is no longer any correlation between where code sits and when it runs, so the attacker must either simulate the dispatcher on paper, tracking the state variable across every hop, or run the code and trace it. For a function of any size this is genuinely painful, and it composes viciously with the earlier layers: tracing a state machine is much worse when every landmark that would orient you (names, strings, constants) has already been removed.

The honest limit

The block bodies themselves are untouched, a() is still visibly a call to a(), and the state machine is mechanical. An attacker who instruments the code and logs states in execution order recovers the original sequence in one run, and academic tooling exists that de-flattens common dispatcher patterns statically. Flattening hides the map, not the towns. Its job in a layered pipeline is to force the attacker into dynamic analysis, which is slower, per-build, and exactly the fight the defender wants.

The static de-flattening attack is worth understanding because it tells you what hardening must defend. A tool recovers the original control flow by finding the dispatcher, identifying the state variable, and then determining, for each case, the value the state takes next; connect those edges and the original graph reappears. That recovery is cheap exactly when the next state is a visible literal (state = 2). The countermeasures all attack that step: compute the next state from a runtime value instead of assigning a constant, make state a large opaque number derived by arithmetic, merge unrelated functions into one dispatcher so the graph is polluted with foreign edges, and guard some transitions with opaque predicates so a static tool cannot tell live edges from dead ones. Each pushes the attacker off static recovery and toward dynamic tracing, which is per-build and cannot be amortized across releases.

Flattening is the conceptual bridge to virtualization: a flattened function is a program interpreting a list of transitions. Push that one step further, make the cases generic operations and feed them encoded instructions, and you have built a virtual machine. That is the next module.

Frequently asked questions

What is a dispatcher in obfuscation?

The loop-plus-switch construct at the heart of a flattened function: it reads a state variable, jumps to the matching case, executes it, and updates the state. All control flow funnels through it, which is what removes the readable shape of the original code.

Can control flow flattening be reversed?

The original source layout is gone, but the execution order can be recovered: dynamically by tracing states in one instrumented run, or statically by tools that model common dispatcher patterns. Hardened variants (computed states, merged functions, opaque transitions) exist to make both recoveries slower and per-build.

How much overhead does flattening add?

Each block transition costs a loop iteration and a switch dispatch instead of falling through, so hot inner loops feel it most. Sensible tools flatten selectively or let heavier settings opt in, which is one reason strength tiers exist.

Keep learning

  • Opaque Predicates: Branches Only the Author Can Trust
  • What Is Bytecode Virtualization?
  • How a Stack Machine Runs Bytecode
  • How Lua Obfuscation Actually Works
  • JavaScript Obfuscator
  • Java & JAR Obfuscator

All lessons