Aggiornato al : feb 18, 2008



Buffer Overflow di Heap

I BOF di Heap sono chiamati così perché interessano l’area di memoria detta Heap, che contiene le variabili allocate dinamicamente. Lo heap è diverso dallo stack, in quanto quest’area di memoria rimane allocata finchè non è esplicitamente liberata, quindi un buffer overflow può essere effettuato ed essere notato solo in seguito, quando l’area è effettivamente utilizzata. Non esiste il concetto di EIP, ma ci sono altri concetti importanti che possono essere sfruttati per ottenere buffer overflow.
Questo tipo di BOF è noto ed è sfruttato da molto tempo, ma se ne parla sempre meno di quello di stack, soprattutto perché è molto più difficile da sfruttare rispetto a quest’ultimo. Comunque non deve essere sottovalutato, perché si tratta di un BOF che può essere estremamente pericoloso e per il quale esistono diverse tecniche, che possono portare a diverse conseguenze. Le tecniche più note sono le seguenti:
Attacchi basati su malloc() e funzioni simili: le funzioni dei vari linguaggi di programmazione interessate a questo tipo di BOF sono chiaramente quelle utilizzate per l’allocazione dinamica delle variabili, ad esempio malloc() di C, HeapAlloc() di Windows e new() di C++. I blocchi di heap allocati da queste variabili (in figura vediamo malloc()), sono generalmente vicini e, dato che non ci sono controlli, è molto semplice inserire nello spazio di A più di 10 elementi e far sì che vadano a sovrascrivere B e volendo anche C.

La stessa cosa può accadere con l’area di memoria BSS, che è l’area che contiene i dati non inizializzati. Anche qui, infatti, quando inizializziamo questi dati, potremmo inserire più dati dello spazio a disposizione, causando un overflow con conseguente sovrascrittura degli spazi adiacenti.
Esistono diverse implementazioni di questo tipo di attacco, in genere fortemente architecture dependent. Ad esempio, uno dei più noti è quello che sfrutta le vulnerabilità della funzione malloc() di Unix, che si basa sulla versione di Doug Lea. In questa implementazione, esistono alcuni bit che possono essere “exploitati”, in particolare la macro unlink() contenuta nella funzione free(). L’exploit può avvenire in due diverse modalità, chiamate “forward consolidation” e “backward consolidation”.
In sostanza, qual è l’obiettivo di questo tipo di BOF? Lo scopo è quello di causare l’overflow di un buffer A in modo da scrivere sul buffer adiacente B il codice di attacco; in questo modo, quando il programma tenterà di usare i dati contenuti in B, eseguirà invece il codice di attacco.
Se ad esempio in memoria è presente un valore di autenticazione, chi attacca può modificarlo per diventare un utente privilegiato, oppure può cambiare alcuni flag in memoria per causare un flusso di esecuzione del programma completamente diverso da quello normale.
Attacchi basati sulla sovrascrittura di puntatori: lo scopo di questi attacchi è quello di effettuare l’overflow di un buffer adiacente ad un puntatore in modo da corrompere quest’ultimo e farlo puntare a qualche altra locazione… La figura esemplifica quanto detto:

Si tratta di un tipo di attacco estremamente portabile; inoltre, può interessare anche l’area di memoria BSS.
Attacchi basati su puntatori a funzioni: come nel caso dei BOF di stack, anche qui abbiamo questa tipologia di attacco, dato che i puntatori possono trovarsi non solo nello stack, ma anche nello heap (e anche nell’area BSS). L’obiettivo è quello di effettuare l’overflow di un buffer vicino ad un puntatore in modo da corrompere quest’ultimo e farlo puntare alla locazione dove è stato inserito il codice di attacco. La figura seguente esemplifica quanto detto:

Per concludere, vediamo un esempio di buffer overflow di heap:


#include
#include
int main(int argc, char **argv) {
int *ret;
char *shellcode = (char*)malloc(64);
sprintf(shellcode,
"xebx1fx5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0b"
"x89xf3x8dx4ex08x8dx56x0cxcdx80x31xdbx89xd8x40xcd"
"x80xe8xdcxffxffxff/bin/sh");
*((int*)&ret+2) = (int)shellcode;
return 0;
}

Il programma appartiene all’utente root ma è impostato il bit SUID, che consente a chiunque di eseguirlo con privilegi di root.
In particolare, il programma alloca una parte di memoria nello heap e vi copia dentro lo shellcode. Subito dopo l’indirizzo di ritorno del main è sovrascritto dall’indirizzo dello shellcode, in modo che quando il main ritorna, fornisce una shell.

Alla ricerca di buffer overflow

Abbiamo visto le più comuni tipologie di buffer overflow. Bisogna comunque tenere conto del fatto che gli esempi finora visti sono scritti per puro scopo didattico, in quanto non troveremo in giro programmi così, pronti per essere sfruttati per accedere al sistema di turno. Chi attacca generalmente non prova a casaccio, analizza il codice del programma (se il programma è open source il lavoro è notevolmente semplificato) alla ricerca di vulnerabilità da sfruttare, o aspetta che sia qualcun altro a farlo; quando poi si sa che la versione x del programma y è affetta da una certa vulnerabilità, allora è il momento di creare l’exploit che permetta di utilizzarla (appunto per questo è bene scaricare sempre le patch per i nostri programmi). Lo scopo di questi programmi è quindi quello di permettere la comprensione di cosa sono e come funzionano i diversi tipi di buffer overflow, in modo da assumere in fase di programmazione un atteggiamento più responsabile e più rivolto alla sicurezza. Inoltre sarà possibile analizzare i propri programmi alla ricerca di vulnerabilità, prima che qualcuno le ricerchi al posto nostro e le sfrutti. Questa analisi può essere fatta a diversi livelli, per ognuno dei quali esistono dei tool appositi. Vediamo quali:
Lexical static code analyzers: generalmente questi tool analizzano il codice confrontandolo con un set di “cattivi” modelli, ad esempio la funzione gets(). Questi tool possono essere semplici come grep o più complessi come RATS e Flawfinder.
Semantic static code analyzers: questi tool si differenziano da quelli precedenti perché in più considerano anche il contesto in cui ci si trova e generalmente emettono i loro messaggi sotto forma di warning. Anche i warning dati dai compilatori possono essere considerati di questo tipo.
Artificial intelligence or learning engines for static source code analysis: questi tool analizzano il codice utilizzando diversi metodi, spesso combinazioni di identificazione sia lessicale che semantica. Inoltre è presente un sistema di apprendimento che migliora via via le analisi effettuate. Un esempio è il programma Application Defense Developer.
Dynamic program tracers: si tratta di tool che analizzano il programma a runtime e, tra le altre cose, sono in grado di individuare BOF di vario tipo. Un esempio è il programma Rational Purify.
Black box testing with fault injection and stress testing, a.k.a. fuzzing: il Fuzzing è una tecnica mediante la quale si prova a dare al programma molti tipi di input, diversi tra loro in struttura e dimensioni, in modo da vedere come il programma si comporta. È possibile stabilire come devono essere questi input di prova.
Reverse engineering: si tratta di decompilare il codice binario in assembly o, se possibile, in un linguaggio di alto livello, in modo da studiarlo in modo più semplice.
Bug-specific binary auditing: analizza il programma compilato con una tecnica euristica, cercando di trovare eventuali buffer overflow. Si può considerare come un’analisi lessicale e semantica, ma portata avanti sul codice assembly. Un esempio è Bugscan.

Difesa contro i Buffer Overflow e… nuovi attacchi

Come abbiamo avuto modo di notare, i BOF sono un problema tutt’altro che semplice da risolvere, in quanto le sue molteplici varianti non consentono di trovare una soluzione unica e definitiva. Comunque, fin da quando è stata chiara la reale minaccia rappresentata dai BOF, si è cercato di arginare per quanto possibile il problema. Diverse sono state le soluzioni trovate e diverse sono state le tecniche da parte dei creatori di exploit per cercare di eluderle. Cerchiamo di analizzare le più importanti e note di queste soluzioni, vedendo anche come è possibile bypassarle.

Difesa: Scelta del linguaggio di programmazione da utilizzare

Sebbene non si tratti di una vera e propria soluzione, è bene conoscere le differenze tra i vari linguaggi di programmazione nel trattamento dei tipi di dato inerenti ai BOF, cioè array e stringhe. Infatti, la scelta del linguaggio di programmazione può avere un effetto significativo sull’apparizione di BOF.
Buona parte dei software, compreso il sistema operativo Unix, sono scritti in C e C++, che non forniscono la giusta protezione contro l’accesso e la sovrascrittura dei dati in memoria (attraverso i puntatori è possibile praticamente spostarsi e scrivere in memoria pressoché dovunque) e contro la scrittura in un array al di fuori dei suoi confini (è il problema principale che causa il buffer overflow). Alcune variazioni del C (ad esempio Cyclone e D) usano svariate tecniche per impedire o limitare alcuni usi scorretti dei puntatori.
Altri linguaggi di programmazione forniscono controlli a runtime che possono inviare warning o generare eccezioni quando si tenta di sovrascrivere dati (es. Java, Python, Ada, Lisp, Smalltalk, ecc.). Quasi ogni tipo di linguaggio “type safe” o interpretato offre protezione contro i buffer overflow, segnalando un errore ben definito.

Difesa: scrivere codice corretto

Sarebbe una soluzione a tutti i problemi sia di BOF, che di exploit in generale…se solo fosse attuabile. Purtroppo rimane semplicemente un’utopia, perché errare è umano, quindi quando si programma è inevitabile che si commettano errori o leggerezze che poi possono portare a delle vere e proprie falle di sicurezza. Inoltre, l’uso di librerie esterne permette spesso di svolgere grosso del lavoro, offrendo un approccio al problema da risolvere più semplice e meno dettagliato, ma spesso nasconde altri errori causati involontariamente da terzi. I software finora sviluppati e l’enorme numero di patch presenti per alcuni di essi lo conferma. Tuttavia, è possibile seguire delle semplici norme che, se da un lato non risolvono il problema, dall’altro possono cercare di migliorare la situazione, rendendo magari la vita più difficile all’hacker di turno. Ad esempio, senza scomodare (almeno per ora) soluzioni esterne, è bene sostituire strcpy con strncpy, strcat con strncat, gets con fgets e sprintf con snprintf. Ovviamente si tratta solo di un primo rudimentale livello di sicurezza, vediamo adesso altre soluzioni più complesse.


Difesa: Attenzione ai programmi SUID

SUID sta per “Set-User-ID” e indica quei programmi che vanno in esecuzione con privilegi di root, chiunque sia ad eseguirli. Alcuni di questi sono necessari per effettuare operazioni comuni, altrimenti possibili solo all’utente root, altri però non lo sono affatto, ma possono rappresentare un problema, dato che possono essere sfruttati da un malintenzionato attraverso un buffer overflow, al termine del quale si troverà con privilegi di root e quindi avrà il controllo della macchina. Dunque, il consiglio è quello di verificare che sul sistema non ci siano troppi programmi di questo tipo, magari normalizzando quelli che non si utilizzano mai.

Difesa: Uso di librerie “safe”

I buffer overflow sono così comuni perché il linguaggio di programmazione utilizzato è a volte poco sicuro. Ad esempio, il linguaggio C non controlla automaticamente che i confini degli array siano rispettati, né che i puntatori siano utilizzati in modo corretto, si tratta di controlli che spettano all’utente. Ma anche le librerie standard (libC) presenti all’interno di esso e che vengono costantemente utilizzate per operazioni come I/O, manipolazione di stringhe, ecc. sono poco sicure.
Un esempio di funzioni insicure:
gets(): utilizzata per inserire in un buffer una stringa presa dall’esterno (standard input). È la funzione non sicura per eccellenza, in quanto non effettua controlli di nessun tipo, quindi inserendo anche solo un carattere in più della lunghezza del buffer, il buffer overflow è assicurato.
strcpy() e strcat(): utilizzate rispettivamente per copiare una stringa all’interno di un’altra e per concatenare due stringhe. Il problema sta nel fatto che non vengono fatti controlli sulla dimensione della stringa di destinazione, quindi il buffer overflow è in agguato. Le versioni strncpy() e strncat(), utilizzate per copiare/concatenare solo alcuni caratteri della stringa sorgente, sono più sicure.
Format functions (es. printf(), sprintf(), fprintf(), ecc.): si tratta di funzioni che prendono come parametro un certo numero di argomenti che rappresentano tipi di dato primitivi di C, che poi vengono stampati sotto forma di stringa in modo che l’utente possa comprenderli. Questi parametri sono salvati sullo stack per valore o per riferimento. A questo punto la funzione analizza la stringa presa in input, leggendo un carattere alla volta. Se non trova il simbolo “%”, allora il carattere è copiato direttamente in output, altrimenti controlla il carattere dopo “%”, che indica il tipo di dato da stampare e va a prendere quest’ultimo sullo stack. Da notare che la funzione sprintf() copia una stringa in un’altra, ma mentre la destinazione è un buffer di dimensioni fisse, la sorgente non lo è, dunque si possono avere gli stessi problemi di buffer overflow presenti in strcpy(). Comunque, i veri problemi si hanno se, per ignoranza o dimenticanza, oppure volontariamente, non si forniscono alla funzione i formati dei tipi di dato da stampare. È possibile utilizzare ad esempio %s e %x per leggere dati dallo stack o da altre locazioni di memoria e %n per scriverci sopra. Questo tipo di vulnerabilità è stato sottovalutato fino al 1999, finchè non cominciarono a comparire i primi exploit che dimostrarono il contrario e che diedero origine a un nuovo filone di studio, chiamato “Format String vulnerabilities”.
scanf(): è utilizzata per inserire in una variabile un dato fornito in input. Può anche inserire una stringa in un array di caratteri già creato e di dimensioni fisse. È proprio qui che nasce il problema: la funzione non effettua alcun controllo, dunque è possibile inserire una stringa di dimensioni maggiori dello spazio del buffer, causando inevitabilmente un buffer overflow.
Per ovviare al problema di queste funzioni non sicure, il cui uso può portare a vere e proprie falle di sicurezza, sono state quindi create delle librerie di tipo “safe”, cioè librerie ben scritte e testate che vanno a sostituire quelle classiche (in C libC) e si occupano di effettuare automaticamente la gestione dei buffer e il controllo dei confini, specialmente laddove i BOF si presentano, cioè stringhe e array. L’uso di queste librerie effettivamente può essere utile per ridurre i BOF, ma da solo non basta ad arginare un fenomeno così vasto: sono infatti molti i BOF che riescono a “passare” lo stesso. Alcune librerie di questo tipo sono:
Libsafe: si tratta di una libreria dinamica caricata in memoria prima delle altre, che effettua l’overriding di alcune delle funzioni di libC. In particolare, Libsafe intercetta le chiamate a queste funzioni e usa invece la propria implementazione di queste funzioni. Dunque la semantica utilizzata è sempre la stessa, ma Libsafe aggiunge il controllo dei confini per evitare Buffer Overflow. Le funzioni sovrascritte sono quelle meno sicure, cioè strcpy, strcat, getwd, gets, scanf, realpath e sprintf. A titolo esemplificativo, notare il confronto tra la funzione strcpy di libC e quella di Libsafe:

char * strcpy(char * dest,const char *src) {
char *tmp = dest;
while ((*dest++ = *src++) != '')
/* nothing */;
return tmp;
}

Come si nota facilmente, nessun controllo è effettuato per verificare se la stringa di destinazione è più piccola di quella su cui copiarla. Vediamo adesso l’implementazione di Libsafe:

char *strcpy(char *dest, const char *src) {
...
if ((len = strnlen(src, max_size)) == max_size)
_libsafe_die("Overflow caused by strcpy()");
real_memcpy(dest, src, len + 1);
return dest;
}

Senza entrare nei dettagli implementativi, è facile notare il controllo effettuato sulla lunghezza della stringa da copiare.
Un problema di Libsafe è che non fornisce alcuna protezione per gli eseguibili prodotti da compilatori che non scrivono il frame pointer sullo stack o che non scrivono l’indirizzo di ritorno immediatamente dopo il frame pointer. Per maggiori informazioni http://www.research.avayalabs.com/project/libsafe.html
The Better String Library: è un’astrazione di un tipo stringa che è decisamente migliore dell’implementazione presente in C (array di char) e a quella di C++ (std::string), delle quali si propone come completo rimpiazzamento. Tra le funzionalità più importanti, oltre alla maggiore facilità di manipolazione delle stringhe, alle maggiori performance e alla portabilità, è annoverata anche la sensibile diminuzione dei problemi di buffer overflow. Per maggiori informazioni http://bstring.sourceforge.net/
Arri Buffer API: fornisce un’interfaccia per creare, scrivere, copiare, duplicare, cancellare e deallocare array. Contiene anche API per manipolare le stringhe, utilizzare i socket, utilizzare l’I/O e funzioni di alto livello per C, che permettono, tra le altre cose, di ridurre il problema dei BOF. Per maggiori informazioni https://gna.org/projects/arri/
Vstr: si tratta di una libreria che fornisce un’implementazione di stringa diversa da quella a cui il C ci ha abituati. Infatti, la stringa non è più vista come qualcosa a cui si può accedere attraverso un puntatore di tipo char, ma come un contenitore formato da più blocchi. Attraverso le funzioni readv() e writev() è possibile rispettivamente leggere e scrivere sulla stringa senza bisogno di occuparsi di allocare o spostare memoria. Anche questa libreria fornisce un valido aiuto per l’eliminazione dei buffer overflow. Per maggiori informazioni http://www.and.org/vstr/
Funzione strlcpy: è nata per rimpiazzare le funzioni di C strcpy e strncpy, alle quali assomiglia molto, dato che è dichiarata nel seguente modo:
size_t strlcpy(char * destination, const char * source, size_t size);
Offre due caratteristiche che possono essere d’aiuto agli sviluppatori: una stringa non vuota copiata da strlcpy è sempre terminata con nul, rendendo più semplice trovare la fine della stringa; inoltre la funzione prende in input anche la lunghezza della stringa, permettendo di evitare il BOF quando la stringa di origine è più grande di quella di destinazione.
Esiste anche la funzione strlcat, che va a sostituire la funzione di C strcat.