Über unsMediaKontaktImpressum
Sergej Dechand 12. Mai 2020

Der Siegeszug von modernem Fuzzing

Unternehmen, die Software entwickeln, betreiben einen hohen monetären und zeitlichen Aufwand, um die Sicherheit und Stabilität ihrer Releases zu verbessern. Eine der oft empfohlenen Best Practices in der Software-Entwicklung ist, Bugs möglichst früh im Entwicklungsprozess zu beheben: Je früher Bugs im Software-Entwicklungsprozess gefunden werden, desto einfacher und exponentiell günstiger wird es, diese zu beheben (Rule of Ten).

Erst kürzlich hat sich modernes Fuzzing als die effektivste Testing-Methode in der Security-Community abgezeichnet. Das liegt an der tatsächlichen Ausführung der zu testenden Software, die den Vorteil bringt, dass fast keine False Positives produziert werden. Google nutzt Fuzzing fast durchgehend für automatisierte Tests ihrer Software: 2019 wurden mit Fuzzing mehr Fehler gefunden als mit jeder anderen Technologie. So wurden beispielsweise mit Clusterfuzz über 16.000 Bugs in Chrome und 11.000 Bugs in für Google wichtigen Open-Source-Projekten gefunden [1]. Dies verdeutlicht nicht nur, dass Fuzzing eine sehr effektive Technik für das Finden von Softwarefehlern ist, sondern auch dessen enormen Automatisierungsgrad.

Da aktuelle Open-Source-Lösungen für Sicherheits- und Fuzzing-Experten entwickelt wurden, ist die Integration in Entwicklungsprozesse und Werkzeuge der meisten Unternehmen zum aktuellen Zeitpunkt noch mit viel manueller Arbeit und Expertise verbunden. Dies ist auch der Hauptgrund, warum das moderne Fuzzing trotz seiner Überlegenheit noch nicht in der Industrie angekommen ist. Es werden zur Zeit Fuzzing-Technologien entwickelt, um das Fuzzing einfacher zu gestalten und damit effektiver in industrielle Anwendungsfälle integrieren zu lassen. Lösungen wie Structure-aware und Stateful Fuzzing und die Unterstützung von gängigen Protokollen und Programmiersprachen aus der Industrie (OSS-Lösungen sind aktuell nur für C/C++ und Go ausgereift) helfen, Bugs tief in der Businesslogik komplexer Software zu erkennen. Außerdem wird die Analyse von gefundenen Bugs durch Debugging Interfaces stark vereinfacht.

Wieso wir Fuzzing brauchen...

Die ältesten Testverfahren sind Unit-Tests und Code-Reviews. Da sie mit großem manuellen Aufwand verbunden sind, sind sie besonders zeitaufwändig und werden, vor allem bei Zeitdruck, oft vernachlässigt. Mangels Alternativen wurden diese nichtsdestotrotz genutzt, bis die ersten statischen Codeanalyse-Tools ernstzunehmende Resultate lieferten.
 
Statische Codeanalyse untersucht den Quellcode mit Heuristiken, ohne diesen auszuführen. Ohne diesen Ausführungskontext (mit welchen Eingaben Nutzer bestimmte Codestellen tatsächlich von außen erreichen können) entstehen in der Praxis riesige Mengen an False-Positive-Warnungen. Diese müssen Entwickler dann einer aufwändigen manuellen Nachkontrolle unterziehen, um echte Bugs herauszufiltern.

Parallel zu den oben genannten Verfahren wurden dynamische Analyseverfahren entwickelt, die sich dadurch auszeichnen, nahezu keine False-Positives zu generieren, indem sie die zu testende Applikation tatsächlich ausführen und nur wirklich auftretende Fehler melden. In diesen Verfahren erfolgt ein Angriff auf die Applikation mit zufälligen Eingaben oder potenziellen Angriffsmustern. Stützt man seine Angriffsmuster ausschließlich auf Zufallseingaben, dringt man in diesem Verfahren aber systembedingt nicht bis in die Tiefe des Programmflusses vor. Zudem nimmt die Qualität der Ergebnisse mit zunehmender Komplexität der Eingabestrukturen ab.

Google’s Erfolgsstory

Google kündigte bereits 2016 an, eine eigene Fuzzing-Initiative zu starten, um Softwareentwickler von Open-Source-Softwareprojekten dabei zu unterstützen, Bugs zu entdecken und diese zu beseitigen. Die bisherige Erfolgsbilanz lässt sich durchaus sehen: Alleine im Browser Chrome spürte Fuzzing nach Firmenangaben wohl mehr als 18.000 Bugs auf. Beim Einsatz in weiteren 200 Open-Source-Projekten wurden 16.000 Fehler automatisiert gefunden. Besonders bemerkenswert ist, dass für das Identifizieren der Schwachstellen das Tooling nur wenige Stunden brauchte und die Bugs dann in der Regel innerhalb eines Tages behoben werden konnten. Im letzten Jahr wurden durch Fuzzing die meisten Bugs bei Google aufgedeckt.

Außerhalb von Google und anderen Tech-Giganten (wie z. B. Microsoft) kommt die Technologie trotz der offensichtlichen Vorteile kaum zum Einsatz. Das hängt mit einigen Einschränkungen, immer noch offenen Baustellen und ungeklärten Fragen zusammen.

Intelligentes Fuzzing 101 

Fuzzing hat in den letzten Jahren einige Fortschritte gemacht. Angefangen beim klassischen Fuzzing, welches laufende Software-Anwendungen automatisiert mit zufälligen, unerwarteten und bewusst unvollständigen oder fehlerhaften Eingaben versorgt. Dies ermöglicht Entwicklern, umfassende Rückschlüsse auf die Testabdeckung und gegebenenfalls auf die Stabilität der Software zu ziehen. Gute Erkennungsmechanismen können dabei das Vorhandensein von Fehlern oder sogar Sicherheitslücken aufzeigen.
 
Modernes Fuzzing zeichnet sich zusätzlich dadurch aus, dass es die notwendigen Eingabestrukturen oder Grammatiken der zu testenden Software versteht und Inputs vermeidet, die dem Strukturformat nicht entsprechen. Dies erlaubt es, sich auf valide Inputs zu konzentrieren und dadurch eine effizientere Überprüfung von Sicherheit und Stabilität der Software zu gewährleisten.

Das moderne Fuzzing hingegen kann unter Zuhilfenahme von Instrumentierung kontinuierlich dazulernen, indem während der Ausführung Feedback – wie zum Beispiel der genaue Ausführungspfad oder Zahl- und Stringvergleiche – an die Fuzzing Engine gegeben wird. Dieses Fuzzing dringt wesentlich tiefer in den Quellcode vor (höhere Testabdeckung) und erleichtert es, potenzielle Fehler und Sicherheitslücken finden.

Modernes Fuzzing, oder auch feedback-basiertes Fuzzing [2], erhält durch die Codeinstrumentierung für jeden einzelnen Input detaillierte Informationen über die Abschnitte, die der Input des Fuzzers erreicht. Die Instrumentierung kann man sich als Marker im Code vorstellen, die ein Feedback an die Fuzzing-Engine geben, welches beschreibt, was genau im Programmfluss bei bestimmten Eingaben einhergeht. Der Fuzzing-Prozess startet mit einer initialen Datenbank von Eingabedaten, mutiert sie musterbasiert und zufällig, bis er Eingaben findet, die neue Codepfade erreichen. Diese Eingaben nimmt er dann in die Datenbank auf, sodass daraus wieder neue Mutationen erstellt werden können.

import java.util.Base64;

public class User {
 private int id;
 private int amout;

 // Constructor + getters + setters
 public void handleOrder(String item) {
   if (id == 123456789) {
     if (amout < 1000 && amount % 3 == 0){
       return;
     }

     String enc = Base64.getEncoder().encodeToString(item.getBytes());
     if (enc.equals("QnVnIQ==")) { // Bug!
       nullObj().hashCode();
     }
   }
   // More handling code
 }

 public Object nullObj() {
   return null;
 }
}

Diese Art von Feedbackschleife ermöglicht es dem Fuzzer, die benötigte Struktur der Eingaben automatisch zu "lernen". Hierdurch können eine höhere Codeabdeckung erreicht und letztendlich Bugs und Sicherheitslücken gefunden werden. Modernes Fuzzing ist somit dem klassischen Fuzzing deutlich überlegen.

Moderne Erkennungsmechanismen für Sicherheitslücken

Herkömmliche Fuzzer erkennen größtenteils Fehler im Quellcode, die direkt zum Absturz eines Programms führen können. Um auch subtilere Bugs zu entdecken, benötigen Fuzzer deswegen zusätzliche Erkennungsmechanismen. Eine der bekanntesten Methoden für C/C++-Code sind Google Sanitizer, die Laufzeit-Checks während der Kompilierung ausführen, um so den Zustand des Programms während der Laufzeit zu beobachten. Wenn ein Fehler identifiziert ist, generieren sie hilfreiche Debug-Informationen, die Benutzern helfen, den Bug zu analysieren und schnell zu verstehen.

Ein Schwachpunkt bisheriger Fuzzing-Tools liegt in der Einschränkung, dass sie die Schwachstellensuche lediglich auf C/C++- und Go-Code begrenzen. Die Open-Source-Angebote für Sprachen, wie Java oder JavaScript, stammen häufig aus dem akademischen Umfeld und sind dementsprechend nur bedingt für den Praxiseinsatz geeignet. Open-Source-Erkennungsmechanismen für andere Programmiersprachen oder Web-Frameworks, wie bspw. OWASP Zap, sind zwar programmiersprachenunabhängig und finden die typischen Fehler im Web-Umfeld, haben allerdings den Nachteil, dass sie noch nicht in der Präzision und Genauigkeit arbeiten, wie die instrumentierten Verfahren für C/C++ oder Go. Damit werden viele getriggerte Fehler übersehen.

Kontinuierliches Fuzzing

Während man statische Tests grundsätzlich jederzeit im Softwareentwicklungsprozess ausführen kann, ist Fuzzing grundsätzlich nur einsetzbar, wenn zumindest eine ausführbare Software vorliegt. Idealerweise sollten aber Entwickler den Softwarecode möglichst früh im Entwicklungsprozess und wiederkehrend testen (vgl. Test-Driven Development und Self-Testing Code), da eine frühe Integration in den Entwicklungsprozess (Stichwort "Shift-left" bzw. "DevSecOps") hilft, die aktuellen und zukünftigen Herausforderungen im Bereich Softwareentwicklung zu bewältigen.

Durch die Möglichkeit, Fuzzing bei Continuous-Integration-Prozessen einzusetzen, können Anwender regelmäßiges Testen weitgehend automatisieren. Dabei übernimmt ein CI-Server die Aufgabe, nach jeder Änderung des Codes diesen zu kompilieren und die vorgesehenen Tests inklusive des Fuzz-Testings auszuführen. Erfreulicherweise hemmen die Abläufe nicht das Durchlaufen der Continuous-Integration-Pipeline, sondern erfolgen gänzlich unbemerkt im Hintergrund.

Dass Fuzzing sich im Bereich Test-Driven Development durchsetzen wird, beweist Google anschaulich.

Ausblick im Fuzzing

Um Fuzzing anzuwenden, ist Vorbereitung notwendig. Zunächst müssen Entwickler Fuzz-Targets implementieren. Sie sind das Pendant zu Testfällen in Unit-Tests, bei denen der Fuzzer Eingaben an die zu testenden APIs weiterleitet. Dazu müssen Anwender gegebenenfalls bibliotheken-spezifische Initialisierungen vornehmen sowie eine Datenaufbereitung und -nachbereitung. Der Code muss mit bestimmten Compiler- und Build-Flags kompilieren, damit die richtigen Erkennungsmechanismen wie den Google Sanitizer und Instrumentierungen hinzugefügt werden.

Anschließend müssen Entwickler die generierten Fuzzer mit der richtigen Konfiguration, bei potenziell unzähligen Optionen und Strategien, in der Continuous-Integration-Pipeline starten. Es gibt noch weitere Herausforderungen, die im Fuzzing gelöst werden müssen, wie z. B. Structure-aware Fuzzing, Fuzzing von Netzwerken und Web-APIs, sowie die Unterstützung von weiteren Programmiersprachen (Erkennungsmechanismen und Feedback durch Instrumentierung), welche in der Security-Research-Community oder durch junge Start-ups extensiv in Angriff genommen werden.

Fazit

Fuzzing ist auf dem Weg, der neue Standard im Software-Testing zu werden. Denn mit den richtigen Werkzeugen kostet Fuzz-Testing weit weniger Zeit als manuelles Testen und findet Fehler effektiver als der reine Einsatz von bspw. statischer Codeanalyse. Das frühe Auffinden von Softwarefehlern im Entwicklungsprozess spart zudem sowohl Zeit als auch Kosten.
 
Je früher Schwachstellen identifiziert werden, desto einfacher und kosteneffektiver stellt sich die Behebung dar. Dementsprechend sollte jedes Unternehmen die Chance nutzen, seine Software durch Fuzzing kontinuierlich zu verbessern. LibFuzzer und AFL sind die prominentesten Open-Source-Vertreter für modernes Fuzzing, erfordern jedoch einiges an Expertise und Arbeit diese in bestehende Projekte einzubinden [3]. Einige wenige kommerzielle Lösungen dagegen bieten Erweiterungen an, um den Einsatz von Fuzzing zu erleichtern und auf weitere Use Cases auszuweiten (wie z. B. eine Integration in die CI/CD-Pipeline).
 
Es sollte in Zukunft keine Software mehr released werden, die Fehler enthält, die man mit modernem Fuzzing hätte finden können!

Autor

Sergej Dechand

Sergej Dechand kann auf langjährige Forschungserfahrung im Bereich Usable Security am Fraunhofer FKIE und der Universität Bonn zurückblicken.
>> Weiterlesen
Kommentare (0)

Neuen Kommentar schreiben