Automatisiertes Fuzz Testing macht es Entwicklern besonders leicht, sichere und robuste Software zu entwickeln. Doch was steckt dahinter?
Je früher im Entwicklungsprozess man eine Schwachstelle findet, desto einfacher lässt sie sich beheben. Immer mehr Entwickler setzen auf automatisiertes Fuzz Testing, um Probleme und Gefahren zu erkennen, bevor die Software in den produktiven Einsatz geht. Fuzz Testing beruht darauf, die zu testende Applikation mit zufällig generierten Testeingaben auszuführen. Einige davon wird die Anwendung verarbeiten, andere abweisen. In manchen Fällen führen die Eingaben jedoch zu für den Entwickler aufschlussreichen Fehlern.
Mit kaum einem anderen Testverfahren lassen sich so viele Bugs aufspüren wie mit Fuzzing. Tech-Riesen wie Google haben das Potenzial von Feedback-basiertem Fuzzing bereits früh erkannt. Allein im Chrome Browser fand Google schon über 29 000 Bugs mit Fuzzing [1] und stöberte weitere 30 000 in anderen Open-Source-Anwendungen auf [2]. Microsoft identifizierte mit Fuzzing 1800 Schwachstellen in Microsoft Office [3], internationale Forscher förderten damit 1000 Bugs im Linux-Kernel zutage [4].
Inzwischen gilt Fuzzing als massentauglich, immer mehr Entwicklerteams nehmen es in ihr Testing-Repertoire auf. Im Vergleich mit anderen Sicherheitsmaßnahmen wie der statischen Codeanalyse erzielt Fuzzing mit wenig Aufwand deutlich zuverlässigere Testergebnisse, ohne lästige False Positives. Zudem setzen immer mehr Industriestandards Fuzzing voraus. In einigen sicherheitsrelevanten Bereichen sind Softwarehersteller schon heute dazu verpflichtet, automatisierte Software- und Sicherheitstests vorzunehmen.
Viele Industriestandards und ISO-Normen empfehlen daher ebenfalls, Fuzz Testing in den Entwicklungsprozess zu integrieren, insbesondere dort, wo die Qualität und Sicherheit von Software über Leib und Leben entscheidet. Ein gutes Beispiel dafür bieten ISO/SAE 21434 und UNECE WP.29, die sich mit der Sicherheit von Software in Automobilen befassen. Doch auch in weiteren Branchen wie der Luftfahrt, dem Energiesektor, dem Finanzsektor und dem Gesundheitswesen ist Fuzzing als modernes Testverfahren schon weitverbreitet (siehe Tabelle “Fuzzing in Normen und Standards”).
|
Standard |
Bereich |
|---|---|
|
Standards, die Fuzzing empfehlen |
|
|
ISO/SAE 21434 |
Straßenfahrzeuge: Cybersecurity |
|
UNECE WP.29 |
Harmonisierung der Regelungen für Kraftfahrzeuge |
|
ISO 26262 |
Straßenfahrzeuge: funktionale Sicherheit |
|
ISA/IEC 62443-4-1 |
sichere Anforderungen an den Lebenszyklus der Produktentwicklung |
|
UL2900-1 und UL2900-2-1 |
Gesundheits- und Wellness-Systeme: Software-Cybersicherheit für vernetzbare Produkte |
|
Standards, die von Fuzzing profitieren |
|
|
ISO/IEC/IEEE 29119 |
Software- und Systemtechnik: Softwaretests |
|
ISO/IEC 12207 |
System- und Softwaretechnik: Software-Lebenszyklus-Prozesse |
|
ISO 27001 |
Informationstechnologie: Sicherheitstechniken, Informationssicherheitsmanagementsysteme |
|
ISO 22301 |
Sicherheit und Resilienz: Systeme für Business Continuity Management |
|
BSI IT-Grundschutz |
auf Grundlage von ISO 27001 |
|
Allgemeine Empfehlungen für den Einsatz von Fuzzing gibt beispielsweise der Bitkom-Leitfaden “Zur Sicherheit von softwarebasierten Produkten”. |
|
Feedback als Basis
Das Grundprinzip von Fuzz Testing besteht primär darin, Software durch automatisch generierte Testeingaben zum Absturz zu bringen. Dabei führt ein sogenannten Fuzzer die zu testende Software aus und füttert sie mit intelligent generierten Eingaben (Abbildung 1). Dabei deckt das Tool auch Eingaben ab, an die Menschen nicht denken, sodass sie bei manuellen Tests in der Regel unter den Tisch fallen. In diese Kategorie gehören zum Beispiel lange und seltene Zeichenkombinationen für Logins, JSON- oder XML-Dokumente oder andere mögliche Eingaben.
Durch das Instrumentieren des Quellcodes sammeln moderne Fuzzer Informationen über die Struktur der Anwendung. Sie analysieren und markieren dann den Pfad, den eine Eingabe durch das Programm nimmt. Im späteren Verlauf kann der Fuzzer dieses Wissen nutzen, um seine Testeingaben entsprechend anzupassen und vor allem solche Eingaben zu testen, die mit großer Wahrscheinlichkeit zu Fehlern und Abstürzen der Anwendung führen.
Zudem erhalten moderne Fuzzer Feedback dazu, welche Funktionen die jeweiligen Eingaben erreichen. Mithilfe dieser Information und genetischer Mutationsalgorithmen entwickeln die Tools ihre Eingaben fortlaufend weiter. Findet der Fuzzer zum Beispiel eine Eingabe, die zum Absturz der Anwendung führt, testet er im nächsten Durchlauf Tausende ähnliche Eingaben, die mit einer gewissen Wahrscheinlichkeit ebenfalls zum Absturz führen. Das Werkzeug ist also in der Lage, Testeingaben intelligent anzupassen, um mit jedem Testdurchlauf noch tiefer in den Quellcode einzudringen.
Fuzz Testing findet Bugs, die andere Testverfahren nicht aufdecken. Nur wenige Wochen, nachdem dort automatisiertes Fuzz Testing integriert wurde, konnten die Entwickler im Quellcode des Ethereum-Netzwerks eine schwerwiegende DoS-Sicherheitslücke (CVE-2020-28362) beheben, die zu einem Shutdown des gesamten Ethereum-Netzwerks hätte führen können. Das betroffene, eigentlich speichersichere Golang-Modul hatte zuvor bereits umfangreiche Sicherheitstest durchlaufen, doch erst das Fuzz Testing förderte die Schwachstelle zutage.
Gute Gründe für Fuzzing
In den letzten Jahren hat Fuzzing Einzug in die verschiedensten Bereiche der Sicherheitsbranche gehalten. Es gibt gute Gründe, warum Fuzzing gerade unter Entwicklern großen Anklang findet.
Vor allem bietet es messbare Code Coverage. Moderne Fuzzing-Plattformen ermöglichen es Entwicklern, die Codeabdeckung von Sicherheitstests genau zu messen. Beim früher eingesetzten dynamischen Software-Testing ließ sich fast unmöglich erkennen, welche Codestellen die Tests erreicht hatten. Es konnte passieren, dass die Tests nur einen kleinen Teil des Codes abdeckten und die Tests somit nahezu nutzlos waren, ohne dass die Entwickler dies bemerkten. Modernes Fuzzing misst mittels Quellcodeinstrumentierung genau, welcher Code getestet wird.
Das geht Hand in Hand mit einer intelligenten Testfallgenerierung. Durch die Instrumentierung gelingt es modernen Fuzzern, Testfälle zu generieren, die eine deutlich höhere Code Coverage erzielen als herkömmliche dynamische Testverfahren. Mit den dort zufällig oder mithilfe potenzieller Angriffsmuster erzeugten Inputs war es schwierig, eine hohe Codeabdeckung zu erreichen und in die Tiefen eines Systems vorzudringen. Moderne Fuzzer nutzen das Feedback, das sie vom instrumentierten Code bekommen, um mit genetischen Algorithmen intelligente Testfälle zu generieren. Deren vollautomatischer Input dringt tief in Programme ein und erzielt mit wenig Aufwand eine hohe Code Coverage.
Zu den großen Stärken von Fuzzing zählt das Ausschließen von False Positives. Mit anderen Testverfahren wie etwa der statischen Codeanalyse lässt sich zwar ebenfalls eine sehr hohe Codeabdeckung erzielen, allerdings entstehen dabei viele falsch positive Testergebnisse. Das liegt an den Approximationen und Heuristiken, die statische Codeanalyse nutzt, um potenzielle Fehler zu finden. Da sie dabei den Code nicht ausführt, lässt sich nicht garantieren, dass es sich bei Treffern um echte Bugs handelt. Zudem kann man kaum sagen, ob und wie sich der Fehler hervorrufen lässt. Solche False Positives von den tatsächlichen Bugs zu unterscheiden, erfordert einen hohen manuellen Aufwand. Das Feedback-basierte Fuzzing dagegen liefert für jede Fehlermeldung tatsächlich eine Eingabe als Nachweis und Ursache für den Absturz der Anwendung mit. Es handelt sich also fast immer um echte Fehler, ohne falsch positive Testergebnisse.
Die nachweisbaren Inputs ermöglichen in der Folge auch ein einfaches Debugging. Mit Unit-Tests oder statischer Code-Analyse dagegen gerät das Debugging schnell zur zeitintensiven und nervenaufreibenden Tätigkeit. Die Entwickler müssen Softwarefehler dabei manuell lokalisieren und nachvollziehen, bevor sie die Schwachstelle beheben können. Das erfordert reichlich Expertise, um die Wurzel des Problems zu identifizieren. Fuzzer dagegen liefern zu jeder Warnung den passenden Input. Entwickler können diesen Input in den Debugger laden, das Programm durchlaufen und den Fehler direkt nachvollziehen. Bis hierher
Modernes Fuzzing erfordert weniger manuellen Aufwand. Das war jedoch nicht immer so: Bis vor nicht allzu langer Zeit war Fuzz Testing eine Technologie, die sich nicht ohne das Fachwissen spezialisierter Sicherheitsexperten nutzen ließ. Open-Source-Fuzzer wie AFL oder Libfuzzer spürten schon sehr früh Sicherheitslücken verlässlich auf, benötigten dazu allerdings lange ein enormes Maß an manueller Konfiguration, Expertenkenntnissen und Detailwissen über die zu testende Anwendung. Die Dokumentation und Anwendbarkeit moderner Fuzzer ist inzwischen jedoch so weit fortgeschritten, dass Entwickler auch ohne spezialisiertes Fuzzing-Wissen auskommen. Aufgrund des hohen Automatisierungsgrads lassen sich mittlerweile Fuzz-Tests mit wenigen Mausklicks aufsetzen. Daneben gibt es auch schon professionelle Lösungen für Fuzzing-as-a-Service.
Entwickler können Fuzzing in ihre Continuous-Integration/Continuous-Delivery-Pipeline (CI/CD) integrieren, um kontinuierliche Sicherheitstests vorzunehmen. Moderne Fuzzing-Lösungen bieten dafür Integrationen beispielsweise für Jenkins, Github oder Docker an. Auf diese Weise durchläuft jeder Commit automatisierte Sicherheitstests, bevor er mit der Codebasis zusammengeführt wird. Dadurch entsteht ein zyklischer Testing-Prozess, bei dem es Sicherheitsfehler deutlich seltener unbemerkt in die späten Phasen des Entwicklungsprozesses schaffen. Ohne solche kontinuierlichen Testzyklen machen sich Sicherheitslücken häufig erst Monate später bemerkbar, wenn sich kein Entwickler mehr so richtig erinnert, wie er den Code seinerzeit gebaut hat. Sich wieder in den alten Code einzuarbeiten kostet Zeit und Nerven. Viel leichter lassen sich Bugs unmittelbar in der Entwicklung beheben. Fuzz Tests sind dabei eine große Hilfe.
Welche Bugs findet Fuzzing?
Viele herkömmliche Testverfahren nehmen positive Tests vor, die nur valide und relevante Eingaben tätigen. Modernes Fuzzing setzt dagegen vor allem auf negative Tests und füttert die zu prüfende Software mit unerwarteten oder invaliden Inputs, indem der Fuzzer beispielsweise ein numerisches Eingabefeld mit Buchstaben füllt. Die Technik ist darum besonders wichtig für Anwendungen, die Benutzereingaben oder externe Daten verarbeiten.
Aktuell lässt sich Fuzzing in verschiedenen Programmiersprachen einsetzen, darunter C/C++, Java, Kotlin, Python und Go. In C/C++ wurden mithilfe von Feedback-basiertem Fuzzing bereits eine Vielzahl von Fehlerklassen [5] aufgedeckt, inklusive Data Validation Issues und Concurrency Issues.
Fuzzing ist extrem effektiv darin, Memory Corruptions in C/C++ zu finden [6], kann aber auch in speichersicheren Sprachen wie Java eine Vielzahl von Sicherheitslücken aufspüren. Bei diesen Sprachen treten allerdings andere, vor allem für die Sicherheit von Webanwendungen und APIs relevante Bug-Klassen in den Vordergrund. Beispiele dafür sind Logic Issues, Audit/Logging Errors, Cookie Issues und die gängigen OWASP-Top-10-Bugs.
|
C/C+ |
Memory Buffer Errors, Data Validation Issues, Pointer Issues, Numeric Errors, Concurrency Issues, Bad Coding Practices |
|
Java (h3) |
OWASP Bugs, Data Validation Issues, Denial of Services, Infinite Loops, Uncaught Exceptions, Cookie Issues, Injection Vulnerabilities, Broken Access Control, Vulnerable and Outdated Components |
An Fuzzing führt kein Weg vorbei
Modernes Fuzz Testing zeigt sich herkömmlichen statischen und dynamischen Testing-Methoden in vielen Bereichen deutlich überlegen. Kontinuierliche Fuzz-Tests ermöglichen einen höheren Automatisierungsgrad, finden mehr Bugs und generieren dabei weniger Fehlalarme. Pentests, egal ob manuell oder automatisiert, sind zwar sehr effektiv, finden Sicherheitslücken allerdings erst gegen Ende des Entwicklungsprozesses. Das erhöht den Aufwand, verlangsamt die Entwicklung und bringt Programmierer und Tester in Konflikt.
Das Ziel von Sicherheitstests sollte es jedoch sein, als Team mit möglichst wenig Aufwand sichere Software zu schaffen. Dazu eignet sich eine Kombination von Fuzz Testing und herkömmlichen Testing-Methoden am besten. Zum Beispiel kann man Feedback-basiertes Fuzzing gemeinsam mit einer Pentesting-Suite wie OWASP ZAP oder Burp anwenden. In kommerziellen Lösungen sind diese häufig integriert und lassen sich durch Fuzzing-Tools automatisch erweitern. Das vom Buzzer gesammelte Feedback dient dann dazu, Testeingaben systematisch zu steuern und Rückschlüsse über deren Erfolg zu ziehen.
Fuzzing ermöglicht es, sichere und robuste Software schnell zu entwickeln und mit jedem Commit auf Sicherheitslücken zu testen. Wird Fuzzing zukünftig einen integralen Teil jedes Entwicklungsprozesses bilden? Langfristig sollten Automatisierung und Usability dazu führen, dass Entwickler auch ohne Sicherheitsexperten die Sicherheit ihrer Software gewährleisten können. SaaS-Fuzzing unterstützt das durch automatisiert generierte, effektive Sicherheitstest.
Fuzz Testing hat sich bereits in vielen Branchen etabliert. Insbesondere die neuen SaaS-Lösungen dürften dazu führen, dass sich die Technologie auf weitere Branchen und Programmiersprachen ausweitet. Es wird jedoch immer Bereiche geben, die auf lokale Lösungen setzen. Auch Angreifer nutzen Fuzzing, um sich unbefugten Zugriff zu Softwaresystem zu verschaffen. Zukünftig sollte man daher jede Applikation (mindestens) einem Fuzz-Test unterziehen, um den Kriminellen einen Schritt voraus zu bleiben.
Fuzzing in der Praxis
Im Folgenden demonstriert ein konkreter Anwendungsfall, wie Fuzzing funktioniert. Als Beispiel dient das Auffinden einer schwerwiegenden Sicherheitslücke (CVE-2021-23899) in JSON Sanitizer durch das quelloffene JVM-Fuzzing-Tool Jazzer.
Bei JSON Sanitizer handelt es sich um ein Open-Source-Projekt, das bereits mit den verschiedensten Methoden getestet wurde. Die beliebte, von Google entwickelte Java-Bibliothek wird von OWASP gepflegt und dient dazu, beliebige Bytes und Strings in gültige JSON-Formate zu konvertieren. Mithilfe von JSON Sanitizer stellen Entwickler sicher, dass Ausgaben keine Substrings enthalten, die Skripte durcheinanderbringen oder sogar XSS-Bugs (Cross-Site-Scripting) verursachen könnten. Bei Sicherheitslücken in der Bibliothek drohen weitreichende Folgen, da sie in vielen Anwendungen zum Einsatz kommt. Inputs, die JSON Sanitizer durchlaufen haben, sollten sich sicher in den Skriptblock einbetten lassen, wie in der orangefarbenen Box in Abbildung 2.
In diesem Beispiel diente ein Property-basierter Fuzzing-Ansatz zum Test von JSON Sanitizer. Dabei wird ein String durch JSON Sanitizer geschickt und sichergestellt, dass das konvertierte, sichere JSON den spezifizierten Substring nicht enthält. Andernfalls wäre die Anwendung anfällig für XSS-Bugs.
Bei einem Fuzz Target handelt es sich um einen Eingangspunkt einer zu testenden Anwendung, der während eines Testprozesses die Testeingaben entgegennimmt – etwa die Main-Methode eines Programms oder eine aufzurufende API-Funktion. Mithilfe des Fuzz Targets aus Abbildung 3 wurde eine ernst zu nehmende Schwachstelle (CVE-2021-23899) gefunden: Durch das Escapen des ersten Buchstabens eines HTML-Tags in der JSON-Zeichenfolge kann dieser den Sanitizer durchlaufen und das ursprüngliche Tag ausgeben. Es liegt in der Natur von HTML, dass dieses abschließende Tag den Skriptblock schließt, selbst wenn es in einem Javascript-String literal enthalten ist. Beim anschließenden Öffnen eines neuen Skriptblocks gibt es eine Warnung, und man erhält den XSS-Exploit.
Da es sich bei JSON Sanitizer um ein beliebtes Open-Source-Tool handelt, wurde er bereits mit vielen verschiedenen Methoden getestet. Dennoch wurde der beschriebene XSS-Bug, obwohl er sich zugegebenermaßen ziemlich leicht triggern lässt, erst durch Fuzzing entdeckt. Lange Rede kurzer Sinn: Erst Fuzz Testing hat es ermöglicht, mit sehr geringem Aufwand eine schwerwiegende Sicherheitslücke in einem beliebten und weitverbreiteten Java-Projekt aufzudecken. (jcb/jlu)
Der Autor
Sergej Dechand blickt auf langjährige Forschungserfahrung im Bereich Usable Security am Fraunhofer FKIE und der Universität Bonn zurück. Als Mitgründer und CEO von Code Intelligence verfolgt er gemeinsam mit seinem Team das Ziel, die Sicherheitsbranche zu revolutionieren. Seiner Ansicht nach muss gute Security-Testing-Software die Developer in den Vordergrund stellen.
Infos
- Clusterfuzz: https://github.com/google/clusterfuzz
- OSS-Fuzz: https://google.github.io/oss-fuzz
- Office-Bugs: https://www.computerworld.com/article/2516563/microsoft-runs-fuzzing-botnet–finds-1-800-office-bugs.html
- “Efficient Sanitizer Metadata Design for Fuzzing”: https://www.usenix.org/system/files/atc20-jeon.pdf
- Code-Intelligence-Blog: https://blog.code-intelligence.com/what-bugs-can-you-find-with-fuzzing
- Guardicore-Blog: https://www.guardicore.com/labs/critical-vulnerability-in-hyper-v-allowed-attackers-to-exploit-azure









