Exploit: intro
Posted: Agosto 20th, 2011 | Author: packz | Filed under: Guide | Tags: assembler, c, exploit, gdb, low level | 2 Comments »L’hacking da che mondo e mondo tratta l’usare uno strumento in un modo non concepito dal suo inventore; chi si occupa di programmazione e non solo si deve preoccupare in maniera critica di non esporre il proprio lavoro a mani poco scrupolose. Io non mi ritengo un esperto ma ci sono alcune cose che mi affascinano, tra queste la programmazione a basso livello e gli exploit (due cose molto legate tra loro).
Chi si fosse chiesto come sia possibile entrare in un computer altrui questo vuol essere un intro non esauriente (spero ulteriori posts seguiranno) che possa gettare luce.
Partiamo da un programma molto semplice
int bau(char* porcodio, int alpha) { return porcodio[0] + alpha; } int main(int argc, char* argv[]) { return bau("bastardo", 137); } |
Questo programma non fa niente di utile (se non un po’ di blasfemia spicciola) ma servirà a noi per capire come funziona sotto il cofano un programma per computer (quanto segue è tutto eseguito su sistemi x86).
Per prima cosa compiliamolo
$ gcc boh.c -o boh
e lanciamolo controllando il valore di ritorno
$ ./boh $ echo $? 235
Entusiasmante vero (235 = ‘p’+ 137 dove il valore ASCII di “p” è 98)? ok, adesso proviamo a vedere passo passo cosa succede; utilizziamo il fido compagno di ogni programmatore, il prode gdb: lanciamolo e impostiamolo in maniera tale da fermarsi alla funzione main e mostrare le istruzioni a livello assembler
$ gdb -q ./boh Reading symbols from /sti/cazzi/boh...(no debugging symbols found)...done. (gdb) b main Breakpoint 1 at 0x80483cb (gdb) display /3i $pc
Piccolo appunto: il linguaggio assembler è il linguaggio a più basso livello con cui si può dialogare con un processore ed è ovviamente unico per ogni tipologia di architettura: l’assembler per x86 è diverso da quello per x86_64 e quello per ARM, ognuno ha le sue peculiarità ma la cosa più importante è capire che sono simili ma incompatibili.
Per chi non conoscesse il linguaggio assembler un piccolo riassunto: siccome parla con il processore le sue variabili possono essere solo i vari registri del processore stesso; in un sistema x86 si hanno i registri eax, ebx, ecx, edx che sono registri generici più dei registri più specifici come %ebp e %esp che gestiscono lo stack di cui parleremo più diffusamente subito dopo. Un registro molto importante è %eip che punta all’indirizzo di memoria che contiene la prossima istruzione da eseguire (in gdb è $pc non so perché).
Lo stack è una parte fondamentale del sistema: è in pratica una area di memoria che contiene le variabili che una data routine deve gestire; infatti siccome i registri sono in numero limitato si ha bisogno di definire una area di memoria per gestire i dati. Lo stack permette di isolare le variabili locali proprie di una data routine definendo la base dello stack (puntata dal registro %ebp) e la cima dello stack (puntata dal registro %esp). Le istruzioni assembler per gestire lo stack sono push e pop, la prima inserisce nello stack il valore del registro indicato come argomento, il secondo invece fa l’opposto; la posizione nello stack è indicata da %esp che viene decrementato nel primo caso e incrementato nel secondo (nei sistemi x86 lo stack cresce verso il basso). In pratica lo stack è una coda LIFO (Last In First Out).
Facciamo girare il nostro programma e vediamo cosa succede in pratica
(gdb) r Starting program: /sti/cazzi/boh Breakpoint 1, 0x080483cb in main () 1: x/3i $pc => 0x80483cb <main+6>: movl $0x89,0x4(%esp) 0x80483d3 <main+14>: movl $0x80484b0,(%esp) 0x80483da <main+21>: call 0x80483b4 <bau>
L’istruzione puntata dalla freccia muove nello stack il valore 137 mentre quella seguente mette la stringa “bastardo”
(gdb) x/s 0x80484b0 0x80484b0: "bastardo"
Infine, l’ultima istruzione, chiama la funzione bau. Il perché di questa procedura consiste nel fatto che i parametri di una funzione devono essere passati nello stack in ordine inverso rispetto all’ordine nella definizione. Ultimo particolare da sapere è che l’istruzione call fa una push nello stack del registro %eip e salta all’indirizzo indicato come argomento.
Saltiamo alla funzione bau() eseguendo il comando si 3 volte così da ritrovarci in quello che viene chiamato prologo
0x080483b4 in bau () 1: x/3i $pc => 0x80483b4 <bau>: push %ebp 0x80483b5 <bau+1>: mov %esp,%ebp 0x80483b7 <bau+3>: mov 0x8(%ebp),%eax
Le prime due istruzioni sono chiamate in questo modo perché definiscono il frame per la funzione corrente, cioè il contesto in memoria dove collocare le variabili locali che saranno utilizzate da questa funzione; per farlo si imposta la base dello stack a coincidere con la cima dello stack. Tutto preceduto dal salvataggio del frame della precedente funzione.
In gdb è possibile ricavare questa info comodamente
(gdb) info frame Stack level 0, frame at 0xbffff310: eip = 0x80483b7 in bau; saved eip 0x80483df called by frame at 0xbffff320 Arglist at 0xbffff308, args: Locals at 0xbffff308, Previous frame's sp is 0xbffff310 Saved registers: ebp at 0xbffff308, eip at 0xbffff30c
Quindi riassumendo, all’avvio di una nuova funzione nello stack saranno posizionate le seguenti variabili
secondo argomento primo argomento indirizzo di ritorno vecchio ebp
una sopra l’altra. Sempre gdb ci aiuta
(gdb) x/4a $ebp 0xbffff308: 0xbffff318 0x80483df <main+26> 0x80484b0 0x89
Se analizziamo in toto la funzione bau, possiamo notare quello che si chiama epilogo nelle due ultime righe
(gdb) disassemble bau Dump of assembler code for function bau: 0x080483b4 <+0>: push %ebp 0x080483b5 <+1>: mov %esp,%ebp => 0x080483b7 <+3>: mov 0x8(%ebp),%eax 0x080483ba <+6>: movzbl (%eax),%eax 0x080483bd <+9>: movsbl %al,%eax 0x080483c0 <+12>: add 0xc(%ebp),%eax 0x080483c3 <+15>: pop %ebp 0x080483c4 <+16>: ret End of assembler dump.
il cui scopo è ripristinare il frame precedente e riportare il flusso di esecuzione a dove era stata chiamata la funzione bau() recuperando dallo stack l’indirizzo di ritorno (ricordate cosa fa call? fa una push del registro %eip, ret lo recupera).
Bene, direte voi, ma chi se ne frega? vogliamo vedere la ciccia!!1!!!! Ok, state calmi, passiamo ad un altro programma tipo questo
int main(int argc, char* argv[]){ char buffer[256]; return strcpy(buffer, argv[1]); } |
Questo programma ha un difetto sostanziale, non controlla l’input dell’utente! abbiamo un buffer che può contenere 256 fucking char (grossi un byte) e ci copiamo dentro (senza fucking controllare) il primo argomento passato alla funzione. Quando non si controlla quello che si inserisce lato utente aspettatevi un problema, può essere un programma in C, in PHP o un controller di una view di una webapp.
Se proviamo a lanciare il programma passandogli 300 caratteri “A” cosa succede?
$ ./c `python -c 'print "A"*300'` Errore di segmentazione (core dumped)
Oh, crasha! mio dio. Ma non finisce qui, se controlliamo con gdb il file core che ha creato scopriamo una cosa interessante
$ gdb -q ./c core Reading symbols from /tmp/c...(no debugging symbols found)...done. [New Thread 19714] warning: Can't read pathname for load map: Errore di input/output. Reading symbols from /lib/i386-linux-gnu/i686/cmov/libc.so.6...Reading symbols from /usr/lib/debug/lib/i386-linux-gnu/i686/cmov/libc-2.13.so...done. done. Loaded symbols for /lib/i386-linux-gnu/i686/cmov/libc.so.6 Reading symbols from /lib/ld-linux.so.2...(no debugging symbols found)...done. Loaded symbols for /lib/ld-linux.so.2 Core was generated by `./c AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'. Program terminated with signal 11, Segmentation fault. #0 0x41414141 in ?? ()
per capirci
(gdb) print $pc $1 = (void (*)()) 0x41414141
Abbiamo sovrascritto il registro $pc, cioè (in teoria) abbiamo modificato il flusso del programma!!!Questo è successo semplicemente perché strcpy() è andata a copiare a partire dalla parte dello stack in cui era definita la variabile buffer andando a sbordare sopra il frame precedente e sovrascrivendo sia il vecchio %ebp che %eip. Infatti controllando i registri si conferma quanto detto
(gdb) info register eax 0xbfce7c20 -1076986848 ecx 0x0 0 edx 0x12d 301 ebx 0xb76d3ff4 -1217576972 esp 0xbfce7d30 0xbfce7d30 ebp 0x41414141 0x41414141 esi 0x0 0 edi 0x0 0 eip 0x41414141 0x41414141 eflags 0x10246 [ PF ZF IF RF ] cs 0x73 115 ss 0x7b 123 ds 0x7b 123 es 0x7b 123 fs 0x0 0 gs 0x33 51
Certo la strada è ancora lunga, ma in potenziale possiamo aprire una shell, ma questo è argomento di una prossima puntata.