
Vorlagenbasierter Binärformat-Fuzzer, der gültige Testeingaben in hoher Geschwindigkeit erzeugt und parst, mit AFL++-Integration für abdeckungsgesteuertes Fuzzing.
FormatFuzzer ist ein Framework für die hocheffiziente, qualitativ hochwertige Generierung und Analyse von Binäreingaben.
Es nimmt eine Binärvorlage entgegen, die das Format einer Binäreingabe beschreibt, und generiert einen ausführbaren Code, der das angegebene Binärformat erzeugt und parst.
Aus einer Binärvorlage für GIF zum Beispiel erzeugt FormatFuzzer einen GIF-Generator – auch bekannt als GIF-Fuzzer.
Die von FormatFuzzer erzeugten Generatoren sind hocheffizient und produzieren Tausende gültiger Testeingaben pro Sekunde – im krassen Gegensatz zu mutationsbasierten Fuzzern, bei denen die überwältigende Mehrheit der Eingaben ungültig ist. Die von FormatFuzzer generierten Eingaben sind unabhängig vom getesteten Programm (oder eigentlich jedem Programm), sodass Sie sie auch in Black-Box-Szenarien verwenden können. FormatFuzzer lässt sich jedoch auch in AFL++ integrieren, um gültige Eingaben zu erzeugen, die gleichzeitig auf maximale Codeabdeckung abzielen. In unseren Experimenten übertrifft dieser „Best of both Worlds"-Ansatz alle anderen Einstellungen; siehe unser Paper für Details.
Die von FormatFuzzer verwendeten Binärvorlagen stammen vom 010 Editor.
Es gibt mehr als 170 Binärvorlagen, die entweder direkt für FormatFuzzer verwendet oder für dessen Nutzung angepasst werden können. Out of the Box erzeugt FormatFuzzer Formate wie AVI, BMP, GIF, JPG, MIDI, MP3, MP4, PCAP, PNG, WAV und ZIP; und wir erweitern diese Liste jede Woche weiter.
Mitwirkende sind willkommen! Besuchen Sie die FormatFuzzer-Projektseite, um Ideen und Probleme zu melden oder Pull-Requests einzureichen. Für Details zur Funktionsweise von FormatFuzzer und zu Vergleichen lesen Sie unser Paper für weitere Informationen.
FormatFuzzer ist auf der FormatFuzzer-Projektseite verfügbar. Sie können die neueste Version von der Release-Seite herunterladen und entpacken.
Für die allerneueste Version können Sie auch das Git-Repository klonen:
git clone https://github.com/uds-se/FormatFuzzer.git
Alle weiteren Aktionen finden im Hauptverzeichnis statt:
cd FormatFuzzer
Um FormatFuzzer auszuführen, benötigen Sie Folgendes:
getopt_long()) wie clang oder gccpy010parser, six und intervaltreezlib-Bibliothek (für Komprimierungsfunktionen)boost-Bibliothek (für Prüfsummenfunktionen)Wenn Sie die Build- und Konfigurationsskripte (.ac- und .am-Dateien) bearbeiten möchten, benötigen Sie außerdem
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
Auf allen Systemen mit pip:
pip install py010parser
pip install six
pip install intervaltree
Hinweis: Alle Build-Befehle erfordern, dass Sie sich im selben Ordner wie diese README-Datei befinden. Das Bauen eines Fuzzers außerhalb dieses Ordners wird noch nicht unterstützt.
Es gibt ein Skript build.sh, das alle Konstruktionsschritte automatisiert.
Führen Sie einfach
./build.sh gif
aus, um einen GIF-Fuzzer zu erstellen.
Dies funktioniert für alle Dateiformate, die in templates/ bereitgestellt werden; wenn es eine Datei templates/FOO.bt gibt, dann baut ./build.sh FOO einen Fuzzer.
Es gibt ein Makefile (Quelle in Makefile.am), das alle Konstruktionsschritte automatisiert.
(Erfordert GNU make.)
Führen Sie zuerst
touch configure Makefile.in
dann
./configure
und dann
make gif-fuzzer
aus, um einen GIF-Fuzzer zu erstellen.
Dies funktioniert für alle Dateiformate, die in templates/ bereitgestellt werden; wenn es eine Datei templates/FOO.bt gibt, dann baut make FOO-fuzzer einen Fuzzer.
Falls die obige make-Methode nicht funktioniert oder Sie mehr Kontrolle wünschen, müssen Sie möglicherweise manuell vorgehen.
Führen Sie den ffcompile-Compiler aus, um die Binärvorlage in C++-Code zu kompilieren. Er benötigt zwei Argumente: die .bt-Binärvorlage und eine zu generierende .cpp-C++-Datei.
./ffcompile templates/gif.bt gif.cpp
Verwenden Sie die folgenden Befehle, um einen Fuzzer gif-fuzzer zu erstellen.
Kompilieren Sie zunächst den generischen Befehlszeilentreiber:
g++ -c -I . -std=c++17 -g -O3 -Wall fuzzer.cpp
(-I . gibt den Speicherort der bt.h-Datei an; -std=c++17 setzt den C++-Standard.)
Kompilieren Sie dann den Binärparser/-compiler:
g++ -c -I . -std=c++17 -g -O3 -Wall gif.cpp
Verknüpfen Sie schließlich den Binärparser/-compiler mit dem Befehlszeilentreiber, um eine ausführbare Datei zu erhalten. Wenn Sie zusätzliche Bibliotheken (wie -lz) verwenden, geben Sie diese hier ebenfalls an.
g++ -O3 gif.o fuzzer.o -o gif-fuzzer -lz
FormatFuzzer kann als eigenständiger Parser, Generator oder Mutator bestimmter Formate ausgeführt werden. Darüber hinaus kann es von allgemeinen Fuzzern wie AFL++ aufgerufen werden, um diese formatspezifischen Fähigkeiten in den Fuzzing-Prozess zu integrieren (siehe den folgenden Abschnitt zur AFL++-Integration).
Der generierte Fuzzer nimmt einen Befehl als erstes Argument entgegen, gefolgt von Optionen und Argumenten für diesen Befehl.
Der wichtigste Befehl ist fuzz zur Erzeugung von Ausgaben. Seine Argumente sind Dateien, die im entsprechenden Format generiert werden sollen.
Führen Sie den Generator aus als
./gif-fuzzer fuzz output.gif
um eine zufällige Binärdatei output.gif zu erstellen, oder
./gif-fuzzer fuzz out1.gif out2.gif out3.gif
um drei GIF-Dateien out1.gif, out2.gif und out3.gif zu erstellen.
Beachten Sie, dass die von uns bereitgestellte Vorlage gif.bt um spezielle Funktionen erweitert wurde, um die Generierung gültiger Dateien zu erleichtern. Wenn Sie eine originale .bt-Vorlagendatei ohne Anpassungen verwenden, erhalten Sie möglicherweise Warnungen während der Generierung und erstellen ungültige Dateien.
Sie können den Fuzzer auch als Parser für Binärdateien mit dem Befehl parse ausführen. Dies ist nützlich, wenn Sie die Genauigkeit der Binärvorlage testen oder eine Eingabe mutieren möchten (siehe „Entscheidungsdateien" unten).
Um den Parser auszuführen, verwenden Sie
./gif-fuzzer parse input.gif
Sie sehen Fehlermeldungen, wenn input.gif nicht erfolgreich geparst werden kann.
Beim Parsen können Sie auch alle Parsing-Entscheidungen (d.h. welche Parsing-Alternativen gewählt wurden) in einer Entscheidungsdatei speichern. Dies ist eine Abfolge von Bytes, die die getroffenen Entscheidungen aufzählt.
Jedes Byte steht für eine einzige Parsing-Entscheidung. Ein Byte-Wert von 0 bedeutet, dass die erste Alternative gewählt wurde, ein Byte-Wert von 1 bedeutet, dass die zweite Alternative gewählt wurde, und so weiter.
Sie können eine solche Entscheidungsdatei generieren, während Sie eine Eingabe parsen:
./gif-fuzzer parse --decisions input.dec input.gif
Hier speichert input.dec die Entscheidungen, die für das Parsen von input.gif getroffen wurden.
Sie können eine solche Entscheidungsdatei auch beim Generieren von Eingaben verwenden. Der Fuzzer trifft dann genau dieselben Entscheidungen wie beim Parsen. Der folgende Befehl generiert eine neue GIF-Datei unter Verwendung der Entscheidungen, die beim Parsen von input.gif ermittelt wurden:
./gif-fuzzer fuzz --decisions input.dec input2.gif
Wenn alles gut funktioniert, sollten beide Dateien identisch sein:
cmp input.gif input2.gif
Durch Mutieren einer Entscheidungsdatei (z.B. durch Ersetzen einzelner Bytes) können Sie Eingaben erstellen, die der ursprünglichen geparsten Datei ähnlich sind. Dies ist nützlich für die Schnittstelle zu spezifischen Teststrategien und Fuzzern wie AFL, bei denen Sie gif-fuzzer und ähnliche als Übersetzer von Entscheidungsdateien in Binärdateien und zurück verwenden können: AFL würde Entscheidungsdateien mutieren, und das getestete Programm würde auf den übersetzten Binärdateien ausgeführt. Im Gegensatz zur direkten Mutation von Binärdateien (wie AFL es normalerweise tun würde) hätte dies den Vorteil, dass immer gültige Eingaben vorhanden sind – und somit viel schneller Fortschritte in Richtung Codeabdeckung erzielt werden.
Zusätzlich zu den formatspezifischen Fuzzern wie gif-fuzzer kann FormatFuzzer auch in formatspezifische Shared Libraries wie gif.so kompiliert werden (dazu führen Sie einfach ./build.sh gif oder make gif.so aus).
Diese Shared Libraries können von allgemeinen Fuzzern wie AFL++ geladen werden.
Um AFL++ mit FormatFuzzer auszuführen, folgen Sie einfach den Anweisungen auf unserer modifizierten Version von AFL++. Wir unterstützen verschiedene Fuzzing-Strategien, darunter:
AFL+FFMut: Führt AFL++ aus und verwendet FormatFuzzer, um formatspezifische intelligente Mutationen bereitzustellen.
AFL+FFGen: Verwendet FormatFuzzer als formatspezifischen Generator, während AFL++ seine Entscheidungs-Seeds mutiert.
Um Ihre eigenen .bt-Binärvorlagen zu schreiben (und damit einen hocheffizienten Fuzzer/Parser für dieses Format zu erstellen), lesen Sie den Abschnitt Introduction to Templates and Scripts aus dem 010 Editor Manual.
In vielen Fällen existiert bereits eine Vorlage für das gesuchte Format (oder ein ähnliches). Werfen Sie einen Blick in die Binärvorlagensammlung des 010 Editors, ob es etwas gibt, das Sie verwenden oder als Grundlage für Ihr Format nehmen können.
Beachten Sie, dass die im Repository bereitgestellten .bt-Dateien im Allgemeinen auf das Parsen von Dateien abzielen. Sie können auch zum Generieren von Dateien verwendet werden; aber ihnen fehlen oft genaue Informationen darüber, welche Teile der Eingabe erforderlich sind.
In diesem Abschnitt besprechen wir einige Möglichkeiten, wie Sie .bt-Dateien anpassen können, damit sie gut mit FormatFuzzer funktionieren.
Zum Beispiel zeigt für das GIF-Format die Datei templates/gif-orig.bt die originale Binärvorlage, die nur für das Parsen ausgelegt war, während die Datei templates/gif.bt eine modifizierte Version ist, die gültige GIFs generieren kann. Beim Vergleich der beiden Dateien sehen wir, dass nur eine geringe Anzahl von Änderungen erforderlich war, um dies zu erreichen.
Wenn Sie einen gif-fuzzer erstellt haben, entweder durch Ausführen von make gif-fuzzer oder durch Verwendung des ffcompile-Werkzeugs, haben Sie bereits eine C++-Datei gif.cpp erhalten, die eine Implementierung des GIF-Generators und -Parsers enthält. Dies ist nützlich, um zu sehen, wie die von Ihnen an der Binärvorlage vorgenommenen Änderungen in ausführbaren Code übersetzt werden. Weitere Details zum C++-Code finden Sie im nächsten Abschnitt.
Die GIF-Binärvorlage verwendet Lookahead-Funktionen ReadUByte() und ReadUShort(), um die Werte der nächsten Bytes in der Datei vorauszuschauen, bevor sie tatsächlich in ein Strukturfeld geparst werden. Zur Generierungszeit erlauben wir diesen Funktionen, ein zusätzliches Argument zu erhalten, das einen Satz guter bekannter Werte angibt, die für die Bytes ausgewählt werden sollen, auf die wir vorausschauen. Darüber hinaus erlauben wir auch, einen globalen Satz guter bekannter Werte anzugeben, der immer verwendet wird, wenn eine bestimmte Lookahead-Funktion wie ReadUByte() aufgerufen wird. Diese werden im Vektor ReadUByteInitValues gespeichert.
Standardmäßig versucht unser Übersetzungsverfahren ffcompile, interessante Werte zu extrahieren, die in Vergleichen gegen Lookahead-Bytes verwendet wurden, und sie als globalen Satz bekannter Werte zu verwenden. Wenn Sie
./ffcompile templates/gif.bt gif.cpp
ausführen, zeigt eine gedruckte Nachricht die identifizierten Lookahead-Funktionen sowie die extrahierten interessanten Werte an:
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"']
Für die GIF-Generierung ist es jedoch besser, den Satz guter bekannter Werte für ReadUByte() individuell bei jedem Aufruf der Funktion anzugeben. Also definieren wir ein leeres Array (Größe 0)
const local UBYTE ReadUByteInitValues[0];
um den Satz globaler ReadUByteInitValues zu überschreiben, und für jeden Aufruf von ReadUByte() verwenden wir ein zusätzliches Argument, um den Satz guter Werte für diese bestimmte Stelle anzugeben.
Die Binärvorlagensprache ist auch mächtig genug, um diese Wahl basierend auf Laufzeitbedingungen zu treffen. Im folgenden Code zeigen wir zum Beispiel, wie die Wahl geeigneter Werte für einen ReadUByte()-Aufruf von der aktuellen GIF-Version abhängen kann, die wir generieren. Eine GIF-Version 89a erlaubt einen zusätzlichen möglichen Wert für das Byte (0x21).
if(GifHeader.Version == "89a")
local UBYTE values[] = { 0x3B, 0x2C, 0x21 };
else
local UBYTE values[] = { 0x3B, 0x2C };
while (ReadUByte(FTell(), values) != 0x3B) {
...
}
Die restlichen Änderungen, die für die GIF-Binärvorlage erforderlich sind, sind ähnlich. Beispielsweise kann für jedes Strukturfeld auch ein Satz bekannter guter Werte angegeben werden. Dies gibt zum Beispiel die korrekten Werte für das Feld Version an: 87a und 89a.
char Version[3] = { {"87a"}, {"89a"} };
Zu Debugging-Zwecken sowie zum Verständnis, wie Sie geeignete Änderungen vornehmen können, um Ihre Generatoren und Parser zu verbessern, kann es nützlich sein, einige interne Funktionsweisen des generierten C++-Codes zu verstehen. Idealerweise sollten Sie in der Lage sein, die Binärvorlagendateien so zu bearbeiten, dass sie mit hoher Wahrscheinlichkeit gültige Dateien generieren können, sodass Sie den generierten C++-Code nicht bearbeiten müssten.
Der C++-Code erstellt eine Klasse für jedes struct und jede union, die in der Binärvorlage definiert sind, sowie für native Typen wie int.
Zum Konstruktionszeitpunkt, wenn eine Variable initialisiert wird, können wir einen Satz guter bekannter Werte definieren, die diese Variable annehmen kann. Beispielsweise würde der Konstruktoraufruf
char_array_class cname(cname_element, { "IHDR", "tEXt", "PLTE", "cHRM", "sRGB", "iEXt", "zEXt", "tIME", "pHYs", "bKGD", "sBIT", "sPLT", "acTL", "fcTL", "fdAT", "IHDR", "IEND" });
17 gute Werte für die Variable cname angeben. Aber das ist oft nicht ausreichend, da die Wahl des geeigneten Chunk-Typs kontextabhängig ist.
Deshalb erlauben wir auch, zur Generierungszeit einen Satz guter Werte anzugeben, wenn ein neuer Chunk generiert wird.
Zum Beispiel könnte dieser Aufruf verwendet werden, um eine Instanz von chunk für den ersten Chunk zu generieren, der den Typ IHDR haben muss.
GENERATE(chunk, ::g->chunk.generate({ "IHDR" }, false));
Beim Generieren des zweiten Chunks könnten wir diese lange Liste möglicher Chunks verwenden, die zwischen dem IHDR-Chunk und dem PLTE-Chunk vorkommen können:
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));
Der Generator wählt dann gleichmäßig einen der guten bekannten Werte für die neue Instanz aus. Wir erlauben auch die Wahl eines bösen Werts, der nicht zu den guten bekannten Werten gehört, mit einer kleinen Wahrscheinlichkeit von 1/128.
Diese Funktion kann jederzeit mit der Methode set_evil_bit aktiviert oder deaktiviert werden.
Alle zufälligen Entscheidungen des Generators werden durch Aufrufen der Methode rand_int() getroffen.
long long rand_int(unsigned long long x, std::function<long long (unsigned char*)> parse);
Wenn das Programm als Generator läuft, sampelt diese Methode eine ganze Zahl von 0 bis x-1, indem sie Bytes aus dem zufälligen Puffer liest.
Wenn das Programm als Parser läuft, verwendet diese Methode die parse()-Funktion, um herauszufinden, welche zufälligen Bytes im zufälligen Puffer vorhanden sein müssen, um die Zieldatei zu generieren, und schreibt dann diese Bytes in den zufälligen Puffer.
Die parse-Funktion erhält als Argument den Puffer an der aktuellen Position der Datei und muss dann den Wert zurückgeben, der vom aktuellen Aufruf von rand_int() zurückgegeben werden müsste, um diese genaue Dateikonfiguration zu generieren.
FormatFuzzer wurde entworfen und geschrieben von Rafael Dutra <[email protected]>.
Das Konzept eines Fuzzer-Compilers wurde eingeführt von Rahul Gopinath <[email protected]> und Andreas Zeller <[email protected]>.
FormatFuzzer ist urheberrechtlich geschützt © 2020, 2021 durch das CISPA Helmholtz-Zentrum für Informationssicherheit. Die folgenden Lizenzen gelten:
Der FormatFuzzer-Code (insbesondere aller C++-Code und Code im Zusammenhang mit seiner Generierung) unterliegt der GNU GENERAL PUBLIC LICENSE, wie in COPYING zu finden.
Als Ausnahme zu obigem ist der von FormatFuzzer generierte C++-Code (d.h. Fuzzer und Parser für bestimmte Formate) gemeinfrei (Public Domain).
Der originale pfp Code, auf dem FormatFuzzer basiert, unterliegt einer MIT-Lizenz, wie in LICENSE-pfp zu finden.