3 - Difese dal Buffer Overflow

 

Ci sono quattro approcci di base per difendersi contro vulnerabilità da buffer overflow:

Il metodo di forza bruta consistente nello scrivere codice sicuro è descritto nel paragrafo 3.1.

La soluzione generale che riguarda il sistema operativo descritta nel paragrafo 3.2 è quella di rendere l'area di memoria, destinata a contenere le variabili, non eseguibile, in modo da prevenire l'eventuale inserimento di codice d'attacco. Questo approccio previene molti attacchi basati su buffer overflow, ma poiché l'attaccante non deve necessariamente inserire codice d'attacco per realizzare un overflow, questo metodo ha delle sostanziali vulnerabilità.

L'approccio diretto orientato al compilatore, descritto nel paragrafo 3.3, è quello di controllare la dimensione degli array ad ogni accesso. Questo metodo elimina completamente i problemi di  overflow, rendendolo impossibile, ma induce sostanziali costi.

L'approccio indiretto orientato al compilatore, descritto nel paragrafo 3.4, è, invece, quello di verificare l'integrità dei puntatori prima di dereferenziarli. Mentre questa tecnica non rende impossibili i buffer overflow, ne impedisce molti, e gli attacchi che non previene sono difficili da realizzare. Inoltre, i vantaggi relativi alla compatibilità ed alle prestazioni rispetto al controllo sulla dimensione degli array sono notevoli.


 

3.1 - Scrivere codice sicuro

 

Scrivere codice sicuro è lodevole ma irrimediabilmente costoso[3,4], specialmente per linguaggi, come il C, che ha idiomi, come le stringhe, che inducono facilmente in errore, ed ha una filosofia che antepone le prestazioni alla correttezza. A dispetto di lunghi studi su come scrivere codice sicuro[5], continuano ad emergere, con regolarità, programmi vulnerabili[6]. Pertanto, sono stati sviluppati strumenti e tecniche per aiutare gli sviluppatori a scrivere programmi che, in qualche modo, contengano meno facilmente vulnerabilità ad attacchi di buffer overflow.

Il metodo più semplice è quello di cercare, nel codice sorgente, con utility come grep, chiamate a funzioni di libreria molto vulnerabili, come strcpy e sprintf, che non controllano la lunghezza degli argomenti che manipolano. Sono anche state sviluppate versioni della libreria standard C che avvertono quando un programma fa il link di tali funzioni, inoltre, gruppi di analisi di codice sorgente sono nati[7,8] con l'esplicito obiettivo di analizzare manualmente grandi quantità di codice, cercando comuni vulnerabilità[7].

In linea di massima, una programmazione sicura dovrebbe seguire le seguenti regole:

 

Principio del minor privilegio possibile: Un programma deve avere i privilegi di cui necessita e null'altro. In questo modo se un programma contiene un buco, l'impatto di un eventuale exploit non sarà distruttivo.

In determinati casi un programma può richiedere privilegi elevati per effettuare operazioni altrimenti non realizzabili. Spesso però questi privilegi sono necessari solo per un'operazione, come l'apertura di un raw socket o una bind ad una porta privilegiata, che può essere effettuata immediatamente. Tutto ciò che si deve fare è poi ridimensionare i privilegi di modo che un eventuale intruso non ottenga onnipotenti modalità operative.

 

Scrivere codice semplice: I programmi monolitici (sendmail ne è il prototipo) sono fonte inesauribile di buchi. Nei programmi di limitate dimensioni viene ridotta la possibilità di introdurre falle alla sicurezza, e la concisione aiuta il testing e la manutenzione del software.

 

Non fidarsi di nessuno: Ogni dato in input al programma, sia esso da linea di comando, da variabili d'ambiente o da qualsiasi altra fonte deve essere trattato con sospetto. I programmi sicuri esaminano ogni dato prima di utilizzarlo, al fine di evitare di eseguire operazioni potenzialmente pericolose.

 

Comunque, vulnerabilità causate da buffer overflow, possono essere, in molti casi, non facilmente individuabili.

Ogni programma scritto con strategie di difesa alternative, come ad esempio usando funzioni tipo strncpy o snprintf, può contenere vulnerabilità se il codice contiene un elementare errore off by-one. Ad esempio, nel programma lprm è stata trovata una vulnerabilità[9], nonostante fosse stato analizzato sotto l'aspetto della sicurezza rispetto ad attacchi di buffer overflow.

Per combattere problemi di questo tipo, dovuti quindi a dei bug nei programmi, sono stati sviluppati avanzati strumenti di debug[10]. L'idea di base è quella di iniettare a caso, nei buffer utilizzati, codice d'attacco per individuare eventuali vulnerabilità.

Mentre questi strumenti sono utili per sviluppare programmi più sicuri, la semantica del C non permette loro di garantire totale sicurezza. Le tecniche di debug possono solo minimizzare il numero di vulnerabilità.


 

3.2 - Buffer non eseguibili

 

Il concetto generale è quello di rendere il segmento dati dello spazio di indirizzamento del programma vittima non eseguibile, in modo da impedire all'attaccante di eseguire il codice inserito. Questa è attualmente la linea di progettazione dei sistemi sui vecchi computer, ma i più recenti sistemi UNIX e MS Windows, dipendono dalla possibilità di inserire codice dinamico nel segmento dati dei programmi, per supportare diverse ottimizzazioni delle prestazioni. Non è possibile per tutti i programmi rendere non eseguibile il segmento dati, senza sacrificare sostanziali compatibilità dei programmi già in uso.

Comunque, è possibile rendere lo stack non eseguibile e preservare la compatibilità della maggior parte dei programmi. Infatti, sono disponibili delle patch per Linux e Solaris[11,12], che implementano questo criterio; dal momento che nessun programma "normale" ha del codice eseguibile nello stack, l'approccio causa pochi problemi di compatibilità. C'è però un caso eccezionale in Linux, in cui codice eseguibile deve essere messo nello stack, ad esempio, nel caso d'invio di segnali. La protezione realizzata da questo criterio è molto valida per attacchi basati sull'iniezione di codice eseguibile nelle variabili automatiche, ma non offre nessuna protezione contro altre forme d'attacco. E' possibile aggirare questa strategia di difesa modificando l'indirizzo di ritorno per farlo puntare a codice già residente nel programma[13], oppure inserire il codice d'attacco in buffer allocati nell'heap o nel segmento dati statici.


 

3.3 - Controllo della dimensione degli array

 

Non basta inserire codice per realizzare un buffer overflow, ma è anche necessario modificare il flusso del programma in esecuzione. A dispetto del criterio precedente, il controllo sulla dimensione degli array  elimina completamente le possibilità di realizzare tali attacchi. Se in un array non si può scrivere oltre la sua dimensione non è possibile corrompere i dati adiacenti nello stack. Implementare questo metodo comporta che ogni lettura e scrittura in un array sia riferita ad uno spazio di memoria pari alla dimensione dell'array in questione. L'approccio diretto è quello di testare tutti i riferimenti agli array, ma è spesso possibile impiegare tecniche di ottimizzazione per eliminare molti di questi controlli. Ci sono diversi approcci per implementare il controllo sulla dimensione degli array.


 

3.3.1 - Compilatore C Compaq

Il compilatore Compaq per CPU Alpha supporta, con l'uso dell'opzione "-check_bounds ", un controllo limitato :

·        sono testati solo riferimenti espliciti;

·        dato che tutti gli array in C sono convertiti in puntatori quando passati come argomenti ad una subroutine, nessun controllo viene effettuato sugli accessi in suddette subroutine;

·        funzioni di libreria pericolose come strcpy() non sono compilate con tale controllo e restano pericolose.

 

Poiché è molto comune in C l'uso dell'aritmetica dei puntatori per gli accessi agli array, il controllo sulla dimensione degli array non dà nessuna garanzia sull'immunità da buffer overflow.


 

3.3.2 - Controllo sulla dimensione degli array per il linguaggio C

Richard Jones e Paul Kelly hanno sviluppato una patch per il compilatore gcc[14] che fa un pieno controllo sulla dimensione degli array. I programmi compilati sono compatibili con altri moduli gcc poiché non è stata cambiata la rappresentazione dei puntatori. Infatti, essi derivano un puntatore base da ogni espressione che ne contiene uno, verificando gli attributi di quel puntatore per determinare se l'espressione rientra nei limiti. I costi in termini di prestazioni sono sostanziali: un programma che fa un forte uso dei puntatori (es: il prodotto di matrici tridimensionali) diventa 30 volte più lento. Dato che la lentezza del programma è proporzionale all'uso dei puntatori, abbastanza diffusi nei programmi con permessi di root, questa caduta di prestazioni è particolarmente svantaggiosa. Inoltre il compilatore non sembra essere molto funzionale né performante, dal momento che programmi come elm non sono rimasti immuni da buffer overflow una volta ricompilati.


 

3.3.3 - Controllo dell'accesso in memoria

Purify[15] e' un tool di debbugging orientato all'uso della memoria per i programmi scritti in C. Purify utilizza l' object code insertion per realizzare tutti gli accessi alla memoria. Dopo aver eseguito la fase di linking con le librerie di Purify, otteniamo un programma eseguibile standard in cui vengono controllati tutti i riferimenti ad array, per assicurare che essi siano legittimi. Benché i programmi protetti con Purify girino normalmente senza richiedere un ambiente particolare, Purify non è attualmente considerato un tool per la produzione di codice sicuro: la sua protezione rallenta l'esecuzione di un programma da 3 a 5 volte.


 

3.3.4 - Linguaggi Type-Safety

Tutte le vulnerabilità legate al buffer overflow sono il risultato dell'assenza della tipizzazione in C. Se fosse possibile eseguire solo operazioni "sicure" su di una data variabile, non si potrebbero utilizzare input creativi nella variabile pippo per cambiare in modo arbitrario il contenuto della variabile pluto. Allo stato dell'arte, se si vuole ottenere del codice "sicuro", è preferibile utilizzare un linguaggio che assicura l'integrità dei dati come JAVA o ML.

Purtroppo, nei moderni sistemi operativi e nelle applicazioni dedicate alla sicurezza, ci sono milioni di linee di codice e la maggior parte di esse è scritta in C. D'altronde anche la Java Virtual Machine (JVM) è un programma C, e uno dei modi per attaccare codice in JAVA è tentare il buffer overflow alla JVM stessa.


 

3.4 - Controllo dell'integrità dei puntatori

 

L'intento del codice per il controllo dell'integrità dei puntatori differisce leggermente dal controllo sui "limiti" di un array. Invece di prevenire la modifica dei puntatori (come visto precedentemente) questo approccio tenta di verificare se un puntatore è stato sovrascritto prima di essere utilizzato.

In questo modo, anche se un attaccante riuscisse a modificare un puntatore, quest'ultimo non sarà mai utilizzato. La verifica sull'integrità dei puntatori non risolve perfettamente il problema del buffer overflow: attacchi mirati alla modifica di componenti del programma che non siano i puntatori andranno comunque a segno. La tecnica descritta presenta, comunque, vantaggi sostanziali in termini di performance, di compatibilità con il codice esistente e di sforzo implementativo.

Il controllo dell'integrità sui puntatori è stato sviluppato in 3 modi distinti. Snarskii implementò una versione personalizzata di libc per FreeBSD. Il progetto StackGuard ha portato alla creazione di un compilatore che in modo del tutto trasparente all'utente produce codice di controllo nel record di attivazione di ogni funzione.

Infine, è in fase di sviluppo PointGuard, un compilatore che generalizzando l'idea ispiratrice di StackGuard, realizza il controllo di tutti i puntatori presenti nel programma.

 


 

 

 

Previous Page

Next Page