SEH overwrite with SafeSEH and SEHOP both enabled
svcstore.exe is a 32-bit Windows Server 2008 R2 service. A 256-byte stack buffer lives in a function compiled without /GS out of a legacy static library, and that function has a __try/__except block. The exception registration record is laid out directly above the buffer: the next-record field is at offset 256 and the handler field at offset 260, while the saved return address is at offset 524. The transport refuses any request longer than 400 bytes, so the return address is out of reach but the SEH record is not. SafeSEH is enforced for the executable and all system DLLs, but the process also loads legacy.dll, which has no SafeSEH table and is linked /DYNAMICBASE:NO, sitting at 0x6B400000. DEP is enabled process-wide. SEHOP is enabled for the system, and you have confirmed the service does not carry an IFEO opt-out. You have a working debugger session and full control of the bytes up to the 400-byte cap.
1. Explain why the textbook SEH overwrite (short jump in the next-record field, handler pointing at a pop-pop-ret) fails on this target, and what survives both mitigations.
Read the worked answer
The textbook technique writes a short jump into the next-record field and a pop pop ret address into the handler field. When the exception dispatches, the handler runs, pops the return address and the exception record pointer, and returns to the registration frame's own address, which is the next-record field, where the short jump takes over.
SEHOP kills this because it walks the chain before dispatching. The next-record field is now an instruction, not a pointer, so the walk leaves the stack and the chain never terminates at ntdll's real final handler. The dispatch is refused and the process is terminated with a stack-buffer-overrun status instead of an access violation. SafeSEH is irrelevant at that point, because SEHOP fires first.
What survives is modifying only the handler field and leaving the chain geometry intact. The next-record pointer still points at the genuine next record, the chain still terminates at the real final handler, and SEHOP's walk passes. SafeSEH then checks the handler address against the module tables, and legacy.dll has no table, so any address inside its 0x6B400000 range is accepted.
The remaining problem is that with the next-record field left intact, the pop pop ret return lands on a stack address containing a valid pointer rather than your jump. So the handler must be a gadget that pivots the stack into your buffer, and the buffer must contain a ROP chain, because DEP blocks executing anything on the stack.
2. Design the pivot and the chain that follows it, given the buffer is 256 bytes and DEP is on.
Read the worked answer
At handler entry the stack holds the dispatcher's return address followed by the exception record, registration frame, context record and dispatcher context. The registration frame is a fixed distance above the buffer, and that distance does not vary between runs because the function's frame is fixed and nothing in it uses alloca. So measure the delta from ESP at handler entry to the start of your buffer once, in the debugger, and hardcode it.
Pick a gadget in legacy.dll of the form add esp, <delta>; ret, so the return lands on the first dword of your chain. Avoid pop pop ret plus a jump because there is no jump to make. A leave; ret is only useful if EBP has been reloaded with something you control, which it has not.
The chain itself only needs to do one thing: make the stack executable, then jump.
- Push four arguments for VirtualProtect: an address computed from ESP rather than a guessed stack address, a size covering the current stack page and a little more, 0x40 for PAGE_EXECUTE_READWRITE, and a writable scratch dword.
- Calling VirtualProtect from a rebased module is the address problem again. legacy.dll is not rebased, so if it imports VirtualProtect, use its IAT slot with a
call dword ptr [slot]gadget. Verify the slot is not delay-loaded. - Remember the
ret 16on a stdcall callee and pad accordingly. - End the chain with the address of the payload that sits after it in the buffer, or with a
jmp espif you kept the payload short.
256 bytes is enough for a chain and a compact payload, but not for a chain, an encoder and a full payload, so plan to reuse space rather than layering them.
3. Before investing in the exploit, how would you verify that SEHOP is genuinely active for this process, and what would change if it were not?
Read the worked answer
Do not trust the operating system default. SEHOP has per-process opt-outs, and they are set in the registry under Image File Execution Options for the image name, which is easy to miss because the default is on and the override is silent.
The empirical test is the good one. Force a controlled exception with a deliberately corrupted next-record pointer and observe the termination. If SEHOP is active, the process dies with a stack-buffer-overrun status (0xC0000409) and the debugger shows no dispatch to your handler at all. If SEHOP is absent, the same corruption produces a normal dispatch into your handler, and you will see the handler address on the stack during the second-chance pass. That single experiment distinguishes the cases conclusively and takes minutes.
Complement it by enumerating the process mitigations in the debugger, which tells you the state of the process rather than the state of the machine.
If SEHOP turned out to be inactive, the exploit gets substantially easier and shorter: the short jump in the next-record field becomes viable again, you no longer need a stack pivot gadget, and the handler only has to be a pop pop ret in legacy.dll. The chain can collapse to whatever the jump lands on. Build the SEHOP-resistant version first anyway, because it is the one that keeps working if the target is patched or moved to a host with defaults intact.