Can an LLM undo control flow flattening?
A cool paper using chain of thought as a trick.
It’s worth reading Analyzing Chain of Thought (CoT) Approaches in Control Flow Code Deobfuscation Tasks!
The problem
Two cheap obfuscation techniques that are quite annoying to reverse.
Opaque predicates wrap real code in a branch that’s always true, like if (x*(x+1) % 2 == 0), and put junk in the else. Your disassembler sees a branch that never fires and can’t tell.
Control flow flattening chops a function into basic blocks and routes them all through a while(1) { switch(state) } dispatcher. The control flow graph goes from readable to one node pointing at everything.
What they did
Rather than fine tune anything, they wrote the reverse engineer’s process into the prompt as five phases: find the dispatcher and state variable, rebuild the state transition graph from the case blocks, topologically sort it back into real control flow, evaluate each opaque predicate and keep only the reachable branch, then delete the dead state variable and tidy up.
Five reasoning models (GPT5, o3, DeepSeek-V2, Qwen-3 MAX, QwQ-32B) against 12 standard C programs obfuscated with Tigress and O-LLVM, at three levels: opaque only, flattening only, and both. They got scored on control flow graph similarity and on whether the recovered program still produced the same output.
Did it work
They say yes. Same models, same benchmarks, only the prompting changes:
Control flow recovered from code hit with both opaque predicates and flattening:
| Model | Zero-shot | With CoT |
|---|---|---|
| GPT5 | 83.6% | 100% |
| DeepSeek-V2 | 82.4% | 89.1% |
| Qwen-3 MAX | 77.5% | 83.2% |
| QwQ-32B | 46.1% | 73.8% |
| o3 | 35.8% | 74.3% |
Averaged out, gpt5 gains about 16% on control flow reconstruction and 20.5% on behaviour preservation just from being told how to think about it.
Their caveats: every model hallucinated to some degree, ranging from forgetting #include <stdlib.h> up to producing clean compilable code that does the wrong thing. Performance also degrades as you add more opaque branches, and degrades faster when the original program’s control flow was already complex, so the ceiling is set by the obfuscator and by what you obfuscated.