SlideShare uma empresa Scribd logo
1 de 27
Baixar para ler offline
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
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
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
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
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
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
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
#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
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
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!!!!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 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
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
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="x61x61x61x61x61x61x61x61x61x61"
          "x61x61x61x61x08x04x84x38";

     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
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
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 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
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
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
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 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
•   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_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 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 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: “Don't go to the Hydrotechnics faculty!!! You are
       wasting time...Barsan, the retirement wants you!!!”.

                                                                                                  18
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
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
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
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
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
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
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
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
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

Mais conteúdo relacionado

Mais procurados

14 - Programmazione: Stream e File
14 - Programmazione: Stream e File14 - Programmazione: Stream e File
14 - Programmazione: Stream e FileMajong DevJfu
 
Capitolo 7 elementi di programmazione c-c++
Capitolo 7   elementi di programmazione  c-c++Capitolo 7   elementi di programmazione  c-c++
Capitolo 7 elementi di programmazione c-c++Giovanni Della Lunga
 
Introduzione al linguaggio PHP
Introduzione al linguaggio PHPIntroduzione al linguaggio PHP
Introduzione al linguaggio PHPextrategy
 
Py a6 python-database
Py a6 python-databasePy a6 python-database
Py a6 python-databaseMajong DevJfu
 
WordCamp Italia 2021: da zero a PHP
WordCamp Italia 2021: da zero a PHPWordCamp Italia 2021: da zero a PHP
WordCamp Italia 2021: da zero a PHPRodolfo Melogli
 
Seminario introduzione all'uso del terminale e della bash in Linux
Seminario introduzione all'uso del terminale e della bash in LinuxSeminario introduzione all'uso del terminale e della bash in Linux
Seminario introduzione all'uso del terminale e della bash in Linuxalberto fiaschi
 
Esercitazione 1 (27 febbraio 2012)
Esercitazione 1 (27 febbraio 2012)Esercitazione 1 (27 febbraio 2012)
Esercitazione 1 (27 febbraio 2012)STELITANO
 
Let's give it a GO!
Let's give it a GO!Let's give it a GO!
Let's give it a GO!MarioTraetta
 
[Ebook ita - security] introduzione alle tecniche di exploit - mori - ifoa ...
[Ebook   ita - security] introduzione alle tecniche di exploit - mori - ifoa ...[Ebook   ita - security] introduzione alle tecniche di exploit - mori - ifoa ...
[Ebook ita - security] introduzione alle tecniche di exploit - mori - ifoa ...UltraUploader
 
Progettazione e sviluppo di applicazioni web 2.0 con PHP e Ajax
Progettazione e sviluppo di applicazioni web 2.0 con PHP e AjaxProgettazione e sviluppo di applicazioni web 2.0 con PHP e Ajax
Progettazione e sviluppo di applicazioni web 2.0 con PHP e AjaxGiovanni Cappellini
 

Mais procurados (20)

7 Sottoprogrammi
7   Sottoprogrammi7   Sottoprogrammi
7 Sottoprogrammi
 
14 - Programmazione: Stream e File
14 - Programmazione: Stream e File14 - Programmazione: Stream e File
14 - Programmazione: Stream e File
 
Capitolo 7 elementi di programmazione c-c++
Capitolo 7   elementi di programmazione  c-c++Capitolo 7   elementi di programmazione  c-c++
Capitolo 7 elementi di programmazione c-c++
 
Introduzione al linguaggio PHP
Introduzione al linguaggio PHPIntroduzione al linguaggio PHP
Introduzione al linguaggio PHP
 
11 I File
11   I File11   I File
11 I File
 
Bash programming
Bash programmingBash programming
Bash programming
 
Corso Bash
Corso BashCorso Bash
Corso Bash
 
Py a6 python-database
Py a6 python-databasePy a6 python-database
Py a6 python-database
 
WordCamp Italia 2021: da zero a PHP
WordCamp Italia 2021: da zero a PHPWordCamp Italia 2021: da zero a PHP
WordCamp Italia 2021: da zero a PHP
 
Seminario introduzione all'uso del terminale e della bash in Linux
Seminario introduzione all'uso del terminale e della bash in LinuxSeminario introduzione all'uso del terminale e della bash in Linux
Seminario introduzione all'uso del terminale e della bash in Linux
 
Inferno Limbo Italian
Inferno Limbo ItalianInferno Limbo Italian
Inferno Limbo Italian
 
Gcc & Make
Gcc & MakeGcc & Make
Gcc & Make
 
Esercitazione 1 (27 febbraio 2012)
Esercitazione 1 (27 febbraio 2012)Esercitazione 1 (27 febbraio 2012)
Esercitazione 1 (27 febbraio 2012)
 
2006 Py03 intermedio
2006 Py03 intermedio2006 Py03 intermedio
2006 Py03 intermedio
 
Le basi di Pytthon 3 - Fondamenti n.1
Le basi di Pytthon 3 - Fondamenti n.1Le basi di Pytthon 3 - Fondamenti n.1
Le basi di Pytthon 3 - Fondamenti n.1
 
Let's give it a GO!
Let's give it a GO!Let's give it a GO!
Let's give it a GO!
 
Py a4 python-file
Py a4 python-filePy a4 python-file
Py a4 python-file
 
[Ebook ita - security] introduzione alle tecniche di exploit - mori - ifoa ...
[Ebook   ita - security] introduzione alle tecniche di exploit - mori - ifoa ...[Ebook   ita - security] introduzione alle tecniche di exploit - mori - ifoa ...
[Ebook ita - security] introduzione alle tecniche di exploit - mori - ifoa ...
 
Progettazione e sviluppo di applicazioni web 2.0 con PHP e Ajax
Progettazione e sviluppo di applicazioni web 2.0 con PHP e AjaxProgettazione e sviluppo di applicazioni web 2.0 con PHP e Ajax
Progettazione e sviluppo di applicazioni web 2.0 con PHP e Ajax
 
I sotto programmi in Python 3
I sotto programmi in Python 3I sotto programmi in Python 3
I sotto programmi in Python 3
 

Semelhante a Attacchi alle applicazioni basati su buffer overflow

Sistemioperativi
SistemioperativiSistemioperativi
Sistemioperativieleonora4g
 
A static Analyzer for Finding Dynamic Programming Errors
A static Analyzer for Finding Dynamic Programming ErrorsA static Analyzer for Finding Dynamic Programming Errors
A static Analyzer for Finding Dynamic Programming ErrorsLino Possamai
 
TYPO3 CMS 6.2 LTS - Le Novità
TYPO3 CMS 6.2 LTS - Le NovitàTYPO3 CMS 6.2 LTS - Le Novità
TYPO3 CMS 6.2 LTS - Le NovitàRoberto Torresani
 
MITM Attack with Patching Binaries on the Fly by Adding Shellcodes
MITM Attack with Patching Binaries on the Fly by Adding ShellcodesMITM Attack with Patching Binaries on the Fly by Adding Shellcodes
MITM Attack with Patching Binaries on the Fly by Adding ShellcodesGianluca Gabrielli
 
Data Warehouse e Business Intelligence in ambiente Oracle - Il sistema di mes...
Data Warehouse e Business Intelligence in ambiente Oracle - Il sistema di mes...Data Warehouse e Business Intelligence in ambiente Oracle - Il sistema di mes...
Data Warehouse e Business Intelligence in ambiente Oracle - Il sistema di mes...Massimo Cenci
 
Working between the clouds (versione completa)
Working between the clouds (versione completa)Working between the clouds (versione completa)
Working between the clouds (versione completa)Davide Cerbo
 
Ridirezionamento di I/O con Bash: un breve approfondimento
Ridirezionamento di I/O con Bash: un breve approfondimentoRidirezionamento di I/O con Bash: un breve approfondimento
Ridirezionamento di I/O con Bash: un breve approfondimentoBabel
 
Elaborazione automatica dei dati: computer e matlab
Elaborazione automatica dei dati: computer e matlabElaborazione automatica dei dati: computer e matlab
Elaborazione automatica dei dati: computer e matlabprofman
 
Elaborazione automatica dei dati: calcolatore e matlab
Elaborazione automatica dei dati: calcolatore e matlabElaborazione automatica dei dati: calcolatore e matlab
Elaborazione automatica dei dati: calcolatore e matlabprofman
 
Guida alla modifica del dsdt 1a parte - le basi
Guida alla modifica del dsdt   1a parte - le basiGuida alla modifica del dsdt   1a parte - le basi
Guida alla modifica del dsdt 1a parte - le basiguest1842a5
 
1informaticadibase 110602151357 Phpapp02
1informaticadibase 110602151357 Phpapp021informaticadibase 110602151357 Phpapp02
1informaticadibase 110602151357 Phpapp02Formazioneenna Enna
 

Semelhante a Attacchi alle applicazioni basati su buffer overflow (20)

Low Level Software Security
Low Level Software SecurityLow Level Software Security
Low Level Software Security
 
Sistemioperativi
SistemioperativiSistemioperativi
Sistemioperativi
 
Modulo 1 - Lezione 2
Modulo 1 - Lezione 2Modulo 1 - Lezione 2
Modulo 1 - Lezione 2
 
A static Analyzer for Finding Dynamic Programming Errors
A static Analyzer for Finding Dynamic Programming ErrorsA static Analyzer for Finding Dynamic Programming Errors
A static Analyzer for Finding Dynamic Programming Errors
 
TYPO3 CMS 6.2 LTS - Le Novità
TYPO3 CMS 6.2 LTS - Le NovitàTYPO3 CMS 6.2 LTS - Le Novità
TYPO3 CMS 6.2 LTS - Le Novità
 
MITM Attack with Patching Binaries on the Fly by Adding Shellcodes
MITM Attack with Patching Binaries on the Fly by Adding ShellcodesMITM Attack with Patching Binaries on the Fly by Adding Shellcodes
MITM Attack with Patching Binaries on the Fly by Adding Shellcodes
 
Data Warehouse e Business Intelligence in ambiente Oracle - Il sistema di mes...
Data Warehouse e Business Intelligence in ambiente Oracle - Il sistema di mes...Data Warehouse e Business Intelligence in ambiente Oracle - Il sistema di mes...
Data Warehouse e Business Intelligence in ambiente Oracle - Il sistema di mes...
 
Working between the clouds (versione completa)
Working between the clouds (versione completa)Working between the clouds (versione completa)
Working between the clouds (versione completa)
 
Ridirezionamento di I/O con Bash: un breve approfondimento
Ridirezionamento di I/O con Bash: un breve approfondimentoRidirezionamento di I/O con Bash: un breve approfondimento
Ridirezionamento di I/O con Bash: un breve approfondimento
 
DDive11 - Notes Moon Attack
DDive11 - Notes Moon AttackDDive11 - Notes Moon Attack
DDive11 - Notes Moon Attack
 
Sist op
Sist opSist op
Sist op
 
Sistemi Operativi
Sistemi OperativiSistemi Operativi
Sistemi Operativi
 
Elaborazione automatica dei dati: computer e matlab
Elaborazione automatica dei dati: computer e matlabElaborazione automatica dei dati: computer e matlab
Elaborazione automatica dei dati: computer e matlab
 
Elaborazione automatica dei dati: calcolatore e matlab
Elaborazione automatica dei dati: calcolatore e matlabElaborazione automatica dei dati: calcolatore e matlab
Elaborazione automatica dei dati: calcolatore e matlab
 
Guida alla modifica del dsdt 1a parte - le basi
Guida alla modifica del dsdt   1a parte - le basiGuida alla modifica del dsdt   1a parte - le basi
Guida alla modifica del dsdt 1a parte - le basi
 
Heartbleed - OpenSSL Bug
Heartbleed - OpenSSL BugHeartbleed - OpenSSL Bug
Heartbleed - OpenSSL Bug
 
Battaglia Navale
Battaglia NavaleBattaglia Navale
Battaglia Navale
 
1informaticadibase 110602151357 Phpapp02
1informaticadibase 110602151357 Phpapp021informaticadibase 110602151357 Phpapp02
1informaticadibase 110602151357 Phpapp02
 
Informatica di base
Informatica di baseInformatica di base
Informatica di base
 
Informatica di base
Informatica di baseInformatica di base
Informatica di base
 

Attacchi alle applicazioni basati su buffer overflow

  • 1. 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
  • 2. 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
  • 3. 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
  • 4. 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
  • 5. 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
  • 6. 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
  • 7. 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
  • 8. #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
  • 9. 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
  • 10. 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!!!!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 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
  • 11. 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
  • 12. 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="x61x61x61x61x61x61x61x61x61x61" "x61x61x61x61x08x04x84x38"; 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
  • 13. 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
  • 14. 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 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
  • 15. 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
  • 16. 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
  • 17. 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 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
  • 18. 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_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 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 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: “Don't go to the Hydrotechnics faculty!!! You are wasting time...Barsan, the retirement wants you!!!”. 18
  • 19. 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
  • 20. 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
  • 21. 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
  • 22. 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
  • 23. 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
  • 24. 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
  • 25. 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
  • 26. 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
  • 27. 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