Disassemblare un eseguibile con GDB

Tempo di lettura: 8 minuti
Data pubblicazione: March 17, 2017

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.

Run in gdb
Run in gdb

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

Codice sorgente
Codice sorgente

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
A sinistra sintassi AT&T - a destra sintassi Intel
A sinistra sintassi AT&T - a destra sintassi 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 00_f445c7, 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 , abbreviato in i r

(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

Stringa stampata
Stringa stampata

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 disassembling, per poi passare al Reverse Engineering con conseguenti esercizi. Per chi volesse approfondire autonomamente, consiglio il riassunto di gdb e le docs online.