SEH overwrite

SEH overwrite

TL;DR: Stack-based overflows on 32-bit Windows can clobber the on-stack EXCEPTION_REGISTRATION_RECORD chain; when the overflow triggers an exception, the dispatcher follows the attacker-controlled handler pointer.

What it is

Structured Exception Handling on 32-bit x86 Windows stores a singly linked list of EXCEPTION_REGISTRATION_RECORD { Next, Handler } on the stack, with fs:[0] (the TIB) pointing at the most recent entry. When an exception fires, ntdll!RtlDispatchException walks the chain calling each Handler. A linear stack overflow that reaches past local variables overwrites first the Next pointer and then the Handler pointer of one of these records — and any exception thrown thereafter lands at the attacker’s address.

Preconditions / where it applies

  • 32-bit Windows process (x86_64 uses table-based PDATA SEH and is not vulnerable to this technique)
  • Stack-overflow primitive that extends past locals into a saved exception record
  • Module providing the pivot must not be SafeSEH-protected (see safeseh-bypass) and not in the SEHOP chain validation
  • An exception is reachable after the overflow — many overflows naturally trigger one when corrupted frame pointers cause an access violation

Technique

Classic three-step layout:

1
2
3
4
5
[ junk filling locals ...                ]
[ ...                                    ]
[ nseh  = short jmp 0x06                 ]  ; next-SEH overwritten with a relative jump
[ seh   = &(pop pop ret) gadget          ]  ; handler overwritten with a ppr in non-SafeSEH module
[ shellcode ...                          ]

How the chain executes:

  1. Overflow corrupts nSEH and SEH pointers on stack
  2. The corrupted frame triggers an access violation
  3. Dispatcher calls Handler(record, ...) — attacker’s address: a pop pop ret
  4. The two pops discard handler args; ret pops the next stack qword which is record — i.e. nSEH
  5. CPU executes nSEH as code: a jmp $+0x06 skipping over SEH itself
  6. Execution lands in the attacker’s shellcode placed after SEH

mona.py automates pointer hunting:

1
!mona seh -m non_safeseh_dll.dll -cp nonull

For larger payloads, combine with an egghunter when the post-SEH buffer is too small.

Detection and defence

  • SafeSEH (safeseh-bypass) — linker-emitted table of valid handlers; dispatcher rejects unknown ones. All modern DLLs ship with SafeSEH; a single non-compliant DLL in-proc breaks this
  • SEHOP — runtime validation of the SEH chain integrity (default Server, off-by-default Client). Confirms the chain still terminates at ntdll!FinalExceptionHandler
  • Stack cookies (/GS) — placed before saved EBP/return address; trips before the overflow reaches SEH on properly compiled code
  • 64-bit ports kill the technique entirely (table-based SEH lives in PE .pdata/.xdata, not on the stack)

References

Related: safeseh-bypass, stack-buffer-overflow, egghunters, mona-py