Spaghetti Code (Jump Chaining)
Dense chains of unconditional jumps shatter a function into scattered fragments, defeating linear flow and inflating the control-flow graph.
Spaghetti code, or jump chaining, takes a straight-line sequence of instructions
and splatters it across the address space, stitching the fragments back together
with a dense web of unconditional jmp instructions. Each real instruction (or
small group of them) is followed by a jmp to the next fragment, which lives
somewhere else entirely — often interleaved with junk bytes or the fragments of
other functions. The program still executes the same logical sequence, but the
physical layout no longer matches the execution order.
This wrecks the linear-sweep assumption that the instruction at address N+len
is the next one to run. After each jmp the bytes that physically follow are
dead — frequently deliberate junk seeded to derail the sweep — so a flat
disassembler decodes garbage between every fragment. Recursive traversal fares
better but pays a heavy price: the control-flow graph balloons into hundreds of
two-instruction basic blocks linked by single edges, and decompilers either
time out or emit an unreadable goto thicket. Packers such as VMProtect and
ASProtect apply it heavily, and droppers like Emotet have used hand-rolled
jump chains in their unpacking stubs.
How it works
A simple four-instruction routine is fragmented so that no two consecutive
operations are adjacent in memory; junk filler is placed in the gaps the jmps
skip over:
; ---- Logical routine (what actually runs) ----
; mov eax, [ebp-4] / add eax, 1 / mov [ebp-4], eax / ret
; ---- Physical layout (what the disassembler sees) ----
00401000: 8B 45 FC mov eax, [ebp-4]
00401003: EB 12 jmp short 0x00401017 ; -> fragment 2
00401005: E8 1F 8B 44 90 call ... ; <- JUNK (never executed, derails sweep)
0040100A: ... (more junk decoded forward by linear sweep)
00401017: 83 C0 01 add eax, 1
0040101A: EB E4 jmp short 0x00401000... ; -> fragment 3 (backward)
; really targets 0x00401005-region label; shown as chain hop
00401005-relocated:
0040102B: 89 45 FC mov [ebp-4], eax
0040102E: EB 05 jmp short 0x00401035 ; -> fragment 4
00401035: C3 retLinear sweep follows 0x00401000 then decodes the junk at 0x00401005
(E8 1F 8B 44 90 becomes a phantom call) instead of jumping to the real
fragment at 0x00401017, immediately desynchronising. Each EB xx short jump
is a legitimate, correctly-encoded branch — the obfuscation is entirely in the
scattering and ordering, not in any malformed instruction. The recursive
engine recovers the logic but produces one basic block per fragment, turning a
4-instruction function into a graph of single-statement nodes wired by a tangle
of edges.
Detection & analysis
Static analysis:
- The signature is an abnormally high ratio of unconditional
jmpinstructions to real work — basic blocks of one or two instructions each ending in ajmp, with many backward and short-range branches threading through the function. - In IDA/Ghidra the graph view explodes into a dense mesh; look for chains where
every block's sole successor is reached by a
jmpand the fall-through bytes are flagged as junk or never referenced. - A CFG-normalisation pass (or a deobfuscation plugin) can re-linearise the
fragments: follow each
jmpedge, concatenate the targets in execution order, and drop the unreferenced filler to recover the original straight-line body.
Dynamic analysis:
- A single-step or instruction trace records the true execution order regardless of layout; replaying the trace lets you reassemble the fragments back into a contiguous, readable routine.
- Code-coverage tools (DynamoRIO, Intel PT) mark only the executed fragment starts, instantly separating live fragments from the dead junk in the gaps.
Detection rule hint:
Flag functions whose unconditional-jmp density exceeds a threshold (e.g. more
than ~30% of instructions are jmp, or the average basic block is under three
instructions) combined with a high count of branches whose fall-through bytes
are never cross-referenced. That profile — many tiny blocks chained almost
exclusively by jmp with dead filler between them — is characteristic of
jump-chaining obfuscation and rare in compiler-generated code.