Obfuscationadvanced
Control-Flow Flattening
Replacing a function's natural branching with a single dispatcher loop driven by a state variable, destroying the original control-flow graph that decompilers rely on.
Control-flow flattening rewrites a function into a while (state) switch (state)
dispatcher. Every original basic block becomes a case that computes the next
state value, so the decompiler sees one giant loop instead of the real CFG.
Shape
int state = 0x1A2B;
while (state != DONE) {
switch (state) {
case 0x1A2B: /* block A */ state = cond ? 0x77F0 : 0x0C31; break;
case 0x77F0: /* block B */ state = 0x4ED2; break;
/* ... */
}
}In disassembly the whole function collapses into one dispatcher loop driven by a state register that indexes a jump table:
mov dword [rbp-4], 0x1A2B ; state = entry value
.dispatch: ; top of the dispatcher loop
mov eax, dword [rbp-4] ; load current state
cmp eax, 0x4ED2 ; DONE sentinel?
je .exit
; map state -> table index, then indirect jump
lea rcx, [rel jmp_table]
movsxd rdx, dword [rcx+rax*4] ; signed table entry
add rdx, rcx
jmp rdx ; switch(state)
.case_1A2B: ; block A
; ... original block A body ...
test eax, eax
mov edx, 0x0C31 ; next state if false
mov ecx, 0x77F0 ; next state if true
cmovne edx, ecx
mov dword [rbp-4], edx ; store next state
jmp .dispatch
.case_77F0: ; block B
; ... original block B body ...
mov dword [rbp-4], 0x4ED2 ; -> DONE
jmp .dispatch
.exit:Recovering the original flow
- Symbolic execution of the dispatcher recovers the state-transition graph (e.g. D-810, HexRaysDeob, or custom miasm/Triton scripts).
- Identify the state variable feeding the
switch, enumerate reachable case values, then stitch blocks back into a real CFG. - Frequently paired with opaque predicates to make the transitions harder to constant-fold.
Votes