Fuzzing für alle : Eigene und fremde Software mit überschaubarem Aufwand prüfen
Neben einer kurzen Einführung in das Thema Fuzzing erklärt der vorliegende Artikel die bekanntesten Methoden und stellt einige gängige Tools vor, die sich zum Fuzzing sowohl für eigene als auch für von Drittherstellern entwickelte Software eignet.
Von Michael Heinzl, Salzburg (AT)
Fuzzing gilt als eine der erfolgreichsten Methoden, um Schwachstellen in Software automatisiert zu entdecken. Allem voran gilt das für Speicherkorruption: Solche Verwundbarkeiten lassen sich häufig gezielt ausnutzen, um beliebigen Code im Kontext der betroffenen Software auszuführen. Im Regelfall bedeutet dies, dass ein Angreifer auf potenziell sensible Daten zugreifen oder betroffene Systeme (komplett) kompromittieren kann.
Obwohl die Methode schon über 30 Jahre alt ist, erschienen besonders in den letzten Jahren viele neue Ansätze und Implementierungen, die Fuzzing für mehr Anwendungen und Technologien zugänglich machen. Während große Unternehmen wie Microsoft und Google schon seit vielen Jahren Fuzzing als integralen Bestandteil ihres „Secure Software Development Lifecycle“ einsetzen (vgl. Abb. 1 und [3,12,13]), sind vielen kleineren und mittleren Unternehmen (KMU) die Vorteile von Fuzzing noch nicht bekannt. Dabei belegen zahlreiche Studien, dass die Kosten für die Behebung von Bugs enorm zunehmen, je später im Entwicklungsprozess (von der Planung bis zur Veröffentlichung) diese entdeckt und behoben werden [1,2].
In der einfachsten Form beschreibt Fuzzing den Vorgang, einer Schnittstelle von Programmen automatisiert Daten mit Anomalien zuzuführen – diese Anomalien können sowohl syntaktischer als auch semantischer Natur sein. Wenn die Daten von der Anwendung verarbeitet werden, können solche Eingaben zu unvorhergesehenen Zuständen innerhalb des Programms führen, in vielen Fällen zu einem Programmabsturz. Durch geschickte Manipulationen können Angreifer einen solchen Programmabsturz (beziehungsweise die eigentliche Speicherkorruption) so ablaufen lassen, dass das Programm nicht einfach beendet, sondern eingeschleuster Schadcode ausgeführt wird.
Die zwei derzeit wohl häufigsten Schnittstellen zur Verarbeitung von (Benutzer-)Eingabedaten sind Dateien (z. B. PDF oder DOC) sowie Netzwerkpakete, die Anwendungen empfangen. Als Entwickler ist es deshalb unabdingbar, zunächst alle Schnittstellen zu identifizieren und analysieren, die Daten von einem Benutzer oder der Umgebung, in der das Zielprogramm ausgeführt wird, empfangen.
Abhängig von der Zielanwendung und -umgebung können viele zusätzliche Schnittstellen vorhanden sein: Registry-Einträge, Programmierschnittstellen (APIs), Systemaufrufe an das Betriebssystem, Umgebungsvariablen, ActiveX-Komponenten, Sensoren und so weiter. In diesem Kontext beschreiben Schnittstellen generell jene Grenzen (Trust Boundaries), bei denen eine Vertrauensbeziehung zwischen dem Programm selbst sowie verschiedenen Möglichkeiten zur Datenzufuhr besteht. Diese Übergangspunkte, die es zu identifizieren gilt, werden häufig als Chokepoints bezeichnet.

Abbildung 1: Fuzzing spielt eine wichtige Rolle bei sicherer Entwicklung, etwa im Microsoft Security Development Lifecycle (SDL).
Tool-Auswahl und -Klassifizierung
Es gibt eine Vielzahl existierender Lösungen, die sich auch in kleineren Betrieben oder Arbeitsgruppen problemlos einsetzen lassen. Bekannte Open-Source-Lösungen, beispielsweise American Fuzzy Lop (AFL, [4] – siehe unten) sowie dessen zahlreiche Ableger, Honggfuzz (https://github.com/google/honggfuzz) und libfuzzer (https://llvm.org/docs/LibFuzzer.html), lassen sich relativ schnell erlernen und auf eigenen Code anwenden.
Wer – beispielsweise aufgrund von Service-Level-Agreements oder Produkt-Support – kommerzielle Software bevorzugt, findet ebenfalls diverse Angebote, darunter beispielsweise Defensics (www.synopsys.com), beSTORM (https://beyondsecurity.com/) und „Microsoft Security Risk Detection“ (www.microsoft.com/en-us/security-risk-detection/).
Neben der Unterteilung in White- und Black-Box-Tests mit vollständigem oder gänzlich fehlendem Zugriff auf Quellcodes sowie der Mischform „Gray-Box“, bei der ein Teileinblick in die zu testende Anwendung gegeben ist, werden Fuzzer häufig anhand der Art unterteilt, wie Testdaten erzeugt werden:
Mutation-based Fuzzing
Diese Art von Fuzzer haben normalerweise keine Kenntnisse über die innere Struktur der zu testenden Eingaben (Dateiformat, Format eines Netzwerkpakets, Benutzereingabe über die Kommandozeile usw.) – ein einfaches Beispiel wäre die Umleitung von /dev/random an die Testanwendung. Der Vorteil solcher Fuzzer ist, dass man sofort mit dem Testen beginnen kann – es ist keine Zeit zum Lernen oder Implementieren der Spezifikation notwendig. Vor allem wenn die zu testende Komponente sehr einfach gehalten ist, lassen sich so oft sehr schnell und mit wenig Aufwand brauchbare Testergebnisse erzielen. Handelt es sich jedoch um ein komplexeres Format oder werden beispielsweise gewisse Kriterien geprüft, bevor die eigentliche Eingabe verarbeitet wird, lassen sich auf diese Art oft keine Ergebnisse erzielen: Die Anwendung bricht die weitere Verarbeitung schlicht ab, sobald eine Verletzung der Spezifikation entdeckt wird – beispielsweise wenn eine Prüfsumme der Eingabe nicht korrekt ist.
Generation-based Fuzzing
Hierbei besitzt der Fuzzer Kenntnis von der Struktur der Eingabe – beispielsweise in Form eines Regelwerks, das die Grundstruktur der Eingabe beschreibt, oder eine komplette Implementierung der Referenzspezifikation. Dadurch können Testfälle erzeugt werden, die sich auf besonders kritische Teilkomponenten der Spezifikation beschränken. Zudem lassen sich Pflichtfelder (z. B. Prüfsummen) automatisch in jedem Testfall anpassen. Mit Generation-based Fuzzers lassen sich oft tiefere Programmabschnitte in der Testanwendung testen, jedoch wird mehr Aufwand für die Implementierung des Fuzzers beziehungsweise der Spezifikation benötigt.
Feedback-based Fuzzing
Beim Feedback-based Fuzzing (auch als „Evolutionary Fuzzing“ bezeichnet) lernt der Fuzzer über eine Feedback-Funktion Informationen über die aktuelle Testiteration hinzu. Wird im aktuellen Testdurchlauf beispielsweise ein neuer Codeabschnitt in der Testanwendung erreicht, so wird diese Information an den Fuzzer retourniert und für zukünftige Testiterationen berücksichtigt. Üblicherweise erreicht man diese Feedback-Funktion mithilfe einer Instrumentierung der Zielanwendung: Dabei werden der Zielanwendung Zusatzinformationen oder zusätzlicher Code hinzugefügt, der die Programmanalyse vereinfacht. Die Instrumentierung lässt sich sowohl direkt über den Quelltext realisieren (etwa während des Kompiliervorgangs), aber auch auf Binärcode anwenden (beispielsweise mittels Dynamic Binary-Instrumentation, DBI – siehe etwa [5]). Über den Feedback-Mechanismus kann der Fuzzer automatisiert neue Erkenntnisse über die Struktur der Testeingaben gewinnen und neue Eingaben erzeugen, welche diese Erkenntnisse berücksichtigen. Durch kontinuierliches Feedback kann man den Fuzzer nachhaltig optimieren und die Qualität der Untersuchung steigern. Dies führt im Regelfall dazu, dass beispielsweise in tiefere Codeabschnitte vorgedrungen und eine bessere Code-Abdeckung erzielt wird.
Test-Ablauf
In der Praxis wird oft mit einem sehr einfachen Fuzzer begonnen – während dieser läuft und erste Testergebnisse produziert, kann man parallel an einem verbesserten (intelligenteren oder optimierten) Fuzzer arbeiten.
Falls die zu testende Applikation bisher noch überhaupt keiner Fuzzing-Kampagne unterzogen wurde, ist es sehr wahrscheinlich, dass existierende Fuzzer mit minimalem Aufwand bereits gute Ergebnisse erzielen. Hierfür kann man problemlos einen älteren, nicht mehr benötigten Rechner einsetzen, der beispielsweise eine Woche lang ununterbrochen durchläuft – oder man startet am Abend, bevor Entwickler ihren Arbeitsplatz verlassen, den Fuzzer zum Ablauf über Nacht.
Fünf Minuten zur ersten Fuzzing-Kampagne mit AFL
American Fuzzy Lop (AFL, [4]), entwickelt von Michal Zalewski, ist einer der bekanntesten freien Fuzzer. Zahlreiche Schwachstellen in weit verbreiteter Software (libtiff, PHP, sqlite, OpenSSL, tcpdump etc.) wurden mit seiner Hilfe entdeckt. AFL zeichnet sich besonders durch seine sehr einfache Bedienung, gute Performance und intelligente Teststrategien aus.
Das folgende Beispiel soll aufzeigen, dass eine Fuzzing-Kampagne mit relativ wenig Aufwand gestartet werden kann. Das Test-Szenario ist eine Linux-Applikation, die (mit dem Parameter -f) JPG-Dateien über die Kommandozeile empfängt, verarbeitet und sich dann selbst beendet. Das Ziel ist es, den Code, der für das Parsen von JPG zuständig ist, zu testen. Ein normaler Aufruf der Testanwendung könnte dabei wie folgt aussehen:
./test_anwendung -f testbild.jpg
Nach dem Download von AFL kann man die Software einfach mit make kompilieren. Manche Linux-Distributionen unterstützen Bezug und Installation von AFL auch direkt über den Paketmanager – Ubuntu beispielsweise per apt-get install afl.
Im nächsten Schritt muss die zu testende Anwendung mit dem AFL-Compiler (AFL-GCC) kompiliert werden:
CC=“afl-gcc“ CXX=“afl-g++“
./configure –disable-shared
make clean all
Als Nächstes sollten einige Beispiel-Dateien, welche die Test-Anwendung verarbeiten kann, gesammelt werden (beispielsweise über die Bildersuche von Google). Diese Beispiel-Dateien (hier also JPGs) dienen als Basis für AFL während der Fuzzing-Kampagne: Das Programm nutzt darauf verschiedene Algorithmen, um Anomalien herbeizuführen.
Anschließend kann man die Fuzzing-Kampagne bereits starten:
./afl-fuzz -i in -o out — ./test_anwendung -f @@
Der Ordner „in“ beinhaltet hierbei die zuvor gesammelten Beispiel-Dateien (JPGs), im Ordner „out“ werden die Ergebnisse gespeichert (Informationen zu Abstürzen). AFL ersetzt das @@ am Ende des Kommandos automatisch mit den veränderten Beispiel-Dateien, die es generiert.
Während der Testdurchführung aktualisiert AFL in Echtzeit den aktuellen Stand der Kampagne – darunter die aktuelle Laufzeit, die Anzahl der Tests pro Sekunde sowie die Gesamtanzahl der durchgeführten Tests, Code-Abdeckung und die Zahl aufgetretener Abstürze.
Zu den Fuzzing-Strategien von AFL gehören unter anderem deterministische Methoden – beispielsweise das sequenzielle Kippen von einzelnen Bits und Bytes, verschiedene arithmetische Operationen (Subtraktionen und Addition) sowie das Einfügen bestimmter Ganzzahlen (Integers), die häufig Grenzfälle darstellen (-1, 256, 1024, MAX_INT, MAX_INT-1 usw.). Zusätzlich nutzt AFL zufällige Operationen auf die Eingaben: darunter beispielsweise das Löschen existierender und Einfügen neuer Blöcke sowie das Verbinden von zwei unterschiedlichen Eingaben zu einer neuen Eingabe. Zudem wird auch die Byte-Reihenfolge (Little-Endian und Big-Endian) in der Mutation berücksichtigt.
Natürlich gibt es viel Platz für Optimierungen und nicht jedes Programm beziehungsweise jede Schnittstelle lässt sich derart schnell testen. Dennoch: Vor allem wenn es sich um eine Eigenentwicklung mit Zugriff auf den Quellcode handelt, kann ein Entwickler im Regelfall relativ einfach die Schnittstellen für das Fuzzing optimieren und eine Fuzzing-Kampagne starten.

Abbildung 2: Interface des Fuzzing-Tools American Fuzzy Lop (AFL)
Vorteile von Fuzzing
Fuzzing-Tests bieten vielfältige Vorzüge:
- Gute Automatisierung: Nachdem eine Fuzzing-Kampagne fertig vorbereitet wurde (Test-Korpus, Umgebung, Monitoring, Logging etc.), läuft der Fuzzer automatisiert – Entwickler werden selten während der Testdurchführung benötigt, sondern erst zur Analyse und Auswertung der Ergebnisse.
- Extrem schnelle Testdurchführung: Mehrere Tausend Testfälle pro Sekunde können häufig problemlos erreicht werden. Läuft der Fuzzer 24/7 über einen längeren Zeitraum, lässt sich eine Vielfalt an Testfällen durchführen – das bewirkt idealerweise eine erhöhte Sicherheit und Robustheit der Software.
- Sehr gute Skalierbarkeit: Im Regelfall lässt sich Fuzzing sehr gut durch leistungsstärkere Hardware oder durch Parallelisierung skalieren und die Arbeitslast auf mehrere Instanzen verteilen.
Besonders für Inhouse-Entwicklungen ist Fuzzing eine erprobte Methode, um die Sicherheit der eigenen Produkte zu verbessern (White-Box-Test). Dies bezieht sich sowohl auf Quellcode, der intern entwickelt wird, als auch beispielsweise auf Bibliotheken von Drittanbietern, die man in eigene Produkte integrieren möchte. Im Idealfall wird Fuzzing als fester Bestandteil des internen Secure-Software-Development-Lifecycle (SDLC) betrieben.
Will man in einem Unternehmen neue Software einführen oder erwägt den Erwerb eines neuen Produkts, steht der Quellcode üblicherweise nicht zur Verfügung. Doch auch dann eignet sich Fuzzing sehr gut, um einen schnellen Überblick über die grundlegende Sicherheitshygiene des Produkts zu erlangen (Black-Box-Test). Software, die vom Hersteller regelmäßig Fuzz-Tests unterzogen wird, sollte keine oder nur wenige Abstürze erleiden, wenn sie einem kurzen Fuzzing-Crashtest mit „Off-the-Shelf“-Fuzzern unterzogen wird (also ohne weitere Anpassung oder Optimierung).
Auswertung
Während der Fuzzing-Prozess selbst stark automatisiert abläuft, ist die Auswertung der Testergebnisse weiterhin ein überwiegend manueller Prozess. Läuft der Fuzzer über einen langen Zeitraum, werden oft hunderte oder tausende Programmabstürze aufgezeichnet. Es gibt freie Tools, die dabei helfen, kritische Bugs mit erhöhtem Sicherheitsrisiko automatisiert zu erkennen und zu klassifizieren (z. B. [6,7]).
Andere Tools können dabei helfen, die Eingabe, die einen Absturz verursacht, zu vereinfachen. Denn beim Fuzzing kommt es häufig vor, dass eine Eingabe mit Anomalien sehr komplex oder groß ist – durch verschiedene Algorithmen und Tools (z. B. [8]) lassen sich solche Eingaben jedoch automatisiert verkleinern und dennoch weiterhin den gleichen Bug auslösen, wodurch sich die Analyse vereinfachen und der Aufwand verringern lässt.
Obwohl alle diese Werkzeuge nicht unfehlbar sind, leisten sie dennoch oft einen guten Dienst, um Ressourcen (Entwickler) auf sicherheitskritische Probleme zu fokussieren. Techniken wie „Time Travel Debugging“ [9] können Entwicklern zudem dabei helfen, die grundlegende Ursache von Bugs und Schwachstellen schneller zu analysieren und zu verstehen. Vor allem wenn der Quellcode verfügbar ist, lässt sich die Grundursache im Regelfall relativ schnell durch einen erfahrenen Entwickler entdecken und beheben.
Handelt es sich hingegen um einen Absturz in einer großen, proprietären Applikation, für die man keinen Zugriff auf den Quellcode besitzt (wie es häufig für Sicherheitsforscher oder Penetration-Tester der Fall ist), kann eine detaillierte Analyse leicht Tage oder Wochen dauern, da gegebenenfalls die Anwendung erst (teilweise) reverseengineered und etwaige Anti-Reverse-Engineering-Hindernisse umgangen werden müssen. Zusätzlicher Aufwand entsteht zudem für Sicherheitsforscher und Penetration-Tester meist dadurch, dass im nächsten Schritt ein Proof-of-Concept entwickelt werden soll, der die Ausnutzung einer gefundenen Schwachstelle praktisch demonstriert. Dieser Mehraufwand kommt für reine Entwickler in den meisten Fällen nicht zum Tragen.
Fazit
Fuzzing eignet sich sowohl als ideale Testmethode für Eigenentwicklungen, Abnahmeprüfungen und Akquisitionen als auch zur Evaluierung von Software von Drittanbietern. Idealerweise stellt Fuzzing eine verbindliche Komponente innerhalb eines holistischen SDLC dar und wird mit anderen Testmethoden (Design-Reviews, statische Analysen etc.) kombiniert.
Ist Fuzzing bereits ein fester Bestandteil des internen SDLC, kann man als nächste Stufe ein kontinuierliches Fuzzing der Software anstreben. Eine Kombination mit sogenannten Sanitizern – etwa Address-, Thread- und Memory-Sanitizer (https://github.com/google/sanitizers/wiki/) – ist ebenfalls erstrebenswert, da dies abermals eine deutliche Rendite in den Fuzz-Ergebnissen erzielt. Zudem ist die Dokumentation einer internen Wissensbasis empfehlenswert, um anderen Entwicklern den Einstieg zu erleichtern und um messbare Metriken nachzuhalten (z. B. Code-Abdeckung).
Durch die Vielzahl an öffentlich verfügbaren Fuzzern können selbst Kleinstunternehmen mit wenig Ressourcen relativ einfach Fuzzing in ihre internen Prozesse integrieren – sei es, um die Qualität und Sicherheit der eigenen Produkte zu verbessern oder um proaktiv nach Schwachstellen von innerhalb des eigenen Unternehmens eingesetzter Software zu suchen.
Sicherheitsverantwortliche, die sich mit der Aufgabe konfrontiert sehen, eine externe Beraterfirma im Bereich der Informations- und Applikationssicherheit auszuwählen, können überdies als Teil des Auswahlverfahrens eruieren, ob und in welchem Umfang Fuzzing ein Teil der Test-Strategie ist, bevor man sich für ein bestimmtes Unternehmen entscheidet.
Michael Heinzl (heinzlmichael+kes@gmail.com) arbeitet als Security-Researcher mit den Schwerpunkten Zero-Day-Vulnerability-Research, Fuzzing und Exploit-Entwicklung.
Literatur
[1] National Institute of Standards & Technology (NIST), The Economic Impacts of Inadequate Infrastructure for Software Testing, Final Report, RTI Project Number 7007.011, Mai 2002, www.nist.gov/system/files/documents/director/planning/report02-3.pdf
[2] Ashish Arora Rahul Telang Steven Frank, Estimating Benefits from Investing in Secure Software Development, US-CERT Archive, Februar 2007, www.us-cert.gov/bsi/articles/knowledge/business-case-models/estimatingbenefits-from-investing-in-secure-software-development
[3] Microsoft Security Engineering, What are the Microsoft Security Development Lifecycle (SDL) practices?, Portalseite, www.microsoft.com/en-us/SDL/process/verification.aspx
[4] Michal Zalewski (lcamtuf), American Fuzzy Lop, Security-oriented Fuzzer, http://lcamtuf.coredump.cx/afl/
[5] Osnat Levi, Pin – A Dynamic Binary Instrumentation Tool, intel Developer Zone, Juni 2012 / Juni 2018, https://software.intel.com/en-us/articles/pin-a-dynamic-binaryinstrumentation-tool
[6] Microsoft SDL Team, msecdbg, !exploitable Crash Analyzer – MSEC Debugger Extensions, Mai 2013, https://archive.codeplex.com/?p=msecdbg
[7] Jonathan Foote, !exploitable GDB plugin, Oktober 2018 (latest commit zum Redaktionsschluss), https://github.com/jfoote/exploitable
[8] Google Project Zero, halfempty – a fast, parallel test case minimization tool, Januar 2020 (latest commit zum
Redaktionsschluss), https://github.com/googleprojectzero/halfempty
[9] Microsoft Hardware Dev Center, Time Travel Debugging – Overview, Januar 2020, https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/timetravel-debugging-overview
[10] Peter Sakal, Hartmut Pohl, Entwicklungshelfer und Stresstester, Tool-gestützte Identifizierung von Sicherheitslücken in verschiedenen Stadien des Softwarelebenszyklus, <kes> 2010#2, S. 63
[11] Hartmut Pohl, Fuzzelarbeit, Identifizierung unbekannter Sicherheitslücken und Software-Fehler durch Fuzzing, <kes> 2011#5, S. 66
[12] Michael Kranawetter, Security Development Lifecycle, Wann kommt die sichere Entwicklung von Software in Deutschland wirklich an?, <kes> 2012#4, S. 56
[13] Gürkan Aydin, Anja Wallikewitz, Hartmut Pohl, Toolbox für den Security Development Lifecycle, <kes> 2013#4, S. 20