Windows uses Structured Exception Handlers (SEH) that can be registered for the process when exceptions are encountered and help the process recover from exceptions when they happen, when possible. Per thread, the Thread Environment Block (TEB) structure keeps track of a linked list of registered exceptions handlers. In WinDbg, we can inspect them as such:
dt nt!_TEB
ntdll!_TEB
+0x000 NtTib : _NT_TIB
dt _NT_TIB
ntdll!_NT_TIB
+0x000 ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD
dt _EXCEPTION_REGISTRATION_RECORD
typedef EXCEPTION_DISPOSITION _except_handler (*PEXCEPTION_ROUTINE) (
IN PEXCEPTION_RECORD ExceptionRecord,
IN VOID EstablisherFrame,
IN OUT PCONTEXT ContextRecord,
IN OUT PDISPATCHER_CONTEXT DispatcherContext
);
We can also invoke the following in WinDbg to understand the exception handler chain currently registered:
!exchain
SEH corruption
For the x86 architecture, SEHs are hosted at the bottom of the stack and,
given a large enough buffer overflow, it’s possible for us to corrupt the
contents of SEHs on the stack. When an exception handler is called, the
EstablisherFrame parameter is passed as the second argument to our
registered exception routine. If our exception routine can be corrupted to some
ROP gadget like POP R32; POP R32; RET
, then we’ll pop the return address
from the stack, pop the first argument from the stack, and then return into the
instructions pointed to by the EstablisherFrame argument - which just so
happens to be our shellcode most of the time.
Commonly in SEH overflow code execution, we’ll need to do some island hopping to
avoid treating the address pointing to our POP R32; POP R32; RET
as
instructions to be executed. The following payload example in Python
delivers an overflow that overwrites the exception handler routine of a SEH
frame to a POP R32; POP R32; RET
ROP gadget. We then begin to execute the
beginning of the buffer which requires us to execute some NOP instructions
and a relative jump of 4 bytes, jumping over our ROP gadget. Finally, we add a
constant value to sp
to move the stack pointer to our shellcode and execute
jmp esp
to begin shellcode execution:
payload = flat(
[
cyclic(cyclic_find("gaab")),
0x06EB9090,
Gadgets.pop_eax_pop_ebx_ret,
b"\x90" * 2,
b"\x66\x81\xc4\x30\x08", # add sp, 0x830
b"\xff\xe4", # jmp esp
]
)
Stack canaries
Overflowing the SEH is a useful technique if the target maintains stack canaries. When a stack canary value is invalid, exception handlers will be invoked and, if we can corrupt enough of the stack to target the SEH tables, we can still gain code execution despite corrupting the canary.
SafeSEH
SafeSEH
is a mitigation to prevent code execution through the corruption of the SEH by
checking the exception handlers registered at runtime before they are executed.
With the /SAFESEH
compiler parameter, the linker produces a program image that
maintains a table of the image’s safe exception handlers.