Der Tag, an dem ich mich in Fuzzing verliebt habe

2007 habe ich einige Modding- Tools für den Freelancer Space Simulator geschrieben. Spielressourcen werden im Format "Binary INI" oder "BINI" gespeichert. Wahrscheinlich wurde das Binärformat aus Gründen der Leistung gewählt: Solche Dateien lassen sich schneller laden und lesen als beliebiger Text im INI-Format.

Die meisten Spielinhalte können direkt aus diesen Dateien heraus bearbeitet werden, indem Namen, Produktpreise, Raumfahrzeugstatistiken geändert oder sogar neue Schiffe hinzugefügt werden. Binärdateien lassen sich nur schwer direkt ändern. Daher besteht der natürliche Ansatz darin, sie in Text-INI zu konvertieren, Änderungen in einem Texteditor vorzunehmen, dann wieder in das BINI-Format zu konvertieren und die Dateien im Spielverzeichnis zu ersetzen.

Ich habe das BINI-Format nicht analysiert und bin nicht der erste, der lernt, wie man sie bearbeitet. Aber die vorhandenen Tools gefielen mir nicht und ich hatte meine eigene Vision, wie sie funktionieren sollten. Ich bevorzuge eine Unix-Oberfläche, obwohl das Spiel selbst unter Windows läuft.

Zu dieser Zeit habe ich gerade die Yacc- Tools (eigentlich Bison ) und Lex (eigentlich Flex ) sowie Autoconf kennengelernt, also habe ich sie genau verwendet. Es war interessant, diese Dienstprogramme in der Praxis auszuprobieren, obwohl ich andere Open-Source-Projekte sklavisch imitierte und nicht verstand, warum alles so gemacht wurde, auf keine andere Weise. Aufgrund der Verwendung von yacc / lex und der Erstellung von Konfigurationsskripten war ein vollständiges Unix-ähnliches System erforderlich. Dies ist alles in der Originalversion der Programme sichtbar.

Das Projekt erwies sich als recht erfolgreich: Ich selbst habe diese Tools erfolgreich eingesetzt und sie wurden in verschiedenen Sammlungen für Freelancer-Modding veröffentlicht.

Refactoring


Mitte 2018 kehrte ich zu diesem Projekt zurück. Haben Sie sich jemals Ihren alten Code mit dem Gedanken angesehen: Was haben Sie überhaupt gedacht? Mein INI-Format erwies sich als viel starrer und strenger als nötig, die Binärdatei wurde auf zweifelhafte Weise aufgezeichnet, und die Assembly funktionierte nicht einmal normal.

Dank zehn Jahren zusätzlicher Erfahrung wusste ich mit Sicherheit, dass ich diese Tools jetzt viel besser schreiben würde. Und ich habe es in ein paar Tagen geschafft und sie von Grund auf neu geschrieben. Dieser neue Code befindet sich jetzt im Master-Thread von Github.

Ich mag es, alles so einfach wie möglich zu machen , deshalb habe ich Autoconf zugunsten eines einfacheren und tragbareren Makefiles losgeworden. Kein Yacc oder Lex mehr, aber der Parser wird von Hand geschrieben. Es wird nur das entsprechende tragbare C verwendet. Das Ergebnis ist so einfach, dass ich das Projekt mit einem kurzen Befehl aus Visual Studio zusammenstelle , sodass das Makefile nicht wirklich benötigt wird. Wenn Sie stdint.h durch typedef ersetzen, können Sie sogar Binitools unter DOS erstellen und ausführen .

Die neue Version ist schneller, kompakter, sauberer und einfacher. Es ist viel flexibler in Bezug auf INI-Eingaben, so dass es einfacher zu verwenden ist. Aber ist es wirklich richtig?

Fuzzing


Ich habe mich seit vielen Jahren für Fuzzing interessiert, besonders für Afl (American Fuzzy Lop). Aber er hat es nie gemeistert, obwohl er einige der Werkzeuge getestet hat, die ich regelmäßig benutze. Aber Fuzzing fand nichts Bemerkenswertes, zumindest bevor ich aufgab. Ich habe meine JSON-Bibliothek getestet und aus irgendeinem Grund auch nichts gefunden. Es ist klar, dass mein JSON-Parser nicht so zuverlässig sein kann, oder? Aber Fuzzing zeigte nichts. (Wie sich herausstellte, ist meine JSON-Bibliothek ziemlich zuverlässig, was zum großen Teil den Bemühungen der Community zu verdanken ist!)

Aber jetzt habe ich einen relativ neuen INI-Parser. Obwohl es den ursprünglichen Satz von BINI-Dateien im Spiel erfolgreich analysieren und korrekt zusammenstellen kann, wurde seine Funktionalität nicht wirklich getestet. Sicherlich wird hier das Fuzzing etwas finden. Außerdem müssen Sie keine einzige Zeile schreiben, um diesen Code auszuführen. Die Standardwerkzeuge arbeiten mit Standardeingaben, was ideal ist.

Angenommen, Sie haben die erforderlichen Tools installiert (make, gcc, afl), so beginnt das Fuzzing von Binitools wie folgt:

 $ make CC=afl-gcc $ mkdir in out $ echo '[x]' > in/empty $ afl-fuzz -i in -o out -- ./bini 

Das Dienstprogramm bini akzeptiert INI am Eingang und gibt BINI aus. Daher ist es viel interessanter, es zu überprüfen als das umgekehrte unbini Verfahren. Da unbini relativ einfache Binärdaten analysiert, hat der (wahrscheinlich) Fuzzer nichts zu suchen. Für alle Fälle habe ich es trotzdem überprüft.



In diesem Beispiel habe ich den Standard-Compiler für afl in die GCC-Shell geändert ( CC=afl-gcc ). Hier ruft afl GCC im Hintergrund auf, fügt der Binärdatei jedoch ein eigenes Toolkit hinzu. Beim Fuzzing verwendet afl-fuzz dieses Toolkit, um den Ausführungspfad eines Programms zu überwachen. In der afl-Dokumentation werden die technischen Details erläutert.

Ich habe auch die Eingabe- und Ausgabeverzeichnisse erstellt, indem ich in das Eingabeverzeichnis ein minimales Arbeitsbeispiel eingefügt habe, das afl einen Ausgangspunkt gibt. Wenn es gestartet wird, mutiert es die Eingabedatenwarteschlange und sucht während der Programmausführung nach Änderungen. Das Ausgabeverzeichnis enthält die Ergebnisse und vor allem die Eingabedaten, die eindeutige Ausführungspfade verursachen. Mit anderen Worten, viele Eingaben werden am Fuzzer-Ausgang verarbeitet, wodurch viele verschiedene Grenzszenarien überprüft werden.

Das interessanteste und beängstigendste Ergebnis ist ein vollständiger Programmabsturz. Als ich den Fuzzer für Binitools zum ersten Mal startete, zeigte bini viele solcher Abstürze. Innerhalb weniger Minuten entdeckte afl eine Reihe subtiler und interessanter Fehler in meinem Programm, die unglaublich nützlich waren. Fazzer fand sogar einen unwahrscheinlichen Fehler eines veralteten Zeigers und überprüfte die unterschiedliche Reihenfolge der verschiedenen Speicherzuordnungen. Dieser besondere Fehler war ein Wendepunkt, an dem ich den Wert des Fuzzing erkannte.

Nicht alle gefundenen Fehler führten zu Fehlern. Ich habe auch die Ausgabe untersucht und untersucht, welche Eingabe zu einem erfolgreichen Ergebnis führte und welche nicht, und habe beobachtet, wie das Programm mit verschiedenen Extremfällen umging. Sie lehnte einige Eingaben ab, von denen ich dachte, dass sie sie verarbeiten würden. Und umgekehrt verarbeitete sie einige Daten, die ich für falsch hielt, und interpretierte einige Daten auf unerwartete Weise für mich. Selbst nachdem ich Fehler mit Programmabstürzen behoben hatte, änderte ich die Parser-Einstellungen, um jeden dieser unangenehmen Fälle zu beheben.

Erstellen Sie eine Testsuite


Sobald ich alle vom Fuzzer erkannten Fehler behoben und den Parser in allen Grenzsituationen angepasst hatte, führte ich eine Reihe von Tests aus dem Fuzzer-Datenpaket durch - allerdings nicht direkt.

Zuerst habe ich den Fuzzer parallel ausgeführt - dieser Vorgang wird in der afl-Dokumentation erläutert -, sodass ich viele redundante Eingaben erhalten habe. Mit Redundanz meine ich, dass die Eingabe unterschiedlich ist, aber denselben Ausführungspfad hat. Glücklicherweise hat afl ein Werkzeug, um damit umzugehen: afl-cmin , ein Werkzeug, um die Shell zu minimieren. Es werden unnötige Eingaben vermieden.

Zweitens waren viele dieser Eingaben länger als erforderlich, um ihren eindeutigen Ausführungspfad aufzurufen. afl-tmin , ein Testfall-Minimierer, der den Testfall reduzierte.

Ich habe gültige und ungültige Eingaben getrennt - und im Repository überprüft. Schauen Sie sich all diese dummen Eingänge an, die der Fuzzer anhand einer einzigen minimalen Eingabe erfunden hat :


Tatsächlich wird hier der Parser in einem Zustand eingefroren, und eine Reihe von Tests stellt sicher, dass sich ein bestimmter Build auf eine ganz bestimmte Art und Weise verhält. Dies ist besonders nützlich, um sicherzustellen, dass Assemblys, die von anderen Compilern auf anderen Plattformen erstellt wurden, sich in Bezug auf ihre Ausgabe gleich verhalten. Meine Testsuite hat sogar einen Fehler in der Dietlibc-Bibliothek festgestellt, da binitools die Tests nach dem Verknüpfen nicht bestanden hat. Wenn es notwendig wäre, nicht triviale Änderungen am Parser vorzunehmen, müsste man im Wesentlichen die aktuellen Tests aufgeben und von vorne beginnen, damit afl einen ganz neuen Körper für den neuen Parser generiert.

Natürlich hat sich das Fuzzing als leistungsstarke Technik etabliert. Er fand eine Reihe von Fehlern, die ich allein niemals hätte entdecken können. Seitdem begann ich, es kompetenter zu nutzen, um andere Programme zu testen - nicht nur meine - und fand viele neue Fehler. Jetzt hat Fuzzer einen festen Platz unter den Werkzeugen in meinem Entwicklungskit eingenommen.

Source: https://habr.com/ru/post/de438662/


All Articles