Aggiornato al : feb 18, 2008



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(0×0D), un line feed(0×0A) 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.

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.

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.

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.

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.

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

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.

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.

Conclusioni

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

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…

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

Articolo scritto da l3golas. Per ulteriori info o suggerimenti contattare l’autore a questo indirizzo.