Impossible Disassembly
A single byte is part of two valid, both-reachable instructions, so no flat linear listing can be correct — only a path-sensitive view recovers it.
Most anti-disassembly tricks merely confuse one decoding strategy — change the start offset and you recover the truth. Impossible disassembly goes further: it crafts byte sequences in which a single shared byte is legitimately reached as part of two different instructions along two different execution paths. A flat listing assigns each byte to exactly one instruction, so by construction no flat disassembly can represent both readings — any linear or recursive listing is provably wrong for at least one path.
The classic form is the inward-pointing jump: a byte serves both as the operand of a branch and as the opcode of the instruction that branch jumps into. Because both interpretations are exercised at runtime, the byte genuinely "belongs" to two instructions at once. Tools that insist on a one-byte-one-instruction model (virtually all flat disassemblers) cannot render it faithfully; correct analysis requires a graph view that allows overlapping basic blocks.
How it works
The minimal gadget is three bytes that fold into themselves:
; Bytes: EB FF C0
; ---- Path A: decode from offset 0 ----
00: EB FF jmp short 0x01 ; displacement 0xFF -> target = 0x00+2 + (-1) = 0x01
; ---- Path B: decode from offset 1 (the jump target) ----
01: FF C0 inc eax ; the SAME 0xFF byte is now an opcode prefixThe byte 0xFF at offset 1 is the displacement operand of the jmp when read
from offset 0, and the opcode of inc eax (FF C0) when read from offset
1 — the actual branch target. Both decodings execute: the CPU runs jmp short 0x01, lands on offset 1, and runs inc eax. No single flat listing can show
the 0xFF as both an operand byte and an opcode byte, which is what makes the
disassembly impossible rather than merely ambiguous.
A larger, more disruptive instance overlaps a multi-byte instruction so the shared byte derails a long stretch of the sweep:
; Bytes: EB 01 E8 ...
00: EB 01 jmp short 0x03 ; skip one byte
02: E8 ... ; linear sweep reads this as "call rel32" (consumes 4 bytes)
; ; but 0x03 is the real target, so:
03: ... ; real instruction begins here, INSIDE the phantom call's operandThe 0xE8 at offset 2 is reachable in the sweep's wrong reading and the byte at
offset 3 is reachable on the real path — the boundary between operand and opcode
is contested, and no flat assignment is consistent with both.
Detection & analysis
Static analysis:
- Recognise the signature short-jump-into-own-operand sequences (
EB FF C0,EB FF,E8 ... 01skips). When the auto-analyser reports overlapping or re-defined bytes, or refuses to create a function chunk at the target, suspect impossible disassembly. - Use a graph-capable disassembler (IDA graph view, Ghidra's overlapping code-unit handling) and manually create instructions at both offsets; accept that the rendered listing is a projection, not the whole truth.
- Cross-reference the branch target: if it points one or two bytes into the preceding instruction, the bytes are shared by design.
Dynamic analysis:
- Single-step in a debugger and record the actual instruction-start addresses (RIP at each step). The trace gives the ground-truth boundaries the static view cannot, even though those boundaries are mutually inconsistent across the two paths.
- Hardware tracing (Intel PT) captures every taken branch, letting a post- processor reconstruct the real, path-specific instruction stream.
Detection rule hint:
Flag any branch whose target lands within the encoding of the branch
instruction itself or the instruction immediately following, such that the same
byte offset is claimed by two reachable instructions. Sequences matching
EB FF, EB FF C0, or a short jmp/jcc skipping into a multi-byte opcode's
operand region are near-certain impossible-disassembly markers and should be
escalated for manual graph analysis.