Attacchi alle applicazioni basati su buffer overflow
1. Università Degli Studi Di Catania
Facoltà di Ingegneria
Corso di Laurea di 2° Livello in
Ingegneria Informatica
Corso di Sicurezza nei Sistemi Informativi
Giacomo Antonino Fazio
2. Cos’è il Buffer Overflow
Il buffer overflow (spesso abbreviato in BOF) è una delle
tecniche più avanzate di hacking del software.
Spesso 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.
Il buffer overflow consiste nel fornire ad un programma più dati
di quanti lo spazio di memoria ad essi assegnato ne possa
contenere, facendo in modo che una parte di questi dati vada
scritta in zone di memoria dove ci sono, o dovrebbero esserci,
altri dati (da ciò il nome, che letteralmente significa “Trabocco
dell’area di memoria”).
3. Cos’è il Buffer Overflow
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).
B contiene il numero 3.
4. Cos’è il Buffer Overflow
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. Proviamo ad inserire una stringa più lunga di 8
caratteri, ad esempio inseriamo “excessive”, che occuperà 9 caratteri
più il carattere di fine stringa.
La porzione di memoria successiva, che era occupata da B, verrà
irrimediabilmente sovrascritta.
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 proviamo a scrivere una stringa ancora più lunga, essa
invadere anche l’area di memoria che si trova dopo di B. Risultato:
segmentation fault!!!
5. Cos’è il Buffer Overflow
Un programma è esposto a Buffer Overflow e
alle conseguenze che esso può causare se:
prevede l'input di dati di lunghezza
variabile e non nota a priori;
li immagazzina entro buffer allocati nel
suo spazio di memoria dati vicino ad altre
strutture dati vitali per il programma stesso;
il programmatore non ha implementato
alcun mezzo di controllo della correttezza
dell'input inserito.
6. Tipi di Buffer Overflow
Esistono diversi modi per portare avanti un
BOF. Tra i più importanti:
Arithmetic overflow: si ha quando il
risultato prodotto da un calcolo è più grande
delle spazio che dovrebbe contenerlo.
Buffer Overflow basati sulla memoria:
vengono distinti in base all’area di memoria
che vanno a interessare. Quelli più diffusi
sono i buffer overflow “di heap” e “di stack”.
7. Arithmetic Overflow
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’. Adesso premiamo ‘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”.
8. Struttura della memoria di un processo
Quando eseguiamo un programma, esso verrà caricato in
memoria in maniera ben strutturata creando diverse zone:
.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;
zona dati, che contiene le variabili globali, sia inizializzate
(contenute in una regione detta .DATA) che non
inizializzate (contenute in una regione detta .BSS);
HEAP, generalmente posto dopo la zona dati, in cui
vengono memorizzate le variabili allocate dinamicamente;
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
9. Struttura della memoria di un processo
Come si può vedere dalla figura, lo heap e
lo stack crescono in maniera diversa: il primo
cresce verso l’alto, il secondo verso il basso.
Lo stack è organizzato a pila, nel senso che
l’ultimo dato inserito è il primo ad essere letto
(LIFO, Last In First 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.
10. E il processore?
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.
12. Programma esemplificativo
Le prime tre istruzioni sono tre operazioni di push, che inseriscono
i valori 2, 1 e 0 nello 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, 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 di essa, per poter riprendere l’esecuzione dall’istruzione
successiva alla chiamata.
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 poi 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 buffer, inserite nello stack seguendo come sempre la modalità
LIFO.
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.
13. Buffer Overflow di Stack
Questo tipo di BOF è quello in assoluto più diffuso e
interessa lo stack. È necessario:
Fare in modo che il codice sia nell’address
space del programma . Si distinguono due casi:
Inserirlo manualmente (Code injection): il
programma chiede in input una stringa, che verrà
inserita dall’attaccante in modo da contenere il
codice di attacco, sotto forma di istruzioni per la
CPU.
Il codice si trova già lì: il codice che ci serve è già
presente, bisogna solo parametrizzarlo a dovere.
14. Buffer Overflow di Stack
Fare in modo che il programma salti al codice di attacco e lo esegua :
Activation Records: 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 con l’indirizzo
del codice di attacco.
Puntatori a funzioni: si effettua l’overflow di un buffer vicino ad un puntatore, in
modo da corrompere quest’ultimo e da farlo puntare alla locazione del codice di
attacco.
Longjmp buffers: sfrutta un meccanismo presente in C che consente di salvare
lo stato di un buffer mediante il comando setjmp(buffer) e di ripristinarlo in seguito
mediante il comando longjmp(buffer). Se abbiamo un buffer adiacente di cui è
possibile effettuare l’overflow, potremmo 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.
Spesso l’inserimento del codice di attacco e la sua esecuzione sono effettuati in una
volta sola, ma non necessariamente.
Lo shellcode è un pezzo di codice macchina eseguito per sfruttare una vulnerabilità.
Deve essere altamente specifico e verificato nei minimi dettagli.
15. Esempio 1 di BOF di Stack
Il programma non fa altro che
chiamare la funzione example(),
la quale alloca la variabile
command con valore calc, che
rappresenta il comando che
vogliamo eseguire (la semplice
calcolatrice di Windows).
Successivamente viene allocata
la variabile name, in cui
vogliamo inserire un nome da
dare allo script, cosa che viene
fatta richiamando la funzione
gets(). La restante parte serve
per farci capire cosa sta
succedendo in memoria, infatti ci
mostra un’istantanea dello stack.
16. Esempio 1 di BOF di Stack
Se come nome dello script inseriamo “hello”:
Tutto ok
Abbiamo inserito
“hello” nella
variabile name e
poi abbiamo
eseguito il
contenuto di
command, cioè
“calc”.
17. Esempio 1 di BOF di Stack
Se come nome dello script inseriamo “xxxxxxxxxxxxxxxxcmd ”:
Buffer overflow e
shell in locale!!!
Abbiamo scritto
al di là della
variabile name,
sovrascrivendo
anche command
che adesso
contiene “cmd”
18. Esempio 2 di BOF di Stack
Il programma prende una stringa in
ingresso e la inserisce all’interno della
variabile var, utilizzando la funzione
strcpy(). 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.
Facendo un po’ di prove, ci accorgiamo
che il programma va in segmentation fault
non appena inseriamo 14 caratteri.
Se diamo in input 14 caratteri e l’indirizzo
della funzione function() (che possiamo
trovare disassemblando il codice)… il gioco
è fatto!
19. Esempio 2 di BOF di Stack
Come fare a passargli l’indirizzo? Esso
infatti è scritto in caratteri esadecimali e
non ASCII. Scriviamo un piccolo
exploit, che si occupa di convertire in
ASCII e di passare al programma
l’indirizzo da noi inserito in
esadecimale.
20. Buffer Overflow di Heap
Questo tipo di BOF è noto e sfruttato da molto tempo, ma se ne parla
sempre meno di quello di stack, soprattutto perché è in genere più difficile
da sfruttare rispetto a quest’ultimo. Esistono diverse tecniche per portarlo
avanti:
Attacchi basati su malloc() e funzioni simili: per ogni variabile allocata
dinamicamente, viene allocato uno spazio di lunghezza prestabilita ma se
non ci sono controlli, è molto semplice scrivere oltre esso, sovrascrivendo
l’area adiacente occupata possibilmente da un’altra variabile.
Attacchi basati sulla sovrascrittura di puntatori: si effettua l’overflow di
un buffer adiacente ad un puntatore in modo da corrompere quest’ultimo
e farlo puntare alla locazione del codice di attacco.
Attacchi basati su puntatori a funzioni: si effettua l’overflow di un buffer
vicino ad un puntatore a funzione, in modo da corrompere quest’ultimo e
farlo puntare alla funzione di attacco.
21. Esempio di BOF di Heap
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.
22. Alla ricerca di Buffer Overflow
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 alla ricerca di vulnerabilità da
sfruttare, o aspetta che sia qualcun altro a farlo.
Quando 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.
L’analisi del codice può essere fatta a diversi livelli:
lessicale, semantico, basato su tecniche di intelligenza
artificiale, a runtime, reverse engineering, ricerca di bug
specifici, ecc.
23. Blaster: un worm costruito su un BOF
Rilevato l’11 Agosto 2003 sui primi computer, si diffuse a
macchia d’olio nel giro di appena 2 giorni
Infetta i computer con SO Microsoft Windows XP o 2000
L’obiettivo finale era colpire Microsoft, mediante un attacco
DDoS al sito di Windows Update.
L’avversione nei confronti di Microsoft è dimostrata anche
dalla stringa trovata nel codice del worm: “billy gates why do
you make this possible ? Stop making money and fix your
software!!”
Effetti: danni in tutto il mondo per oltre 3 milioni di dollari
Colpevole: un ragazzo di 18 anni del Minnesota (USA)
24. Blaster: un worm costruito su un BOF
Blaster si sviluppa su una vulnerabilità descritta da
Microsoft stessa nel Microsoft Security Bulletin MS03-026.
Si tratta di una falla nell’interfaccia RPC (Remote
Procedure Call) di un oggetto DCOM (Distributed
Component Object Model).
DCOM: tecnologia che abilita componenti software che
non si trovano sulla stessa macchina a comunicare
direttamente utilizzando una rete;
RPC: protocollo usato 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.
25. Blaster: un worm costruito su un BOF
Se la richiesta di comunicazione viene posta
all’interfaccia RPC in modo errato, ci possono essere
problemi perché essa non controlla opportunamente le
dimensioni dei messaggi ricevuti in input.
Un malintenzionato potrebbe scrivere un exploit che
invia all’oggetto DCOM un messaggio non corretto e
costruito in modo da causare un buffer overflow, che gli
permetterebbe di avere il controllo completo sulla
macchina. Per poter fare ciò, il malintenzionato deve
utilizzare una tra le porte aperte per RPC, tra cui 135,
139, 445 e 593.
Blaster fa proprio questo!!!
26. Blaster: un worm costruito su un BOF
Supponiamo di avere un computer infetto. Il tutto si svolge in diverse fasi:
Attesa: A deve prima controllare di essere connesso ad Internet, mediante la
funzione InternetGetConnectedState(). Se l’esito è positivo, si va alla fase 2
Generazione indirizzi IP: il programma genera gli indirizzi IP dei computer a cui
lanciare il contagio. Effettua uno scan di essi alla ricerca di computer vulnerabili, tra cui
supponiamo ci sia un ipotetico B.
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!!!
Controllo del contagio: attraverso la porta 135, A controlla se B è già infetto e in
caso affermativo termina; in caso negativo, invece, attiva i socket per comunicare con
B.
La shell CMD.EXE: A questo punto, A lancia su B la shell tramite il comando
cmd.exe, necessaria ad A per far eseguire a B dei comandi.
Download del worm: tramite la shell, B richiede ad A l’eseguibile msblast.exe, che
scarica nella cartella %systemroot%/system32 (cartella di sistema). Il file appena
scaricato viene lanciato.
Aggiornamento delle Registry Keys: utilizzando la shell lanciata nella fase 5, A
apporta delle modifiche ad alcune Registry Keys di B, in modo che il worm venga
eseguito ad ogni avvio del pc.
27. Blaster: un worm costruito su un BOF
Il worm è stato progettato affinchè i computer infetti
effettuino un attacco DDoS in momenti ben precisi: ogni
giorno (nel caso di mesi compresi tra Settembre e
Dicembre) e dal 16 del mese in poi per gli altri mesi.
I sintomi che permettono di accorgersi della presenza di
Blaster sul proprio sistema sono: prestazioni ridotte,
continui riavvii, traffico irregolare sulle porte TCP 135 e
4444 e UDP 69.
Presenza di tool per l’eliminazione automatica del worm
Rilevate ad oggi 4 varianti: Lovesan A, Lovesan B,
Lovesan C e Lovesan F.
28. Difesa contro i BOF e… nuovi attacchi
Diverse tecniche sono state inventate per cercare di frenare il più possibile i buffer
overflow e nuovi attacchi sono stati messi a punto per bypassare esse.
Difesa - scelta del linguaggio di programmazione: C e C++ 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). Altri linguaggi
effettuano controlli (es. Java, Python, Ada, Lisp).
Difesa - Scrivere codice corretto: utopia! Per quanto si possa controllare il
proprio codice, i bug potrebbero risiedere nelle funzioni delle librerie utilizzate.
Difesa – Attenzione ai programmi SUID: programmi che vanno in
esecuzione con privilegi di root, chiunque sia ad eseguirli. Alcuni di essi sono
necessari per effettuare operazioni comuni, altri non lo sono affatto o non vengono
mai usati, 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.
29. Difesa contro i BOF e… nuovi attacchi
Difesa - uso di librerie “safe”: sostituiscono le funzioni “incriminate” di LibC
(es. gets(), scanf(), printf(), ecc.) con versioni “safe” (in teoria) offrendo in alcuni
casi una completa nuova implementazione delle stringhe. Es.:
Libsafe
The Better String Library
Arri Buffer API
Vstr
Funzione strlcpy()
Difesa – Protezione contro lo “stack smashing”: viene scritto nello stack
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 ci si
accorge dell’avvenuta manipolazione dello stack ed è possibile correre ai ripari. 3 tipi di
canaries: Terminator, Random e Random XOR canaries. Esempio di programmi che
implementano questo tipo di protezione:
ProPolice
StackGuard
StackGhost
30. Difesa contro i BOF e… nuovi attacchi
Difesa – Protezione dello spazio eseguibile: protezione
implementata sia a livello hardware che software. L’idea è quella di rendere
parte della memoria non scrivibile o non eseguibile, in modo da evitare la
maggior parte dei BOF, ad esempio quelli basati sulla “code injection” (se la
memoria non è eseguibile, inserisco il codice di attacco ma non posso
eseguirlo).
Soluzione hardware: NX bit
Soluzione software: DEP (Windows), W^X (OpenBSD), PaX (Linux),
Exec Shield (Linux).
Nuovo attacco – Gli attacchi “return-to-libc”: Lo scopo è quello di
sovrascrivere l’indirizzo di ritorno di una funzione 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 non è
necessario eseguire codice che si trova nello stack o nello heap, aggirando
quindi l’ostacolo rappresentato dalla protezione dello spazio eseguibile.
31. Difesa contro i BOF e… nuovi attacchi
Difesa – Address Space Layout Randomization (ASLR):
l’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 gli activation records
e i return-to-libc, a causa della difficoltà di trovare l’indirizzo del codice
da eseguire. Implementata anche in PaX e Exec Shield.
Difesa – Deep Packet Inspection (DPI): consente 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 e di
evitare che passino. Utile ma spesso poco efficace: previene solo gli
attacchi conosciuti.
32. Difesa contro i BOF e… nuovi attacchi
Difesa – Intrusion Detection Systems (IDS): riconoscono i pacchetti che
transitano in rete e che mirano ad effettuare manipolazioni sui sistemi o attacchi contro
servizi vulnerabili e applicazioni. Sono composti da diverse parti:
Sensori: osservano gli eventi che avvengono sul sistema;
Analizzatori: analizzano gli eventi passati loro dai sensori;
Gestore: riceve gli eventi degni di nota dagli analizzatori e prende provvedimenti
sia passivi che attivi.
Nuovo attacco – Shellcode alfanumerici, polimorfici, metamorfici e
automodificanti: utilizzano tecniche spesso messe in pratica dai worm per non farsi
scovare. In particolare:
Shellcode polimorfici: variano continuamente, lasciando immutato l’algoritmo
originale. Spesso per ottenere ciò utilizzano la crittografia, lasciando però una
parte non criptata che contiene le informazioni per decriptare il resto. Gli IDS
mirano a riconoscere proprio questa parte, attraverso una scansione basata su
pattern.
Shellcode metamorfici: ancora peggio di quelli polimorfici, con l’obiettivo di
vanificare le scansioni degli IDS basate su pattern.
33. Difesa contro i BOF e… nuovi attacchi
Conclusioni: La soluzione non esiste!!!
Scrivere codice corretto è un’utopia, perché è facile
sbagliare o commettere una leggerezza o utilizzare codice
di terzi che involontariamente contiene dei bug.
È possibile comunque affrontare il problema, sia
utilizzando il buon senso, che mediante svariate tecniche
che si possono spesso combinare tra loro.
Ricordare che 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.