Saturday July 31st 2010

Le basi dell’assembly e del debugging su unix

Con questo articolo diamo un’occhiata a quelle che sono le principali tecniche di debugging in ambito unix per capire meglio il funzionamento del linguaggio assembly.

Due sono i principali strumenti impiegati per questo genere di operazioni:
- Objdump è un tool che viene utilizzato per esaminare i file binari compilati
- Gdb è un debugger per seguire passo-passo il flusso dei programmi compilati

Tramite questi programmi è facile interrogare i registri presenti nella CPU per visualizzarne il contenuto e capire come viene allocata la memoria. Nel nostro esempio ho adottato una classica architettura Intel i386, presente nella maggior parte dei pc. In questo ambiente i principali registri con i quali la CPU lavora sono i seguenti:

EAX: accumulatore
ECX: contatore
EDX: dati
EBX: base
EIP: puntatore alla istruzione successiva
ESP: puntatore della fine dello stack
EBP: puntatore al frame corrente
SPF: riporta EBP al valore precedente
ESI: indice di origine
EDI: indice di destinazione

Per effettuare le interrogazioni adotteremo il comando x, che sta appunto per examine, specificando anche come visualizzare l’output del registro esaminato:

x/x = esamina in esadecimale
x/o = esamina in ottale
x/u = esamina in decimale senza segno
x/d = esamina in decimale
x/s = esamina in stringa
x/i = esamina un istruzione

Inoltre digitando un numero subito dopo lo slash è possibile specificare quanti byte interrogare. Ricordo che ogni lettera ASCII corrisponde ad un byte (ad esempio la lettera “A” corrisponde 0×41 scritto in esadecimale).

Analizziamo ora il nostro primo programma in C firstprog.c:

#include
int main()
{
  int i;
  for (i=0; i < 10; i++)
  {
    printf("Hello, world!\n");
  }
  return 0;
}

Questo programma non fa altro che stampare 10 volte di seguito la stringa “Hello world”. Tale codice anche se risulta molto semplice e banale, è tuttavia utile a capire in prima battuta il funzionamento dell’assembly. Procediamo dunque con la compilazione:

# gcc -g firstprog.c -o firstprog

Ora che abbiamo l’eseguibile passiamo all’analisi con gdb:

# gdb -q ./firstprog
(gdb) set dis intel
(gdb) list
1       #include
2
3       int main()
4       {
5         int i;
6         for (i=0; i < 10; i++)
7         {
8           printf("Hello, world!\n");
9         }
10        return 0;
(gdb)

Con l’opzione -q evitiamo di stampare banner inutili, mentre con l’opzione set dis intel abbiamo un output dell’assembly molto più leggibile.
Proseguiamo disassemblando il main e dando una prima occhiata alla locazione della memoria:

(gdb) disassemble main
Dump of assembler code for function main:
0x080483c4 :    lea    ecx,[esp+0x4]
0x080483c8 :    and    esp,0xfffffff0
0x080483cb :    push   DWORD PTR [ecx-0x4]
0x080483ce :   push   ebp
0x080483cf :   mov    ebp,esp
0x080483d1 :   push   ecx
0x080483d2 :   sub    esp,0x14
0x080483d5 :   mov    DWORD PTR [ebp-0x8],0x0
0x080483dc :   jmp    0x80483ee
0x080483de :   mov    DWORD PTR [esp],0x80484d0
0x080483e5 :   call   0x80482f4
0x080483ea :   add    DWORD PTR [ebp-0x8],0x1
0x080483ee :   cmp    DWORD PTR [ebp-0x8],0x9
0x080483f2 :   jle    0x80483de
0x080483f4 :   mov    eax,0x0
0x080483f9 :   add    esp,0x14
0x080483fc :   pop    ecx
0x080483fd :   pop    ebp
0x080483fe :   lea    esp,[ecx-0x4]
0x08048401 :   ret
End of assembler dump.

Questo output ci mostra la traduzione della funzione main del nostro programma in assembly. Ad un primo sguardo è possibile capire la sintassi: locazione della memoria in esadecimale: operatore registro destinazione, registro d’origine. Tuttavia in questo momento nessun registro risulta visualizzabile, proprio perchè ancora  il programma non è stato effettivamente lanciato. Poniamo dunque break al main, lanciamo l’eseguibile e analizziamo i registri:

(gdb) break main:
Breakpoint 1 at 0x80483d5: file firstprog.c, line 6.
(gdb) run
Starting program: ./firstprog
 
Breakpoint 1, main () at firstprog.c:6
6         for (i=0; i < 10; i++)
(gdb) i r
eax            0xbfe3fc04       -1075577852
ecx            0xbfe3fb80       -1075577984
edx            0x1      1
ebx            0xb800bff4       -1207910412
esp            0xbfe3fb50       0xbfe3fb50
ebp            0xbfe3fb68       0xbfe3fb68
esi            0x8048420        134513696
edi            0x8048310        134513424
eip            0x80483d5        0x80483d5
eflags         0x200286 [ PF SF IF ID ]
cs             0x73     115
ss             0x7b     123
ds             0x7b     123
es             0x7b     123
fs             0x0      0
gs             0x33     51
(gdb) i r eip
eip            0x80483d5        0x80483d5

In particolare analizziamo il registro l’eip, il quale punta alla prossima istruzione da eseguire, analizzando la memoria sulla quale sta puntando. Entrambi i comandi mostrano lo stesso output proprio perchè effettivamente stiamo effettuando la stessa richiesta.

(gdb) x/x 0x80483d5
0x80483d5 :    0x00f845c7
(gdb) x/x $eip
0x80483d5 :    0x00f845c7
(gdb) x/i $eip
0x80483d5 :    mov    DWORD PTR [ebp-0x8],0x0

Inoltre aggiugendo il numero 6 prima della i di instruction possiamo vedere le 6 successive istruzioni.

(gdb) x/6i $eip
0x80483d5 :    mov    DWORD PTR [ebp-0x8],0x0
0x80483dc :    jmp    0x80483ee
0x80483de :    mov    DWORD PTR [esp],0x80484d0
0x80483e5 :    call   0x80482f4
0x80483ea :    add    DWORD PTR [ebp-0x8],0x1
0x80483ee :    cmp    DWORD PTR [ebp-0x8],0x9

A questo punto si vede come al registro puntato di eip c’è una DWORD PTR [ebp-0x8],0×0. Tale operazione significa che il valore zero viene allocato alla locazione ebp – 8 ossia 0xbfff7d10 che al momento contiene 0xb7febf50:

(gdb) i r ebp
ebp            0xbfff7d18       0xbfff7d18
(gdb) x/x $ebp - 8
0xbfff7d10:     0xb7febf50

Nella successiva locazione di memoria, ossia all’indirizzo 0×80483dc è presente un salto incondizionato jmp alla locazione 0×80483ee. Verifichiamo, proseguendo di una istruzione nexti, che l’eip successivo sia proprio quello:

(gdb) nexti
0x080483dc      6         for (i=0; i < 10; i++)
(gdb) i r eip
eip            0x80483dc        0x80483dc
(gdb) x/i $eip
0x80483dc :    jmp    0x80483ee

Infatti, ora l’eip contiene un salto incondizionato jmp.
Guardiamo adesso le successive 10 istruzioni:

(gdb) x/10i $eip
0x80483dc :    jmp    0x80483ee
0x80483de :    mov    DWORD PTR [esp],0x80484d0
0x80483e5 :    call   0x80482f4
0x80483ea :    add    DWORD PTR [ebp-0x8],0x1
0x80483ee :    cmp    DWORD PTR [ebp-0x8],0x9
0x80483f2 :    jle    0x80483de
0x80483f4 :    mov    eax,0x0
0x80483f9 :    add    esp,0x14
0x80483fc :    pop    ecx
0x80483fd :    pop    ebp

Da questo listato si vede non troppo facilmente per i newbe come viene effettuato il ciclo for.
Inizialmente viene fatto un salto incondizionato sulla locazione 0×80483ee, nella quale viene effettuata una compare cmp, la quale dice che se il il numero presente alla locazione ebp – 8, dove precedentemente era stato inizializzato 0, è minore o uguale di 9 allora esegue l’istruzione successiva jle che risulta essere apppunto un salto condizionato, proprio per via della compare, all’istruzione 0×80483de. Siccome risulta vera, 0 è minore o uguale di 9, rinizia il ciclo.

Nel momento in cui risulterà falsa, ossia ebp – 8 vale 10, allora non effettuerà il salto e continuerà con l’istruzione successiva.

Ora controlliamo proprio che ebp – 8 sia 0 e che quindi jle faccia il salto condizionato

(gdb) x/i $ebp - 8
0xbfff7d10:     add    BYTE PTR [eax],al
(gdb) x/x 0xbfff7d10
0xbfff7d10:     0x00000000
(gdb) x/d 0xbfff7d10
0xbfff7d10:     0
(gdb) print $ebp - 8
$2 = (void *) 0xbfff7d10
(gdb) x/x $2
0xbfff7d10:     0x00000000
(gdb) x/d $2
0xbfff7d10:     0

Sia con il metodo print che analizzando con x/x direttamente sulla locazione di memoria, che con x/d lo visualizziamo in decimale, controlliamo che ha valore 0 e che quindi il jle ha condizione positiva per effettuare il salto.

Ora invece cerchiamo di capire cosa fanno le altre operazioni. Analizzando la locazione 0×80483de è presente un’istruzione che essenzialmente muove il valore dell’indirizzo 0×80484d0 nell’indirizzo esp. Verifichiamo quindi cosa contiene l’indirizzo 0×80484d0:

(gdb) x/x 0x80484d0
0x80484d0:      0x6c6c6548
(gdb) x/6cb 0x80484d0
0x80484d0:      72 'H'  101 'e' 108 'l' 108 'l' 111 'o' 44 ','
(gdb) x/s 0x80484d0
0x80484d0:       "Hello, world!"

Notiamo che contiene il valore 0×6c6c6548 che codificato in ASCII corrisponde ad “Hello,” infatti tramite l’opzione c effettua la codifica di ogni singolo byte in ASCII mentre con s converte proprio tutta la stringa.

Per ultimo l’istruzione alla locazione 0×80483ea non fa altro che incrementare di uno il valore all’interno di ebp – 8, la quale poi viene poi controllata dal salto condizionato.

Giusto per completare il discorso, procediamo con diversi nexti fino alla fine del del ciclo for ossia quando l’ebp – 8 contiene il valore 10. Infine controlliamo che l’eip punta all’uscita del ciclo:

(gdb) x/d 0xbfff7d10
0xbfff7d10:     10
(gdb) i r eip
eip            0x80483ee        0x80483ee
(gdb) x/10i $eip
0x80483ee :    cmp    DWORD PTR [ebp-0x8],0x9
0x80483f2 :    jle    0x80483de
0x80483f4 :    mov    eax,0x0
0x80483f9 :    add    esp,0x14
0x80483fc :    pop    ecx
0x80483fd :    pop    ebp
0x80483fe :    lea    esp,[ecx-0x4]
0x8048401 :    ret
0x8048402:      nop
0x8048403:      nop
(gdb) nexti
0x080483f2      6         for (i=0; i < 10; i++)
(gdb) i r eip
eip            0x80483f2        0x80483f2
(gdb) nexti
10        return 0;
(gdb) i r eip
eip            0x80483f4        0x80483f4

Possiamo vedere dall’output come appunto l’eip punta 0×80483f4 e non più 0×80483de come nei precedenti casi, confermando appunto l’uscita dal ciclo for.

Bene ora che abbiamo appreso i concetti base del debugging immaginate cosa potrebbe capitare se un attaccante riesca a sovrascrivere l‘eip facendolo puntare ad una locazione di memoria arbitraria a lui congeniale!

Buon degugging a tutti.

Related Tags: , , ,

Leave a Comment

In questa categoria

Le basi del BOF Buffer overflow
Le basi del BOF Buffer overflow

Rispolvero tra le bozze un vecchio post che avevo scritto secoli e secoli fa. Non so per quale diavolo di motivo non [Read More]

Windows 7 e Linux: si parlano ma a fatica!
Windows 7 e Linux: si parlano ma a fatica!

Avete mai provato a condividere una cartella su Windows?… bene! Avete mai provato ad accedere a tale cartella da [Read More]

Il miglior client IRC?.. Irssi

In questo post vediamo velocemente come configurare Irssi, un client IRC da shell. Installazione Se utilizzate una [Read More]

Montare samba sul file system

Potrebbe essere necessario nelle varie configurazioni di dover montare sul proprio hd delle macchine remote con [Read More]

Twitter