Nelle lezioni precedenti, abbiamo acquisito una solida comprensione dei concetti fondamentali del reverse engineering e dell'utilizzo di Ghidra per l'analisi statica dei file eseguibili. In questa lezione, approfondiremo due tecniche essenziali nel processo di reverse engineering: il disassemblaggio e la decompilazione. Queste metodologie ci permettono di trasformare il codice binario in rappresentazioni più comprensibili, facilitando l'analisi e la comprensione del funzionamento interno di un programma.
Disassemblaggio vs decompilazione
Prima di immergerci nelle attività pratiche, è fondamentale fare una distinzione tra disassemblaggio e decompilazione:
- Disassemblaggio: è il processo che converte il codice macchina (binario) in linguaggio assembly. Il risultato è un codice a basso livello che rappresenta direttamente le istruzioni eseguite dalla CPU. Questo processo è generalmente più diretto, poiché ogni istruzione binaria ha una corrispondente rappresentazione assembly. Tuttavia, il codice assembly risultante può essere complesso da interpretare, soprattutto per programmi di grandi dimensioni o con logiche intricate.
- Decompilazione: è il processo che tenta di ricostruire un codice sorgente di alto livello (come C o C++) a partire dal codice macchina. Sebbene non sempre perfetta, la decompilazione fornisce una rappresentazione più leggibile e comprensibile della logica del programma. Questo processo è più complesso del disassemblaggio, poiché implica l'interpretazione delle strutture dati, delle funzioni e delle logiche di controllo per ricostruire il codice sorgente originale o una sua rappresentazione approssimativa.
Selezione del file eseguibile per l'analisi con Ghidra
Per esercitarci nel disassemblaggio e nella decompilazione, è essenziale scegliere un eseguibile appropriato. Dopo una ricerca approfondita, abbiamo individuato "Vulnserver", un'applicazione server volutamente vulnerabile progettata per scopi educativi nel campo della sicurezza informatica e del reverse engineering. "Vulnserver" è ampiamente utilizzato per esercitazioni pratiche e offre un'ottima opportunità per analizzare un eseguibile reale. In poche parole, Vulnserver è un server TCP multithread per Windows progettato come strumento didattico per imparare a individuare ed exploitare buffer overflow. E' in ascolto sulla porta 9999
e offre comandi vulnerabili, ciascuno con bug diversi che richiedono approcci specifici per l'exploit.
Accediamo al repository di "vulnserver" su GitHub e scarichiamo il file eseguibile vulnserver.exe
direttamente dalla sezione dei rilasci o compiliamo il codice sorgente seguendo le istruzioni fornite nel repository.
Adesso, avviamo Ghidra e creiamo un nuovo progetto denominato "Analisi Vulnserver". Importiamo il file vulnserver.exe
nel progetto utilizzando l'opzione "File > Import File" e, una volta importato, facciamo doppio clic sul file per aprirlo nel "Code Browser" di Ghidra.
All'apertura del file, Ghidra potrebbe chiedere se desideriamo eseguire l'analisi automatica. Confermiamo selezionando "Yes" per permettergli di identificare funzioni, stringhe e altri elementi chiave nel binario. Al termine dell'analisi, esaminiamo la "Symbol Tree" per avere una panoramica delle funzioni e delle variabili globali identificate.
Infine, nel "Code Browser", navighiamo attraverso le sezioni del codice disassemblato. Possiamo utilizzare la "Listing" per visualizzare le istruzioni assembly generate da Ghidra. Identifichiamo il punto di ingresso del programma, solitamente indicato come "entry
", "_start
" o "_main
". Da qui, possiamo tracciare il flusso del programma seguendo le chiamate di funzione e le istruzioni di salto.
Esaminiamo le funzioni principali, prestando attenzione alle istruzioni critiche come chiamate di sistema, manipolazioni di memoria e operazioni aritmetiche.
Decompilazione del codice con Ghidra
Ghidra offre un decompilatore integrato che tenta di ricostruire il codice sorgente di alto livello. In genere, quando clicchiamo su "Analyze" all'atto dell'importazione del file, Ghidra si occupa già di fare una decompilazione del codice. Qualora non dovesse essere stata fatta in automatico, possiamo avviarla manualmente. Per decompilare una funzione, selezioniamola nella "Symbol Tree" o nel "Listing" e apriamo la finestra del decompilatore tramite "Window" > "Decompile".
Prestiamo attenzione a:
- Strutture di Controllo: identificazione di cicli (
for
,while
) e condizionali (if-else
). - Chiamate di Funzione: riconoscimento di funzioni standard della libreria C o di funzioni definite dall'utente.
- Gestione delle Variabili: osservazione di come le variabili vengono dichiarate, inizializzate e modificate.
Analisi dettagliata del codice disassemblato e decompilato
Ora che abbiamo disassemblato e decompilato vulnserver.exe
, esaminiamo alcuni elementi chiave.
Identificazione del punto di ingresso
Il punto di ingresso (entry
) di un programma è la prima funzione eseguita quando l'applicazione viene avviata. Spesso, nei programmi scritti in C/C++, il punto di ingresso reindirizza l'esecuzione alla funzione main
.
Per identificare il punto di ingresso in Ghidra:
- Apriamo la Symbol Tree e cerchiamo
_start
,entry
o funzioni con nomi simili. - Seguiamo il flusso delle istruzioni fino a individuare la chiamata a
main
.
La funzione _main()
è il punto di ingresso principale del server vulnerabile. Per brevità non riporteremo tutto il codice decompilato ma ci accingeremo solamente ad analizzarne qualche parte.
Operazioni chiave
Esaminiamo alcune delle operazioni chiave a partire dalla gestione degli Argomenti della Riga di Comando.
- Se il numero di argomenti (
_Argc
) è maggiore di 2, viene stampato un messaggio di utilizzo e il programma termina. - Quando viene fornito un solo argomento, viene convertito in numero (atoi(_Argv[1])) e validato.
- Se l'argomento non è valido (ad esempio, troppo lungo o fuori intervallo), il programma termina.
- Se non viene fornito alcun argomento, la porta predefinita
9999
viene assegnata.
Inizializzazione della Rete con Winsock:
WSAStartup()
viene chiamato per inizializzare Winsock.getaddrinfo()
viene utilizzato per ottenere informazioni sull'indirizzo.
Creazione del Socket:
socket()
viene utilizzato per creare un socket.- Se fallisce, viene stampato un errore e il programma termina.
Binding del Socket:
bind()
associa il socket alla porta specificata.- Se fallisce, il socket viene chiuso e il programma termina.
Avvio del Server:
listen()
mette il server in attesa di connessioni.- Se fallisce, il server viene chiuso.
Accettazione delle Connessioni
- Un ciclo
while
attende continuamente connessioni. accept()
accetta connessioni in ingresso.- Se una connessione è accettata, viene avviato un nuovo thread con
_CreateThread()
per gestire il client.
Da questa prima analisi della funzione main
possiamo vedere facilmente che emergono alcune potenziali vulnerabilità. La prima è un possibile Buffer Overflow:
strncpy(local_1c, _Argv[1], 6);
potrebbe non terminare correttamente la stringa sestrlen(_Argv[1])
è esattamente 6.memcpy(local_7c, "...", 0x5e);
potrebbe scrivere oltre i limiti previsti.
La seconda è la mancata validazione dell'input del vlient. Con i dati ricevuti dalla connessione non vengono adeguatamente sanitizzati.
Conclusioni: disassemblaggio e decompilazione con Ghidra
Abbiamo analizzato _main()
di vulnserver.exe
, individuando come il server gestisce connessioni e identificando possibili vulnerabilità. Questo tipo di analisi è essenziale nel reverse engineering per comprendere la logica di un programma e identificare potenziali exploit.