Skip to content
Anti-Disassemblyintermediate

Garbage Byte After Conditional Jump

An always-taken conditional jump is followed by a garbage opcode byte that a linear-sweep disassembler wrongly consumes, derailing the listing.

This is the textbook anti-disassembly primitive: set the flags to a known value, take a conditional jump that is therefore always taken, and place a single junk byte immediately after the jump in the fall-through gap that execution never visits. Because real control flow always branches away, the junk byte is dead — but a linear-sweep disassembler decodes straight through and reads the junk byte as the start of a (usually multi-byte) instruction.

The chosen junk byte is the first byte of a long instruction — an opcode like 0xE8 (call rel32, 5 bytes) or 0xE9 (jmp rel32, 5 bytes) — so the sweep greedily swallows the next several legitimate bytes as a phantom operand. The disassembler emerges from the phantom instruction mis-aligned, and every instruction it decodes afterwards is wrong until it happens to resynchronise. One byte of junk can corrupt many lines of the listing.

How it works

asm
; Bytes:  33 C0 74 01 E8 8B 4D 08 ...
00:  33 C0           xor  eax, eax       ; ZF := 1 (always)
02:  74 01           jz   short 0x05     ; ALWAYS taken because ZF is 1
04:  E8              ; <- GARBAGE byte. 0xE8 = "call rel32" opcode
05:  8B 4D 08         mov  ecx, [ebp+8]  ; the REAL next instruction

Execution: xor eax,eax forces ZF=1, so jz short 0x05 is always taken and lands on offset 0x05, the real mov ecx, [ebp+8]. The byte at 0x04 is never reached. But a linear sweep does not evaluate the predicate. After decoding the jz it continues at 0x04, reads 0xE8 as call rel32, and consumes the four real bytes 8B 4D 08 ... as the call's displacement:

asm
; What the linear sweep WRONGLY produces:
02:  74 01           jz   short 0x05
04:  E8 8B 4D 08 ??  call <garbage>      ; ate the real mov + one more byte
09:  ...              ; desynchronised from here on

The five real bytes starting at 0x05 are now buried inside a phantom call, and the listing stays wrong until it realigns. Any flag-setting instruction that fixes ZF works — xor reg,reg, sub reg,reg, cmp al,al, test reg,reg — and the inverse pairing (jnz after a guaranteed ZF=0) is equally common.

Detection & analysis

Static analysis:

  • Spot the pattern: a flag-deterministic instruction (xor reg,reg, sub reg,reg, test/cmp of equal operands) feeding a jcc, with an opcode-shaped byte (E8, E9, 0F, FF) sitting immediately after the jump.
  • If IDA/Ghidra shows a call/jmp with a nonsensical target right after a conditional jump, undefine the byte after the jcc and force a re-decode at the branch target — the listing realigns and the real instructions reappear.
  • The fall-through byte having no incoming code xref while the branch target does is the structural tell.

Dynamic analysis:

  • Single-step the conditional jump: it is taken on every run, and the junk byte is never the address of an executed instruction. A short trace confirms the byte is dead filler.

Detection rule hint:

Flag any jcc whose flags were set to a constant by the immediately preceding instruction (always-taken predicate) and whose fall-through byte is a long- instruction opcode (E8/E9/0F xx/FF) that is not the start of any recursively-reachable instruction. This exact pairing — deterministic predicate plus unreachable opcode-shaped byte — is the canonical garbage-byte signature and almost never appears in compiler output.

Votes

Comments(0)