Sei sulla pagina 1di 31

Tradotto dal Inglese al Italiano - www.onlinedoctranslator.

com

82 24 peccati capitali della sicurezza del software

replicato dall'attaccante. Ad esempio, il codice C# seguente esegue l'hashing di un nome utente e di una
password e utilizza il risultato come chiave in un campo HTTP per identificare l'utente:

SHA256Managed s = new SHA256Managed();


byte [] h = s.ComputeHash(UTF8Encoding.UTF8.GetBytes(uid + ":" + pwd)); h =
s.ComputeHash(h);
stringa b64 = Convert.ToBase64String(h); // risultato base64

Oppure, codice simile in JavaScript (da HTML o ASP) chiama CAPICOM su Windows:

// Risultato hash esadecimale


var oHash = new ActiveXObject("CAPICOM.HashedData");
oHash.Algoritmo = 0;
oHash.Hash(uid + ":" + pwd);
oHash.Hash(oHash.Valore);
var b64 = oHash.Value; // Risultato esadecimale

Oppure, un codice simile in Perl esegue anche l'hashing del nome e della password dell'utente:

usa Digest::SHA1 qw(sha1 sha1_base64); mio $s =


$uid . ":" . $pwd;
mio $b64 = sha1_base64(sha1($s)); # risultato in base64

Si noti che tutti questi esempi eseguono l'hash dell'hash della stringa concatenata per mitigare una vulnerabilità
chiamataattacchi di estensione della lunghezza.Una spiegazione della vulnerabilità esula dallo scopo di questo libro,
ma per tutti gli usi pratici, non limitarti ad eseguire l'hashing dei dati concatenati, esegui una delle seguenti
operazioni:

Risultato = H(dati1, H(dati2))

Risultato = H(H(data1 CONCAT data2))

Questo problema è trattato in modo un po' più dettagliato in Sin 21, "Usare la crittografia errata".
Ma anche il codice che utilizza solide difese crittografiche potrebbe essere vulnerabile agli attacchi!
Immagina un hash di nome utente e password fino a "xE/f1/XKonG+/ XFyq+Pg4FXjo7g=" e lo aggiungi
all'URL come "verificatore" una volta che il nome utente e la password sono stati verificati. Tutto ciò che
un utente malintenzionato deve fare è visualizzare l'hash e riprodurlo. L'aggressore non ha bisogno di
visualizzare la password! Tutta quella criptovaluta stravagante non ti ha comprato nulla! Puoi risolvere
questo problema con la tecnologia di crittografia del canale come SSL, TLS o IPSec.
Peccato 4: uso di URL magici, cookie prevedibili e campi modulo nascosti 83

L'attaccante prevede i dati


In questo scenario, un utente si connette con un nome utente e una password, possibilmente tramite
SSL/TLS, quindi il codice del server verifica le informazioni sull'account e genera un valore a incremento
automatico per rappresentare tale utente. Ogni interazione di quell'utente utilizza il valore per
identificarlo senza richiedere al server di eseguire i passaggi di autenticazione. Questo può essere
facilmente attaccato tramite SSL/TLS. Ecco come: un utente valido ma malintenzionato si connette al
server e fornisce le sue credenziali valide. Riceve un valore identificativo, 7625, dal server. Questo
valore potrebbe essere sotto forma di un URL o di un cookie. Quindi chiude il browser e riprova con lo
stesso nome utente e password validi. Questa volta recupera il valore 7627. Sembra che questo sia un
valore incrementale e che qualcun altro abbia probabilmente effettuato l'accesso tra i due accessi del
primo utente. Ora tutto ciò che l'attaccante deve fare per dirottare la sessione dell'altro utente è
connettersi (tramite SSL/TLS!), impostando l'identificatore di connessione su 7626. Le tecnologie di
crittografia non aiutano a proteggere da prevedibilità come questa. È possibile impostare
l'identificatore di connessione utilizzando numeri crittograficamente casuali, utilizzando codice come
questo JavaScript e CAPICOM:

var oRNG = new ActiveXObject("CAPICOM.Utilities"); var rng =


oRNG.GetRandom(32,0);

CAPICOM chiama la funzione CryptGenRandom su Windows.

O PHP su Linux o UNIX (supponendo che il sistema operativo supporti /dev/random o /


dev/urandom):

// @ before per evitare che fopen scarichi troppe informazioni all'utente $hrng = @fopen("/
dev/urandom","r");
se ($hrng) {
$rng = base64_encode(fread($hrng,32));
fclose($hrng);
}

Oppure in Java:

Tentativo {

SecureRandom rng = SecureRandom.getInstance("SHA1PRNG"); byte b[] =


nuovo byte[32];
rng.nextBytes(b);
} catch(NoSuchAlgorithmException e) {
// Gestisci l'eccezione
}
84 24 peccati capitali della sicurezza del software

O in VB.Net:

Dim rng As New RNGCryptoServiceProvider() Dim b(32)


As Byte
rng.GetBytes(b)

L'implementazione predefinita di SecureRandom di Java ha un pool di entropia molto piccolo. Può andare bene per la
gestione delle sessioni e l'identità in un'applicazione web, ma probabilmente non è abbastanza buono per le chiavi di
lunga durata.

Detto questo, c'è ancora un potenziale problema con l'utilizzo di numeri casuali imprevedibili: se
l'attaccante può visualizzare i dati, l'attaccante può semplicemente visualizzare il valore casuale e poi
riprodurlo! A questo punto, potresti prendere in considerazione l'utilizzo della crittografia del canale,
come SSL/TLS. Ancora una volta, dipende dalle minacce che ti riguardano.

L'attaccante modifica i dati


Infine, supponiamo che tu non sia realmente preoccupato per un utente malintenzionato che visualizza
i dati, ma sei preoccupato per un utente malintenzionato che modifica dati validi. Questo è il problema
del "campo modulo nascosto con il prezzo incorporato". Se è necessario supportare questo scenario, è
possibile inserire un codice di autenticazione del messaggio (MAC) come voce del campo del modulo; e
se il MAC restituito dal browser non corrisponde al MAC che hai inviato o manca il MAC, allora sai che i
dati sono stati modificati. Pensa a un MAC come a un hash che include una chiave segreta e dati che
normalmente avresti hash. Il MAC più comunemente usato è il codice di autenticazione del messaggio
hash con chiave (HMAC), quindi d'ora in poi useremo solo il termine HMAC. Quindi, per un modulo,
concateneresti tutto il testo nascosto nel modulo (o qualsiasi campo che desideri proteggere) e
eseguirai l'hashing di questi dati con una chiave conservata sul server. In Do#,

HMACSHA256 hmac = nuovo HMACSHA256(chiave);


byte[] dati = UTF8Encoding.UTF8.GetBytes(formdata);
risultato stringa = Convert.ToBase64String(hmac.ComputeHash(data));

Oppure in Perl:

usare rigoroso;
usa Digest::HMAC_SHA1;

my $hmac = Digest::HMAC_SHA1->new($key); $hmac-


>aggiungi($formdata);
my $risultato = $hmac->b64digest;
Peccato 4: uso di URL magici, cookie prevedibili e campi modulo nascosti 85

PHP non ha una funzione HMAC, ma PHP Extension and Application Repository
(PEAR) sì. (Vedere la sezione "Altre risorse" per un collegamento al codice.)
Il risultato dell'HMAC potrebbe quindi essere aggiunto alla forma nascosta, vale a dire:

<INPUT TYPE = HIDDEN NAME = "HMAC" VALUE = "X8lbKBNG9cVVeF9+9rtB7ewRMbs">

Quando il codice riceve il campo del modulo HMAC nascosto, il codice del server può
verificare che le voci del modulo non siano state manomesse ripetendo i passaggi di
concatenazione e hash.
Non usare un hash per questo lavoro. Usa un HMAC perché un hash può essere ricalcolato dall'attaccante; un
HMAC non può farlo a meno che l'attaccante non abbia la chiave segreta memorizzata sul server.

MISURE EXTRA DIFENSIVE


Non ci sono ulteriori misure difensive da adottare.

ALTRE RISORSE
- Enumerazione delle debolezze comuni: http://cwe.mitre.org/
- Specifica W3C HTML Hidden Field: www.w3.org/TR/REC-html32#fields
- Crittografia praticadi Niels Ferguson e Bruce Schneier (Wiley, 1995), §6.3
“Weaknesses of Hash Functions”
- PERA HMAC: http://pear.php.net/package/Crypt_HMAC
- “Hold Your Sessions: An Attack on Java Session-Id Generation" di Zvi Gutterman e
Dahlia Malkhi: http://research.microsoft.com/~dalia/pubs/GM05.pdf

RIEPILOGO
- Faretestare tutti gli input Web, inclusi moduli e cookie con input dannosi.
- Farecapire i punti di forza e di debolezza dei tuoi progetti se non stai
usando primitive crittografiche per riscattarti.
- Nonincorporare dati riservati in qualsiasi costrutto HTTP o HTML, come l'URL, il
cookie o il modulo, se il canale non è protetto utilizzando una tecnologia di
crittografia come SSL, TLS o IPSec o utilizza difese crittografiche a livello di
applicazione.
- Nonfidarsi di qualsiasi dato, confidenziale o meno, in un modulo Web, perché gli utenti malintenzionati
possono facilmente modificare i dati con qualsiasi valore che desiderano, indipendentemente dall'utilizzo o
meno di SSL.
86 24 peccati capitali della sicurezza del software

- Nonutilizzare le intestazioni HTTP referer [sic] come metodo di autenticazione.

- Nonutilizzare dati prevedibili per i token di autenticazione.


- Nonpensare che l'applicazione sia sicura solo perché si prevede di utilizzare la crittografia;
gli aggressori attaccheranno il sistema in altri modi. Ad esempio, gli aggressori non
tenteranno di indovinare numeri crittograficamente casuali; cercheranno di visualizzarli.
II
Implementazione
Peccati

87
Questa pagina è stata lasciata vuota intenzionalmente
5
Superamenti del buffer

89
90 24 peccati capitali della sicurezza del software

PANORAMICA DEL PECCATO


I sovraccarichi del buffer sono stati a lungo riconosciuti come un problema nei linguaggi di basso livello. Il
problema principale è che i dati dell'utente e le informazioni sul controllo del flusso del programma sono
mescolati per motivi di prestazioni e i linguaggi di basso livello consentono l'accesso diretto alla memoria
dell'applicazione. C e C++ sono i due linguaggi più popolari afflitti da sovraccarichi del buffer.
A rigor di termini, un sovraccarico del buffer si verifica quando un programma consente all'input di
scrivere oltre la fine del buffer allocato, ma ci sono diversi problemi associati che spesso hanno lo
stesso effetto. Uno dei più interessanti è il bug delle stringhe di formato, che tratteremo in Sin 6.
Un'altra incarnazione del problema si verifica quando a un utente malintenzionato è consentito scrivere
in una posizione di memoria arbitraria al di fuori di un array nell'applicazione, e mentre, in senso
stretto, questo non è un classico sovraccarico del buffer, lo tratteremo anche qui.
Un approccio un po' più nuovo per ottenere il controllo di un'applicazione consiste nel controllare i
puntatori agli oggetti C++. Capire come utilizzare gli errori nei programmi C++ per creare exploit è
considerevolmente più difficile del semplice overrun di uno stack o di un buffer di heap: tratteremo
questo argomento in Sin 8, "C++ Catastrofi".
L'effetto di un sovraccarico del buffer è qualsiasi cosa, da un arresto anomalo all'aggressore che
ottiene il controllo completo dell'applicazione e, se l'applicazione è in esecuzione come utente di alto
livello (root, amministratore o sistema locale), allora il controllo dell'intero sistema operativo e tutti gli
altri utenti attualmente connessi o che si collegheranno sono nelle mani dell'aggressore. Se
l'applicazione in questione è un servizio di rete, il risultato del difetto potrebbe essere un worm. Il
primo noto worm Internet sfruttava un sovraccarico del buffer nel finger server ed era noto come
finger worm Robert T. Morris (o semplicemente Morris). Anche se sembrerebbe che abbiamo imparato
come evitare i sovraccarichi del buffer da quando uno ha quasi bloccato Internet nel 1988, continuiamo
a vedere frequenti segnalazioni di sovraccarichi del buffer in molti tipi di software.

Ora che siamo diventati ragionevolmente bravi nell'evitare i classici errori che portano a un sovraccarico
dello stack di un buffer di dimensioni fisse, le persone si sono rivolte allo sfruttamento dei sovraccarichi
dell'heap e alla matematica coinvolta nel calcolo delle dimensioni dell'allocazione: gli overflow di numeri interi
sono trattati in Sin 7. Le lunghezze che le persone fanno per creare exploit a volte sono sorprendenti. In "Heap
Feng Shui in JavaScript", Alexander Sotirov spiega come le allocazioni di un programma possono essere
manipolate per ottenere qualcosa di interessante accanto a un buffer di heap che può essere sovraccaricato.

Anche se si potrebbe pensare che solo i programmatori sciatti e incuranti cadano preda di
sovraccarichi del buffer, il problema è complesso, molte delle soluzioni non sono semplici e chiunque
abbia scritto abbastanza codice C/C++ ha quasi sicuramente commesso questo errore. L'autore di
questo capitolo, che insegna ad altri sviluppatori come scrivere un codice più sicuro, ha fornito ai clienti
un overflow singolo. Anche i programmatori molto bravi e molto attenti commettono errori e i migliori
programmatori, sapendo quanto sia facile sbagliare, mettono in atto solide pratiche di test per rilevare
gli errori.
Peccato 5: Superamento del buffer
91

RIFERIMENTI CWE
Questo peccato è abbastanza grande da meritare un'intera categoria:
CWE-119: Mancato vincolo delle operazioni entro i limiti di un buffer di memoria Ci sono un
certo numero di voci secondarie che esprimono molte delle varianti trattate in questo
capitolo:

- CWE-121: overflow del buffer basato su stack

- CWE-122: overflow del buffer basato su heap

- CWE-123: Condizione Scrivi-cosa-dove


- CWE-124: Violazione dell'inizio del confine ("Buffer Underwrite")
- CWE-125: lettura fuori limite
- CWE-128: Errore di wrap-around
- CWE-129: indicizzazione dell'array non controllata

- CWE-131: calcolo errato della dimensione del buffer

- CWE-193: Errore Off-by-one


- CWE-466: ritorno del valore del puntatore al di fuori dell'intervallo previsto

- CWE-120: copia del buffer senza controllare la dimensione dell'input ("overflow del buffer
classico")

LINGUE INTERESSATE
C è il linguaggio più comune utilizzato per creare sovraccarichi del buffer, seguito da vicino da C++. È
facile creare sovraccarichi del buffer durante la scrittura in assembler, dato che non ha alcuna
protezione. Sebbene il C++ sia intrinsecamente pericoloso quanto il C, poiché è un superset di C, l'uso
attento della libreria di modelli standard (STL) può ridurre notevolmente il rischio di maltrattamento
delle stringhe e l'utilizzo di vettori invece di array statici può ridurre notevolmente gli errori e molti
degli errori finiscono in arresti anomali non sfruttabili. La maggiore severità del compilatore C++
aiuterà un programmatore a evitare alcuni errori. Il nostro consiglio è che anche se stai scrivendo
codice C puro, l'uso del compilatore C++ risulterà in un codice più pulito.
I linguaggi di livello superiore inventati più di recente astraggono l'accesso diretto alla memoria dal
programmatore, generalmente a un notevole costo in termini di prestazioni. Linguaggi come Java, C# e
Visual Basic hanno tipi di stringa nativi, forniscono matrici controllate dai limiti e in genere vietano
l'accesso diretto alla memoria. Anche se alcuni direbbero che ciò rende impossibili i sovraccarichi del
buffer, è più corretto affermare che i sovraccarichi del buffer sono molto meno probabili.
92 24 peccati capitali della sicurezza del software

In realtà, la maggior parte di questi linguaggi è implementata in C/C++ o passa i dati forniti
dall'utente direttamente nelle librerie scritte in C/C++ e i difetti di implementazione possono causare
sovraccarichi del buffer. Un'altra potenziale fonte di sovraccarichi del buffer nel codice di livello
superiore esiste perché il codice deve infine interfacciarsi con un sistema operativo e quel sistema
operativo è quasi certamente scritto in C/C++.
C# ti consente di esibirti senza net dichiarando sezioni non sicure; tuttavia, sebbene fornisca
un'interoperabilità più semplice con il sistema operativo sottostante e le librerie scritte in C/C++, è
possibile commettere gli stessi errori che si possono commettere in C/C++. Se programmi
principalmente in linguaggi di livello superiore, l'azione principale per te è continuare a convalidare i
dati passati a librerie esterne, oppure puoi agire come canale per i loro difetti.
Sebbene non forniremo un elenco esaustivo delle lingue interessate, la maggior parte delle lingue
meno recenti è vulnerabile ai sovraccarichi del buffer.

IL PECCATO SPIEGATO
La classica incarnazione di un sovraccarico del buffer è nota come "distruggere lo stack". In un
programma compilato, lo stack viene utilizzato per contenere le informazioni di controllo, come gli
argomenti, a cui l'applicazione deve tornare una volta terminata la funzione e, a causa del numero
ridotto di registri disponibili sui processori x86, molto spesso i registri vengono memorizzati
temporaneamente in pila. Sfortunatamente, anche le variabili allocate localmente vengono
memorizzate nello stack. Queste variabili dello stack sono talvolta erroneamente denominate allocate
staticamente, invece di essere allocate dinamicamente memoria heap. Se senti qualcuno parlare di a
staticosovraccarico del buffer, ciò che realmente significano è apilasovraccarico del buffer. La radice del
problema è che se l'applicazione scrive oltre i limiti di un array allocato nello stack, l'attaccante può
specificare le informazioni di controllo. E questo è fondamentale per il successo; l'attaccante vuole
modificare i dati di controllo ai valori della sua offerta.
Ci si potrebbe chiedere perché continuiamo a utilizzare un sistema così evidentemente pericoloso.
Abbiamo avuto l'opportunità di sfuggire al problema, almeno in parte, con una migrazione al chip Itanium a
64 bit di Intel, dove gli indirizzi di ritorno sono memorizzati in un registro. Il problema è che dovremmo
tollerare una significativa perdita di compatibilità con le versioni precedenti e il chip x64 è diventato il chip più
popolare.
Potresti anche chiederti perché non migriamo tutti al codice che esegue un controllo rigoroso dell'array e
non consente l'accesso diretto alla memoria. Il problema è che per molti tipi di applicazioni le caratteristiche
prestazionali dei linguaggi di livello superiore non sono adeguate. Una via di mezzo consiste nell'usare
linguaggi di livello superiore per le interfacce di livello superiore che interagiscono con cose pericolose (come
gli utenti!) e linguaggi di livello inferiore per il codice di base. Un'altra soluzione consiste nell'utilizzare
completamente le funzionalità di C++ e utilizzare le librerie di stringhe e le classi di raccolta.

Ad esempio, il server Web Internet Information Server (IIS) 6.0 è passato interamente a una classe di stringhe C+
+ per la gestione dell'input e uno sviluppatore coraggioso ha affermato che si sarebbe amputato il mignolo se nel suo
codice fossero stati trovati sovraccarichi del buffer. Al momento della stesura di questo documento, lo sviluppo
Peccato 5: Superamento del buffer
93

oper ha ancora il dito, nessun bollettino di sicurezza è stato emesso contro il server web in due anni
dopo il suo rilascio, e ora ha uno dei migliori record di sicurezza di qualsiasi server web principale. I
compilatori moderni gestiscono bene le classi basate su modelli ed è possibile scrivere codice C++ con
prestazioni molto elevate.
Basta teoria, consideriamo un esempio:

# include <stdio.h>
void DontDoThis(char* input) {

char buf[16];
strcpy(buf, input);
printf("%s\n", buf);
}
int main(int argc, char* argv[]) {

// Quindi non stiamo controllando gli argomenti


// Cosa ti aspetti da un'app che usa strcpy? Non fare così(argv[1]);

ritorno 0;
}

Ora compiliamo l'applicazione e diamo un'occhiata a cosa succede. Per questa dimostrazione,
l'autore ha utilizzato una build di rilascio con i simboli di debug abilitati e il controllo dello stack
disabilitato. Un buon compilatore vorrà anche incorporare una funzione piccola come DontDoThis,
specialmente se viene chiamata solo una volta, quindi ha anche disabilitato le ottimizzazioni. Ecco come
appare lo stack sul suo sistema immediatamente prima di chiamare strcpy:

0x0012FEC0 c8 fe 12 00 Èþ.. <- indirizzo dell'argomento buf Ä.2. <-


0x0012FEC4 c4 18 32 00 indirizzo dell'argomento di input Ðþ.. <- inizio di
0x0012FEC8 d0 fe 12 00 buf
0x0012FECC 04 80 40 00 . [] @.
0x0012FED0 e7 02 3f 4f ç.?O
0x0012FED4 66 00 00 00 f... <- fine buf
0x0012FED8 e4 fe 12 00 äþ.. <- contenuto del registro EBP ?.@. <-
0x0012FEDC 3f 10 40 00 indirizzo di ritorno
0x0012FEE0 c4 18 32 00 Ä.2. <- indirizzo dell'argomento a DontDoThis Àÿ..
0x0012FEE4 c0 ff 12 00
0x0012FEE8 10 13 40 00 . . @. <- indirizzo main() a cui ritornerà

Ricorda che tutti i valori nello stack sono all'indietro. Questo esempio proviene da un sistema
Intel a 32 bit, che è "little-endian". Ciò significa che il byte meno significativo di un valore viene
prima, quindi se vedi un indirizzo di ritorno in memoria come "3f104000", in realtà è l'indirizzo
0x0040103f.
94 24 peccati capitali della sicurezza del software

Ora diamo un'occhiata a cosa succede quando buf viene sovrascritto. La prima informazione
di controllo sullo stack è il contenuto del registro Extended Base Pointer (EBP). EBP contiene il
frame pointer e se si verifica un overflow off-by-one, EBP verrà troncato. Se l'attaccante può
controllare la memoria a 0x0012fe00 (il off-by-one azzera l'ultimo byte), il programma salta a
quella posizione ed esegue il codice fornito dall'attaccante.
Se l'overrun non è vincolato a un byte, l'elemento successivo è l'indirizzo di ritorno. Se
l'attaccante può controllare questo valore ed è in grado di posizionare abbastanza assembly in
un buffer di cui conosce la posizione, si sta osservando un classico sovraccarico del buffer
sfruttabile. Si noti che il codice assembly (spesso noto comecodice della conchigliapoiché l'exploit
più comune è invocare una shell di comando) non deve essere inserito nel buffer che viene
sovrascritto. È il caso classico, ma in generale il codice arbitrario che l'aggressore ha inserito nel
tuo programma potrebbe trovarsi altrove. Non trarre alcun conforto dal pensare che il
superamento sia limitato a una piccola area.
Una volta che l'indirizzo di ritorno è stato sovrascritto, l'attaccante può giocare con gli
argomenti della funzione sfruttabile. Se il programma scrive su uno di questi argomenti prima di
tornare, rappresenta un'opportunità per ulteriore caos. Questo punto diventa importante
quando si considera l'efficacia delle contromisure contro la manomissione dello stack come
Stackguard di Crispin Cowan, ProPolice di IBM e il flag del compilatore /GS di Microsoft.
Come puoi vedere, abbiamo appena dato all'attaccante almeno tre modi per prendere il controllo
della nostra applicazione, e questo è solo in una funzione molto semplice. Se nello stack viene
dichiarata una classe C++ con funzioni virtuali, sarà disponibile la tabella dei puntatori a funzione
virtuale e ciò può facilmente portare a exploit. Se uno degli argomenti della funzione sembra essere un
puntatore a funzione, che è abbastanza comune in qualsiasi sistema a finestre (ad esempio, il sistema X
Window o Microsoft Windows), allora sovrascrivere il puntatore a funzione prima dell'uso è un modo
ovvio per deviare controllo dell'applicazione.
Esistono molti, molti modi più intelligenti per prendere il controllo di un'applicazione di quanto i
nostri cervelli deboli possano immaginare. C'è uno squilibrio tra le nostre capacità di sviluppatori e le
capacità e le risorse dell'attaccante. Non ti è concesso un tempo infinito per scrivere la tua applicazione,
ma gli aggressori potrebbero non avere nient'altro da fare con il loro abbondante tempo libero che
capire come fare in modo che il tuo codice faccia quello che vogliono. Il tuo codice potrebbe
proteggere una risorsa sufficientemente preziosa da giustificare mesi di sforzi per sovvertire la tua
applicazione. Gli aggressori trascorrono molto tempo a conoscere gli ultimi sviluppi nel causare caos e
dispongono di risorse come www.metasploit.com, dove possono puntare e fare clic per raggiungere il
codice della shell che fa quasi tutto ciò che vogliono mentre operano all'interno di un personaggio
vincolato impostato.
Se provi a determinare se qualcosa è sfruttabile, è molto probabile che ti sbagli. Nella
maggior parte dei casi, è solo possibile dimostrare che qualcosa è sfruttabile o che non sei
abbastanza intelligente (o forse non hai speso abbastanza tempo) per determinare come scrivere
un exploit. È estremamente raro poter dimostrare con certezza che un superamento non è
sfruttabile. In effetti, la guida di Microsoft è che tutte le scritture a qualsiasi indirizzo diverso da
null (o null, più un piccolo incremento fisso) sono problemi da risolvere e anche la maggior parte
delle violazioni di accesso durante la lettura di posizioni di memoria errate sono
Peccato 5: Superamento del buffer
95

problemi da risolvere. Vedere http://msdn.microsoft.com/en-us/magazine/cc163311.aspx di


Damien Hasse per maggiori dettagli.
Il punto di questa diatriba è che la cosa intelligente da fare è solo correggere i bug! Ci sono state
più volte in cui i "miglioramenti della qualità del codice" si sono rivelati correzioni di sicurezza in
retrospettiva. Questo autore ha appena trascorso più di tre ore a discutere con un team di sviluppo
sull'opportunità di correggere un bug. Il thread di posta elettronica ha coinvolto un totale di otto
persone e abbiamo passato facilmente 20 ore (mezza persona a settimana) a discutere se risolvere il
problema o meno perché il team di sviluppo voleva la prova che il codice fosse sfruttabile. Una volta
che gli esperti di sicurezza hanno dimostrato che il bug era davvero un problema, la correzione è stata
stimata in un'ora di tempo per lo sviluppatore e alcune ore di tempo di test. È un'incredibile perdita di
tempo.
L'unica volta in cui vuoi essere analitico è immediatamente prima della spedizione di
un'applicazione. Se un'applicazione è nelle fasi finali, ti piacerebbe essere in grado di indovinare
se il problema è sfruttabile per giustificare il rischio di regressioni e destabilizzazione del
prodotto.
È un'idea sbagliata comune che i sovraccarichi nei buffer dell'heap siano meno sfruttabili dei
sovraccarichi dello stack, ma non è così. La maggior parte delle implementazioni dell'heap soffre
dello stesso difetto di base dello stack: i dati utente e i dati di controllo sono mescolati. A seconda
dell'implementazione dell'allocatore di memoria, è spesso possibile fare in modo che il gestore
dell'heap inserisca quattro byte a scelta dell'attaccante nella posizione specificata dall'attaccante.

I dettagli su come attaccare un mucchio sono alquanto arcani. Una recente e chiara
presentazione sull'argomento, "Reliable Windows Heap Exploits", di Matthew "shok"
Conover e Oded Horovitz, è disponibile all'indirizzo http://cansecwest.com/csw04/
csw04-Oded+Connover.ppt. Anche se il gestore dell'heap non può essere sovvertito
per eseguire le offerte di un utente malintenzionato, i dati nelle allocazioni adiacenti
possono contenere puntatori a funzione o puntatori che verranno utilizzati per scrivere
informazioni. Un tempo, lo sfruttamento degli heap overflow era considerato esotico e
difficile, ma gli heap overflow sono ora alcuni dei tipi più frequenti di errori sfruttati.
Molte delle implementazioni heap più recenti ora rendono molti degli attacchi contro
l'infrastruttura heap ovunque da estremamente difficili a impraticabili a causa del
miglioramento del controllo e della codifica delle intestazioni di allocazione,

Implicazioni a 64 bit
Con l'avvento dei sistemi x64 comunemente disponibili, potresti chiederti se un sistema x64
potrebbe essere più resiliente contro gli attacchi rispetto a un sistema x86 (32 bit). Per certi
aspetti lo sarà. Ci sono due differenze fondamentali che riguardano lo sfruttamento dei
sovraccarichi del buffer. Il primo è che mentre il processore x86 è limitato a 8 registri generici
(eax, ebx, ecx, edx, ebp, esp, esi, edi), il processore x64 ha 16 registri generici.
96 24 peccati capitali della sicurezza del software

Dove questo fatto entra in gioco è che la convenzione di chiamata standard per
un'applicazione x64 è la convenzione di chiamata fastcall: su x86, ciò significa che il primo
argomento di una funzione viene inserito in un registro invece di essere inserito nello stack. Su
x64, utilizzare fastcall significa inserire i primi quattro argomenti nei registri. Avere molti più
registri (anche se ancora molto meno dei chip RISC, che in genere hanno 32-64 registri, o ia64,
che ne ha 128) non solo significa che il codice verrà eseguito molto più velocemente in molti casi,
ma che molti valori che erano precedentemente collocati da qualche parte nello stack si trovano
ora in registri in cui sono molto più difficili da attaccare: se il contenuto del registro non viene mai
scritto nello stack, che ora è molto più comune, non può essere attaccato affatto con una
scrittura arbitraria in memoria.
Il secondo modo in cui x64 è più difficile da attaccare è che il bit di non esecuzione (NX) è sempre
disponibile e la maggior parte dei sistemi operativi a 64 bit lo abilita per impostazione predefinita. Ciò
significa che l'attaccante è limitato a poter lanciare attacchi return-into-libC, o sfruttare qualsiasi pagina
contrassegnata come write-execute presente nell'applicazione. Sebbene avere il bit NX sempre
disponibile sia meglio che averlo disattivato, può essere sovvertito in altri modi interessanti, a seconda
di cosa sta facendo l'applicazione. Questo è in realtà un caso in cui i linguaggi di livello superiore
peggiorano le cose: se riesci a scrivere il codice byte, non è visto come eseguibile a livello C/C++, ma è
certamente eseguibile se elaborato da un linguaggio di livello superiore , come C#, Java o molti altri.

La linea di fondo è che gli aggressori dovranno lavorare un po' di più per sfruttare il codice
x64, ma non è affatto una panacea, e devi ancora scrivere un codice solido.

Peccaminoso C/C++
Ci sono molti, molti modi per sovraccaricare un buffer in C/C++. Ecco cosa ha causato il verme del
dito di Morris:

char buf[20];
ottiene(buf);

Non c'è assolutamente modo di usare gets per leggere l'input da stdin senza rischiare un overflow
del buffer: usa invece fgets. I worm più recenti hanno utilizzato problemi leggermente più sottili: il
worm blaster è stato causato da un codice che era essenzialmente strcpy, ma utilizzava un terminatore
di stringa diverso da null:

while (*pwszTemp != L'\\')


* pwszServerName++ = *pwszTemp++;

Forse il secondo modo più diffuso per eseguire l'overflow dei buffer è utilizzare strcpy (vedere l'esempio
precedente). Questo è un altro modo per causare problemi:

char buf[20];
prefisso char[] = "http://";
Peccato 5: Superamento del buffer
97

strcpy(buf, prefisso);
strncat(buf, percorso, sizeof(buf));

Che cosa è andato storto? Il problema qui è che strncat ha un'interfaccia mal progettata. La
funzione vuole il numero di caratteri del buffer disponibile, o lo spazio rimasto, non la dimensione
totale del buffer di destinazione. Ecco un altro modo preferito per causare overflow:

char buf[MAX_PATH];
sprintf(buf, "%s - %d\n", percorso, errno);

È quasi impossibile, tranne che in alcuni casi d'angolo, usare sprintf in modo sicuro. È stato rilasciato un
bollettino critico sulla sicurezza per Microsoft Windows poiché sprintf è stato utilizzato in una funzione di
registrazione di debug. Fare riferimento al bollettino MS04-011 per ulteriori informazioni (vedere il
collegamento nella sezione "Altre risorse" in questo capitolo).
Ecco un altro preferito:

char buf[32];
strncpy(buf, dati, strlen(dati));

Quindi cosa c'è di sbagliato in questo? L'ultimo argomento è la lunghezza del buffer in entrata, non la
dimensione del buffer di destinazione!
Un altro modo per causare problemi è confondere il conteggio dei caratteri con il conteggio dei
byte. Se hai a che fare con i caratteri ASCII, i conteggi sono gli stessi, ma se hai a che fare con Unicode,
ci sono due byte per un carattere (assumendo il piano multilingue di base, che si associa
approssimativamente alla maggior parte degli script moderni) e il caso peggiore sono i caratteri
multibyte, dove non c'è un buon modo per conoscere il conteggio finale dei byte senza prima eseguire
la conversione. Ecco un esempio:

_snwprintf(wbuf, sizeof(wbuf), "%s\n", input);

Il seguente overrun è un po' più interessante:

bool CopyStructs(InputFile* pInFile, conteggio lungo senza segno) {

i lunga senza segno;

m_pStructs = new Structs[count];

for(i = 0; i < conteggio; i++) {

if(!ReadFromFile(pInFile, &(m_pStructs[i])))
rottura;
}
}
98 24 peccati capitali della sicurezza del software

Come può fallire? Considera che quando chiami l'operatore C++ new[], è simile al
codice seguente:

ptr = malloc(sizeof(type) * count);

Se l'utente fornisce il conteggio, non è difficile specificare un valore che oltrepassi internamente
l'operazione di moltiplicazione. Allocherai quindi un buffer molto più piccolo del necessario e
l'attaccante sarà in grado di scrivere sul tuo buffer. Il compilatore C++ in Microsoft Visual Studio 2005 e
versioni successive contiene un controllo interno per rilevare l'overflow di numeri interi. Lo stesso
problema può verificarsi internamente in molte implementazioni di calloc, che esegue la stessa
operazione. Questo è il punto cruciale di molti bug di overflow di numeri interi: non è l'overflow di
numeri interi che causa il problema di sicurezza; è il sovraccarico del buffer che segue rapidamente che
causa mal di testa. Ma ne parleremo di più in Sin 7.
Ecco un altro modo in cui è possibile creare un sovraccarico del buffer:

# define MAX_BUF 256 void


BadCode(char* input) {

breve lenzuolo;
char buf[MAX_BUF];

len = strlen(input);

//ovviamente possiamo usare strcpy in sicurezza


if(len < MAX_BUF)
strcpy(buf, input);
}

Sembra che dovrebbe funzionare, giusto? Il codice è in realtà pieno di problemi. Ne


parleremo più in dettaglio quando parleremo degli integer overflow in Sin 7, ma prima
consideriamo che i letterali sono sempre di tipo signed int. La funzione strlen restituisce
size_t, che è un valore senza segno a 32 o 64 bit, e il troncamento di size_t a short con un
input più lungo di 32K trasformerà len in un numero negativo; verrà aggiornato a un int e
manterrà il segno; e ora è sempre più piccolo di MAX_BUF, causando un overflow.

Un secondo modo in cui incontrerai problemi è se la stringa è più grande di 64K. Ora hai un errore di
troncamento: len sarà un piccolo numero positivo. La correzione principale consiste nel ricordare che size_t è
definito nella lingua come il tipo corretto da utilizzare per le variabili che rappresentano le dimensioni in base
alla specifica della lingua. Un altro problema in agguato è che l'input potrebbe non essere terminato da null.
Ecco come appare il codice migliore:

costante size_t MAX_BUF = 256; void


LessBadCode(char* input) {
Peccato 5: Superamento del buffer
99

size_t len;
char buf[MAX_BUF];

len = strnlen(input, MAX_BUF);

//ovviamente possiamo usare strcpy in sicurezza


if(len < MAX_BUF)
strcpy(buf, input);
}

Peccati correlati
Un peccato strettamente correlato è l'overflow di numeri interi. Se si sceglie di mitigare i sovraccarichi del
buffer utilizzando le chiamate di gestione delle stringhe conteggiate o si sta tentando di determinare la
quantità di spazio da allocare sull'heap, l'aritmetica diventa fondamentale per la sicurezza dell'applicazione.
Gli overflow di numeri interi sono trattati in Sin 7.
I bug delle stringhe di formato possono essere usati per ottenere lo stesso effetto di un sovraccarico del buffer,
ma non sono veri e propri sovraccarichi. Un bug della stringa di formato viene normalmente eseguito senza
sovraccaricare alcun buffer.
Una variante di un sovraccarico del buffer è una scrittura illimitata su un array. Se l'attaccante
può fornire l'indice dell'array e non convalidi correttamente se si trova all'interno dei limiti
corretti dell'array, verrà eseguita una scrittura mirata in una posizione di memoria scelta
dall'attaccante. Non solo può verificarsi la stessa deviazione del flusso del programma, ma anche
l'attaccante potrebbe non dover interrompere la memoria adiacente, il che ostacola eventuali
contromisure che potresti avere in atto contro i sovraccarichi del buffer.

INDIVIDUARE IL MODELLO DEL PECCATO


Ecco i componenti da cercare:

- Input, letto dalla rete, da un file o dalla riga di comando


- Trasferimento dei dati da detto input alle strutture interne

- Uso di chiamate di gestione delle stringhe non sicure

- Uso dell'aritmetica per calcolare una dimensione di allocazione o una dimensione del buffer rimanente

INDIVIDUARE IL PECCATO DURANTE LA REVISIONE DEL CODICE


Individuare questo peccato durante la revisione del codice varia da molto facile a estremamente
difficile. Le cose facili da cercare sono l'uso di funzioni di gestione delle stringhe non sicure. Un
problema da tenere presente è che puoi trovare molti casi di utilizzo sicuro, ma è stata la nostra
esperienza che ci sono problemi nascosti tra le chiamate corrette. La conversione del codice per
utilizzare solo chiamate sicure ha un tasso di regressione molto basso (ovunque da 1/10 a 1/100 del
normale tasso di regressione per la correzione di bug) e rimuoverà gli exploit dal codice.
100 24 peccati capitali della sicurezza del software

Un buon modo per farlo è lasciare che il compilatore trovi chiamate di funzione pericolose
per te. Se non hai definito strcpy, strcat, sprintf e funzioni simili, il compilatore le troverà tutte per
te. Un problema da tenere presente è che alcune app hanno reimplementato internamente tutta
o una parte della libreria di runtime C o forse volevano uno strcpy con un terminatore diverso da
null.
Un'attività più difficile è cercare i superamenti dell'heap. Per farlo bene, devi essere consapevole
degli integer overflow, che tratteremo in Sin 3. Fondamentalmente, devi prima cercare le allocazioni e
poi esaminare l'aritmetica utilizzata per calcolare la dimensione del buffer.
L'approccio migliore in generale consiste nel tracciare l'input dell'utente dai punti di ingresso dell'applicazione
attraverso tutte le chiamate di funzione. Essere consapevoli di ciò che controlla l'attaccante fa una grande differenza.

TECNICHE DI PROVA PER TROVARE IL PECCATO


Test fuzz,che sottopone la tua applicazione a input semi-casuali, è una delle migliori tecniche di test da
utilizzare. Prova ad aumentare la lunghezza delle stringhe di input osservando il comportamento
dell'app. Qualcosa a cui prestare attenzione è che a volte le mancate corrispondenze tra il controllo
dell'input si tradurranno in finestre relativamente piccole di codice vulnerabile. Ad esempio, qualcuno
potrebbe mettere un segno di spunta in un posto che l'input deve essere inferiore a 260 caratteri e
quindi allocare un buffer di 256 byte. Se provi un input molto lungo, verrà semplicemente rifiutato, ma
se raggiungi esattamente l'overflow, potresti trovare un exploit. Le lunghezze che sono multipli di due
e multipli di due più o meno uno troveranno spesso problemi.
Altri trucchi da provare sono la ricerca di qualsiasi punto nell'input in cui la lunghezza di qualcosa è
specificata dall'utente. Modificare la lunghezza in modo che non corrisponda alla lunghezza della stringa e in
particolare cercare le possibilità di overflow di numeri interi, condizioni in cui lunghezza + 1 = 0 sono spesso
pericolose.
Qualcosa che dovresti fare quando fuzz testing è creare una build di test specializzata. Le build di debug
hanno spesso asserzioni che modificano il flusso del programma e ti impediranno di raggiungere condizioni
sfruttabili. D'altra parte, le build di debug sui compilatori moderni contengono in genere un rilevamento del
danneggiamento dello stack più avanzato. A seconda dell'heap e del sistema operativo, è anche possibile
abilitare un controllo del danneggiamento dell'heap più rigoroso.
Una modifica che potresti voler apportare al tuo codice è che se un'asserzione sta controllando l'input
dell'utente, cambia quanto segue from

assert(len < MAX_PATH);

if(len >= MAX_PATH)


{
affermare(falso);
restituire falso;
}
Peccato 5: Superamento del buffer
101

Dovresti sempre testare il tuo codice con una qualche forma di strumento di rilevamento degli errori di
memoria, come AppVerifier su Windows (vedi link nella sezione "Altre risorse") per rilevare in anticipo
sovraccarichi del buffer piccoli o sottili.
Il fuzz testing non deve essere elaborato o complicato: vedere il post sul blog SDL di Michael
Howard "Migliora la sicurezza con 'A Layer of Hurt'" all'indirizzo http://blogs.msdn.com/sdl/
archive/ 2008/07/31/improve -security-with-a-layer-of-hurt.aspx. Un'interessante storia del
mondo reale su quanto possa essere semplice il fuzzing viene dai test effettuati in Office 2007.
Avevamo utilizzato alcuni strumenti abbastanza sofisticati e stavamo raggiungendo i limiti di ciò
che gli strumenti potevano trovare. L'autore stava parlando con un amico che aveva trovato
alcuni bug molto interessanti e gli ha chiesto come lo stesse facendo. L'approccio utilizzato era
molto semplice: prendi l'input e sostituisci un byte alla volta con ogni possibile valore di quel
byte. Questo approccio ovviamente funziona bene solo per input molto piccoli, ma se riduci il
numero di valori che provi a un numero inferiore, funziona abbastanza bene anche per file di
grandi dimensioni.

ESEMPIO PECCATI
Le seguenti voci, che provengono direttamente dall'elenco Common Vulnerabilities and
Exposures, o CVE (http://cve.mitre.org), sono esempi di buffer overrun. Una curiosità
interessante è che a partire dalla prima edizione (febbraio 2005), esistono 1.734 voci CVE
che corrispondono a "buffer overrun". Non aggiorneremo il conteggio, poiché sarà obsoleto
quando questo libro arriverà nelle tue mani, diciamo solo che ce ne sono molte migliaia.
Una ricerca degli avvisi CERT, che documentano solo le vulnerabilità più diffuse e gravi,
produce 107 riscontri su "buffer overrun".

CVE-1999-0042
Overflow del buffer nell'implementazione dei server IMAP e POP dell'Università di Washington.

Commento
Questa voce CVE è ampiamente documentata nell'avviso CERT CA-1997-09; ha comportato un
sovraccarico del buffer nella sequenza di autenticazione dei server POP (Post Office Protocol) e IMAP
(Internet Message Access Protocol) dell'Università di Washington. Una vulnerabilità correlata era che il
server di posta elettronica non riusciva a implementare il privilegio minimo e l'exploit garantiva
l'accesso root agli aggressori. L'overflow ha portato a un diffuso sfruttamento dei sistemi vulnerabili.

I controlli di vulnerabilità della rete progettati per trovare versioni vulnerabili di questo server
hanno rilevato difetti simili in SLMail 2.5 di Seattle Labs, come riportato su www.winnetmag.com/
Article/ArticleID/9223/9223.html.

CVE-2000-0389–CVE-2000-0392
L'overflow del buffer nella funzione krb_rd_req in Kerberos 4 e 5 consente agli aggressori remoti di ottenere i
privilegi di root.
102 24 peccati capitali della sicurezza del software

L'overflow del buffer nella funzione krb425_conv_principal in Kerberos 5 consente agli aggressori remoti
di ottenere i privilegi di root.
L'overflow del buffer in krshd in Kerberos 5 consente agli aggressori remoti di ottenere i privilegi di root.
L'overflow del buffer in ksu in Kerberos 5 consente agli utenti locali di ottenere i privilegi di root.

Commento
Questa serie di problemi nell'implementazione di Kerberos da parte del MIT è documentata come
CERT advisory CA-2000-06, reperibile all'indirizzo www.cert.org/advisories/CA-2000-06.html.
Sebbene il codice sorgente fosse disponibile al pubblico da diversi anni e il problema derivasse
dall'uso di pericolose funzioni di gestione delle stringhe (strcat), è stato segnalato solo nel 2000.

CVE-2002-0842, CVE-2003-0095, CAN-2003-0096


Vulnerabilità della stringa di formato in alcune modifiche di terze parti a mod_dav per la registrazione di
messaggi di gateway errati (ad esempio, Oracle9ioApplication Server 9.0.2) consente agli aggressori remoti di
eseguire codice arbitrario tramite un URI di destinazione che forza una risposta "502 Bad Gateway", che fa sì
che gli specificatori della stringa di formato vengano restituiti da dav_lookup_uri() in mod_dav.c, che viene
quindi utilizzato in una chiamata a ap_log_rerror().
Overflow del buffer in ORACLE.EXE per Oracle Database Server 9io,8io,8.1.7 e 8.0.6
consentono agli aggressori remoti di eseguire codice arbitrario tramite un nome utente lungo
fornito durante l'accesso come sfruttabile tramite applicazioni client che eseguono la propria
autenticazione, come dimostrato utilizzando LOADPSP.
Overflow multipli del buffer in Oracle 9ioDatabase versione 2, versione 1, 8io,8.1.7, e
8.0.6 consente agli aggressori remoti di eseguire codice arbitrario tramite (1) un lungo argomento di
stringa di conversione per la funzione TO_TIMESTAMP_TZ, (2) un lungo argomento di fuso orario per la
funzione TZ_OFFSET o (3) un lungo parametro DIRECTORY per la funzione BFILENAME.

Commento
Queste vulnerabilità sono documentate nell'avviso CERT CA-2003-05, disponibile all'indirizzo
www.cert.org/advisories/CA-2003-05.html. I problemi sono una serie di numerosi problemi riscontrati
da David Litchfield e dal suo team presso Next Generation Security Software Ltd. Per inciso, questo
dimostra che pubblicizzare la propria applicazione come "indistruttibile" potrebbe non essere la cosa
migliore da fare mentre il signor Litchfield sta indagando sulla tua applicazioni.

CAN-2003-0352
L'overflow del buffer in una determinata interfaccia DCOM per RPC in Microsoft Windows NT 4.0,
2000, XP e Server 2003 consente agli aggressori remoti di eseguire codice arbitrario tramite un
messaggio malformato, sfruttato dai worm Blaster/MSblast/LovSAN e Nachi/Welchia.

Commento
Questo overflow è interessante perché ha portato a uno sfruttamento diffuso da parte di due
worm molto distruttivi che hanno entrambi causato notevoli interruzioni su Internet. Il trabocco
Peccato 5: Superamento del buffer
103

era nel mucchio ed era evidenziato dal fatto che era possibile costruire un worm molto stabile.
Un fattore che ha contribuito è stato il fallimento del principio del privilegio minimo: l'interfaccia
non avrebbe dovuto essere disponibile per gli utenti anonimi. Un'altra nota interessante è che le
contromisure di overflow in Windows 2003 hanno degradato l'attacco dall'escalation dei privilegi
al denial of service.
Ulteriori informazioni su questo problema sono disponibili all'indirizzo www.cert.org/
advisories/CA-2003-23.html e www.microsoft.com/technet/security/bulletin/MS03-039.asp.

FASI DI RISCATTO
La strada per tamponare la redenzione superata è lunga e piena di buche. Discutiamo un'ampia varietà
di tecniche che consentono di evitare i sovraccarichi del buffer e una serie di altre tecniche che
riducono i danni che i sovraccarichi del buffer possono causare. Diamo un'occhiata a come puoi
migliorare il tuo codice.

Sostituire le funzioni di gestione delle stringhe pericolose


Dovresti, come minimo, sostituire le funzioni non sicure come strcpy, strcat e sprintf con le
versioni conteggiate di ciascuna di queste funzioni. Hai una serie di scelte su cosa sostituirli. Tieni
presente che le funzioni conteggiate più vecchie hanno problemi di interfaccia e in molti casi ti
chiedono di fare aritmetica per determinare i parametri.
Come vedrai in Sin 7, i computer non sono così bravi in matematica come potresti sperare. Le librerie più
recenti includono strsafe, Safe CRT (libreria di runtime C) fornita in Microsoft Visual Studio 2005 (ed è sulla
buona strada per diventare parte dello standard ANSI C/C++) e strlcat/strlcpy per *nix. È inoltre necessario
prestare attenzione a come ciascuna di queste funzioni gestisce la terminazione e il troncamento delle
stringhe. Alcune funzioni garantiscono la terminazione nulla, ma la maggior parte delle vecchie funzioni
conteggiate no. L'esperienza del gruppo Microsoft Office con la sostituzione delle funzioni di gestione delle
stringhe non sicure per la versione di Office 2003 era che il tasso di regressione (nuovi bug causati per
correzione) era estremamente basso, quindi non lasciare che la paura delle regressioni ti fermi.

Allocazioni di revisione
Un'altra fonte di sovraccarichi del buffer deriva da errori aritmetici. Scopri gli overflow di numeri interi
in Sin 7 e controlla tutto il codice in cui vengono calcolate le dimensioni di allocazione.

Controlla i loop e gli accessi agli array


Un terzo modo in cui vengono causati i sovraccarichi del buffer non è controllare correttamente la
terminazione nei cicli e non controllare correttamente i limiti dell'array prima dell'accesso in scrittura.
Questa è una delle aree più difficili e scoprirai che, in alcuni casi, il problema e il kaboom sconvolgente
si trovano in moduli completamente diversi.
104 24 peccati capitali della sicurezza del software

Sostituisci i buffer delle stringhe C con le stringhe C++


Questo è più efficace della semplice sostituzione delle solite chiamate C, ma può causare enormi
quantità di modifiche nel codice esistente, in particolare se il codice non è già compilato come C+
+. È inoltre necessario conoscere e comprendere le caratteristiche prestazionali delle classi
container STL. È assolutamente possibile scrivere codice STL ad alte prestazioni, ma come in molti
altri aspetti della programmazione, un errore di Read The Fine Manual (RTFM) si tradurrà spesso
in risultati non ottimali. La sostituzione più comune consiste nell'usare le classi modello STL
std::string o std::wstring.

Sostituisci gli array statici con i contenitori STL


Tutti i problemi già segnalati si applicano ai contenitori STL come vector, ma un ulteriore
problema è che non tutte le implementazioni del costrutto vector::iterator controllano l'accesso
fuori dai limiti. Questa misura può essere d'aiuto e l'autore scopre che l'uso di STL gli consente di
scrivere il codice corretto più rapidamente, ma tieni presente che non si tratta di una soluzione
miracolosa.

Usa gli strumenti di analisi


Ci sono alcuni buoni strumenti sul mercato che analizzano il codice C/C++ per i difetti di sicurezza;
esempi includono Coverity, Fortify, PREfast e Klocwork. Come in molti aspetti del business della
sicurezza, quale sia lo strumento migliore può variare abbastanza rapidamente: cerca cosa c'è là fuori
prima di leggere questo. C'è un collegamento a un elenco nella sezione "Altre risorse" in questo
capitolo. Visual Studio 2005 (e versioni successive) include PREfast (utilizzato come /analyze) e un altro
strumento chiamato Source Code Annotation Language (SAL) per aiutare a rintracciare i difetti di
sicurezza come i sovraccarichi del buffer. Il modo migliore per descrivere SAL è attraverso il codice.
Nell'esempio (stupido) che segue, conosci la relazione tra i dati e gli argomenti di
conteggio: i dati sono lunghi count byte. Ma il compilatore non lo sa; vede solo un char
* e un size_t.

void *DoStuff(char *data, size_t count) {


carattere statico buf[32];
return memcpy(buf, dati, conteggio);
}

Questo codice sembra a posto (ignorando il fatto che detestiamo restituire buffer statici, ma ci
assecondiamo). Tuttavia, se il conteggio è maggiore di 32, si verifica un sovraccarico del buffer. Una versione
con annotazioni SAL di questo rileverebbe il bug:

void *DoStuff(_In_bytecount_ (count) char *data, size_t count) {


carattere statico buf[32];
return memcpy(buf, dati, conteggio);
}
Peccato 5: Superamento del buffer
105

Questa annotazione, _In_bytecount_(N), indica che *data è un buffer "In" da cui viene solo
letto e il relativo conteggio dei byte è il parametro "count". Questo perché lo strumento di analisi
sa come sono correlati i dati e il conteggio.
La migliore fonte di informazioni su SAL è il file di intestazione sal.h incluso con Visual C++.

MISURE EXTRA DIFENSIVE


Prendi in considerazione ulteriori misure difensive nello stesso modo in cui pensi alle cinture di sicurezza o
agli airbag nella tua auto. Le cinture di sicurezza spesso riducono la gravità di un incidente, ma non vuoi
comunque incorrere in un incidente. Non riesco a pensare a nessuno che creda di aver avuto una buona
giornata quando hanno avuto bisogno dei loro airbag! È importante notare che per ogni classe principale di
mitigazione del sovraccarico del buffer, esistono condizioni precedentemente sfruttabili che non sono più
sfruttabili affatto; e per ogni data tecnica di mitigazione, un attacco sufficientemente complesso può superare
completamente la tecnica. Diamo un'occhiata ad alcuni di loro.

Protezione pila
La protezione dello stack è stata introdotta da Crispin Cowan nel suo prodotto Stackguard ed è stata
implementata in modo indipendente da Microsoft come opzione del compilatore /GS. Nella sua forma
più elementare, la protezione dello stack inserisce un valore noto come canary nello stack tra le
variabili locali e l'indirizzo di ritorno. Le implementazioni più recenti possono anche riordinare le
variabili per una maggiore efficacia. Il vantaggio di questo approccio è che è economico, ha un
sovraccarico di prestazioni minimo e ha l'ulteriore vantaggio di semplificare il debug dei bug di
danneggiamento dello stack. Un altro esempio è ProPolice, un'estensione Gnu Compiler Collection
(GCC) creata da IBM.
In Visual C++ 2008 e versioni successive, /GS è abilitato per impostazione predefinita dalla riga di comando e
dall'IDE.
Qualsiasi prodotto attualmente in fase di sviluppo dovrebbe utilizzare la protezione dello stack.
Dovresti essere consapevole che la protezione dello stack può essere superata da una varietà di tecniche.
Se una tabella di puntatori di funzione virtuale viene sovrascritta e la funzione viene chiamata prima del
ritorno dalla funzione (i distruttori virtuali sono buoni candidati), l'exploit si verificherà prima che la protezione
dello stack possa entrare in gioco. Questo è il motivo per cui altre difese sono così importanti e ne parleremo
alcune in questo momento.

Stack e heap non eseguibili


Questa contromisura offre una protezione considerevole contro un utente malintenzionato, ma può avere un
impatto significativo sulla compatibilità delle applicazioni. Alcune applicazioni compilano ed eseguono
legittimamente il codice al volo, come molte applicazioni scritte in Java e C#. È anche importante notare che se
l'attaccante può far sì che la tua applicazione cada vittima di un attacco return-into-libC, in cui viene effettuata
una chiamata di funzione legittima per raggiungere fini nefasti, allora la protezione di esecuzione sulla pagina
di memoria potrebbe essere rimossa.
106 24 peccati capitali della sicurezza del software

Sfortunatamente, sebbene la maggior parte dell'hardware attualmente disponibile sia in grado di


supportare questa opzione, il supporto varia anche in base al tipo di CPU, al sistema operativo e alla
versione del sistema operativo. Di conseguenza, non puoi fare affidamento sulla presenza di questa
protezione sul campo, ma devi testarla con essa abilitata per assicurarti che la tua applicazione sia
compatibile con uno stack e un heap non eseguibili, eseguendo la tua applicazione su hardware che
supporta la protezione hardware e con il sistema operativo di destinazione impostato per utilizzare la
protezione. Ad esempio, se scegli come target Windows, assicurati di eseguire tutti i test su un
computer Windows Vista o successivo utilizzando un processore moderno. Su Windows, questa
tecnologia si chiama Data Execution Prevention (DEP); è anche noto come No eXecute (NX.)
Anche Windows Server 2003 SP1 supporta questa funzionalità. PaX per Linux e OpenBSD
supportano anche la memoria non eseguibile.

ALTRE RISORSE
- Scrittura di codice sicuro, seconda edizionedi Michael Howard e David C. LeBlanc
(Microsoft Press, 2002), capitolo 5, "Public Enemy #1: Buffer Overruns"
- “Heap Feng Shui in JavaScript” di Alexander Sotirov: http://
www.phreedom.org/research/heap-feng-shui/heap-feng-shui.html
- “Sconfiggere il meccanismo di prevenzione dell'overflow del buffer basato su
stack di Microsoft Windows Server 2003" di David Litchfield:
www.ngssoftware.com/papers/defeating-w2k3-stack-protection.pdf
- “Sfruttamento non basato su stack delle vulnerabilità di sovraccarico del buffer su
Windows NT/2000/XP” di David Litchfield:
www.ngssoftware.com/papers/non-stack-bo-windows.pdf
- “Sfruttamento cieco delle vulnerabilità di overflow dello stack" di Peter Winter-Smith:
www.ngssoftware.com/papers/NISR.BlindExploitation.pdf
- “Creare codice shell arbitrario in stringhe espanse Unicode: l'exploit
"veneziano" di Chris Anley: www.ngssoftware.com/papers/unicodebo.pdf
- “Smashing the Stack for Fun and Profit” di Aleph1 (Elias Levy):
www.insecure.org/stf/smashstack.txt
- “Il Tao di Windows Buffer Overflow” di Dildog:
www.cultdeadcow.com/cDc_files/cDc-351/
- Bollettino Microsoft sulla sicurezza MS04-011/Aggiornamento della sicurezza per Microsoft
Windows (835732): www.microsoft.com/technet/security/Bulletin/MS04-011.mspx

- Analizzatore di compatibilità delle applicazioni Microsoft:


www.microsoft.com/windows/appcompatibility/analyzer.mspx
- Utilizzo delle funzioni Strsafe.h: http://msdn.microsoft.com/library/en-us/winui/
winui/windowsuserinterface/resources/strings/usingstrsafefunctions.asp
Peccato 5: Superamento del buffer
107

- Chiamate più sicure della funzione buffer: AUTOMATICAMENTE!: http://


blogs.msdn.com/michael_howard/archive/2005/2/3.aspx
- Respingi gli attacchi al tuo codice con le librerie Safe C e C++ di Visual Studio 2005:
http://msdn.microsoft.com/msdnmag/issues/05/05/SafeCandC/default.aspx
- “strlcpy e strlcat—Consistent, Safe, String Copy and Concatenation” di Todd
C. Miller e Theo de Raadt:
www.usenix.org/events/usenix99/millert.html
- Estensione GCC per proteggere le applicazioni da attacchi che distruggono lo
stack: www.trl.ibm.com/projects/security/ssp/
- PaX: http://pax.grsecurity.net/
- Sicurezza di OpenBSD: www.openbsd.org/security.html
- Strumenti di analisi del codice sorgente statico per C: http://spinroot.com/static/

RIEPILOGO
- Farecontrollare attentamente gli accessi al buffer utilizzando le funzioni di gestione sicura delle
stringhe e del buffer.

- Farecomprendere le implicazioni di qualsiasi codice di copia del buffer personalizzato che hai
scritto.

- Fareutilizzare difese basate su compilatore come /GS e ProPolice.

- Fareutilizzare difese contro il sovraccarico del buffer a livello di sistema operativo come DEP e PaX.

- Fareutilizzare la randomizzazione degli indirizzi ove possibile, ad esempio ASLR in Windows


(/dynamicbase).

- Farecapire quali dati controlla l'aggressore e gestirli in modo sicuro nel


codice.
- Nonpensare che le difese del compilatore e del sistema operativo siano sufficienti, non lo sono; sono
semplicemente difese extra.

- Noncreare un nuovo codice che utilizza funzioni non sicure.

- Prendere in considerazioneaggiornando il tuo compilatore C/C++, poiché gli autori del compilatore aggiungono
ulteriori difese al codice generato.

- Prendere in considerazionerimuovendo le funzioni non sicure dal vecchio codice nel tempo.

- Prendere in considerazioneutilizzando stringhe C++ e classi contenitore piuttosto che funzioni stringhe C di
basso livello.
Questa pagina è stata lasciata vuota intenzionalmente
6
Problemi con le stringhe di formato

109
110 24 peccati capitali della sicurezza del software

PANORAMICA DEL PECCATO


I problemi relativi alle stringhe di formato sono uno dei pochi attacchi veramente nuovi emersi
negli ultimi anni. Una delle prime menzioni di format string bug risale al 23 giugno 2000, in un
post di Lamagra Argamal (www.securityfocus.com/archive/1/66842); Pascal Bouchareine li ha
spiegati più chiaramente quasi un mese dopo (www.securityfocus.com/archive/1/70552). Un post
precedente di Mark Slemko (www.securityfocus.com/archive/1/10383) ha rilevato le basi del
problema ma ha mancato la capacità dei bug di stringhe di formato di scrivere memoria.
Come per molti problemi di sicurezza, la causa principale dei bug delle stringhe di
formato è la fiducia nell'input fornito dall'utente senza convalida. In C/C++, i bug delle
stringhe di formato possono essere usati per scrivere in posizioni di memoria arbitrarie e
l'aspetto più pericoloso è che ciò può accadere senza manomettere i blocchi di memoria
adiacenti. Questa funzionalità a grana fine consente a un utente malintenzionato di
aggirare le protezioni dello stack e persino di modificare porzioni di memoria molto piccole.
Il problema può verificarsi anche quando le stringhe di formato vengono lette da una
posizione non attendibile controllata dall'aggressore. Quest'ultimo aspetto del problema
tende a prevalere sui sistemi UNIX e Linux. Sui sistemi Windows, le tabelle delle stringhe
dell'applicazione sono generalmente mantenute all'interno dell'eseguibile del programma o
delle DLL (Dynamic Link Libraries) delle risorse. Se gli aggressori possono riscrivere
l'eseguibile principale o le DLL delle risorse,
Con l'introduzione dell'Address Space Randomization (ASLR), alcuni attacchi non possono essere condotti in
modo affidabile a meno che non vi sia anche una perdita di informazioni. Il fatto che un bug di stringa di formato
possa far trapelare dettagli sul layout dell'indirizzo all'interno dell'applicazione significa che potrebbe trasformare un
attacco precedentemente inaffidabile in un exploit affidabile.
Un ulteriore problema, mentre spostiamo le app da un mondo a 32 bit a 64 bit, è che specifiche di
formato improprie su tipi di dimensioni variabili possono portare al troncamento dei dati o alla
scrittura solo di una parte del valore.
Anche se non hai a che fare con C/C++, gli attacchi con stringhe di formato possono comunque portare a
notevoli problemi. Il più ovvio è che gli utenti possono essere fuorviati da input corrotti o troncati, ma in
alcune condizioni un utente malintenzionato potrebbe anche lanciare script cross-site o attacchi SQL injection.
Questi possono essere utilizzati anche per corrompere o trasformare i dati.

RIFERIMENTI CWE
Il progetto Common Weakness Enumeration include la seguente voce relativa a questo
peccato:
- CWE-134: stringa di formato non controllata

LINGUE INTERESSATE
Il linguaggio più fortemente influenzato è C/C++. Un attacco riuscito può portare immediatamente
all'esecuzione di codice arbitrario e alla divulgazione di informazioni. Altre lingue in genere non
consentono l'esecuzione di codice arbitrario, ma sono possibili altri tipi di attacchi, come
Peccato 6: Problemi con le stringhe di formato
111

abbiamo precedentemente notato. Perl non è direttamente vulnerabile agli specificatori forniti dall'input dell'utente,
ma potrebbe essere vulnerabile se le stringhe di formato vengono lette da dati manomessi.

IL PECCATO SPIEGATO
La formattazione dei dati per la visualizzazione o l'archiviazione può essere un compito piuttosto difficile. Pertanto,
molti linguaggi informatici includono routine per riformattare facilmente i dati. Nella maggior parte delle lingue, le
informazioni di formattazione sono descritte utilizzando una sorta di stringa, chiamata thestringa di formato.La
stringa di formato viene effettivamente definita utilizzando un linguaggio di elaborazione dati limitato progettato per
semplificare la descrizione dei formati di output. Ma molti sviluppatori commettono un facile errore: utilizzano i dati di
utenti non attendibili come stringa di formato. Di conseguenza, gli aggressori possono scrivere stringhe di formato
per causare molti problemi.
Il design di C/C++ lo rende particolarmente pericoloso: il design di C/C++ rende più difficile rilevare
i problemi delle stringhe di formato e le stringhe di formato includono alcuni comandi particolarmente
pericolosi (in particolare %n) che non esistono in linguaggi di stringhe di formato di altri linguaggi.

In C/C++, una funzione può essere dichiarata per accettare un numero variabile di argomenti
specificando i puntini di sospensione (...) come ultimo argomento. Il problema è che la funzione che
viene chiamata non ha modo di sapere, nemmeno in fase di esecuzione, quanti argomenti vengono
passati. L'insieme più comune di funzioni per accettare argomenti di lunghezza variabile è la famiglia
printf: printf, sprintf, snprintf , fprintf, vprintf e così via. Le funzioni di caratteri estesi che eseguono la
stessa funzione presentano lo stesso problema. Diamo un'occhiata a un'illustrazione:

# include <stdio.h>

int main(int argc, char* argv[]) {

if(argc > 1)
printf(argv[1]);

ritorno 0;
}

Roba abbastanza semplice. Ora diamo un'occhiata a cosa può andare storto. Il
programmatore si aspetta che l'utente immetta qualcosa di benigno, ad esempioCiao mondo. Se
ci provi, torneraiCiao mondo.Ora cambiamo un po' l'input: prova %x %x.Su un sistema Windows
XP che utilizza la riga di comando predefinita (cmd.exe), ora otterrai quanto segue:

E:\projects\19_sins\format_bug>format_bug.exe "%x %x" 12ffc0 4011e5

Si noti che se si esegue un sistema operativo diverso o si utilizza un interprete della riga di
comando diverso, potrebbe essere necessario apportare alcune modifiche per inserire questa stringa
esatta nel programma e i risultati saranno probabilmente diversi. Per facilità d'uso, puoi inserire gli
argomenti in uno script di shell o in un file batch.
112 24 peccati capitali della sicurezza del software

Quello che è successo? La funzione printf ha preso una stringa di input che ha causato l'attesa di
due argomenti da inserire nello stack prima di chiamare la funzione. Gli specificatori %x ti hanno
permesso di leggere lo stack, quattro byte alla volta, per quanto desideri. Se avessi utilizzato
l'argomento %p, non solo mostrerebbe lo stack, ma ti mostrerebbe anche se l'app è a 32 o 64 bit. Sul
sistema a 64 bit dell'autore, i risultati sono simili a questi:

C:\projects\format_string\x64\Debug>format_string.exe %p
0000000000086790

Ma su una build a 32 bit ottieni:

C:\projects\format_string\Debug>format_string.exe %p 00000000

E se lo esegui di nuovo, vedi che l'ASLR (Address Space Layout Randomization) viene utilizzato per
questa app:

C:\projects\format_string\x64\Debug>format_string.exe %p
00000000006A6790

Nota come nella prima esecuzione, il nostro output è terminato con "086790", e alla seconda manche
terminava con "6A6790”? Questo è l'effetto della comparsa di ASLR.
Non è difficile immaginare che se avessi una funzione più complessa che memorizza un segreto in
una variabile di stack, l'attaccante sarebbe quindi in grado di leggere il segreto. L'output qui è
l'indirizzo della posizione dello stack (0x12ffc0), seguito dalla posizione del codice in cui tornerà la
funzione main(). Come puoi immaginare, entrambe queste sono informazioni estremamente
importanti che vengono trapelate a un utente malintenzionato.
Ora potresti chiederti come l'attaccante utilizzi un bug di stringa di formato per scrivere la
memoria. Uno degli identificatori di formato meno usati è %n, che scrive il numero di caratteri
che avrebbero dovuto essere scritti finora nell'indirizzo della variabile che hai fornito come
argomento corrispondente. Ecco come dovrebbe essere usato:

byte interi senza segno;


printf("%s%n\n", argv[1], &byte);
printf("Il tuo input è lungo %d caratteri\n, byte");

L'uscita sarebbe

E:\projects\19_sins\format_bug>format_bug2.exe "Alcuni input casuali" Alcuni input


casuali
Il tuo input era lungo 17 caratteri

Su una piattaforma con numeri interi a quattro byte, %Nspecificatore scriverà quattro byte
contemporaneamente e %hnscriverà due byte. Ora gli aggressori devono solo capire come ottenere l'indirizzo
che desiderano nella posizione appropriata nello stack e modificare gli specificatori della larghezza del campo
fino a quando il numero di byte scritti è quello che desiderano.

Potrebbero piacerti anche