Basic blocks, control flow graphs, CFG construction in IDA Pro and Ghidra, and the obfuscation techniques malware uses to corrupt disassembler output — with practical bypass approaches for each.
Sign in to mark this article as read and track your progress.
A Control Flow Graph (CFG) represents all possible execution paths through a function. Nodes are basic blocks — straight-line sequences of instructions. Edges connect blocks through their jump targets.
The CFG is the primary visual aid for understanding program logic at the assembly level. IDA Pro (Spacebar) and Ghidra (Function Graph) both render CFGs interactively.
A basic block is a maximal sequence of instructions where:
; This is ONE basic block — straight-line execution, one exit
mov eax, [ebp+arg_0]
cmp eax, 0
jz Block_3 ; ← exit — ends the block
Basic blocks are terminated by:
JZ, JNZ, JG, etc.)JMP)RETCALL (in some analysis frameworks)A CALL instruction by itself does not end a basic block in most analyses — the function call returns and execution continues in the same block.
In IDA Pro graph view:
| Edge Colour | Meaning |
|---|---|
| Green | Conditional jump taken (true path) |
| Red | Conditional jump not taken (false path) |
| Blue | Unconditional jump |
Back-edges — edges pointing upward (from a lower block to a higher one) — always indicate loops.
Practical workflow:
1. Press Spacebar to enter graph view
2. Scan for back-edges (upward blue arrows) → these are loops
3. Follow green/red edges from CMP+Jcc pairs → reconstruct if/else
4. Large fan-out from one block → likely switch/jump table
5. Add comments as you decode each block's purpose
6. Rename the function when intent is clear
Malware authors use four techniques to corrupt CFG output:
A conditional jump whose outcome is deterministic at runtime but cannot be proved by static analysis. The disassembler creates edges to both targets — one of which contains garbage bytes.
mov eax, 5
imul eax, eax ; eax = 25
and eax, 1 ; 25 is odd → eax = 1 always
jz fake_target ; ZF=0 always → NEVER taken
; real code here
fake_target:
db 0xFF, 0x15 ; garbage — never decoded
Bypass: In IDA: right-click garbage region → Undefine, then mark as data (D key). Confirm with debugger that the jump is never taken.
x86 instructions have variable length. A JMP into the middle of a multi-byte instruction decodes as a completely different instruction.
addr_00: EB 01 ; JMP addr_02 (skip 1 byte)
addr_01: E8 ; this byte is skipped by the jump
addr_02: B8 01000000 ; MOV EAX, 1 (what CPU actually executes)
; Disassembler decoding linearly sees:
; addr_01: E8 B8 01 00 00 → CALL some_address (WRONG)
Bypass: Use recursive descent disassembly (IDA/Ghidra default), or trace in debugger where CPU resolves correct instruction boundaries.
JMP EAX or CALL [EBX+0x10] with runtime-computed targets defeat static CFG construction entirely — the destination is unknown without execution.
Bypass: Set a breakpoint on the indirect jump in x64dbg. When it fires, read the target register. Manually add a cross-reference in IDA with Ctrl+Alt+K (add code cross-reference).
Malware installs a Structured Exception Handler, then deliberately triggers an exception. The OS dispatches execution to the handler — a hidden code path invisible to linear disassembly.
push handler_address ; install SEH
push fs:[0]
mov fs:[0], esp
xor eax, eax
div eax ; divide by zero → triggers exception
; disassembler thinks this continues linearly
; CPU actually goes to handler_address
Bypass: In x64dbg: View → SEH Chain to find all handler addresses. Set breakpoints on handlers before running. When exception fires, debugger pauses at the hidden code path.
1. Open function in IDA/Ghidra graph view
2. Identify all basic blocks (count nodes)
3. Trace all back-edges → map loops
4. Trace all conditional edges → map if/else/switch
5. Identify any indirect jumps → note for dynamic confirmation
6. Check for opaque predicates → verify with debugger
7. Annotate each block with its purpose in plain English
8. Rename function to reflect discovered behaviour
Annotating and renaming is not optional — it is the primary deliverable of code analysis. Comments and renamed functions become the analysis report.