Attacchi alle applicazioni basati su buffer overflow

27
Università di Catania - Corso di laurea in Ingegneria Informatica Sicurezza nei Sistemi Informativi A.A. 2005/2006 Attacchi alle applicazioni basati su Buffer Overflow Fazio Giacomo Antonino

Transcript of Attacchi alle applicazioni basati su buffer overflow

Page 1: Attacchi alle applicazioni basati su buffer overflow

Università di Catania - Corso di laurea in Ingegneria Informatica

Sicurezza nei Sistemi Informativi A.A. 2005/2006

Attacchi alle applicazioni basati su Buffer Overflow

Fazio Giacomo Antonino

Page 2: Attacchi alle applicazioni basati su buffer overflow

1. Introduzione Il buffer overflow (spesso abbreviato in BOF) è una delle tecniche più avanzate di hacking del software. Tutto nasce da un difetto che può caratterizzare un determinato software e, se utilizzato a dovere, può agevolare l'accesso a qualsiasi sistema che utilizza il software in questione. Spesso, infatti, si sente parlare di “exploit”, ossia metodi ad hoc che utilizzano le vulnerabilità scoperte in questo o in quel software e che permettono all’utilizzatore di acquisire privilegi che non gli spettano (ad esempio i tanto agognati privilegi di root) o di portare al “denial of service” del computer attaccato. Molti di questi exploit utilizzano per i loro scopi buffer overflow. Questo tipo di debolezza dei programmi è noto da molto tempo, ma solo di recente la sua conoscenza si è diffusa tanto da permettere anche a dei cracker dilettanti di sfruttarla per bloccare o prendere il controllo di altri computer collegati in rete. In poche parole, il buffer overflow consiste nel fornire al programma più dati di quanto esso si aspetti di ricevere, facendo in modo che una parte di questi dati vadano scritti in zone di memoria dove ci sono, o dovrebbero esserci, altri dati (da ciò il nome, che letteralmente significa “Trabocco dell’area di memoria”). Ad esempio, un programma definisce due variabili: una stringa A di 8 byte e un intero B di 2 byte. A è inizializzata con soli caratteri ‘0’ (ognuno dei quali occupa 1 byte, dunque sono 8 caratteri), mentre B contiene il numero 3. A A A A A A A A B B 0 0 0 0 0 0 0 0 0 3

Adesso supponiamo che sia previsto un inserimento della stringa A da parte dell’utente, ma che non si effettui un controllo sulla lunghezza dell’input inserito. In questo caso, i problemi si hanno se si prova ad inserire una stringa più lunga di 8 caratteri, che è lo spazio riservato nel buffer. Se ad esempio inseriamo la stringa “excessive”, essa occuperà 9 caratteri più il carattere di fine stringa, quindi la porzione di memoria successiva, che era occupata da B, verrà irrimediabilmente sovrascritta. La situazione sarà la seguente:

A A A A A A A A B B 'e' 'x' 'c' 'e' 's' 's' 'i' 'v' 'e' 0 A questo punto, se si prova a leggere l’intero che ci dovrebbe essere in B, un sistema big-endian che utilizza l’ASCII, leggerà ‘e’ seguita dallo ‘0’ come 25856. Se invece provassimo a scrivere una stringa ancora più lunga, essa invaderebbe anche l’area di memoria che si trova dopo di B, causando un errore di segmentation fault con la seguente terminazione forzata del processo. Tutto questo capita tipicamente nei sistemi operativi o nei programmi scritti nei linguaggi Assembly o C, usando funzioni di libreria di input/output che non fanno controlli sulle dimensioni dei dati trasferiti. Questo semplice esempio ci aiuta a capire di cosa è capace un buffer overflow: a seconda di cosa è stato sovrascritto e con quali valori, il programma può dare risultati errati o imprevedibili, bloccarsi, o (se è un driver di sistema o lo stesso sistema operativo) bloccare il computer. Non tutti i programmi sono vulnerabili a questo tipo di inconveniente, perché un dato programma sia a rischio è necessario che:

1. il programma preveda l'input di dati di lunghezza variabile e non nota a priori;

2

Page 3: Attacchi alle applicazioni basati su buffer overflow

2. che li immagazzini entro buffer allocati nel suo spazio di memoria dati vicini ad altre strutture dati vitali per il programma stesso;

3. che il programmatore non abbia implementato alcun mezzo di controllo della correttezza dell'input in corso.

La prima condizione è facilmente verificabile dalle specifiche del programma; le altre due invece sono interne ad esso e riguardano la sua completezza in senso teorico.

2. Tipi di buffer overflow Esistono diversi modi per portare avanti un buffer overflow. I più importanti sono: 2.1 Arithmetic Overflow Questo tipo di overflow è ottenuto quando il risultato prodotto da un calcolo è più grande delle spazio che dovrebbe contenerlo. Possiamo spiegarlo facilmente mediante un esempio. Avviamo la calcolatrice di Windows scegliendo la modalità scientifica dal menu, scriviamo ‘-1’ e premiamo su ‘Hex’. Vedremo così il valore esadecimale di -1, che è ‘FFFFFFFFFFFFFFFF’. Il problema nasce premendo ‘Dec’: ci aspetteremmo di rivedere il nostro ‘-1’, ma invece otteniamo il valore ‘18446744073709551615’ e ciò è dovuto al fatto che la calcolatrice ha cambiato il valore da “signed” a “unsigned”. Questo esempio serve a dimostrare che anche i programmatori potrebbero compiere lo stesso errore, trasformando un numero negativo in un numero elevatissimo che potrebbe creare un buffer overflow. 2.2 Buffer Overflow basati sulla memoria Si tratta degli attacchi di buffer overflow più noti e dannosi e generalmente vengono distinti in base all’area di memoria che vanno a interessare, in quanto sono possibili buffer overflow su tutte le aree di memoria su cui è possibile scrivere. Spesso è sufficiente un solo byte che vada al di là dello spazio assegnato, per rendere possibile un exploit. Quelli più diffusi sono i buffer overflow “di heap” e “di stack”, dato che si tratta delle aree di memoria più colpite. Di essi si parlerà diffusamente in seguito, non prima di aver fatto un rapido excursus sulla struttura della memoria e sul comportamento del processore in occasione dell’esecuzione di un programma, argomenti sicuramente propedeutici alla comprensione dei buffer overflow.

3. Struttura della memoria e comportamento del processore durante l’esecuzione di un programma Quando eseguiamo un programma, esso verrà caricato in memoria in maniera ben strutturata creando diverse zone:

1. .TEXT, che contiene il codice del programma in esecuzione ed è di sola lettura, infatti se si tentasse di scriverci sopra si incorrerebbe in un errore di Segmentation Fault;

2. zona dati, che contiene le variabili globali, sia inizializzate (contenute in una regione detta .DATA) che non inizializzate (contenute in una regione detta .BSS);

3. HEAP, generalmente posto dopo la zona dati, in cui vengono memorizzate le variabili allocate dinamicamente;

4. STACK, che contiene le variabili locali, gli argomenti delle funzioni, le informazioni di stato del chiamante (ad esempio il contenuto di alcuni registri della CPU), l’indirizzo di ritorno necessario per poter ritornare dalla funzione corrente e altre informazioni.

Naturalmente questi spazi non sono illimitati, bensì hanno una determinata lunghezza, dunque anche le variabili che vi verranno allocate dovranno rispettare tale lunghezza. In particolare, come si

3

Page 4: Attacchi alle applicazioni basati su buffer overflow

può vedere dalla seguente figura esemplificativa, lo heap e lo stack crescono in maniera diversa: il primo cresce verso l’alto, il secondo verso il basso.

Si tenga presente che stiamo prendendo in considerazione l’architettura Intel e che lo stack ha una direzione che può variare a seconda del sistema operativo utilizzato, ma ciò non influenza la comprensione degli argomenti trattati. Per quanto riguarda lo stack, qualcosa in più merita di essere specificata: esso è organizzato a pila, nel senso che l’ultimo dato inserito è il primo ad essere letto (LIFO, Last In Last Out); in Assembly esistono dei comandi (push e pop) che permettono rispettivamente di inserire e di prelevare valori in cima allo stack. Man mano che i dati vengono scritti nello stack, esso cresce verso il basso, quindi va da indirizzi di memoria alti ad indirizzi di memoria bassi. Se si cerca di effettuare una operazione di pop prima dell’inizio dello stack si ha un “buffer underflow”, se invece si effettuare un’operazione di push al di là dello stack si incorre in un “buffer overflow”. Anche il processore è interessato dall’esecuzione del programma, in particolare lo sono alcuni suoi registri, strettamente legati alla situazione della memoria durante l’esecuzione:

EBP, che è il puntatore alla base dello stack e, nel caso stiamo eseguendo una funzione, punta alla base della porzione di stack utilizzata da essa;

ESP, tramite il quale possiamo scorrere tutto lo stack per inserire o prelevare dati da un punto ben preciso di esso;

EIP, che punta alla prossima istruzione che la CPU dovrà eseguire dopo quella corrente. Per comprendere a fondo come questi registri e la memoria siano interessati dall’esecuzione del programma, osserviamone uno molto semplice, che chiamiamo example.c #include <stdio.h> void example(int, int, int); main() { example(0,1,2); } void example(int a, int b, int c) { int i=4; char[] buffer="hello"; }

4

Page 5: Attacchi alle applicazioni basati su buffer overflow

Il programma chiama la funzione example passandogli gli interi 0,1 e 2. La funzione si occupa di creare e assegnare la variabile i e la stringa buffer. Compiliamo il programma con uno dei numerosi compilatori C che si trovano in rete (io ho usato Dev C++) e poi disassembliamo l’eseguibile ottenuto example.exe con un disassembler, per esempio Disasm di Sang Cho. Chiaramente il codice macchina ottenuto è molto più lungo del codice C e sono presenti un gran numero di istruzioni dal significato molto poco intuitivo, ma non è difficile individuare quelle che ci interessano: :0040121E 6A02 push 002 :00401220 6A01 push 001 :00401222 6A00 push 000 :00401224 E80B000000 call 00401234 :00401229 83C410 add esp, 010 :0040122C C9 leave :0040122D C3 ret :0040122E 68 65 6C 6C 6F 00 ;;n "hello" ========= :00401234 55 push ebp :00401235 89E5 mov ebp, esp :00401237 83EC28 sub esp, 028 :0040123A C745FC04000000 mov dword[ebp-04], 00000004 :00401241 8D45E0 lea eax, dword[ebp-20] :00401244 8B152E124000 mov edx, dword[0040122E] (StringData)"hello" :0040124A 8955E0 mov dword[ebp-20], edx :0040124D 0FB70532124000 movzx eax, word[00401232] :00401254 668945E4 mov word[ebp-1C], ax :00401258 C9 leave :00401259 C3 ret

Le prime tre istruzioni sono tre operazioni di push, che inseriscono i valori 2, 1 e 0 nello stack (sono i tre parametri della funzione example, inseriti sullo stack in ordine inverso), successivamente si ha una CALL, utilizzata per chiamare la funzione example, infatti si salta all’indirizzo 00401234. Da notare che, ogni qualvolta bisogna fare una CALL, quindi anche in questo caso, il processore salva il valore attuale di EIP nello stack e poi lo modifica per effettuare un salto incondizionato alla funzione, in modo da poterlo ripristinare al termine della funzione, per poter riprendere l’esecuzione dall’istruzione successiva alla chiamata. Siamo all’interno della funzione: per prima cosa EBP viene salvato sullo stack, in EBP viene memorizzato il valore di ESP (cioè l’inizio dello stack per la funzione) e viene sottratto a ESP lo spazio necessario per le variabili con una operazione di SUB. Le istruzioni successive riguardano l’allocazione e l’assegnazione delle variabili i e buff, inserite nello stack seguendo come sempre la modalità LIFO. Lo stack, quindi, in questo momento si presenta pressappoco così:

5

Page 6: Attacchi alle applicazioni basati su buffer overflow

Alla fine, mediante l’istruzione LEAVE, i registri EBP e ESP riacquisiscono i valori che avevano prima di chiamare la CALL e, mediante l’istruzione RET, si ritorna alla funzione principale utilizzando l’indirizzo di ritorno presente nello stack.

4. Buffer Overflow di Stack Come abbiamo detto precedentemente, il BOF si ha quando le variabili non rispettano lo spazio a loro assegnato e vanno a scrivere anche lo spazio al di là di esso, sovrascrivendo i dati precedentemente contenuti. In particolare questo tipo di BOF è quello in assoluto più diffuso e interessa lo stack. Ne esistono diverse varianti, è possibile comunque trovare in tutte delle similitudini, che riguardano in primis lo scopo finale, che è sempre quello di sovvertire la funzione del programma per direzionarlo secondo i propri scopi. Se il programma è sufficientemente privilegiato (ad esempio di tipo SUID), è possibile ottenere il controllo dell’host, generalmente attivando una shell locale, mediante la quale, con i privilegi di root, è praticamente possibile effettuare qualsiasi cosa. Per ottenere un BOF, sono necessari due passi principali:

1) Fare in modo che il codice che ci interessa sia nell’address space del programma 2) Fare in modo che il programma salti ad esso e lo esegua.

Questi due passi sono comunque in stretta correlazione, dato che se inseriamo il codice senza eseguirlo non abbiamo concluso nulla. 4.1 Fare in modo che il codice sia nell’address space del programma Per effettuare l’inserimento del codice, ci sono due modi:

1) Inserirlo manualmente (Code injection): il programma chiede in input una stringa, che verrà inserita dall’attaccante in modo da contenere istruzioni per la CPU. Questa stringa verrà inserita in un determinato buffer, senza necessità di effettuare l’overflow. In poche parole, abbiamo salvato il codice di attacco in un buffer;

2) Il codice si trova già lì: il codice che ci serve è già presente, bisogna solo parametrizzarlo a dovere. Ad esempio, se si ha in UNIX il codice exec(arg) con arg puntatore ad una stringa, basta fare in modo che arg punti a /bin/sh per avere una shell in locale.

4.2 Fare in modo che il programma salti al codice di attacco e lo esegua Per ottenere ciò ci sono diversi modi, ma lo scopo di base è quello di effettuare l’overflow di un buffer che non ha controlli sui confini (o se ci sono, sono molto deboli), in modo da corrompere un’area adiacente. I principali tipi sono:

Activation Records: si tratta della tipologia più diffusa, nota in genere con la frase “Smashing the stack”. Si utilizza all’interno di una funzione e consiste nell’effettuare l’overflow di un buffer, con lo scopo di arrivare a sovrascrivere l’EIP, cioè l’indirizzo di ritorno della funzione. Se esso fosse sovrascritto per sbaglio, o con codice a caso, si avrebbe semplicemente un errore di Segmentation Fault, ma se invece esso è sovrascritto con un indirizzo realmente esistente, il risultato è che si salta alla locazione da esso indicato e si esegue il codice lì presente. Pensiamo a cosa succede se questo indirizzo indica la locazione del codice di attacco…

Puntatori a funzioni: bisogna trovare un buffer vicino ad un puntatore a funzione. Effettuando l’overflow del buffer, viene corrotto anche il puntatore e si fa in modo che esso punti alla locazione del codice di attacco. Questo tipo di BOF può riguardare non solo lo stack, ma anche lo heap e le altre aree della memoria su cui è possibile scrivere.

Longjmp buffers: sfrutta un meccanismo presente in C che consente di salvare lo stato (checkpoint) di un buffer mediante il comando setjmp(buffer) e di ripristinarlo in seguito (rollback) in caso di bisogno mediante il comando longjmp(buffer). Come per i puntatori a funzioni, se abbiamo un buffer adiacente di cui è possibile effettuare l’overflow, potremmo

6

Page 7: Attacchi alle applicazioni basati su buffer overflow

corrompere anche lo stato del buffer di checkpoint in modo che, non appena viene chiamato il comando longjmp, si salta alla locazione del codice di attacco.

4.3 Combinare i due passi precedenti Come abbiamo detto precedentemente, i due passi precedenti sono collegati tra loro, quindi vanno utilizzati insieme. Spesso l’inserimento del codice di attacco e la sua esecuzione sono effettuati in una volta sola: basta trovare un buffer di cui sia possibile fare l’overflow e che si trovi in prossimità dell’EIP, inserire una stringa opportuna contenente il codice di attacco che effettui l’overflow del buffer e modifichi l’EIP. In questo modo abbiamo fatto sia la code injection che l’activation record. Comunque non per forza le due fasi devono avvenire simultaneamente. È possibile ad esempio che il buffer del caso precedente non abbia lo spazio necessario per contenere tutto il codice di attacco, dunque è necessario fare la code injection in un altro buffer di dimensione sufficiente e successivamente utilizzare il buffer vicino l’EIP solo per corrompere quest’ultimo realizzando l’activation record. Se non è necessario effettuare la code injection perché il codice è già presente, bisogna, come spiegato sopra, parametrizzare il codice presente in modo da fargli eseguire ciò che si vuole e poi effettuare l’overflow del buffer vicino l’EIP per far puntare quest’ultimo al codice parametrizzato. 4.4 Lo Shellcode Lo shellcode è un pezzo di codice macchina eseguito per sfruttare una vulnerabilità. Si tratta spesso di un codice che svolge un compito altamente specifico, che è verificato dal primo all’ultimo byte, perché anche un byte fuori posto potrebbe portare al crash dell’applicazione da “exploitare” o alla corruzione della memoria con il conseguente non funzionamento dell’applicazione; ciò potrebbe comportare il riavvio della macchina, che potrebbe avvenire dopo un tempo non proprio breve (soprattutto per quanto riguarda gli ambienti industriali) oppure l’amministratore potrebbe indagare sul crash dell’applicazione e scaricare di conseguenza l’upgrade che magari va a sistemare la falla della versione precedente. Questo porterebbe al completo fallimento del piano di attacco. Ecco perché lo shellcode deve sempre essere un codice preciso e valutato nei minimi dettagli. Una caratteristica importante dello shellcode è l’assoluta mancanza di portabilità tra i diversi sistemi. La maggior parte degli shellcode implementati, per documentare i quali esistono centinaia di testi in rete, sono realizzati per UNIX, in quanto le API di Windows complicano la creazione di shellcode per questo sistema operativo, anche se oggi la situazione sta cambiando rapidamente, grazie a testi specifici come “The Tao of Windows Buffer Overflow” o a shellcode come il “plug and play” shellcode. 4.5 Esempi di Buffer Overflow di Stack Come abbiamo detto, il BOF di stack può essere portato avanti in molti modi, vediamone un paio molto semplici. Programma 1 Si tratta di un programma C che mostra un esempio di buffer overflow che utilizza la funzione gets(), notoriamente pericolosa in quanto non controlla se la stringa immessa dall’utente è più lunga del buffer che dovrà contenerla. Proprio questo causa il buffer overflow che va a corrompere la variabile successiva, che contiene un comando da eseguire. Quindi, inserendo in input un’apposita stringa, sarà possibile eseguire praticamente qualsiasi comando. L’esempio è stato realizzato in ambiente Windows, ma sarebbe la stessa cosa in Linux, in quanto cambierebbero solo gli indirizzi ma non la sostanza.

7

Page 8: Attacchi alle applicazioni basati su buffer overflow

#include <stdio.h> void example(); char *p; int i; main () { example(); } void example() { char command[10]="calc"; char name[10]; printf("Inserisci un nome da dare a questo script "); gets(name); printf("Premere un tasto per eseguire il comando"); getchar(); p=&name[0]+25; for (i=30;i>=0;i--) { printf("\n %p = %c",p,*p); p=p-1; } system(command); } Il programma non fa altro che chiamare la funzione example(), la quale alloca dinamicamente la variabile command con valore calc, che rappresenta il comando che vogliamo eseguire (la semplice calcolatrice di Windows). Successivamente viene allocata dinamicamente la variabile name, in cui vogliamo inserire un nome da dare allo script, cosa che viene fatta richiamando la funzione gets(). In questo momento lo stack (solo la parte relativa alla nostra funzione) si presenta pressappoco in questo modo:

La restante parte serve per farci capire cosa sta succedendo in memoria, infatti ci mostra un’istantanea dello stack, in linea con lo schema mostrato sopra, cioè mostrando in alto gli indirizzi alti e in basso quelli bassi. Essa ci sarà utile nel momento in cui effettueremo l’overflow, per capire cosa effettivamente è accaduto in memoria. In particolare, in questa sezione utilizziamo le due variabili i e p, dichiarate come globali affinché si trovino fuori dallo stack della funzione example().

8

Page 9: Attacchi alle applicazioni basati su buffer overflow

Alla fine verrà lanciato sul sistema il comando command, che normalmente è la calcolatrice. Se compiliamo il programma con un compilatore C e lo mandiamo in esecuzione, ci verrà subito chiesto di dare un nome allo script. Inseriamo inizialmente la scritta hello, che rientra perfettamente nei limiti. Infatti il programma verrà eseguito perfettamente: ci verranno mostrati gli indirizzi della memoria e si avvierà la calcolatrice. Le due variabili si trovano entrambe nello stack. Come è possibile vedere anche dallo screenshot successivo, l’output ci mostra lo stack, notare la variabile name (che contiene la stringa hello) che si trova in testa allo stack e sotto la variabile command (che contiene la stringa calc) ed è separata da essa.

Se a questo punto proviamo a rimandare in esecuzione il programma, inserendo invece di hello la stringa xxxxxxxxxxxxxxxxxxxx, essa riempie il buffer di 10 caratteri a disposizione e poi sovrascrive quello che c’è dopo, arrivando a sovrascrivere anche la variabile command, che adesso conterrà il valore xxxx. Infatti, invece di avviare la calcolatrice, ci viene restituito un messaggio che ci dice che xxxx è un comando sconosciuto. Se però, invece di xxxx, in command ci fosse stato un comando realmente esistente, esso sarebbe andato in esecuzione. Per verificare ciò, inseriamo ad esempio la stringa xxxxxxxxxxxxxxxxcmd, il risultato sarà una shell di sistema in locale. Potremmo inserire anche altri comandi, con risultati ben peggiori… In questo esempio, in particolare, possiamo inserire, invece di cmd, qualsiasi comando di lunghezza fino a 10 lettere, dato che la variabile command è stata dichiarata in questo modo: char command[10]="calc"; Se invece fosse stata dichiarata in quest’altro modo: char command[]="calc"; sarebbe stato possibile inserire comandi di massimo 4 lettere. Quindi basta poco per causare danni di portata incalcolabile, nel primo caso il comando potrebbe anche essere format C:…

9

Page 10: Attacchi alle applicazioni basati su buffer overflow

Programma 2 Questo programma è realizzato in C come il primo, ma l’ho testato su un sistema Linux, in particolare sulla distribuzione Suse Linux 10.0. Si tratta di un ottimo esempio di activation records, in quanto viene sovrascritto l’indirizzo di ritorno di una funzione con l’indirizzo di un’altra funzione che si vuole eseguire e che dovrebbe contenere il codice di attacco. Il programma principale è contenuto nel file prog1.c: #include <stdio.h> void function (); void function () { printf("Ci sei riuscito!!!!\n\n"); exit(0); } main (int argc, char *argv[]) { char var[10]; strcpy(var,argv[1]); } Il programma non fa altro che prendere una stringa in ingresso e inserirla all’interno della variabile var, utilizzando la funzione strcpy(), anch’essa pericolosa perché non effettua controlli sui confini del buffer di destinazione. La funzione function() non viene mai chiamata dal programma. Il nostro obiettivo sarà quello di causare un buffer overflow, inserendo in ingresso una stringa più lunga dei 10 caratteri a disposizione e di sostituire l’indirizzo di ritorno di main() con quello della funzione function(), in modo che essa venga eseguita. Per fare ciò dobbiamo fare un po’ di prove.

10

Page 11: Attacchi alle applicazioni basati su buffer overflow

Cominciamo ad inserire tante x, vediamo che dalla 14a in poi, il programma va in Segmentation Fault. A questo punto avviamo il disassembler gdb (presente nei sistemi operativi Unix), dandogli come programma da disassemblare il nostro prog1 (gdb prog1) e disassembliamo la funzione function():

Vediamo che l’indirizzo di inizio della funzione function() è 0x08048438. Se in pratica riusciamo a fare in modo che il 15° elemento della stringa che diamo in input a prog1 sia questo indirizzo, il programma salterà alla funzione function() e avremo centrato l’obiettivo. Come fare a passargli l’indirizzo? Esso infatti è scritto in caratteri esadecimali e non ASCII. Scriviamo un piccolo exploit, contenuto nel programma exploit.c, che si occupa di convertire in ASCII e di passare al programma l’indirizzo da noi inserito in esadecimale. #include <stdio.h> main () { char buf[31],lancia[35]; int i; for (i=0; i<14; i++) { buf[i]= 'x'; } *(long *)&buf[14]=0x08048438; strcpy(lancia,"/home/giacomo/bof/prog1 "); strcat(lancia,buf); system(lancia); } Questo programma si occupa di lanciare il comando prog1 seguito da 14 caratteri x e dall’indirizzo della funzione function(). Il risultato sarà il seguente:

Ci siamo riusciti!

11

Page 12: Attacchi alle applicazioni basati su buffer overflow

Diamo un ulteriore tocco stilistico al nostro exploit, eliminando il ciclo for e inserendo l’argomento di prog1 tutto in esadecimale: #include <stdio.h> main () { char buf[31],lancia[35]; buf="\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61" "\x61\x61\x61\x61\x08\x04\x84\x38"; strcpy(lancia,"/home/giacomo/bof/prog1 "); strcat(lancia,buf); system(lancia); } 5. 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.

12

Page 13: Attacchi alle applicazioni basati su buffer overflow

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:

13

Page 14: Attacchi alle applicazioni basati su buffer overflow

Per concludere questo paragrafo, vediamo un esempio di buffer overflow di heap: #include <stdio.h> #include <stdlib.h> int main(int argc, char **argv) { int *ret; char *shellcode = (char*)malloc(64); sprintf(shellcode, "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/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.

6. 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.

14

Page 15: Attacchi alle applicazioni basati su buffer overflow

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.

7. Blaster: un worm costruito su un buffer overflow Blaster è un worm che si diffuse sui computer con sistema operativo Microsoft Windows XP e Windows 2000 durante il mese di agosto 2003. I primi computer infetti dal worm furono rilevati l’11 Agosto e l’infezione si diffuse con una velocità spaventosa (nonostante il worm fosse filtrato da molti ISP), fino a raggiungere il picco il 13 Agosto, con circa 120.000 macchine infettate. Il grafico sottostante ci dà un’idea più concreta della diffusione del worm.

Ciò era dovuto al fatto che Blaster sfruttava una grossa vulnerabilità presente nei suddetti sistemi operativi per potersi replicare indisturbato e contagiare altri computer in rete. L’obiettivo finale non era però causare la mera infezione dei computer, bensì quello di lanciare un attacco DDoS (Distributed Denial of Service, una variante del DoS in cui più macchine in modo distribuito attaccano la stessa destinazione) contro la porta 80 del sito windowsupdate.com nel giorno 16 Agosto, cosa che riuscì, ma che non creò grossi problemi a Microsoft, dato che il sito in questione era rediretto al sito windowsupdate.microsoft.com (il vero Windows Update), quindi a Microsoft bastò disattivare temporaneamente il sito bersaglio. Quindi l’obiettivo principale era proprio quello di colpire e screditare il colosso Microsoft, infatti ad un’attenta analisi del codice del virus, sono stati scoperti due messaggi in stringhe nascoste: “I just want to say LOVE YOU SAN!!” (da cui Lovesan, il secondo nome con cui è noto Blaster) e “billy

15

Page 16: Attacchi alle applicazioni basati su buffer overflow

gates why do you make this possible ? Stop making money and fix your software!!” (cioè un chiaro messaggio contro Bill Gates che rappresenta la Microsoft).

Il worm, a causa anche della sua rapida diffusione, causò danni gravi a svariate aziende e il blocco di diversi servizi in tutto il mondo, provocando danni per oltre 3 milioni di dollari. Epilogo della vicenda: il 20 Agosto fu arrestato Jeffrey Lee Parson, un 18enne di una cittadina del Minnesota (USA) che fu condannato a 18 mesi di carcere e a un cospicuo risarcimento alle aziende danneggiate. Ma analizziamo più da vicino la struttura e il comportamento del worm. Come abbiamo detto, Blaster si sviluppa su una falla presente nei sistemi operativi Windows XP e Windows 2000; in realtà la falla è presente anche in Windows 2003 Server e Windows NT, ma il worm non è stato progettato per riprodursi in questi sistemi. In particolare si tratta di una vulnerabilità descritta da Microsoft stessa nel Microsoft Security Bulletin MS03-026, nel quale viene sottolineata la pericolosità del problema e viene proposta una patch da installare per rimediare. Microsoft specifica che si tratta di una falla nell’interfaccia RPC (Remote Procedure Call) di un oggetto DCOM (Distributed Component Object Model): DCOM è una tecnologia che abilita componenti software che non si trovano sulla stessa macchina a comunicare direttamente utilizzando una rete; RPC è un protocollo usato da Windows (derivato da OSF RPC, ma modificato da Microsoft) per la comunicazione e la richiesta di servizi tra le due parti di software, permettendo ad un programma che gira su un certo computer di eseguire codice su un sistema remoto. Affinché questa comunicazione sia possibile, bisogna effettuare le richieste in un determinato modo. Il problema nasce quando queste richieste vengono invece effettuate in maniera errata, infatti, l’interfaccia RPC dell’oggetto DCOM sul sistema remoto non controlla opportunamente le dimensioni dei messaggi ricevuti in input. Dunque un malintenzionato potrebbe sfruttare la falla attraverso un exploit che invia all’oggetto DCOM un messaggio non corretto e costruito in un certo modo, così si avrebbe un buffer overflow che gli permetterebbe di avere controllo completo sulla macchina e di eseguire quindi qualsiasi cosa. Per poter fare ciò, il malintenzionato deve utilizzare una tra le porte aperte per RPC, tra cui 135, 139, 445 e 593. Il worm fu creato pochi giorni dopo l’apparizione in rete di questo bollettino e la sua rapida diffusione dimostra che, nonostante gli avvertimenti di Microsoft e di numerosi altri siti e la patch disponibile, pochi sono corsi ai ripari. L’eseguibile del worm è il file msblast.exe di 6176 byte (quindi velocissimo da scaricare per qualsiasi computer con qualsiasi connessione), capace di sfruttare la suddetta falla. Partendo da un computer A già contaminato, il worm invia ad altri computer dati mediante i quali effettuerà l’attacco. Il tutto si svolge in diverse fasi:

1) Attesa: A deve prima controllare di essere connesso ad Internet, quindi entra in un ciclo infinito, aspettando il valore di ritorno della funzione InternetGetConnectedState(). Se l’esito è positivo, il programma è sicuro di essere connesso ad Internet, quindi può passare alla fase 2.

16

Page 17: Attacchi alle applicazioni basati su buffer overflow

2) Generazione indirizzi IP: il programma genera gli indirizzi IP dei computer a cui lanciare il contagio. Per trovare le macchine adatte (non tutte sono vulnerabili alla suddetta falla, o perché hanno installato la patch, oppure perché utilizzano un firewall o un sistema operativo diverso da quelli indicati) deve effettuare una scansione di un certo range di indirizzi IP, cominciando da un indirizzo IP nella forma X.Y.Z.W, che viene scelto secondo la seguente procedura: viene rilevato l’indirizzo IP di A e si sceglie in modo random un valore tra 1 e 20; se il valore è compreso tra 1 e 12, viene utilizzato l’indirizzo IP di A come base per la ricerca, impostando W a 0 e decrementando Z di 20 se Z > 20; se invece il valore è compreso tra 13 e 20, X andrà da 1 a 254, Y e Z da 0 a 253, D sarà sempre a 0. Il programma scandisce 20 host per volta, trovando macchine vulnerabili, tra cui supponiamo ci sia un ipotetico B.

3) Attacco al RPC: utilizzando la porta TCP 135, A invia pacchetti formulati in modo errato (ma costruiti ad hoc per ottenere l’effetto nefasto) al servizio RPC/DCOM di B che, essendo affetto dalla falla, non effettua controlli sulla lunghezza di essi. Risultato: buffer overflow!!!

4) Controllo del contagio: attraverso la porta 135, A controlla se B è già infetto chiamando la funzione GetLastError() che controlla appunto se è già presente su B l’eseguibile del worm o meno. In caso affermativo, Blaster chiama la funzione ExitProcess() e termina, perché se il computer è già infetto, non c’è bisogno di fare nulla. In caso negativo, A attiva i socket per comunicare con B (mediante la funzione WSAStartup()) e per la comunicazione TFTP necessaria per il contagio (mediante la funzione GetModuleFileName()).

5) La shell CMD.EXE: A questo punto, A ha già assunto il controllo di B, quindi lancia sulla macchina da infettare la shell tramite il comando cmd.exe, necessaria ad A per far eseguire a B dei comandi. I due host comunicano mediante la porta TCP 4444.

6) Download del worm: tramite la shell lanciata nella fase precedente, B invia comandi in remoto per riconnettersi ad A, che rimane in ascolto sulla porta UDP 69, aspettando una richiesta di copia del worm. B richiede ad A l’eseguibile msblast.exe, che scarica nella cartella %systemroot%/system32 (cartella di sistema) attraverso il protocollo TFTP. Il file appena scaricato viene lanciato.

7) Aggiornamento delle Registry Keys: utilizzando la shell lanciata nella fase 5, A apporta delle modifiche ad alcune Registry Keys di B ed inserisce nella directory HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run il valore “windows auto update” = “msblast.exe” in modo che il worm venga eseguito ad ogni avvio del pc. A questo punto A termina il suo compito, B è ormai infetto ed esegue lo stesso ciclo che ha eseguito A, cercando altri computer da infettare.

Tramite il sistema che abbiamo appena visto, Blaster è riuscito ad espandersi a macchia d’olio. Ma, come abbiamo detto, il vero scopo è quello di colpire Microsoft mediante un attacco di tipo DDoS da parte dei vari computer infetti, ottenuto sovraccaricando la porta 80 del sito windowsupdate.com con pacchetti SYN e HTTP, questi ultimi lunghi 40 byte e trasmessi ogni 50 secondi. L’attacco proprio a questo sito serve anche per impedire ai computer infetti di scaricare la patch necessaria per rimediare alla vulnerabilità. Inoltre, il worm è stato progettato per effettuare questo tipo di attacco in determinati momenti:

• ogni giorno, nel caso di mesi compresi tra Settembre e Dicembre. • dal 16 del mese in poi, per gli altri mesi (ecco perché, essendo stato creato nel mese di

Agosto, l’attacco era previsto per il 16). Blaster, inoltre, deve essere eseguito su un sistema con:

• Windows XP infettato o riavviato durante la routine nociva • Windows 2000 infettato durante la routine nociva e che non è stato riavviato dopo

l’infezione • Windows 2000 riavviato dopo l’infezione, durante la routine nociva, e dove l’utente è

attualmente registrato I sintomi che permettono di accorgersi della presenza di Blaster sul proprio sistema sono:

17

Page 18: Attacchi alle applicazioni basati su buffer overflow

• Prestazioni della macchina sensibilmente ridotte • Continui riavvii, dovuti al fatto che l’interfaccia RPC accetta i pacchetti formulati in modo

errato, ma non riesce a trattarli, dunque va in crash o si riavvia (vedi figura). • Se si analizza il traffico di pacchetti sulle porte TCP 135 e 4444 e sulla porta UDP 69, ci si

accorgerà che qualcosa non va…

In ogni caso, basta lanciare un antivirus con le firme aggiornate ed effettuare la scansione del sistema per riconoscere ed eliminare il virus. In caso di esito negativo, si può tentare con una rimozione mediante tool specifici che si trovano in rete, rilasciati per esempio da Symantec o McAfee, oppure si può tentare una rimozione manuale seguendo i seguenti passaggi:

1) Chiudere il processo attivo msblast.exe dal Task Manager 2) Eliminare il file msblast.exe che si trova in %systemroot%\system32 3) Eliminare il valore “windows auto update = msblast.exe” dalla registry key

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run, cosa possibile utilizzando l’editor di registro Regedit, fornito da Windows.

4) Eliminare dalla cartella Esecuzione Automatica (Startup nella versione inglese) presente nel menu Start un file chiamato TFTP o simile, responsabile del messaggio di errore che appare all’avvio del computer

5) Riavviare il computer 6) Scaricare ed installare la patch dal sito Windows Update 7) Svuotare il Cestino ed eliminare i punti di ripristino in System Restore, che potrebbero far

tornare il worm Ad oggi, sono state catalogate alcune varianti di Blaster:

• LOVESAN.A: è il worm originale • LOVESAN.B: attraverso un dropper scarica da un sito due file e li copia in

%systemroot%\system32. I due file sono Root32.exe (backdoor) e teekids.exe (codice del worm). Inoltre aggiunge il riferimento ai due file nella registry key HKEY_LOCAL_MACHINE \SOFTWARE\Microsoft\Windows\CurrentVersion\Run\.

• LOVESAN.C: il nome del file del worm è stato cambiato in penis32.exe • LOVESAN.F: il nome del file del worm è stato cambiato in enbiei.exe e l’obiettivo del

DDoS non è più il sito Windows Update , ma il sito tuiasi.ro, che è inesistente, quindi vanifica l’attacco. Al registro di Windows è aggiunta la chiave HKEY_LOCAL_MACHINE \SOFTWARE\Microsoft\Windows\CurrentVersion\Run\www.hidro.4t.com. Contiene un messaggio nascosto: “Nu datzi la fuckultatea de Hidrotehnica!!! Pierdetzi timp ul degeaba...Birsan te cheama pensia!!! Ma pis pe diploma!!!!!!”, che tradotto in inglese corrisponde al seguente messaggio: “Don't go to the Hydrotechnics faculty!!! You are wasting time...Barsan, the retirement wants you!!!”.

18

Page 19: Attacchi alle applicazioni basati su buffer overflow

8. 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. 8.1 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. 8.2 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. 8.3 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.

19

Page 20: Attacchi alle applicazioni basati su buffer overflow

8.4 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:

20

Page 21: Attacchi alle applicazioni basati su buffer overflow

char * strcpy(char * dest,const char *src) { char *tmp = dest; while ((*dest++ = *src++) != '\0') /* 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.

21

Page 22: Attacchi alle applicazioni basati su buffer overflow

8.5 Difesa: Protezione contro lo “stack smashing” Lo scopo di questo tipo di protezione è quello di evitare i più comuni buffer overflow analizzando lo stack al ritorno da una funzione, per verificare se è stato modificato o meno. In caso positivo, il programma esce con una “segmentation fault”. Questo obiettivo è generalmente raggiunto modificando l’organizzazione dei dati nello stack di una funzione, in modo da includere un “canary”, cioè un valore noto sistemato tra un buffer e i dati di controllo. In caso di buffer overflow, il canary viene sovrascritto, dunque al ritorno dalla funzione è subito scovato ed è possibile correre ai ripari. Esistono diversi tipi di canary:

Terminator (o hard-to-insert) canaries: nascono dall’osservazione che la maggior parte dei BOF sono basati su operazioni che terminano con i terminatori, dunque sono formati da un Null byte, un carriage return(0x0D), un line feed(0x0A) e un EOF nella rappresentazione libC (0xFF). Il difetto è che sono conosciuti fin dall’inizio, dunque un attaccante potrebbe sovrascrivere sia il canary che le informazioni di controllo (portando così a compimento il BOF) e poi utilizzare un overflow più piccolo per risistemare il canary e passare dunque inosservato (fortunatamente i casi in cui è possibile effettuare un doppio overflow sono rari).

Random (o hard-to-spoof) canaries: sono generati in modo casuale, per ovviare ai problemi dei terminator canaries. Quindi il canary inserito nello stack è generato all’inizializzazione del programma e memorizzato in una variabile globale riempita di solito da “unmapped pages”, in modo che qualsiasi trucco utilizzato per leggere il suo valore causi una “segmentation fault”, terminando il programma. Comunque, il problema non viene del tutto eliminato, in quanto è sempre possibile leggere il valore del canary dallo stack.

Random XOR canaries: si tratta di Random canaries, con la differenza che stavolta viene effettuato lo XOR (operatore di confusione ideale secondo Shannon) tra il canary e l’indirizzo di ritorno, in modo che se si modifica l’indirizzo di ritorno e poi si rimette a posto il canary, il risultato dello XOR sarà comunque diverso perché l’indirizzo di ritorno è cambiato. Nonostante questo, i Random XOR canaries complicano solo un po’ la vita a chi attacca, ma non risolvono i problemi del tipo precedente.

Le implementazioni più famose della protezione contro lo “stack smashing” sono: ProPolice (GCC Stack-Smashing Protector): si tratta di una patch per GCC 3.X, inclusa

poi in parte in GCC 4.1. E’ diventata standard in alcuni sistemi operativi Unix, fra cui la distribuzione Gentoo Linux, anche se in essa non è abilitata di default. Alcune azioni portate avanti da ProPolice riguardano il riordino delle variabili locali, che vengono sistemate dopo i puntatori per evitare che l’overflow di un buffer corrompa un puntatore che si trova dopo di esso. Supporta Terminator e Random canaries.

StackGuard: si tratta di un’altra estensione di GCC per proteggere lo stack in modo del tutto trasparente all’utente. È nota soprattutto per avere introdotto i Random XOR canaries. L’entusiasmo iniziale che riscosse questo progetto andò via via scemando, forse perché i benchmark effettuati hanno dimostrato un sostanziale incremento nel costo di ogni chiamata a funzione, tanto che la versione 2.0 annunciata dalla società Immunix è tuttora irreperibile.

StackGhost: rende i BOF più difficili da sfruttare utilizzando una caratteristica hardware presente solo sull’architettura SPARC e SPARC64 per rilevare le modifiche agli indirizzi di ritorno. Lavora in maniera del tutto trasparente all’utente e con un impatto sulle performance < 1%, peccato si tratti di una tecnologia fortemente “hardware-based”.

Dunque queste soluzioni risolvono solo in parte il problema dei BOF, rendendoli solo più complessi da sfruttare, ma non eliminandoli del tutto. Una protezione più forte sarebbe quella di dividere in due parti lo stack, di cui una per i dati e l’altra per gli indirizzi di ritorno, soluzione sfruttata dal linguaggio di programmazione Forth, che comunque non risolve il problema, in quanto ci sono altri dati importanti a parte l’indirizzo di ritorno che questa soluzione non protegge.

22

Page 23: Attacchi alle applicazioni basati su buffer overflow

8.6 Difesa: Protezione dello spazio eseguibile Un’altra strada per prevenire i BOF è quella di proteggere lo spazio eseguibile, cosa che può essere implementata sia a livello hardware che software. La protezione a livello hardware è una tecnologia chiamata NX (No eXecute) bit, che si occupa di marcare una parte della memoria affinchè sia utilizzata solo per i dati e non permetta quindi alle istruzioni del processore di risiedere in essa. In pratica, questa parte della memoria diventa non eseguibile e non scrivibile. Questo aiuta a prevenire diversi buffer overflow, in particolare quelli che applicano la code injection, tra i quali Sasser e Blaster (di cui si parla ampiamente nel paragrafo 6). Il termine “NX bit” si riferisce al bit 63 (l’ultimo bit in un integer di 64 bit) nella entry della tabella di paginazione di un processore x86. Se questo bit è settato a 0, il codice di quella pagina può essere eseguito, se invece è settato a 1, si tratta solo di dati e non di istruzioni, dunque essi non possono essere eseguiti. Non si tratta certo di una tecnologia nuova, dato che esisteva qualcosa del genere anche nei primi processori Intel 80286 e nelle architetture SPARC, Alpha e PowerPC, ma è stata reimplementata in chiave moderna prima da AMD (che chiamò “NX bit” la tecnologia) e poi da Intel (che per le solite strategie commerciali, chiamò la tecnologia “XD bit”, dove XD sta per eXecute Disable) ed inserita all’interno di alcuni dei loro processori, tra i quali quelli a 64 bit. Per quanto riguarda la protezione a livello software, diverse tecnologie sono state sviluppate e inserite all’interno di vari sistemi operativi. Vediamole più in dettaglio:

Data Execution Prevention (DEP): si tratta della tecnologia di casa Microsoft, implementata per la prima volta in Windows XP Service Pack 2 e in Windows 2003 Server Service Pack 1. Essa lavora in due modalità: hardware-enforced DEP (nel caso in cui il processore supporta NX bit, che viene riconosciuta e attivata dal sistema operativo) e software-enforced DEP (nel caso in cui il processore non supporta NX-bit, che quindi viene in qualche modo emulata via software, di default solo per i servizi essenziali di Windows). Processori supportati: AMD64, IA-64, Efficeon, EM64T, Pentium M (later revisions), AMD Sempron (later revisions).

W^X: da pronunciare W XOR X, è la tecnologia implementata in OpenBSD, che supporta NX bit nei processori Alpha, AMD64, HPPA e SPARC ed offre la sua emulazione nei processori IA-32 (x86). Essa prevede che ciascuna pagina sia scrivibile o eseguibile, ma non contemporaneamente (da qui il nome W XOR X, che sta per Write Xor eXecute): ciò causa il fallimento di diversi stack overflow, perché anche se il codice viene iniettato nello stack perché la memoria è scrivibile, il programma non può eseguirlo e si limita a terminare. Per limitare la complessità, W^X non fa uso di NX bit, è semplicemente una tecnologia diversa.

PaX: si tratta di una patch per il kernel Linux per la protezione delle pagine di memoria. L’idea alla base è quella di permettere ai programmi di fare solo ciò che devono fare per poter eseguire correttamente, e nient’altro. PaX marca la parte dati della memoria come non eseguibile e la parte del programma come non scrivibile. Inoltre implementa la “address space layout randomization”, di cui parleremo più avanti. In sostanza PaX previene molti BOF, in particolare rendendo inefficaci i code injection e rendendo indeterminati (basati sulla fortuna di chi attacca) i return-to-libc. Può utilizzare NX bit se supportato dal processore (Alpha, AMD64, IA-64, MIPS, PA-RISC, PowerPC e SPARC) o emularne le funzionalità in caso contrario (ad esempio sui processori x86). Fa parte del progetto Grsecurity ed è implementata in Hardened Gentoo, oltre che in Trusted Debian, il progetto di Adamantix di una distribuzione sicura di Linux basata su Debian.

23

Page 24: Attacchi alle applicazioni basati su buffer overflow

Mascotte di PAX

Exec Shield: come PaX, si tratta di una patch per il kernel Linux. È nata inizialmente per

emulare le funzionalità di NX bit sui processori a 32 bit x86, ma poi ha integrato il supporto hardware per NX bit. Alla richiesta di inserirla nella prossima versione del kernel la risposta fu negativa, in quanto Exec Shield introduceva diversi cambiamenti al codice. Come PaX cerca di marcare la parte dati della memoria come non eseguibile e la parte del programma come non scrivibile, evitando diversi BOF. Fornisce anche tecniche di “address space layout randomization”, che vedremo più avanti. Exec Shield non richiede che i programmi siano ricompilati per funzionare, ad eccezione di alcune applicazioni come wine ed emacs.

8.7 Nuovo attacco: gli attacchi di tipo “return-to-libc” Lo scopo di questo tipo di attacchi (il cui nome è spesso abbreviato in ret2libc) è quello di chiamare una funzione di libC al ritorno da una funzione, sovrascrivendo l’indirizzo di ritorno non con quello della locazione di memoria dove si trova lo shellcode, bensì con quello di una funzione di libC, spesso system(), magari passandogli come argomento qualcosa come /bin/sh (che ci dà una shell in locale). In questo modo forziamo l’esecuzione di una funzione, senza bisogno di eseguire codice che si trova nello stack o nello heap, aggirando quindi l’ostacolo rappresentato dalla protezione dello spazio eseguibile. Invece possono essere ostacolati dalla protezione contro lo stack smashing (dato che questi sistemi sono in grado di rilevare la corruzione dello stack) e dalla “address space layout randomization”, che li rende molto difficili da eseguire. 8.8 Difesa: Address space layout randomization (ASLR) È una tecnologia la cui idea di base è quella di organizzare alcune parti chiave della memoria di un processo (ad esempio stack, heap, librerie e parti eseguibili) in maniera casuale nell’address space di un processo. Ciò rende difficili alcuni tipi di attacco, in particolare quelli che sovrascrivono l’EIP per puntare alla locazione dello shellcode opportunamente inserito e gli attacchi return-to-libc; ciò è dovuto al fatto che diventa difficile per chi attacca conoscere l’indirizzo del codice da eseguire, dato che essendo generato in modo random si sposta sempre all’interno della memoria e spesso l’unica tecnica che si può applicare per individuarlo è il brute forcing. Questa tecnologia è implementata da molti sistemi di sicurezza, per esempio PaX e Exec Shield. 8.9 Difesa: Deep Packet Inspection (DPI) Questa tecnologia permette di esaminare i pacchetti che transitano in una rete, confrontandoli con le informazioni a disposizione presenti in un database e riguardanti attacchi conosciuti. Ciò permette di trovare gli eventuali pacchetti che portano le tracce di un buffer overflow o di un altro tipo di attacco (ad esempio pacchetti con una lunga serie di istruzioni No-Operation, spesso utilizzati nei buffer overflow) e di evitare che passino. Un pacchetto di questo tipo può essere bloccato, marcato, rediretto, ecc. La DPI è utilizzata anche dalle compagnie telefoniche per conoscere i pacchetti che si stanno ricevendo attraverso Internet. Il nome comincia con “deep” per indicare una verifica accurata dei pacchetti, che va dal secondo al settimo livello del modello OSI, e per distinguerla dalla Shallow

24

Page 25: Attacchi alle applicazioni basati su buffer overflow

Packet Inspection (anche detta Just Packet Inspection), che invece controlla solo l’header del pacchetto. Si tratta di una tecnologia utile ma spesso poco efficace, in quanto può prevenire solo gli attacchi conosciuti, senza contare che chi attacca si dà sempre da fare per inventare nuove armi, come dimostrano i nuovi shellcode alfanumerici, polimorfici, metamorfici e auto modificanti. 8.10 Difesa: Intrusion Detection Systems (IDS) Gli IDS sono utilizzati per riconoscere i pacchetti che transitano in rete e che mirano ad effettuare manipolazioni sui sistemi. Essi agiscono là dove i firewall convenzionali non arrivano, riconoscendo attacchi contro servizi vulnerabili, attacchi mirati alle applicazioni, attacchi utilizzati per acquisire privilegi di root o per accedere ad informazioni riservate. Sono composti da diverse parti, tra cui sensori, che si comportano da generatori di eventi, Console, che controlla i sensori ed effettua il monitoraggio degli eventi e una Engine centrale che registra gli eventi in un database e genera avvertimenti basati su un sistema di regole. Esistono diversi tipi di IDS, distinti in base al tipo e alla locazione dei sensori e in base alla metodologia utilizzata dalla Engine per generare gli avvertimenti. 8.11 Nuovo attacco: Shellcode alfanumerici, polimorfici, metamorfici e auto modificanti Si tratta della nuova frontiera raggiunta dagli shellcode, come risposta alle tecnologie via via inventate per cercare di arginarli. Sono spesso tecniche utilizzate anche da alcuni virus per evitare di essere scoperti e sono spesso molto simili tra loro. In particolare, i nuovi shellcode spesso sono:

Alfanumerici: sono shellcode scritti utilizzando esclusivamente codici alfanumerici, ad esempio il codice ASCII, con l’obiettivo di indurre le applicazioni, ad esempio i Web forms, ad accettare il codice utilizzato per gli exploit. Ovviamente bisogna conoscere bene il codice macchina dell’architettura su cui effettuare l’attacco, tenendo conto che esso varia da architettura ad architettura.

Polimorfici: si tratta di shellcode che variano lasciando però immutato l’algoritmo originale. Questa tecnica, spesso utilizzata da alcuni virus, è utilizzata anche da alcuni shellcode con l’obiettivo comune di nascondere la propria presenza, sapendo che spesso gli Intrusion Detection Systems controllano i pacchetti che transitano in rete, cercando di scoprire pacchetti che corrispondono a virus o exploit conosciuti. Uno strumento spesso utilizzato dai creatori di shellcode polimorfici è la crittografia: il codice viene criptato, in modo da non consentire agli IDS di riconoscerlo; tuttavia una piccola parte che contiene le informazioni per decriptarlo deve rimanere non criptata, ed è proprio su quella che gli IDS puntano per riconoscere lo shellcode. Per difendersi da essi, coloro che scrivono shellcode polimorfici riscrivono questa piccola parte ogni volta che il worm viene propagato, ma gli IDS rispondono effettuando una ricerca basata su pattern, in modo da riconoscere comunque lo shellcode. Insomma, la battaglia non ha mai fine…

Metamorfici: si tratta di shellcode in grado di riprogrammare se stessi, assumendo rappresentazioni che li fanno sembrare totalmente diversi da come ci si aspetta. Anche questa è una tecnica utilizzata dai virus e serve per vanificare i controlli basati su pattern, infatti si tratta di shellcode più pericolosi di quelli polimorfici.

Auto modificanti (self-modifying): gli shellcode di questo tipo non vogliono rivelare la loro presenza e per ottenere ciò si servono spesso di codice polimorfico, tanto che gli shellcode polimorfici spesso sono chiamati auto modificanti primitivi.

8.12 Conclusioni Dopo aver esposto il problema ed averlo analizzato, bisognerebbe esporre la soluzione. In questo caso però la soluzione non esiste…

25

Page 26: Attacchi alle applicazioni basati su buffer overflow

Per quanto detto precedentemente, scrivere codice corretto è un’utopia, perché è facile sbagliare o commettere una leggerezza o utilizzare codice di terzi che involontariamente contiene dei bug. Anche se il codice è stato testato e sembra corretto sotto tutti i punti di vista, probabilmente arriverà qualcuno che ha trovato una vulnerabilità che si può sfruttare per accedere al sistema, magari utilizzando un buffer overflow. Assodato dunque che scrivere codice completamente corretto è pressoché impossibile e dunque non si può prevenire il problema, si è cercato allora di trovare dei buoni metodi per curarlo. Diverse tecnologie sono state messe a punto a tal proposito, alcune delle quali molto sofisticate, che lavorano su fronti diversi con l’obiettivo comune di infliggere un duro colpo ai buffer overflow. Molte di queste funzionano egregiamente e addirittura è possibile combinarle tra loro per assicurare una sicurezza maggiore, ma il problema è lungi dall’essere risolto. Se qualcuno lavora per produrre armi che possano competere con le armi del nemico, il nemico non sta con le mani in mano e nello stesso tempo lavora per migliorare le sue: ed ecco che ad un attacco corrisponde una difesa, seguita da un nuovo attacco con relativa difesa, e così via. Insomma, ci sono tutte le basi per presupporre che la battaglia non avrà mai fine…

26

Page 27: Attacchi alle applicazioni basati su buffer overflow

Bibliografia [1] Gillette: A Unique Examination of the Buffer Overflow Condition

[2] Fayolle, Glaume: A Buffer Overflow Study – Attacks and Defenses

[3] Cowan, Wagle, Pu, Beattie, Walpole: Buffer Overflows: Attacks and Defenses for the Vulnerability of the Decade

[4] Foster, Osipov, Bhalla, Heinen: Buffer Overflow Attacks – Detect, Exploit, Prevent

[5] Siti http://en.wikipedia.org e http://it.wikipedia.org

[6] Alfano, Chirico, Moscariello, Palumbo, Santoro: Il Worm Blaster – Il Superbug di Windows

[7] Auriemma: Buffer overflow: spiegazione tecnica ed esempio pratico [8] Dapino: Tecniche: Buffer Overflow [9] R[]l4nD: Guida al Buffer Overflow, al calcolo di uno shellcode e alla stesura di un exploit [10] Piccardi: GaPiL [11] Wheeler: Secure programmer: Countering buffer overflows [12] Sito www.informit.com : Understanding Buffer Overflows [13] Microsoft Security Bulletin MS03-026

27