Skip to content
Anti-Disassemblyintermediate

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:

asm
; ---- 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                ret

Linear 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 jmp instructions to real work — basic blocks of one or two instructions each ending in a jmp, 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 jmp and 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 jmp edge, 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.

Votes

Comments(0)