Vai al contenuto
New

DeepSeek V4.1 Flash free for 7 days: 10M tokens a day after a top-up of $10 or more. Until Nov 30.

How it works

Introduzione a Ghidra con un binario innocuo compilato da te

Compila in due modi un piccolo controllo di licenza in C, poi usa Ghidra per trovarne la stringa, seguire i riferimenti e confrontare il decompilatore con il tuo sorgente.

Intermedio13 min di letturaDi NotHoly

Il reverse engineering si impara più facilmente su codice scritto da te. Sai cosa deve fare il programma, quindi puoi controllare ogni affermazione dello strumento. Questo articolo segue quella strada: scrivi un piccolo programma C con un controllo di licenza giocattolo, lo compili due volte e apri entrambe le build in Ghidra. Poi confronti quello che dice il decompilatore con il sorgente che hai digitato.

Metti l'output del decompilatore accanto al tuo codice e capisci in fretta cosa azzecca lo strumento, cosa tira a indovinare e cosa perde quando il compilatore ottimizza. Serve più che imparare a memoria i menu.

Se l'idea di leggere il codice macchina ti è nuova, leggi prima Che cos'è il reverse engineering?. Non ti serve esperienza con l'assembly. Ti servono un terminale e circa un'ora.

Che cos'è Ghidra

Ghidra è un framework di reverse engineering del software sviluppato dalla National Security Agency degli Stati Uniti e pubblicato su GitHub nel repository NationalSecurityAgency/ghidra. Gira su Windows, macOS e Linux. Su un programma compilato può fare quattro cose che qui ci interessano:

  • Disassemblare il codice macchina in istruzioni assembly.
  • Decompilare le funzioni in pseudocodice simile al C con il decompilatore integrato.
  • Trovare stringhe e riferimenti incrociati (XREF), che ti dicono in quale punto del programma viene usata una stringa o un indirizzo.
  • Salvare le tue modifiche in un progetto, compresi i nomi che dai a funzioni e variabili.

Ghidra non recupera il sorgente originale. Produce un'approssimazione. Di solito lo pseudocodice ha una forma simile, ma nomi, tipi e parte del flusso di controllo possono essere diversi da quelli che hai scritto. Questo laboratorio serve proprio a far emergere quella differenza.

Perché analizzare un programma scritto da te

Potresti scaricare un binario di esempio da un forum e iniziare a guardarlo. Non farlo. I binari sconosciuti possono essere malware, e non avresti modo di controllare l'output dello strumento, perché non conosci la risposta giusta.

Un programma che compili tu ti dà tre cose:

  1. Una verità di riferimento. Sai che check() confronta l'argomento con "LAB-1234".
  2. Due build dello stesso codice. Una conserva informazioni di debug e simboli. L'altra è strippata e ottimizzata. Confrontandole vedi cosa ha tolto il compilatore.
  3. Un bersaglio sicuro. Stampa solo ok o no. Non fa chiamate di rete e non scrive file.

Tutto il laboratorio resta sulla tua macchina. Analizzi un file tuo, ed è l'unico perimetro che serve a questo esercizio.

Prepara il laboratorio

Cosa ti serve

  • Un compilatore C. Su Linux, installa gcc con il gestore di pacchetti. Su macOS, per questo laboratorio va bene clang degli strumenti da riga di comando di Xcode, e i flag sono gli stessi. Su Windows, usa WSL o un container con GCC.
  • Un JDK per Ghidra. Ghidra gira su Java, quindi il launcher parte solo se hai già installato un JDK.
  • Ghidra, dalle release ufficiali su GitHub.
  • Un terminale con sha256sum. Git Bash su Windows lo include.

Installa un JDK e Ghidra

Installa il JDK richiesto dalla release di Ghidra. Per quel numero non affidarti a una guida vecchia. Quando ho controllato il README del repository e la guida Getting Started alla data di stesura, entrambi chiedevano un JDK 25 a 64 bit, e il README rimandava alle build di Eclipse Temurin. Leggi il README o le note di rilascio della versione esatta che scarichi e usa quella. Una versione di Java sbagliata è un motivo frequente per cui Ghidra non parte.

Scarica Ghidra solo dalla pagina ufficiale delle release sotto github.com/NationalSecurityAgency/ghidra. Il repository rimanda ai file di rilascio. Estrai la release in una cartella nuova. La guida Getting Started dice di non estrarre Ghidra sopra un'installazione esistente. Avvialo con ghidraRun su Linux e macOS, oppure con ghidraRun.bat su Windows.

Verifica il download

Prima di estrarre qualsiasi cosa, confronta il file con un valore SHA-256 pubblicato. Esegui questo comando nella cartella in cui hai salvato la release:

bash
sha256sum ghidra_*.zip

Confronta l'hash stampato con il valore SHA-256 che GitHub mostra accanto al file nella pagina della release, o con quello pubblicato dal progetto. L'hash deve coincidere esattamente. Se non trovi uno SHA-256 pubblicato per quel file, annotalo nei tuoi appunti di laboratorio. Non trattare un hash copiato da un mirror qualsiasi come il checksum di chi pubblica il file.

Controllare un hash dimostra solo che il file corrisponde a quello indicato da chi lo pubblica. Non dimostra che chi lo pubblica sia affidabile. Usa la fonte ufficiale e tieni il nome del file e l'hash nei tuoi appunti.

Scrivi il programma giocattolo

Crea una cartella vuota per il laboratorio. Tieni insieme il file sorgente e i binari, e tieni i progetti Ghidra fuori da quella cartella, così non la ingombrano.

bash
mkdir -p ghidra-lab && cd ghidra-lab
cat > toy.c <<'EOF'
#include <stdio.h>
#include <string.h>
int check(const char *k){ return strcmp(k, "LAB-1234") == 0; }
int main(int c, char **v){ if(c>1 && check(v[1])) puts("ok"); else puts("no"); return 0; }
EOF

Il programma legge un argomento dalla riga di comando. Se l'argomento è uguale a LAB-1234, stampa ok. Altrimenti stampa no. Questo controllo non ha alcun valore di sicurezza: è una stringa fissa in un file che chiunque può leggere, ed è proprio questo il punto. Troverai la stringa nel binario in pochi minuti.

Registra l'hash del sorgente, così in seguito potrai dimostrare quale file hai analizzato:

bash
sha256sum toy.c

Sulla macchina che ho usato, il comando ha stampato:

text
d9746d8fb118fa954a46b4d501ddf9b0ed86e986e7c99adb05e4a1a60ca0071d  toy.c

Il tuo hash sarà diverso se il file ha spazi o fine riga differenti.

Compilalo due volte

Compila lo stesso sorgente in due binari con impostazioni diverse:

bash
gcc -O0 -g -o toy_debug toy.c
gcc -O2 -s -o toy_stripped toy.c

I flag fanno cose diverse. Vengono tutti dal manuale di GCC, che dovresti leggere almeno una volta:

  • -O0 disattiva la maggior parte delle ottimizzazioni. È il valore predefinito, e il manuale di GCC lo descrive come l'impostazione che mantiene prevedibile il debug. Il codice segue da vicino il sorgente.
  • -O2 attiva la maggior parte delle ottimizzazioni che non sacrificano la dimensione per la velocità. Il manuale dice che attiva anche l'inlining delle funzioni piccole, comprese quelle non dichiarate inline. Il codice può risultare molto diverso dal sorgente.
  • -g aggiunge informazioni di debug nel formato nativo del sistema operativo. Ghidra può leggerne una parte, i debugger le usano tutte.
  • -s toglie la tabella dei simboli dall'output in fase di link. I nomi di funzione check e main non sono più memorizzati nel file.

A rendere utile il laboratorio sono proprio i due binari con lo stesso sorgente e flag diversi. Conservali entrambi.

Esegui entrambe le build

Eseguile prima di aprire Ghidra. Un programma che non hai eseguito è un programma che ancora non capisci.

bash
./toy_debug LAB-1234
./toy_debug wrong
./toy_debug
./toy_stripped LAB-1234
./toy_stripped wrong

Ho eseguito questi comandi in un container Debian con GCC 14.2.0 su un host x86-64. L'output è stato:

text
ok
no
no
ok
no

Le due build si comportano allo stesso modo. Lo stripping e l'ottimizzazione non hanno cambiato la logica, come ci si aspetta. Se i tuoi output sono diversi, fermati e scopri perché prima di continuare.

Esamina i binari dalla riga di comando

Prima di Ghidra, guarda cosa c'è in ogni file. Bastano due strumenti: nm elenca i simboli e strings elenca il testo stampabile.

bash
nm toy_debug | grep -E " (check|main)$"
nm toy_stripped
strings toy_stripped | grep LAB
ls -l toy_debug toy_stripped

Sulla macchina che ho usato, l'output è stato:

text
0000000000001149 T check
0000000000001175 T main
nm: toy_stripped: no symbols
LAB-1234
-rwxr-xr-x 1 root root 17384 Nov  8 09:17 toy_debug
-rwxr-xr-x 1 root root 14480 Nov  8 09:17 toy_stripped

Leggi con attenzione queste righe:

  • La build di debug contiene ancora i nomi check e main. Quei nomi vengono dal tuo sorgente.
  • La build strippata non ha simboli. nm non ha niente da elencare.
  • La stringa LAB-1234 sopravvive in entrambi i file. Una stringa è un dato, e lo stripping non toglie i dati che servono al programma durante l'esecuzione. La stringa è nel file come costante, ed è questo che rende facile iniziare il laboratorio.

Ora disassembla check nella build di debug:

bash
objdump -d -M intel toy_debug | grep -A14 "<check>:"

L'output era simile a questo:

text
0000000000001149 <check>:
    1149:	55                   	push   rbp
    114a:	48 89 e5             	mov    rbp,rsp
    114d:	48 83 ec 10          	sub    rsp,0x10
    1151:	48 89 7d f8          	mov    QWORD PTR [rbp-0x8],rdi
    1155:	48 8b 45 f8          	mov    rax,QWORD PTR [rbp-0x8]
    1159:	48 8d 15 a4 0e 00 00 	lea    rdx,[rip+0xea4]        # 2004 <_IO_stdin_used+0x4>
    1160:	48 89 d6             	mov    rsi,rdx
    1163:	48 89 c7             	mov    rdi,rax
    1166:	e8 d5 fe ff ff       	call   1040 <strcmp@plt>
    116b:	85 c0                	test   eax,eax
    116d:	0f 94 c0             	sete   al
    1170:	0f b6 c0             	movzx  eax,al
    1173:	c9                   	leave
    1174:	c3                   	ret

In queste istruzioni riconosci il sorgente. Una chiamata a strcmp, un test sul valore di ritorno e un sete che trasforma "uguale a zero" in 0 o 1. Tienilo a mente quando leggi il decompilatore.

Ora guarda la build ottimizzata. Ho disassemblato toy_stripped allo stesso modo. Il compilatore ha messo il confronto direttamente in main, che nella mia build si trova a 0x1060: un cmp edi,0x1 per il test c>1, poi mov rdi,[rsi+0x8] per caricare v[1], poi call strcmp@plt. Ha comunque conservato una copia separata di check a 0x1190, perché la funzione non è static e un altro file potrebbe chiamarla. Gli indirizzi cambiano con la toolchain, quindi usali come mappa di cosa cercare e non come numeri da ritrovare uguali.

Importa in Ghidra

Le etichette esatte dei menu dipendono dalla versione di Ghidra, e sulla macchina su cui ho controllato questo articolo non ho eseguito la GUI di Ghidra. I passi qui sotto seguono la documentazione ufficiale e l'aspetto abituale dell'interfaccia. Controlla ogni nome sulla tua versione. Alla fine di ogni passo fai uno screenshot del tuo schermo, e non riusare quelli di un tutorial.

  1. Avvia Ghidra con ghidraRun (o ghidraRun.bat).
  2. Scegli File > New Project. Seleziona Non-Shared Project e dagli un nome, per esempio toy-lab. Salvalo in una cartella che controlli tu.
  3. Scegli File > Import File. Seleziona toy_debug. Accetta il formato rilevato se corrisponde al tuo sistema. Un file compilato su Linux x86-64 dovrebbe risultare ELF con processore x86.
  4. Ripeti l'importazione per toy_stripped. Ora il progetto ha due voci.
  5. Apri ogni file nello strumento CodeBrowser. Quando Ghidra chiede se analizzare il programma, per questo laboratorio accetta l'analisi predefinita.

Aspetta che l'analisi finisca. Ghidra mostra l'avanzamento nella barra in basso. Non rinominare niente finché l'analisi non è conclusa.

Trova la stringa e segui i suoi riferimenti

Inizia da toy_stripped, che è il caso più difficile. Poi confronta con toy_debug.

  1. Apri Window > Defined Strings.
  2. Trova LAB-1234 nell'elenco. Fai doppio clic per saltare al suo indirizzo nel listing.
  3. Fai clic destro sull'indirizzo della stringa e apri il menu dei riferimenti. Cerca una voce come References > Show References to. Ogni riferimento è un punto del codice che legge questo indirizzo.
  4. Segui il riferimento. Ti porta nella funzione che confronta il tuo input con la stringa.

Nella build di debug, Ghidra dovrebbe mostrare anche il nome di funzione check preso dalla tabella dei simboli. Nella build strippata mostrerà un nome generato, per esempio FUN_00101190. Il numero dipende dalla build, quindi usa il nome che vedi sul tuo schermo.

Fai lo stesso con le stringhe "ok" e "no". Entrambe sono usate in main. Seguendole da main ottieni lo stesso quadro visto dall'altro lato.

Leggi il decompilatore accanto al sorgente

Apri il pannello del decompilatore sulla funzione che confronta la stringa. Metti toy.c accanto, in un'altra finestra.

Questo è il tipo di risultato da cercare. È un esempio di come leggerlo, non l'output di una versione specifica di Ghidra:

  • Una funzione con un argomento che è un puntatore a caratteri.
  • Una chiamata a una funzione di nome strcmp o a un simbolo che lo strumento etichetta come strcmp.
  • Un confronto che restituisce 1 quando il risultato è zero.

Ora elenca le differenze che vedi. Quelle tipiche sono:

  • Tipi. Il decompilatore può mostrare un char * come char *, oppure usare un tipo più generico come undefined8. Può non sapere che l'input è const.
  • Nomi. Il tuo parametro k comparirà come param_1 o un segnaposto simile.
  • Valore di ritorno. L'espressione C strcmp(...) == 0 può comparire come un confronto seguito da un return condizionale. Il significato è lo stesso, la forma no.
  • Gestione degli argomenti in main. Il tuo controllo c > 1 diventa un confronto sul numero di argomenti. Fai attenzione all'off-by-one: argv[0] è il nome del programma e v[1] è il primo argomento reale.

Non dare voti al decompilatore. Nota dove la sua versione e la tua coincidono e dove divergono. Quando divergono, torna al disassembly e al sorgente e decidi chi ha ragione.

Rinomina le variabili nella build strippata

Adesso la parte che rende leggibile un binario strippato.

  1. Nel pannello del decompilatore, fai clic sul parametro che contiene la stringa di input.
  2. Fai clic destro e scegli Rename Variable, oppure seleziona la variabile e premi L. Chiamala input.
  3. Nel listing o nell'albero dei simboli, rinomina la funzione in check_input.
  4. In main, rinomina in argv la variabile dell'array di argomenti e in argc il contatore.

Ghidra salva questi nomi nel tuo progetto. Il binario su disco non cambia. È un punto importante: stai documentando la tua lettura, non stai modificando il file che hai compilato.

Nota quanto diventa più facile il codice dopo la rinomina. All'inizio i nomi delle variabili erano ipotesi. Ora corrispondono al tuo sorgente. È il procedimento che i reverse engineer usano ogni giorno, ed è anche il motivo per cui i nomi in un programma decompilato vanno trattati come ipotesi finché non li hai verificati.

Confronta le due build

Metti fianco a fianco la decompilazione di debug e quella strippata. Rispondi a queste domande nei tuoi appunti:

  • Quali nomi di funzione sono sopravvissuti nella build strippata?
  • La build -O2 ha fatto l'inlining di check in main o ha conservato una funzione separata? Cerca nel listing un punto di chiamata separato di strcmp e un ret separato.
  • Quali parti del tuo sorgente si vedono facilmente in entrambe le viste?
  • Quali parti sono difficili da trovare nella vista strippata?

Scrivi una frase per risposta. Se a una domanda non sai rispondere, anche quello è un dato. Torna al listing e trova l'istruzione che crea confusione.

Errori comuni

Saltare l'esecuzione. Se apri il binario in Ghidra prima di eseguirlo, non sai qual è il comportamento corretto. Eseguilo prima e registra gli output.

Fidarsi dei nomi del decompilatore. Un nome come FUN_00101190 o local_18 è un segnaposto. Rinominarlo è una tua affermazione, e ha bisogno di prove.

Confondere le build. I binari di debug e strippato hanno indirizzi e nomi diversi. Tieni i nomi dei file nei tuoi appunti e controlla in quale voce del progetto ti trovi prima di rinominare qualcosa.

Usare un campione di provenienza non affidabile. Il file compilato da te è il punto di partenza sicuro. Quando ti senti a tuo agio, il passo successivo è ancora codice tuo o un campione di una fonte che hai controllato, e la fonte va verificata prima. L'articolo su hash dei file ed etica di VirusTotal spiega perché un hash da solo non risolve la questione.

Trattare il laboratorio come un test di sicurezza. Il controllo della stringa qui si aggira banalmente. Trovarla non dice niente sul software reale. Il vero codice di licenza e protezione è più difficile, e analizzarlo deve restare nel perimetro in cui puoi lavorare.

Dimenticare la versione del JDK. Se Ghidra non parte, confronta la versione di Java con le note di rilascio prima di toccare qualsiasi altra cosa.

Per andare oltre

Quando il programma giocattolo funziona, rendi l'esercizio più difficile in modo controllato:

  • Cambia "LAB-1234" con una stringa più lunga e ricompila. Ritrovala in entrambe le build.
  • Aggiungi una seconda funzione di controllo che confronta l'input con un numero, poi trova dove Ghidra mostra la conversione.
  • Compila con -O0 -s e confronta il risultato con -O2 -s. La differenza mostra quanto l'ottimizzatore cambia la forma del codice senza togliere la logica.
  • Leggi la documentazione di Ghidra sul decompilatore e sulla vista listing. Il sito ufficiale e la cartella docs su GitHub trattano l'interfaccia più a fondo di questo articolo.

Se vuoi capire meglio i flag del compilatore, le fonti primarie sono le sezioni del manuale di GCC sulle opzioni di ottimizzazione e di debug. Leggile prima di fare affidamento su un flag in un report.

Conserva i tuoi appunti. Un buon quaderno di laboratorio ha l'hash del sorgente, i comandi di compilazione, gli output delle esecuzioni, la versione di Ghidra, la versione del JDK e i nomi che hai assegnato, ciascuno con le prove a sostegno. Quella traccia trasforma questo esercizio in una competenza da riusare sul programma successivo.

Fonti

Fonti

  1. Ghidra: Getting Started (GitHub repository)
  2. Ghidra repository README
  3. Ghidra project site
  4. GCC manual: Optimize Options
  5. GCC manual: Debugging Options

Altro su Reverse engineering

Reverse engineering

Perché usare NotHoly per il reverse engineering

Tieni Ghidra, radare2 o IDA. Incolla l'output del decompilatore e il modello lo spiega e scrive lo script, senza rifiutare. Triage statico per iniziare.

Intermedio3 min di lettura