SafeSEH bypass
TL;DR: SafeSEH validates SEH handler addresses against a per-module list at register time; bypass by sourcing the handler address from a module that opted out, or from non-image memory the validator allows.
What it is
SafeSEH is a linker-level mitigation (/SAFESEH) that emits an IMAGE_LOAD_CONFIG_DIRECTORY table listing every legitimate exception-handler RVA for a module. At dispatch, _except_handler4/RtlIsValidHandler checks the SEH chain pointer’s target against those lists. An overwritten handler that does not point inside a registered list is refused and the process terminates. The bypass is to redirect to code residing in a module that ships without /SAFESEH (no table → validator falls back to allowing any address inside its executable section), or to a region outside any image where some allow-list rules apply.
Preconditions / where it applies
- 32-bit Windows (SEH overwrite is a 32-bit-specific technique; 64-bit uses table-based unwinding).
- An seh-overwrite primitive — a stack buffer overflow that reaches the SEH record.
- At least one loaded module compiled without
/SAFESEH(common in older third-party DLLs, custom EXEs). - Optional: SEHOP not enforced (Windows 10+ enables SEHOP by default for many processes).
Technique
1. Identify a non-SafeSEH module.
1
!mona modules
Look for modules where the SafeSEH column reads False. Older runtime DLLs (msvcr71.dll, vendor plug-ins) often qualify.
2. Find a POP-POP-RET in that module. The handler address you overwrite into the SEH record must point to a pop r32; pop r32; ret so that on dispatch, two SEH-frame fields are popped and execution returns to the address you placed at the SEH Next field. !mona seh -m foo.dll enumerates candidates.
3. Lay out the overflow.
1
2
3
4
[ A * offset_to_nseh ]
[ short jmp + nops ] <-- nSEH field (becomes the EIP target after pop-pop-ret)
[ pop-pop-ret addr ] <-- SEH handler (from non-SafeSEH module)
[ NOP sled + shellcode ]
On the exception, dispatcher loads SEH handler → pop-pop-ret → returns into nSEH → short jmp into shellcode.
4. SEHOP layer. Structured Exception Handling Overwrite Protection walks the SEH chain at dispatch and ensures it terminates at ntdll!_FinalExceptionHandler. If SEHOP is on, a smashed chain is detected. Bypass: forge the chain so the final record points at the legitimate terminator; requires precise layout and stack knowledge — much harder.
5. ASLR caveat. The non-SafeSEH module must not be ASLR-randomised, or you need a leak first; many legacy modules are non-ASLR.
Detection and defence
- Build everything with
/SAFESEHand/DYNAMICBASE; enforce process-wide ASLR (MitigationPolicy_ForceASLR). - Enable SEHOP for all processes via Image File Execution Options or EMET / Exploit Protection.
- Migrate 32-bit targets to 64-bit where SEH overwrite no longer applies (table-based EH).
- WinDbg
!exchainafter a crash shows whether the chain looks corrupted before dispatch.
References
- Microsoft: SafeSEH overview — linker doc
- Corelan SEH tutorial — full walk-through
- HackTricks — exploiting SEH — modern recap
See also: seh-overwrite, mona-py, stack-buffer-overflow.