
american fuzzy lop - un fuzzer orientato alla sicurezza
Sviluppato originariamente da Michal Zalewski [email protected].
Vedi QuickStartGuide.txt se non hai tempo di leggere questo file.
Il fuzzing è una delle strategie più potenti e collaudate per identificare problemi di sicurezza in software reali; è responsabile della stragrande maggioranza dei bug di esecuzione remota di codice e di escalation dei privilegi trovati finora in software critici per la sicurezza.
Purtroppo, il fuzzing è anche relativamente superficiale; mutazioni casuali e cieche rendono molto improbabile raggiungere determinati percorsi del codice nel software testato, lasciando alcune vulnerabilità del tutto fuori dalla portata di questa tecnica.
Ci sono stati numerosi tentativi di risolvere questo problema. Uno dei primi approcci — di cui è stato pioniere Tavis Ormandy — è la distillazione del corpus. Il metodo si basa su segnali di copertura per selezionare un sottoinsieme di seed interessanti da un corpus massiccio e di alta qualità di file candidati, e poi sottoporli a fuzzing con mezzi tradizionali. L'approccio funziona eccezionalmente bene, ma richiede che un tale corpus sia prontamente disponibile. Inoltre, le misurazioni della copertura a blocchi forniscono solo una comprensione molto semplicistica dello stato del programma e sono meno utili per guidare lo sforzo di fuzzing nel lungo periodo.
Altre ricerche più sofisticate si sono concentrate su tecniche come l'analisi del flusso del programma ("esecuzione concolica"), l'esecuzione simbolica o l'analisi statica. Tutti questi metodi sono estremamente promettenti in contesti sperimentali, ma tendono a soffrire di problemi di affidabilità e prestazioni nell'uso pratico — e attualmente non offrono un'alternativa valida alle tecniche di fuzzing "cieco".
American Fuzzy Lop è un fuzzer brute-force accoppiato a un algoritmo genetico estremamente semplice ma solidissimo, guidato dalla strumentazione. Utilizza una forma modificata di copertura degli archi per cogliere senza sforzo sottili cambiamenti su scala locale nel flusso di controllo del programma.
Semplificando un po', l'algoritmo complessivo può essere riassunto come:
Caricare i casi di test iniziali forniti dall'utente nella coda,
Prendere il file successivo dalla coda,
Tentare di ridurre il caso di test alla dimensione più piccola che non alteri il comportamento misurato del programma,
Mutare ripetutamente il file usando una varietà equilibrata e ben studiata di strategie di fuzzing tradizionali,
Se una qualsiasi delle mutazioni generate ha prodotto una nuova transizione di stato registrata dalla strumentazione, aggiungere l'output mutato come nuova voce nella coda.
Tornare al punto 2.
I casi di test scoperti vengono inoltre periodicamente eliminati per rimuovere quelli resi obsoleti da nuove scoperte a copertura più elevata; e vengono sottoposti a diversi altri passaggi di minimizzazione dello sforzo guidati dalla strumentazione.
Come risultato collaterale del processo di fuzzing, lo strumento crea un corpus piccolo e autosufficiente di casi di test interessanti. Questi sono estremamente utili per fare da seme ad altri regimi di test che richiedono molto lavoro o molte risorse — ad esempio, per lo stress-test di browser, applicazioni da ufficio, suite grafiche o strumenti closed-source.
Il fuzzer è stato testato a fondo per offrire prestazioni immediate di gran lunga superiori al fuzzing cieco o agli strumenti basati solo sulla copertura.
Quando il codice sorgente è disponibile, la strumentazione può essere iniettata da uno strumento complementare che funziona come sostituto diretto di gcc o clang in qualsiasi processo di build standard per codice di terze parti.
La strumentazione ha un impatto prestazionale abbastanza modesto; in combinazione con altre ottimizzazioni implementate da afl-fuzz, la maggior parte dei programmi può essere sottoposta a fuzzing con la stessa velocità o addirittura più velocemente di quanto sia possibile con gli strumenti tradizionali.
Il modo corretto per ricompilare il programma target può variare a seconda delle specifiche del processo di build, ma un approccio quasi universale sarebbe:```shell $ CC=/path/to/afl/afl-gcc ./configure $ make clean all
Per i programmi C++, vorrai anche impostare `CXX=/path/to/afl/afl-g++`.
I wrapper clang (afl-clang e afl-clang++) possono essere usati allo stesso modo;
gli utenti di clang possono anche scegliere di sfruttare una modalità di strumentazione ad alte prestazioni,
come descritto in llvm_mode/README.llvm.
Quando si testano librerie, è necessario trovare o scrivere un semplice programma che legga
dati da stdin o da un file e li passi alla libreria testata. In tal
caso, è essenziale collegare questo eseguibile a una versione statica della
libreria strumentata, o assicurarsi che il file .so corretto venga caricato in
runtime (di solito impostando `LD_LIBRARY_PATH`). L'opzione più semplice è una build
statica, di solito possibile tramite:```shell
$ CC=/path/to/afl/afl-gcc ./configure --disable-shared
Impostando AFL_HARDEN=1 quando si chiama 'make', il wrapper CC
abiliterà automaticamente le opzioni di hardening del codice che rendono più facile
rilevare semplici bug di memoria. Libdislocator, una libreria ausiliaria inclusa con AFL (vedi
libdislocator/README.dislocator) può aiutare a scoprire anche problemi di corruzione dell'heap.
PS. Agli utenti ASAN si consiglia di leggere il file notes_for_asan.txt per importanti avvertenze.
Quando il codice sorgente NON è disponibile, il fuzzer offre supporto sperimentale per una strumentazione rapida e on-the-fly di binari black-box. Ciò è realizzato con una versione di QEMU eseguita nella meno nota modalità "user space emulation".
QEMU è un progetto separato da AFL, ma puoi comodamente compilare la funzionalità eseguendo:```shell $ cd qemu_mode $ ./build_qemu_support.sh
Per ulteriori istruzioni e avvertenze, consultare qemu_mode/README.qemu.
La modalità è circa 2-5 volte più lenta rispetto all'instrumentazione a tempo di compilazione, è meno adatta alla parallelizzazione e può presentare alcune altre stranezze.
## 5) Scelta dei casi di test iniziali
Per funzionare correttamente, il fuzzer richiede uno o più file di partenza che contengano un buon esempio dei dati di input normalmente attesi dall'applicazione presa di mira. Ci sono due regole di base:
- Mantieni i file piccoli. Sotto 1 kB è l'ideale, anche se non strettamente necessario.
Per una discussione sul perché la dimensione conta, vedi [perf_tips.txt](https://github.com/google/afl/blob/master/docs/perf_tips.txt).
- Usa più casi di test solo se sono funzionalmente diversi tra loro.
Non ha senso usare cinquanta diverse foto di vacanze per fare fuzzing su una libreria di immagini.
Puoi trovare molti buoni esempi di file di partenza nella sottodirectory testcases/ fornita con questo strumento.
PS. Se è disponibile un ampio corpus di dati per lo screening, potresti voler usare l'utilità afl-cmin per identificare un sottoinsieme di file funzionalmente distinti che esercitano diversi percorsi di codice nel binario target.
## 6) Fuzzing dei binari
Il processo di fuzzing stesso è eseguito dall'utilità afl-fuzz. Questo programma richiede una directory di sola lettura con i casi di test iniziali, un posto separato per memorizzare i suoi risultati, oltre a un percorso del binario da testare.