Skip to content

INT 2D Anti-Debugging

The INT 2D kernel-debugging interrupt skews the instruction pointer under a debugger, so a debugger that mishandles it is revealed.

INT 2D is the interrupt the Windows kernel uses for kernel-mode debugger services (DbgPrint/__debugbreak style signalling). When executed in user mode, its behaviour depends on whether a debugger is present, and crucially on how the kernel adjusts the instruction pointer afterwards.

The key quirk: when INT 2D is handled, the kernel treats the byte immediately following the INT 2D opcode as part of the interrupt servicing and advances EIP/RIP by one extra byte. Without a debugger, no exception surfaces to the process and execution simply continues. With a ring-3 debugger attached, an EXCEPTION_BREAKPOINT is raised — but because of the one-byte skew, a debugger that resumes naively lands in the middle of the next instruction, desynchronising the disassembly and often crashing or diverging the trace.

Malware places a one-byte "poison" instruction right after INT 2D. If that byte is silently swallowed (no debugger, or correct handling), the real code runs; if a debugger mis-handles the skew, control derails into a decoy or fault. This makes INT 2D both a debugger-presence check and an active disassembly-confusion trick.

How it works

asm
    ; INT 2D with an EIP-skew trap
    int   2dh
    nop                 ; this byte is "eaten" by the EIP advance...
    ; ...so without a debugger, execution resumes HERE:
    jmp   real_code

    ; A debugger that does not account for the +1 skew resumes one
    ; byte early, decoding the wrong instruction stream:
trap:
    db    0EBh          ; partial opcode that derails a naive debugger

Wrapped in SEH, the sample can turn the resulting exception state into a clean boolean:

c
BOOL DebuggerViaInt2D(void)
{
    BOOL detected = TRUE;
    __try {
        __asm { int 0x2d }   // kernel advances EIP past the next byte
        __asm { nop      }   // swallowed when no debugger mishandles us
        detected = FALSE;    // reached only on a "clean" continuation
    } __except (EXCEPTION_EXECUTE_HANDLER) {
        detected = TRUE;     // debugger surfaced/ mishandled the exception
    }
    return detected;
}

The exact outcome varies across Windows builds and CPU vendors, so malware typically pairs INT 2D with INT 3 and INT 1 probes and treats any anomalous result as "being debugged".

Detection & analysis

Static analysis:

  • Search for the opcode bytes CD 2D (int 2dh), especially when followed by a single filler byte (nop, or a lone byte that the disassembler renders as a truncated instruction) and a jump.
  • The combination of int 2dh inside a __try/SEH frame is a strong anti-debug indicator; it has no legitimate user-mode purpose.

Dynamic analysis:

  • After the INT 2D exception fires in the debugger, manually correct the instruction pointer for the one-byte skew before resuming, then re-synchronise the disassembly on the true next instruction.
  • Alternatively, set a hardware breakpoint past the trap region and let the SEH path run, or patch the CD 2D bytes to 90 90 (nop nop) so the interrupt never fires.

Detection rule hint:

Flag CD 2D opcodes in user-mode code — particularly when wrapped in structured exception handling or immediately preceding a filler byte and a control-flow transfer — as an INT 2D debugger-evasion / disassembly-desync trap.

Votes

Comments(0)