
Fuzzer di formati binari guidato da template che genera e analizza input di test validi ad alta velocità, con integrazione AFL++ per fuzzing guidato dalla copertura.
FormatFuzzer è un framework per la generazione e l'analisi di input binari ad alta efficienza e alta qualità.
Prende un template binario che descrive il formato di un input binario e genera un eseguibile che produce e analizza il formato binario dato.
Da un template binario per GIF, ad esempio, FormatFuzzer produce un generatore di GIF - noto anche come GIF fuzzer.
I generatori prodotti da FormatFuzzer sono estremamente efficienti, producendo migliaia di input di test validi al secondo - in netto contrasto con i fuzzer basati su mutazione, dove la stragrande maggioranza degli input non è valida. Gli input generati da FormatFuzzer sono indipendenti dal programma sotto test (o in realtà da qualsiasi programma), quindi puoi usarli anche in contesti black-box. Tuttavia, FormatFuzzer si integra anche con AFL++ per produrre input validi che mirano anche alla massima copertura. Nei nostri esperimenti, questo approccio "best of two worlds" supera tutte le altre impostazioni; consulta il nostro articolo per i dettagli.
I template binari utilizzati da FormatFuzzer provengono dall'editor 010.
Ci sono più di 170 template binari, che possono essere utilizzati direttamente per FormatFuzzer o adattati al suo uso. Pronti all'uso, FormatFuzzer produce formati come AVI, BMP, GIF, JPG, MIDI, MP3, MP4, PCAP, PNG, WAV e ZIP; e continuiamo ad ampliare questo elenco ogni settimana.
I contributi sono benvenuti! Visita la pagina del progetto FormatFuzzer per segnalare idee e problemi o aggiungere pull request. Per i dettagli su come funziona FormatFuzzer e come si confronta, leggi il nostro articolo per maggiori informazioni.
FormatFuzzer è disponibile dalla pagina del progetto FormatFuzzer. Puoi scaricare e decomprimere l'ultima release dalla pagina delle release.
Per la versione più recente e aggiornata, puoi anche clonare il suo repository git:
git clone https://github.com/uds-se/FormatFuzzer.git
Tutte le ulteriori operazioni avvengono nella sua cartella principale:
cd FormatFuzzer
Per eseguire FormatFuzzer, hai bisogno di:
getopt_long()) come clang o gccpy010parser, six e intervaltreezlib (per le funzioni di compressione)boost (per le funzioni di checksum)Se prevedi di modificare gli script di build e configurazione (file .ac e .am), avrai anche bisogno di:
sudo apt install git g++ make automake python3-full zlib1g-dev libboost-dev
python3 -m venv ~/fuzz
source ~/fuzz/bin/activate
pip3 install py010parser six intervaltree
xcode-select --install
brew install python3 automake boost
pip3 install py010parser six intervaltree
Su tutti i sistemi, usando pip:
pip install py010parser
pip install six
pip install intervaltree
Nota: tutti i comandi di compilazione richiedono di trovarsi nella stessa cartella di questo file README. La compilazione di un fuzzer al di fuori di questa cartella non è ancora supportata.
C'è uno script build.sh che automatizza tutti i passaggi di costruzione.
Semplicemente esegui
./build.sh gif
per creare un GIF fuzzer.
Funziona per tutti i formati di file forniti in templates/; se esiste un file templates/FOO.bt, allora ./build.sh FOO compilerà un fuzzer.
C'è un Makefile (sorgente in Makefile.am) che automatizza tutti i passaggi di costruzione.
(Richiede GNU make.)
Prima esegui
touch configure Makefile.in
poi
./configure
e poi
make gif-fuzzer
per creare un GIF fuzzer.
Funziona per tutti i formati di file forniti in templates/; se esiste un file templates/FOO.bt, allora make FOO-fuzzer compilerà un fuzzer.
Se il metodo make sopra non funziona, o se vuoi più controllo, potresti dover procedere manualmente.
Esegui il compilatore ffcompile per compilare il template binario in codice C++. Prende due argomenti: il template binario .bt e un file .cpp C++ da generare.
./ffcompile templates/gif.bt gif.cpp
Usa i seguenti comandi per creare un fuzzer gif-fuzzer.
Prima, compila il driver generico da riga di comando:
g++ -c -I . -std=c++17 -g -O3 -Wall fuzzer.cpp
(-I . indica la posizione del file bt.h; -std=c++17 imposta lo standard C++.)
Poi, compila il parser/compilatore binario:
g++ -c -I . -std=c++17 -g -O3 -Wall gif.cpp
Infine, collega il parser/compilatore binario con il driver da riga di comando per ottenere un eseguibile. Se usi librerie extra (come -lz), assicurati di specificarle anche qui.
g++ -O3 gif.o fuzzer.o -o gif-fuzzer -lz
FormatFuzzer può essere eseguito come parser, generatore o mutatore standalone di formati specifici. Inoltre, può essere chiamato da fuzzer di uso generale come AFL++ per integrare queste capacità specifiche del formato nel processo di fuzzing (vedi la sezione sotto sull'integrazione con AFL++).
Il fuzzer generato prende un comando come primo argomento, seguito da opzioni e argomenti per quel comando.
Il comando più importante è fuzz, per produrre output. I suoi argomenti sono i file da generare nel formato appropriato.
Esegui il generatore come
./gif-fuzzer fuzz output.gif
per creare un file binario casuale output.gif, o
./gif-fuzzer fuzz out1.gif out2.gif out3.gif
per creare tre file GIF out1.gif, out2.gif e out3.gif.
Nota che il template gif.bt che forniamo è stato arricchito con funzioni speciali per facilitare la generazione di file validi. Se usi file template .bt originali senza adattamenti, potresti ottenere avvisi durante la generazione e creare file non validi.
Puoi anche eseguire il fuzzer come parser per file binari, usando il comando parse. Questo è utile se vuoi testare l'accuratezza del template binario, o se vuoi mutare un input (vedi 'Decision Files', sotto).
Per eseguire il parser, usa
./gif-fuzzer parse input.gif
Vedrai messaggi di errore se input.gif non può essere analizzato con successo.
Durante l'analisi, puoi anche memorizzare tutte le decisioni di parsing (cioè quali alternative di parsing sono state prese) in un decision file. Si tratta di una sequenza di byte che enumera le decisioni prese.
Ogni byte rappresenta una singola decisione di parsing. Un valore di byte 0 significa che è stata presa la prima alternativa, un valore di byte 1 significa che è stata presa la seconda alternativa, e così via.
Puoi generare un tale decision file quando analizzi un input:
./gif-fuzzer parse --decisions input.dec input.gif
Qui, input.dec memorizza le decisioni prese per analizzare input.gif.
Puoi anche usare un tale decision file quando generi input. Il fuzzer prenderà allora le stesse identiche decisioni trovate durante il parsing. Il seguente comando genera un nuovo file GIF usando le decisioni determinate durante l'analisi di input.gif:
./gif-fuzzer fuzz --decisions input.dec input2.gif
Se tutto funziona bene, entrambi i file dovrebbero essere identici:
cmp input.gif input2.gif
Mutando un decision file (ad esempio sostituendo singoli byte), puoi creare input simili al file originale analizzato. Questo è utile per interfacciarsi con specifiche strategie di test e fuzzer come AFL, dove puoi usare gif-fuzzer e simili come traduttori da decision file a file binari e viceversa: AFL muterebbe i decision file, e il programma sotto test verrebbe eseguito sui file binari tradotti. A differenza della mutazione diretta dei file binari (come farebbe normalmente AFL), questo avrebbe il vantaggio di avere sempre input validi - e quindi di progredire molto più velocemente verso la copertura.
Oltre ai fuzzer specifici per formato, come gif-fuzzer, FormatFuzzer può anche essere compilato in librerie condivise specifiche per formato, come gif.so (per farlo, esegui semplicemente ./build.sh gif o make gif.so).
Queste librerie condivise possono essere caricate da fuzzer di uso generale, come AFL++.
Per eseguire AFL++ con FormatFuzzer, segui le istruzioni sulla nostra versione modificata di AFL++. Supportiamo diverse strategie di fuzzing, tra cui:
AFL+FFMut: esegue AFL++ usando FormatFuzzer per fornire mutazioni intelligenti specifiche del formato.
AFL+FFGen: usa FormatFuzzer come generatore specifico del formato, mentre AFL++ muta i suoi semi decisionali.
Per scrivere i tuoi template binari .bt (e quindi creare un fuzzer/parser ad alta efficienza per questo formato), leggi la sezione Introduction to Templates and Scripts dal Manuale dell'Editor 010.
In molti casi, un template del formato che stai cercando (o uno simile) potrebbe già esistere. Dai un'occhiata alla collezione di template binari dell'editor 010 per vedere se c'è qualcosa che puoi usare o su cui basare il tuo formato.
Nota che i file .bt forniti nel repository sono generalmente orientati all'analisi di file. Possono essere usati anche per la generazione di file; ma spesso mancano informazioni esatte su quali parti dell'input sono obbligatorie.
In questa sezione, discutiamo alcuni dei modi in cui puoi personalizzare i file .bt per funzionare bene con FormatFuzzer.
Ad esempio, per il formato GIF, il file templates/gif-orig.bt mostra il template binario originale, che era progettato solo per l'analisi, mentre il file templates/gif.bt è una versione modificata in grado di generare GIF validi. Confrontando i due file, vediamo che è stato necessario un piccolo numero di modifiche per ottenere questo risultato.
Se hai creato un gif-fuzzer, eseguendo make gif-fuzzer o usando lo strumento ffcompile, hai già ottenuto un file C++ gif.cpp che contiene un'implementazione del generatore e parser GIF. Questo è utile per vedere come le modifiche apportate al template binario vengono tradotte in codice eseguibile. Maggiori dettagli sul codice C++ sono presentati nella prossima sezione.
Il template binario GIF fa uso di funzioni di lookahead ReadUByte() e ReadUShort() per guardare avanti ai valori dei prossimi byte nel file prima di analizzarli effettivamente in un campo di struct. In fase di generazione, permettiamo a queste funzioni di ricevere un argomento aggiuntivo che specifica un insieme di buoni valori conosciuti da scegliere per i byte che guardiamo avanti. Inoltre, permettiamo anche di specificare un insieme globale di buoni valori conosciuti da usare sempre quando si chiama una particolare funzione di lookahead, come ReadUByte(). Questi sono memorizzati nel vettore ReadUByteInitValues.
Per impostazione predefinita, la nostra procedura di traduzione ffcompile cerca di estrarre valori interessanti che sono stati usati in confronti contro i byte di lookahead e di usarli come insieme globale di valori conosciuti. Quando esegui
./ffcompile templates/gif.bt gif.cpp
un messaggio stampato mostra le funzioni di lookahead identificate, così come i valori interessanti estratti:
Finished creating cpp generator.
Lookahead functions found:
ReadUByte
ReadUShort
Mined interesting values:
GlobalColorTableFlag: ['1']
LocalColorTableFlag: ['1']
ReadUByte: ['0x3B', '0x2C']
ReadUShort: ['0xF921', '0xFE21', '0x0121', '0xFF21']
Signature: ['"GIF"']
Per la generazione di GIF, tuttavia, è meglio specificare l'insieme di buoni valori conosciuti per ReadUByte() individualmente a ogni chiamata della funzione. Quindi definiamo un array vuoto (dimensione 0)
const local UBYTE ReadUByteInitValues[0];
per sovrascrivere l'insieme globale di ReadUByteInitValues e per ogni chiamata a ReadUByte(), usiamo un argomento aggiuntivo per specificare l'insieme di buoni valori da usare per quella particolare posizione.
Il linguaggio del template binario è anche abbastanza potente da permettere che questa scelta sia fatta in base a condizioni runtime. Ad esempio, nel codice seguente mostriamo come la scelta di valori appropriati per una chiamata a ReadUByte() può dipendere dalla versione GIF corrente che stiamo generando. Una versione GIF 89a permette un valore extra possibile per il byte (0x21).
if(GifHeader.Version == "89a")
local UBYTE values[] = { 0x3B, 0x2C, 0x21 };
else
local UBYTE values[] = { 0x3B, 0x2C };
while (ReadUByte(FTell(), values) != 0x3B) {
...
}
Le rimanenti modifiche necessarie per il template binario GIF sono simili. Ad esempio, per ogni campo di struct puoi anche specificare un insieme di buoni valori conosciuti. Ad esempio questo specifica i valori corretti per il campo Version: 87a e 89a.
char Version[3] = { {"87a"}, {"89a"} };
Per scopi di debug, così come per capire come apportare modifiche appropriate per migliorare i tuoi generatori e parser, può essere utile comprendere alcuni meccanismi interni del codice C++ generato. Idealmente, dovresti essere in grado di modificare i file template binari finché non possono essere usati per generare file validi con alta probabilità, quindi non dovresti dover modificare il codice C++ generato.
Il codice C++ crea una classe per ogni struct e union definiti nel template binario, così come per i tipi nativi, come int.
Al momento della costruzione, quando si inizializza una variabile, possiamo definire un insieme di buoni valori conosciuti che questa variabile può assumere. Ad esempio, la chiamata al costruttore
char_array_class cname(cname_element, { "IHDR", "tEXt", "PLTE", "cHRM", "sRGB", "iEXt", "zEXt", "tIME", "pHYs", "bKGD", "sBIT", "sPLT", "acTL", "fcTL", "fdAT", "IHDR", "IEND" });
specificherebbe 17 buoni valori da usare per la variabile cname. Ma spesso questo non è sufficiente, poiché la scelta dei tipi di chunk appropriati è sensibile al contesto.
Quindi permettiamo anche di specificare un insieme di buoni valori al momento della generazione quando si genera un nuovo chunk.
Ad esempio, questa chiamata potrebbe essere usata per generare un'istanza di chunk per il primo chunk, che deve essere di tipo IHDR.
GENERATE(chunk, ::g->chunk.generate({ "IHDR" }, false));
Quando si genera il secondo chunk, potremmo usare questa lunga lista di possibili chunk che possono trovarsi tra il chunk IHDR e il chunk PLTE:
GENERATE(chunk, ::g->chunk.generate({ "iCCP", "sRGB", "sBIT", "gAMA", "cHRM", "pHYs", "sPLT", "tIME", "zTXt", "tEXt", "iTXt", "eXIf", "oFFs", "pCAL", "sCAL", "acTL", "fcTL", "fdAT", "fRAc", "gIFg", "gIFt", "gIFx", "sTER" }, true));
Il generatore sceglierà quindi uniformemente uno dei buoni valori conosciuti da usare per la nuova istanza. Permettiamo anche la scelta di un valore malvagio che non è uno dei buoni valori conosciuti con una piccola probabilità di 1/128.
Questa funzionalità può essere abilitata o disabilitata in qualsiasi momento usando il metodo set_evil_bit.
Tutte le scelte casuali effettuate dal generatore sono fatte chiamando il metodo rand_int().
long long rand_int(unsigned long long x, std::function<long long (unsigned char*)> parse);
Quando il programma viene eseguito come generatore, questo metodo campiona un intero da 0 a x-1 leggendo byte dal buffer casuale.
Quando il programma viene eseguito come parser, questo metodo usa la funzione parse() per scoprire quali byte casuali devono essere presenti nel buffer casuale per generare il file di destinazione, e poi scrive quei byte nel buffer casuale.
La funzione parse riceve come argomento il buffer alla posizione corrente del file e deve poi restituire quale valore dovrebbe essere restituito dalla chiamata corrente a rand_int() per generare questa esatta configurazione del file.
FormatFuzzer è stato progettato e scritto da Rafael Dutra <[email protected]>.
Il concetto di un compilatore di fuzzer è stato introdotto da Rahul Gopinath <[email protected]> e Andreas Zeller <[email protected]>.
FormatFuzzer è Copyright © 2020, 2021 da CISPA Helmholtz Center for Information Security. Si applicano le seguenti licenze:
Il codice di FormatFuzzer (in particolare, tutto il codice C++ e il codice relativo alla sua generazione) è soggetto alla GNU GENERAL PUBLIC LICENSE, come indicato in COPYING.
In deroga a quanto sopra, il codice C++ generato da FormatFuzzer (cioè, fuzzer e parser per formati specifici) è di pubblico dominio.
Il codice pfp originale, su cui si basa FormatFuzzer, è soggetto a una licenza MIT, come indicato in LICENSE-pfp.