
Fuzzing von IoT-Geräten am Beispiel des Routers TL-WR902AC
Dies ist die HTML-Version meiner Studienarbeit, die als PDF hier heruntergeladen werden kann.
Fuzzing hat sich zu „einer der effektivsten Methoden“ entwickelt, um Fehler in Software zu finden. Mit dieser oder ähnlichen Behauptungen beginnen viele aktuelle Arbeiten zum Thema Fuzzing [google-scholar]. Das Hauptziel unserer letzten Studienarbeit zum Thema „Internet of Vulnerable Things“ war es, einen speicherbezogenen Fehler zu finden und anschließend einen Exploit für diese Schwachstelle zu schreiben. Wir konnten eine Schwachstelle durch das Reverse Engineering der Firmware finden, aber es wurden keine speicherbezogenen Fehler gefunden. Einen Pufferüberlauf durch manuelles Reverse Engineering einer Binärdatei zu finden, ist nicht nur zeitaufwendig, sondern erfordert auch viel Erfahrung. Fuzzing zielt gleichzeitig darauf ab, der „effektivste Weg“ zu sein, um solche speicherbezogenen Schwachstellen zu finden. Google zum Beispiel hat OSS-Fuzz eingeführt, das kontinuierlich Open-Source-Software fuzzt und bereits über 10.000 Schwachstellen in 1.000 Projekten gefunden hat [oss-fuzz].
Das Ziel dieser Studienarbeit ist es erneut, eine speicherbezogene Schwachstelle zu finden, diesmal jedoch mithilfe von Fuzzing. Die angestrebte Schwachstelle soll über das Netzwerk ausnutzbar sein, ohne Kenntnis der Administrator-Anmeldedaten. Dieses Papier beschreibt den Weg zur Erreichung dieses Ziels. Dazu ist das Papier in zwei Teile gegliedert. Der erste Teil konzentriert sich darauf, wie man ein vielversprechendes Ziel findet, welche Werkzeuge verwendet werden können und was ein gutes Fuzzing-Ziel ausmachen sollte. Der zweite Teil beschreibt dann, wie man einen Harness entwickelt und debuggt, der in der Lage ist, eine bestimmte Funktion in einer Binärdatei zu fuzzen. Anschließend wird der entwickelte Harness von AFL++ verwendet, um die Zielfunktion zu fuzzen. Im Folgenden werden ein kurzer Hintergrund und der aktuelle Stand der Technik im Bereich IoT-Geräte-Fuzzing dargestellt.
Alle im Rahmen dieser Studienarbeit erstellten Dateien werden ebenfalls vollständig auf GitHub veröffentlicht und können über die folgende URL abgerufen werden: otsmr/blackbox-fuzzing.
Fuzzing von IoT-Geräten ist nicht so einfach wie das Fuzzing eines Open-Source-Projekts. Oft ist der Quellcode proprietär, was Gray-Box-Fuzzing, bei dem der Quellcode für die beste Fuzzing-Leistung instrumentiert wird, unmöglich macht [afl-persistent]. Außerdem wird die CPU-Architektur von Fuzzern oft nicht nativ unterstützt, was einen Emulator wie QEMU [qemu] erfordert, was ebenfalls die Fuzzing-Geschwindigkeit verlangsamt [afl-persistent]. Ein weiteres Problem sind die Hardware-Peripheriegeräte, was die Entwicklung eines allgemeinen Ansatzes erschwert. Die Arbeit „Embedded Fuzzing: A Review of Challenges, Tools, and Solutions“ [embedded-fuzzing] gibt einen Überblick über verschiedene Fuzzing-Strategien, wie hardwarebasiertes Embedded-Fuzzing. Die meisten dieser Strategien benötigen den Quellcode des Zielprogramms, z. B. wenn der Quellcode von Fuzzern wie AFL auf ARM-basierte IoT-Geräte portiert wird, um den Fuzzer auf der IoT-Hardware auszuführen. Auch das Ausführen des Fuzzers auf der Hardware des Geräts führt zu Leistungsproblemen, da diese häufig über CPUs mit geringer Leistung verfügen, die langsamer sind als normale Desktop-CPUs. Ein weiterer in dieser Arbeit vorgestellter Ansatz ist emulationsbasiertes Embedded-Fuzzing. Dabei wird entweder ein einzelnes Zielprogramm in einem Emulator ausgeführt, um coverage-guided Fuzzing durchzuführen, oder das gesamte System.
Die oben genannten Ansätze zielen alle direkt auf eine Binärdatei ab, indem sie einen Emulator verwenden oder den
Quellcode instrumentieren. Diese Ansätze erfordern ein Fuzzing-Setup, das oft speziell für ein einzelnes IoT-Gerät
maßgeschneidert werden muss und schwer zu verallgemeinern ist. Aus diesem Grund haben Forscher ein Programm
IoTFuzzer entwickelt, das ein automatisiertes Fuzzing-Framework sein soll, das darauf abzielt,
„Speicherkorruptionsschwachstellen ohne Zugriff auf ihre Firmware-Images zu finden
[iotfuzzer].“
IoTFuzzer basiert auf der Beobachtung, dass die meisten IoT-Geräte eine mobile App zur Steuerung haben und solche
Apps Informationen über das Protokoll enthalten, das zur Kommunikation mit dem Gerät verwendet wird. Das Programm
identifiziert und verwendet dann programmspezifische Logik wieder, um die Testfälle zu mutieren und IoT-Ziele effektiv zu
testen [iotfuzzer].
Ein Harness beschreibt eine Sequenz von API-Aufrufen, die die vom Fuzzer bereitgestellten Eingaben verarbeiten. Im Gegensatz zu einer normalen Anwendung, die oft keinen Harness benötigt, muss eine Bibliothek, die wiederverwendbare Funktionen implementiert, mit den richtigen Parametern und auch in der richtigen Reihenfolge aufgerufen werden, damit der Zustand zwischen mehreren gemeinsamen Funktionsaufrufen aufgerufen werden kann. Wenn die Bibliothek zufällig gefuzzt wird, ohne die Zustandsmaschine aufzubauen, wird das wahrscheinlich nicht erfolgreich sein und stattdessen eine Menge falsch-positiver Abstürze erzeugen, wenn die Abhängigkeiten der Bibliothek nicht erzwungen werden. Dies kann zum Beispiel passieren, wenn eine Puffergrößenprüfung vom Fuzzer übersprungen wird, was zu einem scheinbaren Pufferüberlauf führt.
In diesem Papier werden normale Anwendungen gefuzzt, aber wegen der Hardware-Abhängigkeiten bei der Verwendung von Sockets und Multithreading müssen wir auch für sie einen Harness erstellen. Der Harness wird im Kontext der Binärdatei geladen und kann interne Funktionen des Zielprogramms aufrufen, wie in Code 10 gezeigt.
Der Begriff „Corpus“ beschreibt gültige Eingabebeispiele oder Testfälle und dient als grundlegende Referenz für die Erzeugung neuer Eingabedaten während des Fuzzing-Prozesses. In Code 10 wäre dies beispielsweise eine HTTP-Anfrage. Fuzzer nutzen diesen Corpus, um mutierte oder diversifizierte Testfälle zu erzeugen, was die Erkennung von Softwareschwachstellen durch die Erkundung verschiedener Eingabeszenarien unterstützt.
Der zeitaufwendigste Teil des Blackbox-Fuzzing ist das Finden einer potenziell verwundbaren Funktion in der Firmware.
Der erste Schritt besteht darin, interessante Binärdateien zu finden, die zum Beispiel über das Netzwerk erreichbar sind,
unsichere Funktionen verwenden oder keine Sicherheitsfunktionen wie Stack-Canary aktiviert haben, was ein Schutz vor
Pufferüberläufen ist. Unsere letzte Arbeit
([iovt])
hat bereits beschrieben, wie man die Firmware aus dem Zielrouter extrahiert und wie man eine potenziell gefährliche
Binärdatei findet. Dazu wurde das Tool EMBA [emba] verwendet. EMBA bewertet alle
in der Firmware gefundenen Binärdateien nach der Anzahl unsicherer Funktionen wie strcpy, Netzwerkzugriff und
Sicherheitsmechanismen wie Stack-Canary oder dem NX-Bit, die bei der Ausnutzung eines Pufferüberlaufs interessant werden,
was in Code 1 zu finden ist.
Code 1: EMBAs Ergebnis von unsicheren Verwendungen der Funktion strcpy.
Da das Ziel dieses Papers darin besteht, eine Speicherschwachstelle zu finden, die über das Netzwerk ohne Kenntnis der Admin-Zugangsdaten ausgenutzt werden kann, muss die verwundbare Funktion über das Netzwerk aufrufbar sein und direkt mit der bereitgestellten Benutzereingabe interagieren. Aber eine Netzwerkinteraktion bedeutet nicht, dass die Binärdatei auch direkt über das Netzwerk zugänglich ist. Um herauszufinden, welche Binärdateien lauschen, können wir die UART-Root-Shell verwenden, die bereits in [iovt] eingerichtet wurde.
```txt ~ # netstat -tulpn Active Internet connections (only servers) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 0 0 127.0.0.1:20002 0.0.0.0:* LISTEN 1045/tmpd tcp 0 0 0.0.0.0:1900 0.0.0.0:* LISTEN 1034/upnpd tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 1027/httpd tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 1224/dropbear udp 0 0 0.0.0.0:20002 0.0.0.0:* 1048/tdpd [...] ```Code 2: Verwendung der UART-Root-Shell zur Ausführung von netstat
Das erste vielversprechende Binärprogramm ist wscd. Das Binärprogramm enthält die meisten unsicheren strcpy-Aufrufe (abgesehen von der Bibliothek libcmm.so) und Netzwerkinteraktion, was bei wscd bedeutet, dass es eine Verbindung zu einem UPnP-Gerät herstellt und nicht auf einem bestimmten Port lauscht. Es hat, wie später gezeigt wird, eine Funktion, die sich einfach fuzzen lässt, weshalb dieses Binärprogramm in diesem Dokument als Beispiel zur Erläuterung des allgemeinen Vorgehens ausgewählt wurde. Bevor wir mit dem Reverse Engineering beginnen, können wir die UART-Root-Shell nutzen, um herauszufinden, ob das Binärprogramm ausgeführt wird und wie es gestartet wurde.
Code 3: Verwenden des Befehls ps, um alle laufenden Programme anzuzeigen.
Mit ps sehen wir nicht nur, dass die Binärdatei ausgeführt wird, sondern auch, welche Argumente übergeben werden, die wichtig sind, um zu überprüfen, ob eine potenzielle Funktion überhaupt aufgerufen wird. Die Bedeutung dieser Argumente kann der CLI-Hilfe entnommen werden, die angezeigt wird, wenn die Binärdatei ohne Argumente aufgerufen wird.
Code 4: Optionen der Binärdatei wscd.
Wie in Code 4 gezeigt, wird wscd mit "Enabled UPnP Device service" gestartet, was vielversprechend
aussieht. Nachdem verifiziert wurde, dass die Binärdatei tatsächlich auf dem Router läuft, kann sie dann
mit Ghidra analysiert werden, um nach verdächtigen Funktionen zu suchen. Für das Fuzzing
sind Parsing-Funktionen besonders interessant, weil sie normalerweise komplex sind und die Eingabe,
die geparst wird, oft Längenfelder für die enthaltenen Daten besitzt, so wie das TCP-Paket die Länge der
Payload enthält.

Abbildung 1: Verwenden von Ghidra zur Suche nach Parsing-Funktionen.
Ein weiterer Vorteil von Parsing-Funktionen ist, dass sie oft nicht mit anderen Teilen des Codes interagieren und keine Benutzerinteraktion über das Netzwerk erfordern. Die Parsing-Funktion kann also direkt mit der Eingabe aufgerufen werden, ohne die Binärdatei zu verändern oder andere Funktionen zu überschreiben, sodass die Funktion gefuzzt werden kann.
Bevor mit dem Fuzzing der Funktion begonnen wird, sollte überprüft werden, ob die Funktion überhaupt ausgelöst wird,
denn die Funktion ist nur interessant, wenn sie mit einer vom Benutzer kontrollierten Eingabe aufgerufen wird. Dazu
kann Ghidra verwendet werden, um nach Referenzen auf die Zielfunktion zu suchen. Im Fall der
Funktion parser_parse gibt es mehrere Möglichkeiten. Da wir wissen, wie das Programm gestartet wird, können die
Aufrufe auf einen einzigen Funktionsaufrufbaum reduziert werden, wie in Code 5 gezeigt.
Code 5: Aufrufbaum der Funktion parser_parse
Nachdem eine Zielfunktion gefunden wurde, können wir nun ein Fuzzing-Setup erstellen, um die Funktion zu fuzzen, was im nächsten Teil beschrieben wird. Aber zuerst werden andere vielversprechende Funktionen vorgestellt.
Für diese Arbeit wurden mehrere potenzielle Binärdateien manuell auf verdächtige Funktionen analysiert. Im Folgenden finden Sie eine kurze Zusammenfassung anderer möglicher Ziele, die gefunden wurden.
Die Binärdatei httpd ist das Backend für die Admin-Weboberfläche. Die Binärdatei ist über das Netzwerk auf Port 80 erreichbar. Eine interessante Funktion in httpd ist die Funktion httpd_parser_main. Beim Überfliegen der Parser-Implementierung mit Ghidra konnten mehrere verdächtige Codeteile identifiziert werden. Einer der verdächtigen Teile ist das Parsen des Content-Type. Im Folgenden finden Sie eine einfache HTTP-Anfrage.```txt
POST / HTTP/1.1\r\n
Content-Type: multipart/form-data; boundary=X;\r\n
Host: example.com\r\n
\r\n
\r\n
DATA\r\n
Unten ist ein Ausschnitt aus der Funktion `httpd_parser_main`, die den `Content-Type` aus der
vom Benutzer bereitgestellten HTTP-Anfrage parst.
<div id="c6"></div>```c
// user_input_ptr points to
// "Content-Type: multipart/form-data; boundary=X;\r\nHost: example.com\r\n..."
cursor = strstr(user_input_ptr,"multipart/form-data");
if (user_input_ptr == cursor) {
cursor = strstr(user_input_ptr,"boundary=");
user_input_ptr = cursor + 9;
// user_input_ptr points now to "X;\r\nHost: example.com\r\n..."
if (cursor != (char *)0x0) {
do {
while (cursor = user_input_ptr, *cursor == " ") {
user_input_ptr = cursor + 1;
}
user_input_ptr = cursor + 1;
} while (*cursor == "\t");
// cursor points now to "X;\r\nHost: example.com\r\n..."
// strchr returns a pointer to the first occurrence of ";" in the user request.
// If ";" is not found, the function returns a null pointer.
user_input_ptr = strchr(cursor, ";");
if (user_input_ptr != (char *)0x0) {
// The character ";" is replaced by an null byte to terminate the string
*user_input_ptr = "\0";
// cursor points now to "X\0\r\nHost: example.com\r\n..."
}
// DAT_00444050 global array from 0x00444050 to 0x0044414f (255 Bytes)
strcpy(&DAT_00444050, cursor);
// DAT_00444050 contains now "X"
}
}
Code 6: Aufrufbaum der Funktion parser_parse
Die Schwachstelle in diesem Code ist der Funktionsaufruf strcpy und die Annahme, dass der Content-Type mit einem Semikolon endet. Da strcpy den Puffer bis zum nächsten Nullbyte kopiert und, wie in Code 6 gezeigt, das Nullbyte nur dann hinzugefügt wird, wenn ein Semikolon gefunden wird. Durch das Entfernen des Semikolons befindet sich das nächste Nullbyte am Ende des Eingabepuffers, z. B. am Ende der HTTP-Anfrage. Dadurch kann die globale Variable DAT_00444050 überlaufen, wodurch Daten jenseits der Adresse 0x0044414f überschrieben werden. Die Herausforderung besteht nicht nur darin, eine interessante globale Variable jenseits dieser Adresse zu finden, die überschrieben werden könnte, sondern auch darin, dass wegen strcpy keine Nullbytes verwendet werden können. Wenn es aber einen solchen Fehler gibt, sind wahrscheinlich noch mehr zu finden.
Die Binärdatei tdpd wird von der mobilen App verwendet und ist über UDP im lokalen Netzwerk erreichbar. tdpd hat fast dieselben Funktionen wie tmpd, die jedoch größtenteils nie aufgerufen werden. Die Hauptfunktion lauscht lediglich auf dem UDP-Port auf Nachrichten und antwortet immer mit grundlegenden Informationen über den Router, etwa Name oder Modell. Es gibt kaum eine Interaktion mit der vom Benutzer bereitgestellten Eingabe, weshalb sie für das Fuzzing nicht interessant ist.
Ein weiteres interessantes Binärpaar sind upnpd und ushare. Beide Binärdateien verarbeiten UPnP-Nachrichten, die daher XML parsen müssen. Da sich in der Binärdatei eine Copyright-Zeichenkette finden lässt, kann davon ausgegangen werden, dass diese Programme nicht von TP-Link entwickelt wurden.```sh
$ strings usr/bin/ushare | grep "(C)"
Benjamin Zores (C) 2005-2007, for GeeXboX Team.
Beide Binärdateien laden die gemeinsam genutzten Bibliotheken `libupnp.so` und `libixml.so`, die dieselben
Funktionen wie das Open-Source-Projekt `pupnp` [\[pupnp\]](https://github.com/pupnp/pupnp/) besitzen. Da
der Fokus dieses Papers auf Black-Box-Fuzzing liegt, werden diese Binärdateien ignoriert. Aber Gray-Box-Fuzzing dieser
Bibliothek könnte Potenzial haben, denn 2021 wurde ein Speicherleck in `libixml.so` gefunden
[\[pupnp-mem-leak\]](https://github.com/pupnp/pupnp/issues/249).
Die Binärdatei **tmpd** ist das Backend der mobilen App. Das Interessante daran ist, dass der Router und
die mobile App über ein benutzerdefiniertes binäres Protokoll kommunizieren. Im Folgenden wird eine Nachricht vom
Client an den Server gezeigt.
<div id="c7"></div>```txt
00000000 01 00 05 00 00 08 00 00 00 00 00 17 50 7b 6e fe |............P{n.|
00000010 01 01 02 00 00 00 00 00 |........ |
Code 7: Nachricht von der Mobile-App an den Router.
Um das binäre Protokoll zu verstehen, wurde die Binärdatei tmpd mithilfe von Ghidra zurückentwickelt. Mit diesen
Informationen kann die Nachricht in Code 7 wie folgt aufgeschlüsselt werden:```txt
01 00 05 00 : Version
00 08 00 00 : Size (8 Bytes)
00 00 00 17 : Datatype
50 7b 6e fe : Checksum (CRC32)
01 01 : Options
02 00 : Function id
00 00 00 00 : Function parameters
<p class="text-align: center">Code 8: Benutzerdefiniertes binäres Protokoll aufgeschlüsselt.</p>
Das sieht vielversprechend aus, da solche binären Protokolle geparst werden müssen. Aber der
verdächtigste Teil des binären Protokolls ist nicht das Längenfeld, sondern die Verwendung der
Funktions-ID und der Funktionsparameter.
<figure id="f2">
<p><img src="https://assets.kitploit.com/production/public/readmes/48851/43cb089d7f85d30ba036234ff0fdb29b6a54dd4a3382b2bd68e065f9f6c75369.png" style="width:90.0%" /></p>
<figcaption>
<p style="text-align: center">Abbildung 2: Rekonstruierte Funktion aus tmpd, die die Funktions-ID und deren Parameter parst.</p>
</figcaption>
</figure>
[Abbildung 2](#f2) zeigt einen Teil der dekompilierten Parserfunktion des benutzerdefinierten
Protokolls. In Zeile 16 wird die Funktions-ID extrahiert, und die entsprechende Funktion wird dann
in Zeile 29 aufgerufen. Das verdächtige Verhalten besteht darin, dass die Funktion mit Parametern
aufgerufen wird, die ohne jede Prüfung aus dem benutzergesteuerten Eingabepuffer extrahiert werden.
Wir könnten nun versuchen, eine Funktion in der in [Abbildung 3](#f3) gezeigten Sprungtabelle zu
finden, bei der dies gefährlich sein könnte, etwa wenn der Parameter verwendet wird, um einen Puffer
zu indizieren oder als Zeichenkette interpretiert wird. Anstatt manuell zu rekonstruieren und die
über 100 Funktionen zu durchsuchen, was zeitaufwändig wäre, können wir einen Fuzzer verwenden, der
dies automatisch erledigt.
<figure id="f3">
<p><img src="https://assets.kitploit.com/production/public/readmes/48851/708cf59aee459b840ca6dbcac32948d2b80fb426c3d4cc745157eac4f9b7c28e.png"
style="width:90.0%" /></p>
<figcaption><p style="text-align: center">Abbildung 3: Rekonstruierte Funktion aus tmpd, die die Funktions-ID
und ihre Parameter parst.</p></figcaption>
</figure>
Leider ist das Binary `tmpd` über das Netzwerk nur lokal erreichbar, wie in [Code 2](#c2) gezeigt.
Um sich mit diesem Binary zu verbinden, verbindet sich die App zunächst per SSH im Modus
`direct-tcpip` mit dem Router, der die Pakete lediglich an den lokalen Prozess weiterleitet. Und die
SSH-Verbindung ist durch die Admin-Zugangsdaten geschützt. Aber wie in
[\[iovt\]](https://raw.githubusercontent.com/otsmr/internet-of-vulnerable-things/main/Internet_of_Vulnerable_Things.pdf)
beschrieben, kann die SSH-Verbindung leicht kompromittiert werden, da der Server-Host-Key von der App
nie überprüft wird. Indem man jedes an das Internet weitergeleitete Paket verwirft, kann der Admin
dazu verleitet werden, sich am Router anzumelden, während ein Man-in-the-Middle-Angriff durchgeführt
wird, um die Zugangsdaten zu stehlen.
## Fuzzing mit AFL++ und QEMU
In diesem Abschnitt wird ein Harness entwickelt, das auf eine der zuvor gefundenen Funktionen
abzielt. Nachdem das Harness entwickelt wurde, wird der State-of-the-Art-Fuzzer AFL++
[\[aflpp\]](https://github.com/AFLplusplus/AFLplusplus) verwendet, um die Zielfunktion zu fuzzen.
Da die Binaries für die `mipsel`-Architektur kompiliert sind, wird der Emulator QEMU verwendet, um
das Binary auszuführen. Das grundlegende Fuzzing-Setup, das in dieser Arbeit verwendet wird, ist
weitgehend von dem Blogeintrag "Firmware Fuzzing 101" von Adam Van Prooyen
[\[b101\]](https://www.mayhem.security/blog/firmware-fuzzing-101) inspiriert.
### Fuzzing-Umgebung
Um eine einfach reproduzierbare Fuzzing-Umgebung zu erstellen, ist Docker die beste Wahl. Wir haben
ein Dockerfile erstellt, das alle notwendigen Werkzeuge installiert, wie einen Cross-Compiler für
die `mipsel`-CPU-Architektur oder `gdb-multiarch`, mit dem das Harness debuggt werden kann.
Darüber hinaus wird AFLplusplus heruntergeladen und zusammen mit QEMU kompiliert, das in einer
Version mit kleinen Anpassungen gebaut wird, um nicht instrumentierte Binaries unter afl-fuzz
ausführen zu können.```docker
FROM debian:latest
RUN apt update && apt install -y \
curl \
vim \
gcc-mipsel-linux-gnu \
openssh-server \
qemu-user-static \
gdb-multiarch
# Qemu statics are installed at /usr/bin/qemu-mipsel-static
# Compiling AFL++
RUN apt install -y git make build-essential clang ninja-build pkg-config libglib2.0-dev libpixman-1-dev
RUN git clone https://github.com/AFLplusplus/AFLplusplus /AFLplusplus
WORKDIR /AFLplusplus
RUN make all
WORKDIR /AFLplusplus/qemu_mode
RUN CPU_TARGET=mipsel ./build_qemu_support.sh
RUN echo "#!/bin/bash\n\nsleep infinity" >> /entry.sh
RUN chmod +x /entry.sh
WORKDIR /share
ENTRYPOINT [ "/entry.sh" ]
Dockerfile, das die notwendigen Werkzeuge installiert.
Das Image kann dann mit docker build erstellt werden.```sh
docker build -t fuzz .
Wenn das Image erstellt wurde, kann es einfach mit `docker run` verwendet werden, das dann den Container startet.```sh
docker run -d --rm -v $PWD/:/share --name fuzz fuzz
Mit der Option -d wird der Container im Hintergrund gestartet. Mit docker exec können mehrere Shells
im Container gestartet werden, was hilfreich ist, um das ausführbare Programm in einer Sitzung mit
QEMU und in der anderen Sitzung gdb-multiarch zu starten.```sh
docker exec -it fuzz /bin/bash
### Die main-Funktion überschreiben
Im vorherigen Abschnitt wurde ein leistungsfähiges Fuzz-Ziel identifiziert. Das Problem ist, dass beim Ausführen der
Binärdatei der Funktionsaufruf nie erreicht wird, da die Funktion `parser_parse` nur aufgerufen wird, wenn
ein TCP-Paket über einen Socket empfangen wird. Das wäre nicht nur schlecht für die Leistung, sondern auch schwer zu
Einrichten. Deshalb sollte der Einstiegspunkt des Fuzzers an einer anderen Stelle liegen als die normale
main-Funktion. Dazu kann die Umgebungsvariable `LD_PRELOAD` verwendet werden, die das Injizieren eines Harness,
das Zugriff auf interne Funktionen hat, ermöglicht. Wie die Manpage von `ld.so`, die für das
Verlinken der gemeinsam genutzten Bibliotheken, die eine ausführbare Datei zur Laufzeit benötigt, verantwortlich ist, beschreibt, kann `LD_PRELOAD` verwendet werden
„Funktionen in anderen gemeinsam genutzten Objekten selektiv zu überschreiben
[\[man-pages\]](https://www.man7.org/linux/man-pages/man8/ld.so.8.html)."
Die Funktion `__uClibc_main` eignet sich am besten für diesen Zweck. Um diese Funktion zu überschreiben, muss eine C-Datei
erstellt werden, die eine Funktion mit demselben Namen enthält.```c
void __uClibc_main(void *main, int argc, char** argv) {
// Harness code, e.g. call the function parser_append
printf("My custom __uClibc_main was called!");
}
Die C-Datei kann dann mit mipsel-linux-gnu-gcc zu einem Shared Object für die mipsel-Architektur cross-kompiliert werden. Die Option -fPIC ermöglicht „Position Independent Code", was bedeutet, dass der Maschinencode nicht davon abhängt, an einer bestimmten Adresse platziert zu sein, da relative Adressierung anstelle von absoluter verwendet wird.```txt
$ mipsel-linux-gnu-gcc parser_parse_hook.c -o parser_parse_hook.o -shared -fPIC
Die neu erstellte Shared Library kann dann geladen werden, indem die Umgebungsvariable `LD_PRELOAD`
zum QEMU-Befehl hinzugefügt wird.```txt
$ chroot root /qemu-mipsel-static -E LD_PRELOAD=/parser_parse_hook.o /usr/bin/wscd
My custom __uClibc_main was called!
Mit dem Befehl chroot können das aktuelle Verzeichnis und das Root-Verzeichnis für den angegebenen Befehl geändert werden.
Dies ist hilfreich, da die ausführbare Datei wscd andere Dateien öffnet, wie gemeinsam genutzte Bibliotheken aus der
Firmware. Wir können dieses Verhalten sehen, indem wir das Argument -strace zu QEMU hinzufügen.```txt
chroot root /qemu-mipsel-static -E LD_PRELOAD=/parser_parse_hook.o -strace /usr/bin/wscd /corpus/notify.txt
38180 mmap(NULL,4096,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS|0x4000000,-1,0) = 0x7f7e7000
38180 stat("/etc/ld.so.cache",0x7ffffa48) = -1 errno=2 (No such file or directory)
38180 open("/parser_parse_hook.o",O_RDONLY) = 3
38180 fstat(3,0x7ffff920) = 0
38180 close(3) = 0
38180 munmap(0x7f7e6000,4096) = 0
38180 open("/lib/libpthread.so.0",O_RDONLY) = 3
38180 open("/lib/libc.so.0",O_RDONLY) = 3
[...]
Wie wir sehen können, öffnet die ausführbare Datei mehrere Bibliotheken im `/lib/`-Ordner auf der Firmware und nicht auf dem Host.
### Entwicklung und Debugging des Harness
Nachdem das Setup erstellt wurde, können wir nun mit der Entwicklung eines Harness beginnen. Wie im Hintergrundabschnitt beschrieben, ist der Harness der Treiber zwischen dem Fuzzer und der Zielfunktion. Der Harness lädt die Fuzz-Eingabe, die von AFL++ in einer Datei gespeichert wird. Mit dem Dateipfad als Parameter ruft der Harness dann das Fuzzing-Ziel auf; in diesem Fall wäre das `parser_append`. Die Funktionen können über die Adresse aufgerufen werden.
<div id="c10"></div>```c
void __uClibc_main(void *main, int argc, char** argv)
{
// Verify that a filename is provided
if (argc != 2) exit(1);
// Create function pointer to the fuzz target
int (*parser_request_init)(void *, int) = (void *) 0x00412564;
int (*parser_append)(void *, void *, int) = (void *) 0x00412e98;
// Open the fuzz input file
int fd = open(argv[1], O_RDONLY);
char fuzz_buf[2048 + 1];
int fuzz_buf_len = read(fd, fuzz_buf, sizeof(fuzz_buf) - 1);
if (fuzz_buf_len < 0) exit(1);
fuzz_buf[fuzz_buf_len] = 0;
// Call the target functions
uint8_t parsed_data[220];
parser_request_init(parsed_data, 8);
int status = parser_append(parsed_data, fuzz_buf, fuzz_buf_len);
printf("Response is %d\n", status);
exit(0);
}
Code 10: Harness-Code mit dem Fuzz-Target `parser_append` in der Binärdatei wscd.
Wie in Code 10 gezeigt, wird die Funktion parser_parse nicht direkt aufgerufen, sondern über die Funktion parser_append. Bevor diese Funktion aufgerufen wird, muss die Initialisierungsfunktion parser_request_init aufgerufen werden, die die Ausgabestruktur der Funktion parser_parse initialisiert.
Während der Harness im Fall von parser_parse recht einfach einzurichten ist, erfordern andere Ziele anspruchsvollere Harnesses wie die Funktion httpd_parser_main. Beispielsweise muss vor dem Aufruf des Ziels die Funktion http_init_main aufgerufen werden, die in einem SIGSEGV endet. Um herauszufinden, wo dieser Segmentation Fault verursacht wird, ist es nützlich, den Code mit einem Debugger wie gdb zu debuggen. Dazu kann QEMU mit der Option -g gestartet werden, die einen gdb-server auf dem angegebenen Port startet.```sh
chroot root /qemu-mipsel-static -strace -g 1234 -E LD_PRELOAD="/httpd_parser_main.o" /usr/bin/httpd
corpus/httpd/simple.txt
Da die Binärdatei in der `mipsel`-Architektur vorliegt, muss `gdb-multiarch` verwendet werden. Nachdem gdb
gestartet wurde, kann das folgende Init-Skript mit gdb geladen werden, indem `sources <path to script>` verwendet wird.```sh
set solib-absolute-prefix /share/root/
file /share/root/usr/bin/httpd
target remote :1234
# break bevor fuzz target is called
# break __uClibc_main
break http_parser_main
display/4i $pc
Aufgrund des Chroots änderte das Skript zuerst den absoluten Präfixpfad, sodass gdb die Datei findet, wenn das Binary ein Shared Object lädt. Danach wird die Zieldatei festgelegt, da der gdb-Server von QEMU keinen Dateitransfer unterstützt, sodass gdb versucht, die Dateien stattdessen von der Festplatte zu laden. Nachdem gdb konfiguriert ist, verbindet sich das Skript mit target remote mit dem gdb-Server und erstellt einen Breakpoint am Anfang der Zielfunktion. Mit display wird die Ausgabe nur verbessert, sodass beim schrittweisen Durchgehen die nächsten vier Assemblerzeilen angezeigt werden. Mit si können wir eine einzelne Instruktion ausführen, was nützlich ist, wenn das Harness mit dem Standard-Korpus einen Segmentierungsfehler aufweist, was immer funktionieren sollte. Wie in Code 11 gezeigt, hat das Binary einen Segmentierungsfehler in der Funktion fprintf.
Program received signal SIGSEGV, Segmentation fault.
<p style="text-align: center">Code 11: Segmentation Fault in printf.</p>
Um den Fehler zu untersuchen, kann Ghidra verwendet werden, um herauszufinden, mit welchen Parametern die Funktion aufgerufen wird.```c
fprintf(
*(FILE **)(iVar1 + 0x101c),
"HTTP/1.1 %d %s\r\n",
*(undefined4 *)(&DAT_0042ee68 + (uint)(byte)(&DAT_00414570)[statuscode & 0x3f] * 8),
(&PTR_DAT_0042ee6c)[(uint)(byte)(&DAT_00414570)[statuscode & 0x3f] * 2]
);
Der SIGSEGV wird wahrscheinlich dadurch verursacht, dass der erste Parameter kein Dateideskriptor, sondern ein Nullzeiger ist. Dabei ist iVar1 nur ein Verweis auf die Eingabe der Funktion httpd_parser_main. Das bedeutet, dass die Fuzzing-Eingabe an Position 0x101c einen Dateideskriptor aufweisen muss. Die Eingabe muss also an die folgende Struktur angepasst werden.```c
typedef struct {
int _a; // 4 Bytes
int _b; // 4 Bytes
int socket; // 4 Bytes
int ip; // 4 Bytes
int mac; // 4 Bytes
unsigned char body[0x1008]; // 0x101c - 4*5 = 0x1008 Bytes
FILE * fd_out; // expected to be a valid file descriptor
} HttpMainT;
Da `fd_out` nur ein gültiger Dateideskriptor-Zeiger sein muss, kann es problemlos auf `stdout` gesetzt werden.
Die erneute Ausführung von `httpd_parser_main` erzeugt nun eine gültige HTTP-Ausgabe.```c
$ chroot root /qemu-mipsel-static -E LD_PRELOAD=/httpd_parser_main.o \
/usr/bin/httpd /httpd_corpus.txt
bind: No such file or directory
[ dm_shmInit ] 086: shmget to exitst shared memory failed. Could not create shared memory.
rdp_getObj is called with: 4274932gdpr_getSystemGDPREntry Error
gdpr_getNewSystemGDPREntry OK
#Msg: getsockname error
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 24257
Set-Cookie: JSESSIONID=deleted; Expires=Thu, 01 Jan 1970 00:00:01 GMT; Path=/; HttpOnly
Connection: close
<!DOCTYPE html>
[...]
Der Harness funktioniert jetzt und kann verwendet werden, um die Funktion mit AFL++ zu fuzzen, was im nächsten Abschnitt erläutert wird.
Wie im Hintergrund erwähnt, beschreibt ein Seed-Korpus gültige Eingabebeispiele, die als grundlegende Referenz für die Erzeugung neuer Eingabedaten während des Fuzzing-Prozesses dienen.
Diese Eingaben werden in der Regel so gewählt, dass sie verschiedene Aspekte der Zielprogramme repräsentieren. Der Seed-Korpus wird von einem Fuzzer verwendet, um mutierte oder weiterentwickelte Testfälle zu erzeugen, die dann gegen die Zielsoftware ausgeführt werden, um Fehler, Abstürze oder andere Probleme aufzudecken. Dieser Korpus spielt eine wichtige Rolle dabei, den Fuzzer auf relevante Bereiche des Programms zu lenken und die Wahrscheinlichkeit zu erhöhen, Schwachstellen oder unerwartete Verhaltensweisen zu erkennen. Durch die Bereitstellung einer vielfältigen und repräsentativen Menge an Anfangseingaben hilft der Seed-Korpus dem Fuzzer, verschiedene Pfade im Ziel schneller zu erkunden, und erhöht so die Abdeckung.
Bei Funktionen, die Netzwerkdaten parsen, können diese Eingaben erstellt werden, indem man Wireshark verwendet, um verschiedene Pakete aufzuzeichnen.
Für die Funktion httpd_parse_main wurden vier verschiedene Korpora erstellt. Jeder zielt auf verschiedene Pfade in der Binärdatei ab. Ein Beispiel ist die Login-Anfrage, die Benutzername und Passwort enthält. Für diesen Korpus musste der Harness modifiziert werden, weil TP-Link (schwache) Kryptografie verwendet, um das Passwort zu „schützen“. Dazu wird das Passwort im Browser mit AES verschlüsselt und dann im Backend entschlüsselt. Wobei das Passwort im Browser generiert und dann mit RSA verschlüsselt wird. Anschließend werden die verschlüsselten Daten signiert. Da ein Fuzzer keine Signatur erstellen oder Daten verschlüsseln kann, wurden einige Funktionen überschrieben, die die Daten nun nur noch aus Base64 dekodieren. Dazu wurden die Daten zunächst im Klartext aus dem Browser extrahiert, unter Verwendung des Debuggers, der in Abbildung 4 gezeigt wird.

Abbildung 4: Extrahieren der Daten vor der Verschlüsselung.
Im Ziel wurde die Funktion rsa_tmp_decrypt_bypart dann überschrieben, um die Logik von der Entschlüsselung der Daten auf ein reines Dekodieren aus Base64 umzustellen.```c
// Replacing the logic with b64_decode
int rsa_tmp_decrypt_bypart(uint8_t *input, int input_len, uint8_t *output) { // other params just key data
int (*b64_decode)(uint8_t *, int, uint8_t *, int) = (void *) 0x0040bf00;
b64_decode(output, 0x1000, input, input_len);
int * seqnumber = (int *) 0x00444db0;
*seqnumber = 0x3ac28e29-input_len+12;
return 0; // says it was okay
}
<p style="text-align: center">Code 12: Die Funktion rsa_tmp_decrypt_bypart decodiert jetzt nur noch base64
anstatt die Daten zu entschlüsseln.</p>
Während der Ausführung des Korpus gibt die Zielfunktion immer ein HTML-Dokument mit dem Fehler „408
Request Timeout“ zurück. Mit Ghidra und GDB konnte das Problem identifiziert werden. Der Fehler tritt immer
nach dem Aufruf der Funktion `http_stream_fgets` auf. Die problematische Zeile war die Prüfung auf das
Zeilenumbruchzeichen `\n`.```c
if (((cVar1 == '\n') && (param_3 < pcVar4)) && (pcVar4[-1] == '\r')) {
Diese Bedingung erzwingt, dass nach jedem Zeilenumbruch ein Wagenrücklauf folgen muss. Nach dem Hinzufügen des Wagenrücklaufs funktionierten alle erstellten Korpora.
Im letzten Abschnitt haben wir mehrere Harnesses entwickelt und mit QEMU ausgeführt. In diesem Abschnitt
wird QEMU durch AFL++ ersetzt, das die generierten Korpora als Seed-Eingaben erhält, um die Zielfunktion
zu fuzzen. Im Abschnitt „Fuzzing-Umgebung“ wurde ein Docker-Image erstellt, das bereits AFL++ von GitHub
lädt und dann ein von AFL++ bereitgestelltes Skript verwendet, um eine gepatchte Version von QEMU zu bauen.
So kann AFL++ nun mit dem folgenden Befehl gestartet werden, der verschiedene Parameter erhält, wie z. B.
-Q, das AFL++ anweist, die gepatchte Version von QEMU zu verwenden.```sh
QEMU_LD_PREFIX=/share/root AFL_PRELOAD=/share/root/httpd_parser_main.o
/AFLplusplus/afl-fuzz -Q
-i /share/root/corpus/httpd/ -o /share/afl-out/httpd/
-- /share/root/usr/bin/httpd @@
<p style="text-align: center">Code 13: Fuzzing der Binärdatei <code>httpd</code> mithilfe des Harness
und <code>afl-fuzz</code>.</p>
Anders als zuvor ist der Befehl `chroot` nicht mehr erforderlich und wird durch die Variable
`QEMU_LD_PREFIX` ersetzt. Diese teilt QEMU mit, wo nach Shared Objects gesucht werden soll. Außerdem wird die `LD_PRELOAD`-
Variable durch die AFL-spezifische Version `AFL_PRELOAD` ersetzt. Das letzte Argument im Befehl sind
die beiden `@`-Zeichen. Sie werden von AFL++ durch einen Dateipfad ersetzt, der den Fuzzing-Input
enthält. Beim Start zeigt AFL++ den Fortschritt über die Terminal-UI an, die in [Abbildung 5](#f5) dargestellt ist.
<div id="f5"></div>
<figure>
<p><img src="https://assets.kitploit.com/production/public/readmes/48851/ac4b12fcf84c2041c9edc2a0871c5bb89b576d976b2eba0a2d0f843e7e708795.png" style="width:95.0%" /></p>
<figcaption><p style="text-align: center">Abbildung 5: Der Statusbildschirm von AFL++.</p></figcaption>
</figure>
Der `AFL++`-Statusbildschirm liefert wesentliche Einblicke in den aktuellen Fuzzing-Prozess. Die Dokumentation von
`AFL++` enthält eine schöne Übersicht der im Statusbildschirm verwendeten Begriffe
[\[afl-screen\]](https://aflplus.plus/docs/status_screen/). Beim Debuggen des Korpus mit den folgenden
Umgebungsvariablen kann die UI deaktiviert und mit `AFL_DEBUG` eine detaillierte Protokollierung
aktiviert werden, die den aktuellen Fuzzer-Input und die `stdout`-Ausgabe des Zielprogramms anzeigt.```sh
export AFL_DEBUG=1 && export AFL_NO_UI=1
unset AFL_DEBUG && unset AFL_NO_UI
Wie in Abbildung 5 gezeigt, kann das Fuzzing einer Binärdatei durchaus einige Zeit in Anspruch nehmen. Laut der Dokumentation „sollte man damit rechnen, dass es Tage oder Wochen läuft" und „einige Jobs dürfen monatelang laufen." Um die benötigte Zeit zu verbessern, sollte die Ausführungsgeschwindigkeit über 100 Execs/Sek. liegen. Als beispielsweise das Ziel httpd_main_parser gefuzzt wurde, lag die Ausführungsgeschwindigkeit anfangs bei etwa 30/Sek. Um die Geschwindigkeit zu verbessern, wurde die Ziel-Binärdatei nach verdächtigen Funktionen durchsucht, die wahrscheinlich die Ursache für die Verlangsamung sind. Eine der verdächtigen Funktionen war rsa_gdpr_generate_key, da das Erzeugen eines RSA-Schlüssels bekanntermaßen langsam ist. Nach dem Überschreiben der Funktion verbesserte sich die Geschwindigkeit auf 600 Ausführungen pro Sekunde.
Ein Indikator, der dabei hilft zu erkennen, wann das Fuzzing beendet werden sollte, ist der Zykluszähler. AFL++ hebt die Zahl grün hervor, wenn „der Fuzzer schon länger keine Aktivität gesehen hat", was dabei hilft, die Entscheidung zu treffen, den Fuzzer zu stoppen.
Aber die interessanteste Zahl ist wahrscheinlich „total crashes". Diese zeigt an, wann das Programm aufgrund der aktuellen Fuzzing-Eingabe abstürzt, und ist wahrscheinlich ein speicherbezogener Fehler. Um zu überprüfen, ob es sich um einen echten Fehler handelt, kann erneut gdb verwendet werden, um die Stelle des Fehlers zu finden.
Fuzzing ist möglicherweise der effektivste Weg, um Sicherheitslücken zu finden. In dieser Seminararbeit wurden drei verschiedene Funktionen gefuzzt, aber es wurden keine gefunden. Während das Black-Box-Fuzzing-Setup selbst nicht besonders komplex und zeitaufwendig ist, sind es das Finden eines geeigneten Ziels und die Entwicklung eines funktionierenden Harnesses. Die meiste Zeit muss der Harness debuggt werden, und dann muss die zugrunde liegende Logik in der Binärdatei nachvollzogen (reversed) werden, was wiederum viel Zeit in Anspruch nimmt.