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:
; 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
retThe 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:
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 viaRtlAddVectoredExceptionHandlerfollowed 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/Ripto 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 violationso the program receives it. - Single-step from the fault into the handler, watch the
Eip/Ripfixup, 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.