In this article, I will introduce the concepts of Egg Hunter shellcode and provide demonstrations.
Murmur: Originally, I planned to introduce Egg Hunter technique while exploiting CVE-2014-4158. I then decided to separating them because I think the Egg Hunter technique is worth a single blog post!
Egg Hunter
Egg Hunter is actually a game played during Easter. Somehow, researchers adopted the name for a technique used in buffer overflow exploitation.
The core mechanism is to use a two-stage shellcode. The first stage is the Hunter; the second stage is the Egg.
In some scenarios, a vulnerable application does not have a large enough buffer to load the full shellcode. Alternatively, the address of the shellcode may not be constant when it is loaded. In these cases, we can apply the Egg Hunter technique to overcome these issues by loading both the Hunter and the Egg Into memory and having the Hunter locate and execute the Egg.
The major headache is the extremely large memory space. Every 32-bit Windows application “believes” it has a 4-GB memory space. Searching for specific shellcode across a large memory space, while avoiding invalid memory regions and doing so as quickly as possible, can be very difficult.
After several generations of development, researchers introduced many useful techniques. In this article, I will use the techniques introduced by skape.
Murmur: I believe it is worth studying other legacy methods to understand how assembly works, but I will save them for future articles.
NtDisplayString
A hunter acts as a tiny first-stage scanner. It searches process memory for a uniqute 4-byte tag repeated twice (e.g., R0CKR0CK) that we have prepended to our actual shellcode.
The hunter invokes the NtDisplayString system call to check whether a memory page is accessible.
Note: Sometimes, if we use other functions to read memory, they might cause an exception (access violation) because the memory region may not be readable. The system call handles this condition and returns the appropriate status, allowing the hunter to catch the error, skip to the next valid memory page, and continue scanning without crashing the application.
Once it finds the double-tag sequence in memory, it redirects execution straight to our main payload (the Egg).
The Egg Hunter shellcode can be implemented as follows:
loop_check: pushedx; push edx into the stack push0x43; push 0x43 (kernel index of NtDisplayString)
popeax; pop 0x43 into eax int0x2e; syscall cmpal, 0x05; is 0xc0000005 (ACCESS_VIOLATION) ?
popedx; restore edx
loop_check_8_valid: je loop_inc_page ; if ACCESS_VIOLATION has been raised, then move to next memory page
is_egg: moveax, 0x50905090; a tag for our egg (main payload) movedi, edx; move the value of edx into edi scasd; validate the tag => if yes: edi = edi + 4 jnz loop_inc_one ; if not: jump to loop_inc_one, continue scanning
scasd; compare eax and [edi] (compare twice in total)
jnz loop_inc_one ; if not matched, jump to loop_inc_one
matched: jmpedi; if matched, then this is the egg!
There are several details worth discussing.
0x43
The first one is the value 0x43. It is a syscall number (or System Service ID). It represents the function NtDisplayString.
The kernel cannot identify the string NtDisplayString. Instead, it identifies system services by their syscall numbers.
The value 0x43 does not remain constant across all Windows operating systems. You can find all the syscall numbers on this website.
You don’t have to memorize all these numbers! Instead, we can find the number in the following ways.
The first method is using WinDbg.
Murmur: Since I had been failing to install WinDbg on Windows XP, I am going to demonstrate it on Windows 10 x64…
Attach WinDbg to explorer.exe:
Enter the command below:
1
u ntdll!NtDisplayString L8
The program executes syscall after calling mov eax, 0DCh. Therefore, we can assume that 0xDC is the syscall number of NtDisplayString on Windows 10. If the release or edition of your Windows 10 is different from mine, you might get a different value.
Note: The letter h in 0DCh is not a hexadecimal digit. Instead, it indicates that the value is hexadecimal, similar to the 0x prefix. The notation with the h suffix is actually the same as that used in Verilog.
However, you might not be able to install WinDbg on Windows XP (like me)… Don’t worry! I will provide you with another approach!
Anyway! Let’s see how NtDisplayString is located in ntdll.dll:
1 2 3 4 5 6 7 8
4c8bd1 mov r10,rcx b8dc000000 mov eax,0DCh f604250803fe7f01 test byte ptr [SharedUserData+0x308 (00000000`7ffe0308)],1 7503 jne ntdll!NtDisplayString+0x15 (00007ffa`72a6f115) 0f05 syscall c3 ret cd2e int 2Eh c3 ret
The opcode of mov eax,0DCh: b8dc000000. We can see that this instruction starts with \xb8.
Therefore, we can write a simple C++ program to find the syscall number of a function in ntdll.dll:
Note: -static is required since some DLL files might be missing on Windows XP.
Then, we can use the program to find the syscall number of NtDisplayString:
int 0x2e
In NASM, int stands for Interrupt. It pauses the execution of the current user-mode program and forces the CPU to jump into kernel-mode (the operating system’s core). It acts as a secure bridge between user-space (Ring 3) and kernel-space (Ring 0).
When developing an Egg Hunter on Windows XP, you cannot directly scan the process memory using user-mode code. If your code accesses an unallocated memory address, the CPU triggers a page fault, which can cause your exploit to crash.
Note: You may want to crash your target application, but definitely not your exploit.
By utilizing int 0x2e, we are asking the Kernel to check the memory address for us. Because the Kernel has Ring 0 privileges, it has the ability to safely probe the address. If the address is invalid, the Kernel won’t crash; it will simply return a failure status code (STATUS_ACCESS_VIOLATION) to your code. Again, the CPU and Kernel don’t identify the string name, instead, they use numbers only. just like 0x43.
Why mov then pop?
In the loop_check section, the push 0x43 and pop eax seems very redundant:
1 2 3 4 5 6 7 8 9
loop_check: pushedx; push edx into the stack push0x43; push 0x43 (kernel index of NtDisplayString)
popeax; pop 0x43 into eax int0x2e; syscall cmpal, 0x05; is 0xc0000005 (ACCESS_VIOLATION) ?
popedx; restore edx
So, why don’t we just use mov eax, 0x43? The reason is that NASM automatically adds \x00 bytes when encoding the instruction:
Therefore, we just push and pop instead of mov directly.
Why scasd for Twice?
The last question about the Hunter is in the is_egg section:
1 2 3 4 5 6 7 8 9
is_egg: moveax, 0x50905090; a tag for our egg (main payload) movedi, edx; move the value of edx into edi scasd; validate the tag => if yes: edi = edi + 4 jnz loop_inc_one ; if not: jump to loop_inc_one, continue scanning
scasd; compare eax and [edi] (compare twice in total)
jnz loop_inc_one ; if not matched, jump to loop_inc_one
Why do we call scasd to compare eax and [edi] for twice? In practice, we will replace the tag 0x50905090 with four custom characters. This tag can only be hardcoded, which raises an issue: What if the Hunter scans itself?
The answer is that the Hunter might mistake itself for the Egg!
Therefore, we will place our four custom characters twice in the Egg and validate the four custom characters twice in the Hunter. Since the Hunter only has a tag with four characters, it will not mistake itself for the Egg.
Finally, we can get back! I hope you didn’t forget what we were doing originally…
Note that current ESP is 0x0022FF00, while the end of the stack is 0x0022FFFC, which only gives us 0xFC (252 in decimal) bytes to store our shellcode. Obviously, it is insufficient.
Find an address that executes jmp esp:
Finally, the exploit script can be implemented as follows:
In this article, I introduced the concepts behind the egg hunter technique. Its primary purpose is to overcome the problem of insufficient memory space on the stack.
I learned a great deal while developing the shellcode using C++ and NASM.
In the next article, I will demonstrate how to exploit Kolibri v2.0 using the egg hunter technique.
That wraps up this article. If you have any comments or suggestions, please feel free to leave them below!