Premetto che non sono un esperto di sicurezza informatica, pur tentando di tenermi aggiornato sull'argomento. Qualche giorno fa ho partecipato ad un interessante corso sulla sicurezza informatica dove, fra le altre cose, si e' parlato di buffer overflow. Il buffer overflow e' una tecnica vecchia ma ancora molto in voga di compromissione: in breve si tratta di iniettare nello stack di una applicazione una quantita' di dati opportuna in modo da sovrascrivere l'indirizzo di ritorno dello stack per farlo puntare a del codice malevolo. E qui le opinioni si dividono: di chi e' colpa per questo atteggiamento? Sicuramente del programmatore che ha lasciato una potenziale falla nello stack (mancanza di controllo sul limite dei dati) ma, secondo me, anche e soprattuto del sistema operativo.
Con questo post intendo spiegare le mie ragioni dietro a tale affermazione e vedere a grandi linee come ci si potrebbe difendere.

LA COLPA E' DEL SISTEMA OPERATIVO
Si e' detto che affinche' il buffer overflow di tipo stack funzioni e' necessario "iniettare" codice malevolo nello stack (o comunque nell'area dati - qui si pensi solo allo stack) e far saltare il controllo a tale codice (ossia modificare il registro IP). Cio' significa che il processore salta ed esegue codice in un'area di memoria scrivibile. Si presti attenzione: il processore manda in esecuzione dati da una zona di memoria scrivibile, che quindi potrebbe essere modificata in ogni momento. Sarebbe come dire che un demone di sistema abbia i permessi wx sulla sua immagine su disco stessa, e quindi che possa essere sostituito da qualunque programma si voglia. Apriti cielo! Il ritorno dei programmi automodificanti banditi fin dagli anni 50!
Come ci si potrebbe difendere? Beh, il canary e' una buona tecnica, anzi la migliore probabilmente: si inserisce un valore random nello stack che deve essere controllato al ritorno. Se il processore non trova tale valore corretto allora l'esecuzione e' abortita. Ma chi introduce il canarino? Anche qui ulteriore diatriba al corso: non e' il sistema operativo, e' il compilatore. O meglio, il compilatore inserisce nel prologo e nell'epilogo del metodo istruzioni per il controllo del canarino. Ora, se vogliamo dare ragione al docente del corso, il canarino viene controllato dal sistema operativo che esegue il programma...ma comunque senza la modifica al codice prodotto in fase di compilazione (Propolice) cio' non sarebbe possibile.

W^X
W^X e' un sistema concettualmente molto semplice, quasi disarmante nella sua semplicita', ad opera del team di OpenBSD incluso nella release 3.3 (si, la 3.3). L'idea e' quella di avere le pagine di memoria scrivibili private in toto del flag di esecuzione e viceversa: non e' quindi possibile eseguire lo stack poiche' lo stack e' scrivibile. Se l'architettura hardware supporta il flag NX (Not eXecutable) allora il trucco funziona direttamente: lo stack viene marcato come non eseguibile e quindi il buffer overlow e' evitato. Purtroppo non tutte le architetture supportano in hardware tale tecnica. Ma anche sulle architetture che supportano nativamente il flag la cosa si complica: lo stack utente ha tipicamente in cima un trampolino, denominato sigtramp, che serve come punto di lancio per l'intercettazione dei segnali unix. Tale zona puo' essere in sola lettura (il trampolino non cambia una volta che lo stack di chiamata si attiva) e quindi occorre muovere tale trampolino in una posizione non scrivibile, in modo da pulire lo stack. Inoltre c'e' il problema delle librerie condivise, che ad esempio nel loro spazio dati contengono i cosiddetti ctors e dtors, ossia i costruttori e distruttori c++. Quindi anche le librerie condivise hanno uno spazio dati che oltre ad essere scrivibile e' anche eseguibile. Perfino il GOT (Global Offset Table) e il PLT (Procedure Linkage Table) risultano scrivibili, permettendo quindi la modifica degli indirizzi di ritorno. W^X rende il GOT solo leggibile e il PLT (che deve essere eseguibile) non scrivibile, mettendo un po' di ordine nel caos delle librerie condivise. Analogamente i ctors e dtors vengono resi non scrivibili, garantendo anche in questo caso che salti di codice anomalo non siano possibili.
Nelle architetture che supportano il flag NX a livello di pagina il riordinamento di cui sopra e' sufficiente a garantire la protezione richiesta, nelle altre architetture occorre operare un ulteriore trucco. In sostanza si divide la memoria in due regioni (una scrivibile e una eseguibile) inserendo una linea di separazione virtuale. Compilatore e linker devono lavorare per spezzare i vari blocchi del programma e ordinarli in memoria al fine di inserire ogni pezzo nella giusta area di memoria.

RANDOMIZZAZIONE DEGLI INDIRIZZI
La randomizzazione degli indirizzi e' un'altra tecnica, meno efficace di W^X (secondo me), di protezione contro i buffer overflow. L'idea e' quella di inserire in ogni stack un gap random, cosi' da modificare l'effettivo inizio dei dati sullo stack. In questo modo diventa molto piu' complesso individuare dove il codice malevole e' stato piazzato, poiche' ad ogni esecuzione la sua locazione varia e quindi l'attacco ha meno possibilita' di riuscita. Secondo Theo de Raadt questa randomizzazione richiede 3 linee di codice di modifica nel kernel, e quindi non si vede come mai ancora molti vendor commerciali non l'abbiano inclusa.
La randomizzazione puo' essere anche rivolta alla allocazione della memoria: mmap e malloc solitamente funzionano con allocazioni contigue e quindi prevedibili. E' possibile randomizzare lo spazio di indirizzamento in modo che le aree di memoria siano addirittura intervallate fra loro e quindi che non sia possibile saltare da un'area all'altra.


Per maggiori informazioni consiglio di dare un'occhiata qui.

The article La sicurezza è paranoia (?) has been posted by Luca Ferrari on March 30, 2011