Obfuscated Jump Tables
Encoded or computed switch tables hide branch targets behind arithmetic, defeating automatic control-flow recovery and leaving dead-end indirect jumps.
Compilers turn dense switch statements into jump tables: an index scales into
an array of code pointers, and a single jmp [table + idx*4] dispatches to the
selected case. Disassemblers lean on the recognisable shape of this idiom — a
bounds check, a scaled memory operand, a contiguous table of in-section
addresses — to recover every branch target and reconnect the control-flow graph.
Obfuscated jump tables break each of those cues so the recovery silently fails.
The trick is to make the table entries not be plain addresses. Malware stores
deltas, XOR-encoded pointers, or base-relative offsets that are only resolved at
runtime by arithmetic the disassembler will not execute. The final jmp reg
then targets a value the static analyser cannot compute, so it gives up: the
indirect branch becomes a dead end, every case body is left as unreferenced
bytes, and the function appears to terminate at the dispatch. Packers like
VMProtect and droppers such as Emotet use this to fragment whole functions into
islands the auto-analysis never links.
How it works
A clean jump table dispatches through raw pointers; the obfuscated version stores encoded entries and decodes the chosen one before jumping, so the static table holds no usable address:
; --- Recognisable form a disassembler can resolve ---
00: 3C 04 cmp al, 4 ; bounds check on the index
02: 77 13 ja default
04: FF 24 85 40 70 40 00 jmp [tbl + eax*4] ; tbl holds plain code addresses
; tbl: dd off_401000, off_401040, ... ; <- recovered as 5 targets
; --- Obfuscated form: entries are XOR-encoded, decoded at runtime ---
00: 8B 04 85 40 70 40 00 mov eax, [tbl + eax*4] ; eax = ENCODED entry
07: 35 5A A5 5A A5 xor eax, 0xA55AA55A ; runtime-only decode key
0C: FF E0 jmp eax ; target unknown statically
; tbl: dd 0xA55AD55A, 0xA55A957A, ... ; <- looks like data, not addrsThe disassembler sees jmp eax with eax derived from a memory load and an
xor it does not constant-fold, so it cannot enumerate the destinations. The
table bytes XOR-decode to real addresses (0xA55AD55A ^ 0xA55AA55A = 0x401000)
only when the CPU runs the stub, leaving the case bodies as orphaned code.
A base-relative variant stores 32-bit deltas and adds an image base recovered at load time, so the same table relocates anywhere and still defeats analysis:
; Bytes: 8B 1C 81 03 DF FF E3
00: 8B 1C 81 mov ebx, [ecx + eax*4] ; ebx = signed delta from table
03: 03 DF add ebx, edi ; edi = runtime base of code section
05: FF E3 jmp ebx ; absolute target = base + delta
; tbl: dd 0x00000000, 0x00000040, ... ; deltas, not addressesBecause the table contains offsets relative to a register (edi) the
disassembler never resolves, the jmp ebx is unbounded and the switch arms stay
disconnected from the function.
Detection & analysis
Static analysis:
- Look for an indirect
jmp reg/jmp [mem]preceded by a scaled load ([base + idx*4]) plus anxor,add, orleaagainst the loaded value — the arithmetic between load and jump is the decode step. - Treat a contiguous array of section-relative or high-entropy 32-bit words sat just before the dispatch as a candidate encoded table; apply the inline decode key to each entry to materialise the real case addresses.
- In IDA/Ghidra, manually define the resolved targets as code and add the missing xrefs so the orphaned case bodies reattach to the function graph.
Dynamic analysis:
- Set a breakpoint on the indirect
jmpand log the target register across many inputs; each observed value is one real case address to feed back into the static listing. - Emulators (Unicorn) or a coverage trace (Intel PT, DynamoRIO) execute the decode arithmetic and reveal every reached arm, reconstructing the full table.
Detection rule hint:
Flag a scaled-index memory load whose result flows through an xor/add/lea
into an indirect jmp within the same basic block, especially when the referenced
table is a run of equal-stride 32-bit words that do not point into any executable
section as-is. Compilers emit jump tables of direct in-section addresses; an
intervening runtime decode of each entry is a high-confidence obfuscation marker.