Schwachstellensuche mit Fuzzing

Fuzzing hat die automatisierte Suche nach Programmierfehlern revolutioniert: Mit kaputten Daten lassen sich sogar ohne Zugriff auf den Quellcode Programmabstürze provozieren und Fehler erkennen.

In Pocket speichern vorlesen Druckansicht
Lesezeit: 15 Min.
Von
  • Christiane Rütten
Inhaltsverzeichnis

Seit einigen Jahren ist ein kontinuierlicher Anstieg der Zahl aufgedeckter Sicherheitslücken zu beobachten. Dafür ist zu einem großen Teil die Entwicklung automatisierter Verfahren zum Aufspüren von Programmierfehlern in Software verantwortlich. Eines der spannendsten Verfahren ist das vergleichsweise junge Fuzzing, für das man prinzipiell weder Quellcode noch die Binärprogramme selbst benötigt: Man überflutet ein laufendes Programm so lange mit Zufallsdaten, bis es abstürzt, was ein offensichtlicher Hinweis auf einen Programmierfehler ist.

Schon bei einem einfachen Absturz kann man in der Regel von einer Sicherheitslücke sprechen. Mit der Kenntnis, welche Eingabedaten zu Abstürzen führen, kann ein Angreifer gezielt Programme oder Internet-Dienste lahmlegen (Denial of Service). Die weitere Analyse der Absturzursache liefert Anhaltspunkte, ob und wie sich der Fehler beispielsweise sogar zum Einschmuggeln von Schadcode nutzen lassen könnte.

Einen der medienwirksamsten Erfolge feierte das Fuzzing 2005, als Michael Zalewski damit die äußerst kritische IFRAME-Lücke im Internet-Explorer aufspürte. Im Juli 2006, dem "Monat des Browser-Bugs", präsentierte Initiator und Sicherheitsexperte H. D. Moore jeden Tag eine neue Schwachstelle in einem der großen Internet-Browser Mozilla, Safari, Opera und Internet Explorer. Allein im Microsoft-Browser konnte er durch Fuzzing 25 sicherheitsrelevante Programmierfehler aufdecken - und das, obwohl er wie Zalewski keinen Zugriff auf den Quellcode hatte.

Ein gängiges Theorem besagt: Jedes Programm enthält Fehler, wenn es eine gewisse Komplexität überschreitet. Menschen mit Programmiererfahrung können das leicht nachvollziehen. Bei der Suche nach Programmierfehlern sind drei Fälle zu unterscheiden: Man hat Zugriff auf den Quellcode, man hat nur Zugriff auf das Binärprogramm oder keins von beidem, wie es etwa bei Internet-Servern der Fall sein kann.

Die Verfügbarkeit der Quellen erleichtert jede Form der Fehlersuche enorm. Die klassische Form der Fehlersuche, das manuelle Durchsuchen des Quelltextes, stößt schnell an ihre Grenzen. Schon mittelgroße Programmierprojekte kommen auf enorme Mengen Quelltext: Das Verschlüsselungsprogramm GPG etwa besteht aus rund 100 000 Zeilen C-Code. Der Linux-Kernel mit seinen hunderten Hardwaretreibern bringt es in Version 2.6.16 sogar auf fast fünf Millionen.

Es gibt bereits diverse Tools für alle gängigen Programmiersprachen, die Quellcode nach potenziellen Fehlern durchforsten können [1]. Sie liefern jedoch auch nur Anhaltspunkte für eine weitere manuelle Überprüfung und erwischen in der Regel nur einen Bruchteil der Programmierfehler.

Liegt wie bei vielen kommerziellen Produkten lediglich die ausführbare Binärdatei vor, ermöglichen immer noch Disassembler und Debugger eine - wenn auch ungleich mühsamere - Inspektion des Programmes. Doch selbst dies ist bei vielen Diensten auf Internet-Servern nicht mehr möglich. Ihnen kann man lediglich Eingabedaten über eine Netzwerkverbindung schicken und auf Antwort warten.

Fuzzing eignet sich für alle drei Fälle. Der Zugriff auf Quellcode oder Binärprogramm erleichtert jedoch seine Vorbereitung und Durchführung.

Das erste Tool und Namensgeber für die Fuzzing-Methode, fuzz [2], wurde bereits 1990 zum Testen von Unix-Kommandos entwickelt. Es generiert lediglich zufällige Zeichenketten bis zu einer vorgegebenen Maximallänge, die ein weiteres Programm an die Testapplikation weiterleitet. Schon mit dieser einfachen Herangehensweise war es den Forschern Barton P. Miller, Lars Fredriksen und Bryan So möglich, rund ein Drittel der getesteten Utilities zum Absturz zu bringen, darunter vi, emacs und telnet.

Das Bombardieren mit willkürlichen Zufallswerten hat jedoch einen Haken: Die meisten Programme erwarten Eingangsdaten in einem festgelegten Format oder Protokoll. Beispielsweise beginnt ein JPEG-Bild immer mit den Bytes "FF D8" und Netzwerkpakete enthalten an vorgegebenen Stellen Längenangaben und Prüfsummen. In der Regel brechen Programme jede weitere Verarbeitung ab, wenn Eingabedaten nicht die erwartete Struktur aufweisen oder offensichtliche Fehler enthalten.

Häufig sind Formate und Protokolle auch ineinander verschachtelt, so etwa DivX- und MP3-Daten innerhalb eines AVI-Containers oder HTTP-Daten in TCP-Paketen. Fehlerhafte Header in unteren Format- oder Protokollschichten verhindern ebenfalls, dass die eigentlichen Nutzdaten zu den Verarbeitungsroutinen gelangen. Um beispielsweise den MP3-Dekoder eines Videoplayers zu testen, müssen die verwendeten Eingabedaten einen korrekten AVI-Header enthalten, da das Programm sonst frühzeitig mit einer Fehlermeldung abbrechen würde.

Mit reinen Zufallsdaten verschwendet man also viel Zeit mit dem Verfüttern von Daten, die zu frühzeitigen Abbrüchen führen. Dadurch erreichen sie nur einen Bruchteil der zu untersuchenden Funktionen und können nur wenige interne Programmzustände hervorrufen.

Sämtliche möglichen Eingabedaten abzuklappern ist allein angesichts ihrer enormen Vielzahl nicht sinnvoll. Dies gleicht einem Schiffeversenken im Blindflug mit dem gesamten Pazifik als Spielfeld. Setzt man beispielsweise eine durchschnittliche Bearbeitungszeit von einer Millisekunde pro Datenpaket voraus, könnte man in 1000 Jahren gerade einmal einen guten Teil aller bis zu sechs Byte großen Pakete durchprobieren.

Eine wesentliche Strategie beim Fuzzing ist daher, die Menge der zu testenden Eingabedaten - auch Eingaberaum genannt - möglichst weit zu reduzieren. Je kleiner der Eingaberaum ist, desto größer ist die Wahrscheinlichkeit, in akzeptabler Zeit einen Programmabsturz herbeizuführen.

Effizientes Fuzzing setzt demnach eine gründliche Analyse des verwendeten Datenformats beziehungsweise Protokolls voraus. Der Tester bekommt dadurch häufig auch eine Vorstellung, wo Programmfehler zu erwarten sind und welche Felder in den Eingabedaten und Protokollen für das Fuzzing interessant sind. An die dazu nötigen Informationen gelangt man beispielsweise durch Dokumentation, Quellcode, Disassemblierung oder Protokollieren des Netzwerkverkehrs. Dies ist auch der Grund für die Vielzahl der hoch spezialisierten Fuzzing-Tools wie beispielsweise CSS-Die für die Stylesheet-Unterstützung von Browsern, AXman für das ActiveX-API des IE oder IRCfuzz für Chatprogramme (siehe Tabelle). Der Artikel "Die Axt im Walde" auf heise Security befasst sich näher mit ausgewählten Tools und ihrem praktischen Einsatz.

Im Raum der möglichen Eingabedaten genau einen der wenigen Fehlerzustände zu treffen, ist bei reinen Blindschüssen eher unwahrscheinlich und damit sehr langwierig.

Diese Überlegungen führen direkt zu einer weiteren Extremvariante des Fuzzing: der Datenmutation. Grundidee dabei ist, bekannte gültige Eingabedaten an zufälligen Stellen zu verändern. Dadurch ist sichergestellt, dass die mutierten Eingabedaten in den meisten Arbeitsschritten des Programms Fehler provozieren können. Diese Methode funktioniert in der Regel sogar ohne eingehende Beschäftigung mit dem Datenformat.

Die Veränderung genügend vieler gültiger Eingabedaten kann schnell zum Erfolg führen. Fehler durch völlig mutierte Daten sind mit diesem Verfahren jedoch kaum zu entdecken.

Allerdings ist auch bei der Datenmutation ein gezieltes Vorgehen sinnvoll. Die wahllose Mutation von Bilddaten etwa produziert fast ausschließlich Pixelfehler, die nur selten zum Programmabsturz führen. Für Mutationen, die die Länge der Daten verändern, ist wiederum die Analyse des Datenformates etwa zur Korrektur von Längenangaben nötig. Im Allgemeinen ist diese Methode jedoch nahezu blind für Fehler, die erst durch sehr stark mutierte Daten ans Licht kommen. Praxisnahe Ansätze verfolgen daher einen Mittelweg zwischen wahllosem Fuzzing und reiner Datenmutation.

In einem wegweisenden Paper beschreibt der Sicherheitsexperte Dave Aitel eine universelle Analysemethode für Netzwerkprotokolle: die blockbasierte Protokollanalyse [4]. Sie macht sich zunutze, dass die meisten gängigen Web-Protokolle letztlich als Folge von Blöcken aus Längenangaben und Datenfeldern übertragen werden, selbst wenn sie wie beispielsweise CSS in HTML in HTTP ineinander verschachtelt sind.

Die Grundidee der blockbasierten Protokollanalyse: Auch ineinander verschachtelte Datenstrukturen sind als lineare Folge von Bytes darstellbar.

Man schreibt dazu ein Programm, das bei jedem Durchlauf mit Zufall angereicherte Pakete im Protokollformat ausspuckt. Die einzelnen Datenfelder des Protokolls werden durch Funktionsaufrufe ersetzt, die passende Inhalte wie Integer-Zahlen, Zeichenketten oder Konstanten ausgeben. Die Strukturierung der Datenfunktionen durch Aufrufe von Blockfunktionen macht es beispielsweise möglich, Längenangaben innerhalb der Blöcke mit Hilfe eindeutiger Bezeichner automatisch anzupassen.

Bei der blockbasierten Protokollanalyse gibt es zwei gegensätzliche Herangehensweisen. Die arbeitsintensivere, aber erfolgreichere Methode ist die vollständige Nachbildung eines gut analysierten Protokolls durch die passenden Funktionsaufrufe. Dadurch erreicht man Fuzzing mit einem sehr sorgfältig ausgewählten Eingaberaum. Die wesentlich schneller umsetzbare Variante ist, sich aus einem mitgeschnittenen Datenpaket lediglich einzelne vielversprechende Blöcke und Datenfelder herauszupicken und durch die nötigen Funktionsaufrufe zu ersetzen. Der Rest des Paketes wird einfach als konstanter Wert übernommen. Dies entspricht der Datenmutation in ausgewählten Feldern eines Paketes.

In der Praxis beginnt ein Tester in der Regel mit der zweiten Methode, ohne sich intensiv mit den Protokollspezifika auseinandergesetzt zu haben. Als besonders leicht identifizierbare und ergiebige Felder haben sich beispielsweise Längenangaben erwiesen, deren Fuzzing mit vergleichsweise hoher Wahrscheinlichkeit zu Pufferüberläufen führt. Durch zusätzliche Analysen von Quellcode und Binärprogramm kann sich der Tester Feld für Feld in Richtung einer vollständigen Funktionaldarstellung des Protokolls vorarbeiten, wie sie sich bei der ersten Methode ergeben hätte. Kritische Programmfehler finden sich aber meist schon lange vorher.

Auf dieser Grundlage hat Aitel das Fuzzing-Framework Spike in der Programmiersprache C entwickelt. Es stellt Funktionen für verschiedene Datentypen und Blockstrukturen sowie diverse Netzwerkfunktionen bereit. Für das Fuzzing der Datenfelder zieht es sowohl Zufallswerte als auch bestimmte Zeichenketten und Zahlenwerte heran, die typische Abstürze etwa durch unzureichende Filterung oder Integer-Überläufe provozieren. Diverse Protokollnachbauten liegen dem Framework ebenfalls bei, etwa für MS-RPC, CIFS, IMAP und PPTP.

Die blockbasierte Protokollanalyse ist jedoch unabhängig von Spike. Sie liefert auch eine Grundlage für vollständig selbst entwickelte Fuzzing-Tools in anderen Programmiersprachen. Ihre Prinzipien lassen sich auf andere Bereiche als Netzwerkprotokolle verallgemeinern, beispielsweise für das Fuzzing von Dateiformaten, und auch andere Fuzzing-Frameworks machen sich die Methode zunutze.

Der Sicherheitsexperte Dan Kaminski beschrieb in seinem Vortrag "Black Ops 2006" auf dem Chaos Communications Congress in Berlin [5] einen Ansatz für ein intelligentes Fuzzing, das ohne aufwändige blockbasierte Protokollanalyse auskommt und trotzdem die Datenstruktur berücksichtigt. Die Grundidee ist, die Struktur der zu fuzzenden Daten mit Hilfe eines geeigneten Algorithmus zu erkennen, um diese beim Fuzzing vorwiegend intakt zu lassen.

Kaminski verwendete den Kompressionsalgorithmus Sequitur [6], der Eingabedaten in eine Grammatik (die Struktur) und ein Wörterbuch (die Nutzdaten) zerlegt. Sein bislang noch unausgereifter Proof-of-Concept-Fuzzer CFG9000 (Context Free Grammar Fuzzer) [7] ist auf dieser Basis in der Lage, beispielsweise ein HTML-Dokument derart durcheinanderzuwürfeln, dass Browser es trotz seiner offensichtlichen Defekte meist noch irgendwie darstellen. Ein Verständnis des HTML-Protokolls ist dafür nicht nötig – der Fuzzer arbeitet mit beliebigen Daten.

Das Context Free Grammar Fuzzing liefert auch ohne HTML-Blockanalyse eine Seite, die irgendwie immer noch nach heise.de aussieht.

Ein wesentliches Problem beim Fuzzing ist, zuverlässig das Auftreten eines Programmfehlers festzustellen. Läuft das Programm auf einem Rechner, zu dem der Tester direkten Zugang hat, gibt es vielfältige Möglichkeiten. Eine gängige Methode ist das Anhängen eines Debuggers wie IDA Pro oder Ollydbg unter Windows beziehungsweise gdb unter Linux an den laufenden Prozess. Kommt es während des Fuzzing zu einer Zugriffsverletzung, fängt sie der Debugger sofort für weitere Analysen ab.

Außerdem geben eine sprunghaft ansteigende CPU- oder Speicherauslastung Hinweise darauf, dass der Fuzzer einen Programmierfehler erwischt hat. Dazu lassen sich die gängigen Betriebssystemtools oder spezielle Testprogramme wie Valgrind für Speicher-Leaks einsetzen. Tracing, also das schrittweise Protokollieren des Programmablaufs, ist oft nicht praktikabel, weil es die zu testenden Programme in der Regel stark verlangsamt.

Ohne direkten Zugriff auf den Rechner mit dem Untersuchungsobjekt versagen jedoch diese Methoden völlig. Bei einem solchen Black-Box-Fuzzing lässt aber der mitprotokollierte Netzwerkverkehr unter Umständen brauchbare Rückschlüsse zu. Registriert der Tester ungewöhnliche oder völlig ausbleibende Antwortpakete oder ist der Dienst längere Zeit nicht erreichbar, ist er möglicherweise abgestürzt.

Manche Fuzzing-Frameworks wurden speziell für diese Zwecke entwickelt. Autodafe beispielsweise bietet spezielle Funktionen, um mit Hilfe von Debuggern und Tracern die Reaktionen der zu untersuchenden Programme auf die präsentierten Eingabedaten zu analysieren. Allerdings ist dies noch ein relativ junges Forschungsgebiet.

Tester stoßen beim Fuzzing aber auch auf andere, typische Praxisprobleme. Gelegentlich verbergen sich kritische Programmierfehler hinter anderen, lapidaren Fehlern, die das Programm beispielsweise vorher abstürzen lassen. Außerdem können die vielen Datenmüllberge aufgrund von eher harmlosen Memory-Leaks in den Programmen den verfügbaren Arbeitsspeicher in kurzer Zeit aufbrauchen. Unter Umständen sind häufige System-Reboots die Folge. Grafische Bedienoberflächen wie die von Browsern können mit aufpoppenden Dialogfenstern ebenfalls Steine in den Weg des Testers rollen.

Umgehungsmöglichkeiten wie Apples Skripting-Sprache Applescript, mit der sich Tastatur- und Mauseingaben teilweise automatisieren lassen, bieten häufig nur unzureichende Funktionen oder sind auf anderen Systemen gar nicht erst verfügbar. Testern bleibt in solchen Fällen meist nur der Griff zum Editor oder zum Debugger, um die Probleme im Quellcode oder durch umständliches Patchen des Binärprogramms aus dem Weg zu räumen.

Die Popularität des Fuzzing ist in den vergangenen Jahren sprunghaft angestiegen, was sich gut an der wachsenden Zahl der hoch spezialisierten Fuzzing-Tools ablesen lässt. Einige der Tools lassen sich bereits ohne fundierte technische Kenntnisse einsetzen. Dementsprechend häufen sich in letzter Zeit auf einschlägigen Mailinglisten vermeintliche Sicherheits-Advisories, die deutlich erkennen lassen, dass der Poster den Fehlern mit Fuzzing auf die Spur gekommen ist. Frei nach dem Strickmuster "Programm X in Version Y stürzt ab, wenn man eine Datei mit Inhalt Z öffnet" fehlt ihnen jeglicher Tiefgang, und oft lassen sie selbst Sicherheitsexperten über tatsächliche Auswirkungen und geeignete Gegenmaßnahmen im Dunkeln.

Bislang gibt es erst für wenige Protokolle und Formate spezielle Fuzzing-Tools und in vielen Bereichen besteht noch Forschungsbedarf. Obwohl das Fuzzing noch in den Kinderschuhen steckt, ist es bereits überaus erfolgreich. Dass noch viel Raum für neue Ideen und verbesserungen besteht, haben neuere Ansätze wie das Context Free Grammar Fuzzing gezeigt. Programmierfehler werden nun am laufenden Band entdeckt. Doch weil die ohnehin ausgelasteten Entwickler mit der Behebung gemeldeter Schwachstellen kaum nachkommen, macht es die Computerwelt zurzeit keinesfalls sicherer.

Solange Softwarefirmen durch die Sicherheit der eigenen Produkte kein unmittelbarer finanzieller Vorteil entsteht, werden sie in der Regel keine zusätzlichen Kapazitäten für die Behebung von Sicherheitslücken bereitstellen. Eine wünschenswerte Entwicklung für die Zukunft wäre, dass die Firmen ihr Know-how nutzen, um effizientes Fuzzing als festen Bestandteil ihrer Qualitätssicherung zu etablieren.

Fuzzing ist ein äußerst mächtiges Werkzeug, mit dem Experten in kürzester Zeit ungeahnte Fehlerquellen aufspüren können – nicht mehr, aber auch nicht weniger. Um die aufgedeckten Schwachstellen zu analysieren und deren Tragweite beispielsweise mit funktionsfähigen Exploits zu verdeutlichen, braucht es nach wie vor viele helle Köpfe.

[1] Tools zur statischen Code-Analyse

[2] Erstes Fuzzing-Tool fuzz

[3] Miller, Fredriksen, So, Fatale Fehlerträchtigkeit, Eine empirische Studie zur Zuverlässigkeit von Unix-Utilities, iX 3/91, S. 104

[4] Blockbasierte Protokollanalyse

[5] Daniel Kaminskis Vortrag "Black Ops 2006" (PPT)

[6] Funktionsweise des Sequitur-Algorithmus

[7] CFG9000-Fuzzer

[8] Links zu Fuzzing-Tools

Tools für Webbrowser
AXman Active X
COMbust Common Object Model
CSS-Die Stylesheet-Unterstützung
DOM-Hanoi Data Object Model
Hamachi DHTML-Code
jsparsefuzz JavaScript
MangleMe HTML-Tags
Tools für Betriebssysteme
ISIC IP-Stacks
Sfuzz Unix-Sockets
Stress2 FreeBSD-Kernel
Sysfuzz syscall()-Funktionen
Tools für Netzwerkdienste
DHCPfuzz DHCP-Clients
FTPStress FTP-Server
Fuzzball2 TCP/IP-Header
IRCfuzz IRC-Clients
Protos Tool-Sammlung für SIP, SNMP, DNS und andere
SSHredder Sammlung mutierter SSH-Pakete
Sonstige Tools
Mangle mutiert Datei-Header
Bugger mutiert Daten im Speicher laufender Programme
CFG9000 intelligente Mutation unbekannter Eingabedaten
Frameworks
Autodafe Netzwerkprotokolle, mit Laufzeitanalyse
Bed Netzwerkprotokolle, Sprache: Perl
Peach universell einsetzbar, Sprache: Python
Smudge Netzwerkprotokolle
Spike Netzwerkprotokolle, Sprache: C
Spikefile Dateiformate, Sprache: C

(cr)