Jump-to-Same-Target Opaque Predicate
A conditional jump whose taken and fall-through paths reach the same address, planting a junk byte in the gap that derails a linear-sweep disassembler.
This technique combines a degenerate conditional branch with a single garbage
byte. The conditional jump is arranged so that both the taken and the
fall-through edges converge on the same destination — making the branch
semantically a no-op for control flow — but the bytes physically placed in the
gap between the jcc and its target are never executed. The obfuscator drops a
carefully chosen junk byte into that dead gap.
To a human reading the listing the construct looks like a normal conditional, but a disassembler that decodes the fall-through path linearly will consume the junk byte as the start of an instruction, desynchronising from there. Because neither real path ever reaches the junk byte, the program runs correctly while the static listing shows garbage. The "predicate" part is incidental — even an unconditional pair of jumps to the same spot achieves it; the conditional form just looks more natural and survives some recursive analysis.
How it works
; Bytes: 74 03 75 01 E8 <real code at the merge point>
00: 74 03 jz short 0x05 ; if ZF=1 -> target 0x05
02: 75 01 jnz short 0x05 ; if ZF=0 -> target 0x05 (same address!)
04: E8 ; <- JUNK byte. 0xE8 is the opcode for "call rel32"
05: ... ; real code — both branches land herejz/jnz are exhaustive: exactly one of them is taken for any value of ZF, and
both target 0x05, so execution always resumes at 0x05. The byte at 0x04
(0xE8) is unreachable. A recursive disassembler that follows both edges to
0x05 may recover correctly, but a linear sweep decodes straight past the
second jump and hits 0x04: it reads 0xE8 as call rel32 and swallows the
next four bytes — the first real instructions — as a phantom call displacement.
A common refinement uses a single conditional that is provably always-taken so even a recursive engine that prunes the false edge still falls into the trap:
; Bytes: 33 C0 74 01 E9 ...
00: 33 C0 xor eax, eax ; sets ZF = 1, deterministically
02: 74 01 jz short 0x05 ; ALWAYS taken (ZF is 1)
04: E9 ; <- junk: 0xE9 is "jmp rel32", consumes 4 real bytes
05: ... ; real codeHere xor eax,eax guarantees ZF=1, so jz is always taken and the byte at
0x04 is dead — but the linear sweep cannot prove the predicate and decodes the
0xE9 jmp, eating four legitimate bytes.
Detection & analysis
Static analysis:
- Look for adjacent
jz/jnz(or anyjcc/inverse pair) with identical targets — a structurally impossible "both-ways" branch that real compilers never emit. - For the always-taken single form, trace the flag-setting instruction
immediately above the
jcc(xor reg,reg,cmp/testof equal operands,sub reg,reg): if the flag is constant the branch is opaque and the byte after thejccis junk. - In IDA/Ghidra, undefine the byte right after the conditional jump and re-decode starting at the real merge target to clear the desync.
Dynamic analysis:
- Single-step the conditional in a debugger; you will see control reach the merge address every time, and the junk byte is never the start of an executed instruction — confirming it as filler.
Detection rule hint:
Flag any jcc whose fall-through byte is not the start of any
recursively-reachable instruction, especially when (a) a complementary jcc
to the same target precedes it, or (b) the flags consumed by the jcc were set
to a constant by the immediately preceding instruction. This pairing of a
degenerate/always-taken branch with an unreachable opcode-shaped byte is a
strong anti-disassembly signature.