GDB, o GNU Project debugger, è un software sviluppato dal progetto GNU, e da la possibilità di analizzare numerosi eseguibili, come C, C++, Go e molti altri.
Esso ti permette di analizzare un programma durante la sua esecuzione e di:
- avviare l’eseguibile, specificando le componenti;
- interrompere il programma, specificando le condizioni;
- esamanire ciò che avviene quando il programma si è fermato;
- modificare il programma stesso, in modo da analizzarne gli effetti (positivi o negativi) direttamente in modalità live.
In questo articolo andremo ad analizzare le principali funzioni, cercando anche di capire cosa avviene a livello di istruzioni e codice.
Attenzione: per questo articolo consiglio delle basi di programmazione C e qualche nozione di assembly (registri, puntatori, indirizzi di memoria).
Introduzione
Come primo esempio utilizzerò il classico Ciao Mondo, scritto in C e definito come
#include int main() { int i; for(i=0;i<10;i++) { printf("Ciao mondo!\n"); } }
Per compilarlo e poter visionare il codice sorgente anche in gdb, il comando da eseguire sarà:
gcc -g -o HelloWorld HelloWorld.c
Ora possiamo eseguire gdb, passando come argomento l’eseguibile appena creato:
gdb HelloWorld
e per eseguire il programma in gbd il comando sarà run.
Le righe iniziali potete sopprimerle passando l’argomento -q (quiet), in modo che non stampi ogni volta la descrizione del software.
Quando compiliamo con l’opzione -g avremo la possibilità di vedere il codice sorgente del programma creato con il comando list. Ovviamente se avremo solo l’eseguibile, non sarà possibile
Ora che abbiamo visto che funziona anche in gdb, iniziamo a decompilare ed estrarre qualche informazione iniziale. Uno dei comandi più utili è il breakpoint, il quale ci darà la possibilità di fermare il programma sull’istruzione o riga prescelta e analizzare i registri presenti al momento del break.
Inserisco il breakpoint sul main (dove il programma sta per iniziare) e eseguo nuovamente il codice.
(gdb) break main Breakpoint 1 at 0x5ad: file HelloWorld.c, line 6. (gdb) run Starting program: /home/mrtouch/Desktop/gdb/HelloWorld Breakpoint 1, main () at HelloWorld.c:6 6 for(i=0;i<10;i++)
Ed ecco che si è fermato, dandoci la possibilità di analizzare i registri o continuarne l’esecuzione.
Piccola precisazione prima di iniziare a disassemblare. GDB disassembla con la sintassi di AT&T, ma c’è anche la possibilità di utilizzare la sintassi Intel. Personalmente preferisco la seconda, più semplice da capire, ed è quello che utilizzerò nell’articolo. I comandi e le istruzioni non cambiano, saranno solamente stampate in maniera differente. Per impostare la sintassi Intel, basta digitare in gdb
set disassembly-flavor intel
In tutti e due i casi, dove è presente la freccia è posizionato il breakpoint e dove siamo di conseguenza posizionati noi. Le istruzioni precedenti sono definite come prologo di funzione e sono generate dal compilatore per impostare la memoria delle variabili statiche.
Esaminare l’eseguibile
Un altro comando essenziale in gdb è x (examine), il quale da la possibilità di esaminare un registro, una variabile o qualsiasi altra informazione passata per argomento. È possibile definire il formato con cui ricevere l’informazione:
- x/o: stampa in ottale;
- x/x: stampa in esadecimale;
- x/t: stampa in binario;
- x/s: stampa, se esistente, la stringa (in questo caso gdb converte automaticamente da esadecimale ad ascii);
- x/i: stampa il codice assembly.
Nel caso volessimo esaminare più valori della variabile passata per argomenti, possiamo aggiungere il numero degli stessi con lo stesso comando. Proviamo, ad esempio, ad analizzare il registro EIP, il quale contiene l’indirizzo di memoria che punta all’istruzione successiva.
(gdb) x/x $eip 0x800005ad <main+29>: 0xc7 ;punta al breakpoint inserito da noi (gdb) x/x 0x800005ad ;codice HEX di EIP, non c'è differenza 0x800005ad <main+29>: 0xc7 (gdb) x/i 0x800005ad => 0x800005ad <main+29>: mov DWORD PTR [ebp-0xc],0x0 (gdb) x/6x 0x800005ad ;continuo di eip 0x800005ad <main+29>: 0xc7 0x45 0xf4 0x00 0x00 0x00 (gdb) x/xw 0x800005ad ;eip scritto su una singola word 0x800005ad <main+29>: 0x00f445c7
Nell’ultima riga ho inserito un nuovo comando, ossia la stampa della word intera. Nel caso volessimo avere grandezze differenti, le lettere sono:
- b per un singolo byte;
- h per 2 bytes;
- w per 4 bytes, la word, appunto;
- g per 8 bytes.
La penultima e ultima riga sono uguali, ma rappresentate secondo l’ordine little-endian, in quanto sono eseguiti su un calcolatore x86. Con questa modalità, viene preso l’ultimo bytes (00), il penultimo (f4), gli viene aggiunto 45 ed infine c7, per formare 00f445c7, appunto il codice stampato nell’ultima riga. Per una più esaustiva spiegazione, Big & Little Endian Order.
Analisi del codice
Ora che abbiamo visto come esaminare le variabili, passiamo ad analizzare il codice. Dove abbiamo posizionato il break, è presente l’istruzione mov DWORD PTR [ebp-0xc],0x0, la quale copia il valore 0 in epb-0xc, ossia dove la variabile i del ciclo for sarà posizionata. Esamino quindi ebp con il comando info register <registro>, abbreviato in i r <registro>
(gdb) i r ebp ebp 0xbffff2e8 0xbffff2e8 (gdb) x/x $ebp 0xbffff2e8: 0x00000000 (gdb) x/x $ebp-0xc 0xbffff2dc: 0x80000611 (gdb) print $ebp-0xc $1 = (void *) 0xbffff2dc
L’ultimo comando permette di stampare la locazione di memoria del registro. Visto che per ora, non abbiamo rilevato nessuna informazione utile, passiamo alla successiva istruzione con il comando nexti.
(gdb) i r eip eip 0x800005b4 0x800005b4 <main+36> (gdb) x/i $eip => 0x800005b4 <main+36>: jmp 0x800005cc <main+60> (gdb) x/2i main+60 0x800005cc <main+60>: cmp DWORD PTR [ebp-0xc],0x9 0x800005d0 <main+64>: jle 0x800005b6 <main+38>
EIP effettua ora un jump all’istruzione 60, la quale eseguirà una comparazione tra il valore di i e 9. Nel caso sia positivo (riga successiva) tornerà alla riga 38.
Proseguo con nexti fino ad arrivare all’istruzione in cui viene stampata la stringa
(gdb) nexti 0x800005bf 8 printf("Ciao mondo!\n"); (gdb) x/4i $eip => 0x800005bf <main+47>: push eax 0x800005c0 <main+48>: call 0x800003f0 <puts@plt> 0x800005c5 <main+53>: add esp,0x10 0x800005c8 <main+56>: add DWORD PTR [ebp-0xc],0x1
Noto che viene aggiunto il registro EAX (con push) e poi stampata (puts) la stringa. Visiono quindi il registro ed ecco che contiene l’output del programma
Ricordo che per ogni architettura è diverso. Potreste avere altri registri in uso e le istruzioni potrebbero essere invertite. Serve solo un pò di pazienza (almeno all’inizio) e voglia di imparare.
Conclusioni
GDB, come ogni disassemblatore o debugger, ha infinite altre opzioni, e ogni eseguibile è chiaramente un mondo a sè stante. Scriverò altri articoli per capire meglio il software e soprattutto il disassemblaggio, per poi passare al Reverse Engineering con conseguenti esercizi. Per chi volesse approfondire autunomamente, consiglio il riassunto di gdb e le docs online.
Se ti è piaciuto l’articolo condividi, dona, spargi il verbo! HackTips