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.
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.
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.
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:
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.
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:
check() confronta l'argomento con "LAB-1234".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.
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.sha256sum. Git Bash su Windows lo include.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.
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:
sha256sum ghidra_*.zipConfronta 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.
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.
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; }
EOFIl 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:
sha256sum toy.cSulla macchina che ho usato, il comando ha stampato:
d9746d8fb118fa954a46b4d501ddf9b0ed86e986e7c99adb05e4a1a60ca0071d toy.cIl tuo hash sarà diverso se il file ha spazi o fine riga differenti.
Compila lo stesso sorgente in due binari con impostazioni diverse:
gcc -O0 -g -o toy_debug toy.c
gcc -O2 -s -o toy_stripped toy.cI 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.
Eseguile prima di aprire Ghidra. Un programma che non hai eseguito è un programma che ancora non capisci.
./toy_debug LAB-1234
./toy_debug wrong
./toy_debug
./toy_stripped LAB-1234
./toy_stripped wrongHo eseguito questi comandi in un container Debian con GCC 14.2.0 su un host x86-64. L'output è stato:
ok
no
no
ok
noLe 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.
Prima di Ghidra, guarda cosa c'è in ogni file. Bastano due strumenti: nm elenca i simboli e strings elenca il testo stampabile.
nm toy_debug | grep -E " (check|main)$"
nm toy_stripped
strings toy_stripped | grep LAB
ls -l toy_debug toy_strippedSulla macchina che ho usato, l'output è stato:
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_strippedLeggi con attenzione queste righe:
check e main. Quei nomi vengono dal tuo sorgente.nm non ha niente da elencare.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:
objdump -d -M intel toy_debug | grep -A14 "<check>:"L'output era simile a questo:
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 retIn 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.
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.
ghidraRun (o ghidraRun.bat).toy-lab. Salvalo in una cartella che controlli tu.toy_debug. Accetta il formato rilevato se corrisponde al tuo sistema. Un file compilato su Linux x86-64 dovrebbe risultare ELF con processore x86.toy_stripped. Ora il progetto ha due voci.Aspetta che l'analisi finisca. Ghidra mostra l'avanzamento nella barra in basso. Non rinominare niente finché l'analisi non è conclusa.
Inizia da toy_stripped, che è il caso più difficile. Poi confronta con toy_debug.
LAB-1234 nell'elenco. Fai doppio clic per saltare al suo indirizzo nel listing.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.
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:
strcmp o a un simbolo che lo strumento etichetta come strcmp.Ora elenca le differenze che vedi. Quelle tipiche sono:
char * come char *, oppure usare un tipo più generico come undefined8. Può non sapere che l'input è const.k comparirà come param_1 o un segnaposto simile.strcmp(...) == 0 può comparire come un confronto seguito da un return condizionale. Il significato è lo stesso, la forma no.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.
Adesso la parte che rende leggibile un binario strippato.
L. Chiamala input.check_input.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.
Metti fianco a fianco la decompilazione di debug e quella strippata. Rispondi a queste domande nei tuoi appunti:
-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.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.
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.
Quando il programma giocattolo funziona, rendi l'esercizio più difficile in modo controllato:
"LAB-1234" con una stringa più lunga e ricompila. Ritrovala in entrambe le build.-O0 -s e confronta il risultato con -O2 -s. La differenza mostra quanto l'ottimizzatore cambia la forma del codice senza togliere la logica.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.
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.
Reverse engineering
Che cosa scarta la compilazione, in che cosa i disassembler differiscono dai decompilatori e quali limiti legali conoscere prima di analizzare un binario.