Skip to content

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.

Published on 4 min read

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.py plugin. 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:

The SEH chain — records live on the stack, so an overflow reaches them
current SEH record{ next, handler }
next
next SEH record{ next, handler }
next
final record0xFFFFFFFF terminator
dataterminator

When 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:

SEH overwrite layout — nSEH holds a short jump, handler holds pop-pop-ret
  1. buffer overflow…fills the local buffer
    ◀ rsp
  2. nSEH: jmp +6 (short jump)dispatcher returns here after pop-pop-ret
  3. handler: addr of pop pop retdispatcher calls this on the exception
  4. shellcodecode
paddingpointergadgetvalue

The flow:

  1. The overflow sets nSEH (next record) to a short jump-forward and handler to the address of a pop pop ret gadget in a loaded module.
  2. The overflow corrupts the stack enough to raise an exception.
  3. The dispatcher calls handler → the pop pop ret executes.
  4. pop pop ret discards two slots and returns into nSEH — our short jump.
  5. 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:

MitigationEffect on the SEH overwrite
SafeSEHThe dispatcher checks the handler against a linker-built table of valid handlers; an arbitrary pop pop ret is rejected
SEHOPThe dispatcher validates the integrity of the whole chain at runtime; a corrupted chain is detected
DEPEven with control, stack shellcode will not execute — you need ROP
ASLRThe 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 /SAFESEH and 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 nSEH with a short jump and handler with a pop pop ret; the exception dispatcher then lands execution in your shellcode.
  • mona.py finds the offset and a suitable pop pop ret in 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.

Related guides

0xa000 · Windows Exploitation

Bypassing DEP on Windows with ROP

DEP makes stack shellcode unrunnable, so a Windows ROP chain calls VirtualProtect to mark the shellcode region executable, then jumps to it. Build it with mona, then see ASLR and CFG respond.

0x9000 · Kernel Exploitation

Kernel Exploitation: From a Bug to root

A kernel memory-corruption bug is about privilege, not a shell. The credential model, the commit_creds(prepare_kernel_cred(0)) payload, and returning cleanly to userspace — in a lab VM.

0x9000 · Kernel Exploitation

ret2usr, SMEP and SMAP

The kernel once trusted userspace memory, so exploits just pointed kernel execution at a user payload. SMEP and SMAP ended that — and how kernel ROP works around them.