Understanding and analyzing control flow obfuscation in iOS applications
Control flow flattening is a powerful obfuscation technique that transforms the natural structure of code into a state machine-like representation, making the original program logic extremely difficult to understand.
func calculateDiscount(price: Double, memberLevel: Int) -> Double { var state = 0 var discount = 0.0 var result = 0.0 while true { switch state { case 0: // Check member level >= 3 if memberLevel >= 3 { state = 1 } else { state = 2 } case 1: // Set premium discount discount = 0.20 state = 4 case 2: // Check member level == 2 if memberLevel == 2 { state = 3 } else { state = 5 } case 3: // Set standard discount discount = 0.10 state = 4 case 4: // Calculate final price result = price * (1.0 - discount) state = 6 case 5: // Set basic discount discount = 0.05 state = 4 case 6: // Exit return result default: return 0.0 } }}
The flattened version has the same functionality but is significantly harder to understand at a glance. The natural if-else structure is completely obscured.
Adding conditions that always evaluate to true or false, but aren’t obviously so:
// Always true, but not obviousif (x * x) >= 0 { state = nextState}// Always false for integersif (x * x) < 0 { state = dummyState // Never executed}
Using non-sequential or encoded state values:
// Instead of 0, 1, 2, 3...case 0x4A3B: // Block 1 state = 0x7F2Ecase 0x7F2E: // Block 2 state = 0x1C94
Nesting multiple switch statements:
while true { switch dispatcher { case 0: switch state { case 0: /* ... */ case 1: /* ... */ } case 1: switch state { case 2: /* ... */ case 3: /* ... */ } }}
Computing next state instead of hardcoding:
// Next state is computed, not a constantstate = (state ^ 0x5A) + memberLevel * 7
; Typical flattened code in ARM64.loop: ldr w8, [sp, #state] ; Load state variable cmp w8, #0 ; Compare with case value b.ne .case1 ; Jump if not equal.case0: ; Block 0 code mov w8, #1 ; Next state str w8, [sp, #state] ; Store state b .loop ; Back to dispatcher.case1: cmp w8, #1 b.ne .case2 ; Block 1 code ...
Control Flow Graph
Star topology: All basic blocks connect back to a central dispatcher
Flat structure: No natural hierarchical nesting
High indegree: Dispatcher block has edges from all other blocks
Low cohesion: Unrelated code blocks at same level
Code Metrics
High cyclomatic complexity: Many paths through code
Deep nesting: Multiple levels of switches or computed jumps
Long functions: Many lines with repetitive structure
Low readability: Decompiled code is difficult to understand
# Extract the IPA fileunzip ControlFlowFlattening.ipa -d ControlFlowFlattening# Navigate to the app bundlecd ControlFlowFlattening/Payload/*.app# Identify the main binaryls -la
Don’t dive into details immediately. First understand the overall structure and identify the dispatcher pattern.
2
Focus on Critical Paths
Not all code paths are equally important. Identify and analyze paths that handle sensitive operations first.
3
Use Dynamic Analysis
Static analysis alone is insufficient. Combine it with runtime tracing to validate your understanding.
4
Document State Transitions
Keep a detailed map of state values, their corresponding blocks, and transition conditions.
5
Leverage Automation
Use scripts and tools to automate repetitive tasks like state tracking and path enumeration.
Deobfuscating control flow flattening is time-intensive. Budget adequate time and consider whether full deobfuscation is necessary for your analysis goals.