Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
com
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:
Oppure, codice simile in JavaScript (da HTML o ASP) chiama CAPICOM su Windows:
Oppure, un codice simile in Perl esegue anche l'hashing del nome e della password dell'utente:
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:
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
// @ 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 {
O in VB.Net:
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.
Oppure in Perl:
usare rigoroso;
usa Digest::HMAC_SHA1;
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:
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.
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
87
Questa pagina è stata lasciata vuota intenzionalmente
5
Superamenti del buffer
89
90 24 peccati capitali della sicurezza del 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-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[]) {
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:
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
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:
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:
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:
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:
breve lenzuolo;
char buf[MAX_BUF];
len = strlen(input);
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:
size_t len;
char buf[MAX_BUF];
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.
- Uso dell'aritmetica per calcolare una dimensione di allocazione o una dimensione del buffer rimanente
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.
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.
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.
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.
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:
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++.
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.
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
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 contro il sovraccarico del buffer a livello di sistema operativo come DEP e PaX.
- 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
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>
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:
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
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:
L'uscita sarebbe
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.