1- Introduzione
Il
Buffer Overflow è stata la più comune forma di vulnerabilità nella sicurezza per gli ultimi dieci
anni. Essa domina l'area relativa alla penetrazione in reti remote, dove, un
anonimo utente di Internet, cerca di ottenere un controllo parziale o totale di
un host. Se la vulnerabilità del buffer overflow potesse essere effettivamente
eliminata, una gran parte dei più seri trattati di sicurezza andrebbe eliminata
con essa.
1.1 - Buffer Overflow
Gli attacchi di tipo buffer overflow formano una sostanziale porzione di tutti gli attacchi alla sicurezza semplicemente perché le vulnerabilità del buffer overflow sono comuni e quindi facili da sfruttare. In ogni modo, le vulnerabilità del buffer overflow dominano la classe degli attacchi di intrusione perché esse offrono all'attaccante la possibilità di iniettare ed eseguire codice dannoso. Il codice d'attacco, iniettato, gira con i privilegi del programma vulnerabile e permette all'attaccante di poter invadere qualche altra funzionalità che gli interessa per controllare il computer host.
Per
esempio sui cinque attacchi remoto verso locale usati nel 1998[1] il
controllo sull'intrusione dei laboratori Lincoln ha evidenziato che tre erano
essenzialmente attacchi che ficcavano il naso nelle credenziali degli utenti,
mentre gli altri due erano buffer overflow.
1.2 - Gestione del
Buffer
1.2.1
- Alcune definizioni
Un buffer è
un blocco contiguo di memoria che contiene più istanze dello stesso tipo di
dato. In C un buffer viene chiamato array. Gli array in C possono essere dichiarati
statici o dinamici: le variabili statiche sono allocate al momento del caricamento
sul segmento dati, quelle dinamiche sono allocate al momento del caricamento
sullo stack. L'overflow consiste nel riempire un buffer oltre il limite.
I buffer di interesse sono quelli dinamici.
1.2.2
- Organizzazione della memoria di un processo
Per capire
come funzionano i buffer sullo stack occorre sapere com'è organizzata la
memoria di un processo. I processi sono divisi in tre regioni: testo, dati e
stack. La regione testo è fissata, contiene il codice del programma ed è
a sola lettura. Qualsiasi tentativo di scrittura provoca una violazione di
segmento. La regione dati contiene i dati inizializzati e non
inizializzati. Le variabili statiche vengono memorizzate in questa regione. La
dimensione di questa area può essere cambiata con la chiamata di sistema brk().
|
1.2.3 - Cos'è uno stack
Lo stack è
un tipo di dato astratto. Uno stack di oggetti ha la proprietà Last In First
Out (LIFO), cioè l'ultimo oggetto inserito è il primo ad essere rimosso. Le
due operazioni principali sono push e pop: push aggiunge
un elemento in cima allo stack e pop lo rimuove.
I linguaggi
di programmazione moderni hanno il costrutto di procedura o funzione. Una
chiamata a procedura altera il flusso di controllo come un salto (jump),
ma, diversamente da un salto, una volta finito il proprio compito, una funzione
ritorna il controllo all'istruzione successiva alla chiamata. Quest'astrazione
può essere implementata con il supporto di uno stack.
Lo stack è
usato anche per allocare dinamicamente le variabili locali usate nelle
funzioni, per passare parametri alle funzioni e per restituire valori dalle
stesse.
Uno stack è
un blocco di memoria contiguo contenente dei dati. Un registro noto come stack
pointer (SP) punta alla cima dello stack. La base dello stack è un
indirizzo fisso. La dimensione è variata dinamicamente dal kernel. La
CPU implementa le operazioni push e pop.
Lo stack
consiste di un insieme di segmenti logici (stack frame) che vengono
impilati sullo stack quando viene chiamata una funzione e spilati quando la
funzione ritorna. Uno stack frame contiene i parametri della funzione,
le sue variabili locali, i dati necessari per ripristinare il precedente stack
frame, incluso l'indirizzo dell'istruzione successiva alla chiamata
(contenuto nell'instruction pointer o IP).
A seconda
dell'implementazione, lo stack, cresce verso l'alto o il basso. In questa
trattazione si assume che cresca verso il basso, che è quanto succede sui
processori Intel, Motorola, SPARC e MIPS. Anche lo stack
pointer dipende dall'implementazione: può puntare all'ultimo indirizzo
sullo stack o al prossimo indirizzo libero. In questa discussione si assume che
punti all'ultimo indirizzo sullo stack.
Oltre allo stack
pointer si ha anche un frame pointer (FP) che punta ad una locazione
fissa nel frame, noto anche come base pointer (BP). Le variabili locali
possono essere referenziate specificando l'offset dallo stack pointer.
Tuttavia, poiché nuovi dati sono impilati e spilati, tale offset cambia. Quindi
si usa referenziare le variabili locali e i parametri con un offset da FP.
La prima cosa che fa una procedura quando viene chiamata, è salvare il FP precedente (per poterlo ripristinare al ritorno). Poi copia SP su FP per creare il nuovo FP ed incrementa SP per puntare allo spazio riservato per la successiva variabile locale. Questo codice è il prologo della procedura. Al momento dell'uscita dalla procedura, lo stack deve essere ripulito. Le funzioni delle CPU Intel per fare ciò sono ENTER e LEAVE, quelle delle CPU Motorola sono LINK e UNLINK. La trattazione successiva riguarda un sistema Linux su architettura Intel.
1.2.3.1
- Un esempio: La regione dello stack
Si consideri
un esempio e si analizzi lo stack:
void function(int a, int b,
int c) {
char buffer1[5];
char buffer2[10];
}
void main() {
function(1,2,3);
}
Per comprendere cosa faccia il programma per chiamare function(), lo si
compila per generare un output in codice assembly:
gcc -S -o example1 example1.c
Il codice assembly della chiamata di function() è:
pushl $3
pushl $2
pushl $1
call function
L'istruzione
call impila l'instruction pointer sullo stack. Questo IP salvato viene
chiamato indirizzo di ritorno o RET. Si consideri il prologo della funzione:
pushl %ebp
movl %esp,%ebp
subl $20,%esp
Questo
codice impila il base pointer EBP sullo stack, copia lo SP attuale su
EBP, rendendolo il nuovo FP. Il FP salvato viene detto saved frame pointer
o SFP. Poi alloca dello spazio per le variabili locali sottraendo la loro
dimensione a SP.
La memoria
può essere indirizzata solo a indirizzi multipli della dimensione di una
parola. Nel caso in questione una parola è di 4 byte. Quindi il buffer
dichiarato di 5 byte in realtà occupa 8 byte di memoria e quello di 10 byte ne
occupa 12. Perciò SP viene diminuito di 20. Lo stack è così fatto quando viene
chiamata function():
|
|
||
|
|
|
|
|