OSEE study materials
OSEE study resources

Null-free egg hunter in a 76-byte stager

OSEE · Put your knowledge to work in context.

All case studies

Null-free egg hunter in a 76-byte stager

A 32-bit Windows 7 chat client overflows a 512-byte stack buffer while copying an incoming message body with strcpy, so a 0x00 byte terminates the copy. The offset to the saved return address is 524. The server truncates any single message body at 600 bytes, which leaves 76 bytes of controllable space after the return address and nothing more. DEP is enabled for the client and all system libraries, ASLR is on everywhere, and there is no non-ASLR module in the process. The client keeps up to 200 received messages alive simultaneously in heap buffers whose addresses you do not know, and it does not free them until the connection closes. You can send as many messages as you like before the malformed one that triggers the overflow, and the client will happily accept bodies containing arbitrary non-zero bytes.

1. Show the arithmetic that rules out simply putting a decoder plus payload in the stager, and explain what the 76 bytes must do instead.

Read the worked answer

Start with what a decoded payload costs. A minimal Windows payload that does anything observable (spawn a process, connect back) is roughly 200 bytes before encoding, and an encoder that is itself null-free typically expands it by 20 to 50 percent. Even an extremely tight payload does not fit in 76 bytes, and the decoder that would expand it needs 20 to 40 bytes of its own. The sum is several times the budget.

DEP closes the obvious escape. The 76 bytes sit on the stack, and the stack is not executable, so the stager cannot simply be shellcode even if it fit.

What the 76 bytes can do is search. The client has 200 live heap buffers of attacker-influenced content, and you control every one of them. If each carries a distinctive marker followed by an encoded payload, a small hunter in the stager can walk the address space, find a marker, and hand control to a decoder located right behind it. That keeps the stager to a scan loop plus a jump and puts everything bulky in the heap.

The constraint that shapes the whole design is strcpy: a single 0x00 anywhere in the stager truncates the copy and the return address is never reached. So the hunter must be written null-free, which is a meaningfully harder problem than writing a hunter, and is where most of the engineering effort in this exploit actually goes.

2. What properties must the marker bytes satisfy, and how do you stop the hunter from matching its own search constants?

Read the worked answer

The marker needs four properties at once.

It must be null-free, because it is delivered through the same strcpy path and an embedded 0x00 would truncate the message body before the payload. In practice that means a marker made of bytes chosen deliberately from a safe set, not a value like 0x00414141.

It must be long enough to be unique in a 2 GB address space. Four bytes is roughly a one-in-four-billion collision per dword comparison, but you are comparing at every byte offset across hundreds of megabytes of mapped memory, and there is a lot of low-entropy data in a Windows process: zeroed pages, repeated structure fields, and your own repeated messages. Use eight bytes, split into two four-byte compares, so a match requires both halves.

It must not appear inside the hunter itself. That is the trap: the hunter contains the marker as an immediate operand, and if the scan ever reaches its own stack frame it will match, jump, and execute garbage. Two ways out: make the compare operands assembled at runtime from arithmetic on other constants rather than stored literally, or place the marker in the search so that the hunter's own copy uses a different byte ordering. The first is more work and more reliable.

Finally, the marker must be followed by a known-distance landing point, because the hunter cannot know the offset of the decoder from the marker unless you fix it. Fix it in the message template so every copy is identical.

3. Scanning from 0x00010000 upward will hit unmapped pages constantly. How do you keep the scan from crashing the process, given that the stager is null-free?

Read the worked answer

Unhandled faults are the default outcome, so the hunter has to probe pages in a way that survives them. The classic approach is a syscall whose whole purpose is to report whether a memory access would succeed without raising an exception the process has to handle, which turns the scan into a branch instead of a fault. The alternative is to install an exception handler around the probe loop, but on x86 that means writing a registration record on the stack and a handler address into it, and both of those cost bytes and both tend to introduce null bytes.

Whichever mechanism you choose, verify two things empirically rather than by reasoning. First, that the chosen syscall number is correct for Windows 7 and not for a neighbouring version; these numbers move between releases. Second, that the probe actually returns failure for a guard page rather than succeeding and faulting on the read that follows, which is the classic off-by-one in hand-rolled hunters.

Search range also matters for practicality. Scanning from 0x00010000 to 0x7FFEFFFF covers 524,288 pages. At a few hundred cycles per probe that is a noticeable but acceptable pause, well under a second, and it is far more reliable than guessing a heap base. Start the scan above 0x00010000 to avoid the null page entirely, and do not skip large regions in the name of speed until you have measured the actual cost.