Skip to content

SEH-Based Anti-Debugging

Malware raises an exception routed through its own SEH handler; if a debugger swallows it first the handler never runs, revealing the analysis.

Windows Structured Exception Handling (SEH) lets a program install a callback that runs when a CPU exception (e.g. INT3, INT 2D, an access violation, or divide-by-zero) is raised. When a process runs under a debugger, the debugger receives a first-chance exception notification before the application's handler does. If the analyst simply passes the exception on — or, worse, the debugger is configured to consume it — control never reaches the program's own handler.

SEH-based anti-debugging weaponises this asymmetry. The malware installs a handler whose job is to fix up state and continue execution along the real code path. When no debugger is present, the exception propagates to that handler, which runs, repairs the instruction pointer, and proceeds. When a debugger swallows the exception, the handler is skipped, execution either dies or follows a decoy branch, and the difference betrays the debugger.

Because the legitimate execution flow requires the exception to fire, the technique is also self-defending: an analyst who blindly tells the debugger to ignore the exception breaks the program, and one who passes it to the app must let the anti-debug handler run.

How it works

A 32-bit thread's SEH chain lives at fs:[0]. The sample pushes a handler onto the chain, raises an exception, and lets the handler advance CONTEXT.Eip past the faulting instruction into the real code:

asm
    ; install a custom SEH frame: fs:[0] -> our handler
    push  offset seh_handler
    push  fs:[0]
    mov   fs:[0], esp

    int   3                 ; raise the exception on purpose
    ; --- if a debugger eats INT3, execution never returns here correctly ---
    jmp   decoy_path        ; reached only when handler did NOT fix EIP

seh_handler:
    ; ExceptionRecord/ContextRecord passed on the stack
    mov   ecx, [esp+0Ch]    ; CONTEXT*
    add   dword ptr [ecx+0B8h], 1   ; CONTEXT.Eip += 1  (skip the INT3)
    xor   eax, eax          ; ExceptionContinueExecution = 0
    ret

The handler is also where the malware plants its real logic — for example decrypting the next stage — so the legitimate path only exists inside the exception callback:

c
EXCEPTION_DISPOSITION __cdecl seh_handler(
    EXCEPTION_RECORD *rec, void *frame, CONTEXT *ctx, void *disp)
{
    ctx->Eip += 1;          // step over the planted INT3
    decrypt_next_stage();   // real work happens only if we got here
    return ExceptionContinueExecution;
}

Detection & analysis

Static analysis:

  • Look for direct manipulation of fs:[0] (32-bit) or registration of a handler via RtlAddVectoredExceptionHandler followed immediately by a deliberate fault: int 3, int 2dh, ud2, a divide-by-zero, or a write to a guard page.
  • A handler that modifies CONTEXT.Eip/Rip to resume past the faulting instruction — rather than logging or cleaning up — is the signature of control-flow-by-exception.

Dynamic analysis:

  • Configure the debugger to pass first-chance exceptions to the application so the SEH handler actually runs. In x64dbg/WinDbg this means not breaking on, or explicitly continuing, the INT3/access violation so the program receives it.
  • Single-step from the fault into the handler, watch the Eip/Rip fixup, and follow the corrected target to find the real code path; set your next breakpoint there.

Detection rule hint:

Flag a sequence that installs an SEH or vectored handler, then within a few instructions issues a deliberate fault (int 3, int 2dh, ud2, div by zero), where the handler resumes by adjusting the saved instruction pointer — legitimate code does not route its normal control flow through self-inflicted exceptions.

Votes

Comments(0)