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.