SEH Overwrites: Hijacking Windows Exceptions
Windows keeps a linked list of exception handlers on the stack. Overflow into one, point it at a pop-pop-ret, trigger a fault, and control is yours — the technique Windows made famous.
This is the first walkthrough in the Windows Exploitation area. Windows shares the memory-corruption fundamentals of the Linux guides but has its own signature technique: overwriting a Structured Exception Handler to hijack control when a program faults.
Practise on a program you compiled, in a disposable Windows VM you can revert. Tools: a debugger such as WinDbg or x32dbg with the
mona.pyplugin. See the lab rules.
SEH is a linked list on the stack
On 32-bit Windows, exception handling uses a chain of records stored on the stack. Each record is two pointers:
current SEH record{ next, handler }next SEH record{ next, handler }final record0xFFFFFFFF terminatorWhen an exception occurs, the dispatcher walks the chain calling each handler. Because the records sit on the stack above local buffers, a stack overflow can overwrite a record's next and handler fields.
The classic overwrite
Overflowing far enough overwrites a SEH record. If we also cause an exception (easy — the overflow usually corrupts enough to trigger one), the dispatcher calls our overwritten handler. But we cannot just point handler at our shellcode directly, because at that moment registers do not point at it. The reliable trick uses the dispatcher's own calling convention:
- ◀ rsp
buffer overflow…fills the local buffer nSEH: jmp +6 (short jump)dispatcher returns here after pop-pop-rethandler: addr of pop pop retdispatcher calls this on the exceptionshellcodecode
The flow:
- The overflow sets
nSEH(next record) to a short jump-forward andhandlerto the address of apop pop retgadget in a loaded module. - The overflow corrupts the stack enough to raise an exception.
- The dispatcher calls
handler→ thepop pop retexecutes. pop pop retdiscards two slots and returns intonSEH— our short jump.- The short jump hops over the handler field into the shellcode.
Finding the pieces with mona
mona.py in the debugger automates the two hard parts — the offset to the SEH record and a pop pop ret from a module without SafeSEH:
!mona findmsp # locate the SEH record offset from a cyclic pattern
!mona seh # list pop-pop-ret gadgets in non-SafeSEH modules
Choose a pop pop ret whose address contains no bad bytes (e.g. no \x00, \x0a), set nSEH = \xeb\x06 (short jmp +8) plus padding, set handler to that gadget, and place your shellcode after it.
# exploit.py — 32-bit SEH overwrite (lab target)
import struct
offset = 4061 # from !mona findmsp
ppr = 0x10015a7b # pop pop ret, non-SafeSEH module
nseh = b"\xeb\x06\x90\x90" # jmp +6 then nops
payload = b"A" * offset
payload += nseh
payload += struct.pack("<I", ppr)
payload += b"\x90" * 16 + shellcode # NOP sled + shellcode
open("poc.txt","wb").write(payload)
Turn the mitigations back on
The classic SEH overwrite depends on the handler being unvalidated and the chain being untracked. Two mitigations end it:
| Mitigation | Effect on the SEH overwrite |
|---|---|
| SafeSEH | The dispatcher checks the handler against a linker-built table of valid handlers; an arbitrary pop pop ret is rejected |
| SEHOP | The dispatcher validates the integrity of the whole chain at runtime; a corrupted chain is detected |
| DEP | Even with control, stack shellcode will not execute — you need ROP |
| ASLR | The pop pop ret module address randomizes; you need a non-ASLR module or a leak |
Both SafeSEH and SEHOP are covered in this area's third guide.
What this teaches a defender
- Compile with
/SAFESEHand enable SEHOP. These are the direct answers to this technique and cost nothing at runtime. The reason the exploit above works is a module built without them. - The stack is exception metadata too. SEH overwrites are a reminder that control-flow data on the stack is not just the return address. Bound your copies; the root cause is the same overflow.
- Legacy 32-bit software is the risk. Modern 64-bit builds use a different, table-based exception model not stored on the stack. The exposure is old and third-party 32-bit binaries — inventory and rebuild or sandbox them.
Key takeaways
- 32-bit Windows stores exception handlers as a linked list on the stack, reachable by a stack overflow.
- Overwrite
nSEHwith a short jump andhandlerwith apop pop ret; the exception dispatcher then lands execution in your shellcode. mona.pyfinds the offset and a suitablepop pop retin a non-SafeSEH module.- SafeSEH and SEHOP defeat the classic technique; DEP and ASLR force ROP and a leak on top.
Next: bypassing DEP on Windows with ROP, calling VirtualProtect to make your shellcode executable.