Skip to main content
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.

What is Control Flow Flattening?

Control flow flattening transforms code by:
1

Breaking Down Control Flow

The original program is divided into basic blocks (sequences of instructions with no branches).
2

Adding a Dispatcher

A central switch statement acts as a dispatcher that determines which block executes next.
3

Flattening the Structure

All blocks are placed at the same nesting level, hiding the original hierarchical structure.
4

Using State Variables

A variable tracks the current state, controlling which block the dispatcher executes.
This technique is also known as control flow obfuscation or dispatcher-based obfuscation.

How It Works

Before Flattening

func calculateDiscount(price: Double, memberLevel: Int) -> Double {
    var discount = 0.0
    
    if memberLevel >= 3 {
        discount = 0.20
    } else if memberLevel == 2 {
        discount = 0.10
    } else {
        discount = 0.05
    }
    
    return price * (1.0 - discount)
}

After Flattening

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.

Advanced Techniques

Adding conditions that always evaluate to true or false, but aren’t obviously so:
// Always true, but not obvious
if (x * x) >= 0 {
    state = nextState
}

// Always false for integers
if (x * x) < 0 {
    state = dummyState  // Never executed
}

Identifying Flattened Code

Look for these patterns when analyzing binaries:
; 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
    ...
  • 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
  • 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

Example Analysis: ControlFlowFlattening.ipa

The example file is located at: /home/daytona/workspace/source/ObfuscatedAppExamples/ControlFlowFlattening.ipa

Extracting the Binary

# Extract the IPA file
unzip ControlFlowFlattening.ipa -d ControlFlowFlattening

# Navigate to the app bundle
cd ControlFlowFlattening/Payload/*.app

# Identify the main binary
ls -la

Analyzing with Hopper/IDA

1

Load the Binary

Open the binary in Hopper Disassembler or IDA Pro.
2

Locate Dispatcher Functions

Look for functions with:
  • Large switch statements
  • Loops at the beginning
  • Many basic blocks at the same level
3

Identify State Variables

Track variables that:
  • Are loaded at the beginning of loops
  • Control switch/branch decisions
  • Are updated at the end of basic blocks
4

Map Control Flow

Create a state transition diagram showing:
  • Each state value
  • Which block executes for each state
  • Conditions for state transitions

Using Frida for Dynamic Analysis

// Hook the obfuscated function to trace state transitions
Interceptor.attach(Module.findExportByName(null, "_obfuscatedFunction"), {
    onEnter: function(args) {
        console.log("[+] Function called");
        
        // Trace state variable changes
        var stateAddr = this.context.sp.add(0x10); // Adjust offset
        
        this.interval = setInterval(function() {
            var state = stateAddr.readU32();
            console.log("    State: " + state);
        }, 10);
    },
    onLeave: function(retval) {
        clearInterval(this.interval);
        console.log("[+] Function returned: " + retval);
    }
});
Recording state transitions during execution can help you understand the original control flow and create a state transition diagram.

Deobfuscation Strategies

Manual Reconstruction

Manually trace execution paths and rebuild the original control flow structure by analyzing state transitions.

Symbolic Execution

Use tools like angr or Triton to symbolically execute the flattened code and extract path constraints.

Pattern Matching

Identify common patterns in the flattened structure and apply automated transformations to simplify.

Dynamic Tracing

Record actual execution paths using Frida or LLDB, then reconstruct the simplified flow.

Automated Tools

D810 is an IDA Pro plugin that attempts to deobfuscate control flow flattening:
# Install D810
git clone https://github.com/eshard/d810
cp -r d810 ~/idapro/plugins/

# Use in IDA: Edit -> Plugins -> D810

Best Practices for Analysis

1

Start with High-Level View

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.

Further Reading

Detection Techniques

Learn systematic approaches to detecting obfuscation patterns.

String Encryption

Explore another common obfuscation technique often used alongside control flow flattening.

Build docs developers (and LLMs) love