This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification.
Student ID: PA-7449
The goal of this assignment is to understand how egghunters work and to craft a working, customizable PoC code.
How it works
If there isn’t enough space for large shellcode, then egghunters can be used to look for the actual executable payload in memory. The only goal of an egghunter is to scrape through memory, find the repeating pattern that marks the start of the shellcode and execute the newly found shellcode. Egghunters need to be fast, small and robust.
I used skape’s paper on egghunters to craft my own version of the egghunter. I would recommend reading this to everyone who wants to have a basic understanding on what egghunter is and how it does its trick.
Here is the graph call for the egghunter shellcode.
For the embedded code, I removed most of the inline comments. If you want to check out the full source code with all the comments, feel free to do it on my Github.
The application starts out by XORing the EDX register. EDX register holds the addresses that the application will check for access and for the egg.
_start: xor edx, edx
The next step sets the EDX address to end with FFF, which is one less than PAGE_SIZE. Setting it to FFF and incrementing by one means that we move up the address exactly by a PAGE_SIZE’s worth. Why? Virtual memory is reserved in PAGE_SIZE chunks by the OS, so if we can’t access memory in one of these blocks, we don’t need to check each and every address in that block, we can move up to the next memory page and check for the egg much-much faster.
This is also the “outside loop”. The shellcode goes through the memory and checks every 4096 bytes whether it is a valid memory address or not. If it’s not, then we move up by 4096 (set to 0xfff and later increment by one), if it is then we move forward in the code.
incr_page: or dx, 0xfff ; 4095, one less than the PAGE_SIZE -
In incr_addr we increment addresses one by one and load the address + 4 bytes into EBX which is used for checking access. To initiate the syscall, I pushed byte 0x21 onto the stack and popped it into EAX. Then based on the result of the syscall, we can decide whether the address we are checking is a valid memory address or not. If it’s not valid, we go back to incr_page, if it is, then we continue.
incr_addr: inc edx ; next address lea ebx, [edx+0x4] ; load edx+4 push byte +0x21 ; access syscall pop eax ; access syscall int 0x80 cmp al,0xf2 ; 0xf2 - EFAULT aka invalid memory address jz short incr_page ; invalid memory address, we can go a PAGE_SIZE up
Once we are at a valid memory address location we can start checking for our egg. I first moved w00t into EAX in reverse order, then I moved the address into EDI. After that we check for w00t at the EDI register twice. To our convinience scasd increments EDI for us, so we don’t need to increment it manually which saves us valuable bytes in our shellcode. If it only finds one egg or it doesn’t find the egg, we jump back to incr_addr and check the next byte. Otherwise we just jump to the newly found shellcode and execute it.
mov eax, 0x74303077 ; t00w mov edi, edx ; scasd ; search for the first instance of EAX (w00t) in EDI jnz short incr_addr scasd ; search for the second instance EAX (w00t) in EDI jnz short incr_addr jmp edi ; execute shellcode
Stack trace displaying some of the syscalls for finding the egg and the execution of the reverse shell. In the terminal below, netcat receives the connection and a proof is displayed that the reverse shell works.
For this assignment, egghunter.c is written so it’s easily customizeable both for the egghunter or for the reverse TCP shellcode. To modify any of the payloads, you need to convert your values into ASCII hex and replace the specified constants. If you want to use it elsewhere, just copy-paste the shellcode from the source.
During this assignment I learned a lot about egghunters. Can’t wait to put what I learned into practice during the OSCE.