
Fuzzing eingebetteter Systeme mittels Hardware-Breakpoints
Dies ist der begleitende Code für das Paper: 'Fuzzing Embedded Systems using Debugger Interfaces'. Ein Preprint des Papers kann hier gefunden werden https://publications.cispa.saarland/3950/. Der Code ermöglicht es den Nutzern, die im Paper berichteten Ergebnisse zu reproduzieren und zu erweitern. Bitte zitieren Sie das obige Paper, wenn Sie die Ergebnisse berichten, reproduzieren oder erweitern.
.
├── benchmark # Skripte zum Erstellen von Googles Fuzzer-Testsuite und zum Ausführen von Experimenten
├── dependencies # Enthält ein Makefile zur Installation der Abhängigkeiten von GDBFuzz
├── evaluation # Rohdaten der Experimente, die im Paper vorgestellt werden
├── example_firmware # Eingebettete Beispielanwendungen, die für die Evaluierung verwendet werden
├── example_programs # Enthält ein kompiliertes Beispielprogramm und Konfigurationen zum Testen von GDBFuzz
├── src # Enthält die Implementierung von GDBFuzz
├── Dockerfile # Zum Erstellen eines Docker-Images mit allen installierten Abhängigkeiten von GDBFuzz
├── LICENSE # Lizenz
├── Makefile # Makefile zum Erstellen des Docker-Images oder zur lokalen Installation von GDBFuzz
└── README.md # Diese README-Datei
Die Idee von GDBFuzz ist es, Hardware-Breakpoints von Mikrocontrollern als Feedback für coverage-gesteuertes Fuzzing zu nutzen. Dazu wird GDB als generische Schnittstelle verwendet, um eine breite Anwendbarkeit zu ermöglichen. Für die binäre Analyse der Firmware wird Ghidra verwendet. Der Code enthält ein Benchmark-Setup zur Evaluierung der Methode. Zusätzlich sind Beispiel-Firmware-Dateien enthalten.
GDBFuzz ermöglicht coverage-gesteuertes Fuzzing für eingebettete Systeme, kann aber – zu Evaluierungszwecken – auch beliebige Benutzeranwendungen fuzzen. Für das Fuzzing auf Mikrocontrollern empfehlen wir eine lokale Installation von GDBFuzz, um Fuzz-Daten nahtlos an das zu testende Gerät senden zu können.
GDBFuzz wurde unter Ubuntu 20.04 LTS und Raspberry Pi OS 32-bit getestet. Voraussetzungen sind Java und Python3. Erstellen Sie zunächst eine neue virtuelle Umgebung und installieren Sie alle Abhängigkeiten.
virtualenv .venv
source .venv/bin/activate
make
chmod a+x ./src/GDBFuzz/main.py
GDBFuzz liest Einstellungen aus einer Konfigurationsdatei mit den folgenden Schlüsseln.
[SUT]
# Path to the binary file of the SUT.
# This can, for example, be an .elf file or a .bin file.
binary_file_path = <path>
# Address of the root node of the CFG.
# Breakpoints are placed at nodes of this CFG.
# e.g. 'LLVMFuzzerTestOneInput' or 'main'
entrypoint = <entrypoint>
# Number of inputs that must be executed without a breakpoint hit until
# breakpoints are rotated.
until_rotate_breakpoints = <number>
# Maximum number of breakpoints that can be placed at any given time.
max_breakpoints = <number>
# Blacklist functions that shall be ignored.
# ignore_functions is a space separated list of function names e.g. 'malloc free'.
ignore_functions = <space separated list>
# One of {Hardware, QEMU, SUTRunsOnHost}
# Hardware: An external component starts a gdb server and GDBFuzz can connect to this gdb server.
# QEMU: GDBFuzz starts QEMU. QEMU emulates binary_file_path and starts gdbserver.
# SUTRunsOnHost: GDBFuzz start the target program within GDB.
target_mode = <mode>
# Set this to False if you want to start ghidra, analyze the SUT,
# and start the ghidra bridge server manually.
start_ghidra = True
# Space separated list of addresses where software breakpoints (for error
# handling code) are set. Execution of those is considered a crash.
# Example: software_breakpoint_addresses = 0x123 0x432
software_breakpoint_addresses =
# Whether all triggered software breakpoints are considered as crash
consider_sw_breakpoint_as_error = False
[SUTConnection]
# The class 'SUT_connection_class' in file 'SUT_connection_path' implements
# how inputs are sent to the SUT.
# Inputs can, for example, be sent over Wi-Fi, Serial, Bluetooth, ...
# This class must inherit from ./connections/SUTConnection.py.
# See ./connections/SUTConnection.py for more information.
SUT_connection_file = FIFOConnection.py
[GDB]
path_to_gdb = gdb-multiarch
#Written in address:port
gdb_server_address = localhost:4242
[Fuzzer]
# In Bytes
maximum_input_length = 100000
# In seconds
single_run_timeout = 20
# In seconds
total_runtime = 3600
# Optional
# Path to a directory where each file contains one seed. If you don't want to
# use seeds, leave the value empty.
seeds_directory =
[BreakpointStrategy]
# Strategies to choose basic blocks are located in
# 'src/GDBFuzz/breakpoint_strategies/'
# For the paper we use the following strategies
# 'RandomBasicBlockStrategy.py' - Randomly choosing unreached basic blocks
# 'RandomBasicBlockNoDomStrategy.py' - Like previous, but doesn't use dominance relations to derive transitively reached nodes.
# 'RandomBasicBlockNoCorpusStrategy.py' - Like first, but prevents growing the input corpus and therefore behaves like blackbox fuzzing with coverage measurement.
# 'BlackboxStrategy.py', - Doesn't set any breakpoints
breakpoint_strategy_file = RandomBasicBlockStrategy.py
[Dependencies]
path_to_qemu = dependencies/qemu/build/x86_64-linux-user/qemu-x86_64
path_to_ghidra = dependencies/ghidra
[LogsAndVisualizations]
# One of {DEBUG, INFO, WARNING, ERROR, CRITICAL}
loglevel = INFO
# Path to a directory where output files (e.g. graphs, logfiles) are stored.
output_directory = ./output
# If set to True, an MQTT client sends UI elements (e.g. graphs)
enable_UI = False
Eine Beispielkonfigurationsdatei befindet sich in ./example_programs/ zusammen mit einem Beispielprogramm, das mit unserem Fuzzing-Harness in benchmark/benchSUTs/GDBFuzz_wrapper/common/ kompiliert wurde. Starten Sie das Fuzzing für eine Stunde mit dem folgenden Befehl.
chmod a+x ./example_programs/json-2017-02-12
./src/GDBFuzz/main.py --config ./example_programs/fuzz_json.cfg
Wir sehen zuerst die Ausgabe von Ghidra, das die ausführbare Binärdatei analysiert, und anschließend Meldungen, wenn Breakpoints versetzt oder getroffen werden.
Abhängig vom angegebenen output_directory in der Konfigurationsdatei sollte nun ein Ordner trial-0 mit der folgenden Struktur vorhanden sein
.
├── corpus # Ein Ordner, der den Eingabekorpus enthält.
├── crashes # Ein Ordner, der absturzverursachende Eingaben enthält – falls vorhanden.
├── cfg # Der Kontrollflussgraph als Adjazenzliste.
├── fuzzer_stats # Statistiken der Fuzzing-Kampagne.
├── plot_data # Tabelle, die zeigt, zu welcher relativen Zeit in der Fuzzing-Kampagne welcher Basisblock erreicht wurde.
├── reverse_cfg # Der reverse Kontrollflussgraph.
Durch Setzen von start_ghidra = False in der Konfigurationsdatei verbindet sich GDBFuzz mit einer Ghidra-Instanz, die im GUI-Modus läuft. Dazu muss das ghidra_bridge-Plugin manuell aus dem Skript-Manager gestartet werden. Während des Fuzzings werden erreichte Programmblöcke grün hervorgehoben.
Zum Fuzzing von Linux-Benutzeranwendungen nutzt GDBFuzz den standardmäßigen LLVMFuzzOneInput-Einstiegspunkt, der von fast allen Fuzzern wie AFL, AFL++, libFuzzer, ... verwendet wird. In benchmark/benchSUTs/GDBFuzz_wrapper/common gibt es einen Wrapper, mit dem jeder kompatible Fuzz-Harness zu einem eigenständigen Programm kompiliert werden kann, das Eingaben über eine Named Pipe unter /tmp/fromGDBFuzz abruft. Dies ermöglicht es, ein eingebettetes Gerät zu simulieren, das Daten über eine wohldefinierte Eingabeschnittstelle konsumiert, und somit GDBFuzz auf jeder Anwendung auszuführen. Der Einfachheit halber haben wir ein Skript in benchmark/benchSUTs erstellt, das alle Programme aus unserer Evaluierung mit unserem Wrapper kompiliert, wie später erklärt.
HINWEIS: GDBFuzz ist nicht dafür gedacht, Linux-Benutzeranwendungen zu fuzzen. Verwenden Sie stattdessen AFL++ oder andere Fuzzer. Der Wrapper dient nur Evaluierungszwecken, um Benchmarks und Vergleiche in großem Maßstab durchführen zu können!
Die allgemeine Wirksamkeit unseres Ansatzes wird in einem groß angelegten Benchmark gezeigt, der als Docker-Container bereitgestellt wird.
make dockerimage
Um das obige Experiment im Docker-Container auszuführen (für eine Stunde, wie in der Konfigurationsdatei angegeben), binden Sie die Ordner example_programs und output als Volumes ein und starten Sie GDBFuzz wie folgt.
chmod a+x ./example_programs/json-2017-02-12
docker run -it --env CONFIG_FILE=/example_programs/fuzz_json_docker_qemu.cfg -v $(pwd)/example_programs:/example_programs -v $(pwd)/output:/output gdbfuzz:1.0
Ein Ausgabeordner sollte im aktuellen Arbeitsverzeichnis erscheinen mit der oben erläuterten Struktur.
Unsere Evaluierung ist in zwei Teile gegliedert.
GDBFuzz kann mit jedem GDB-Server arbeiten und daher mit den meisten Debug-Probes für Mikrocontroller.
Bezüglich RQ1 aus dem Paper führen wir GDBFuzz auf verschiedenen Mikrocontrollern mit unterschiedlichen Firmwares aus, die sich in example_firmware befinden. Für jedes Experiment führen wir GDBFuzz mit der RandomBasicBlock- und der RandomBasicBlockNoCorpus-Strategie aus. Letztere verhält sich wie Fuzzing ohne Feedback, aber wir können dennoch die erreichte Coverage messen. Zur Beantwortung von RQ1 vergleichen wir die erreichte Coverage der RandomBasicBlock- und der RandomBasicBlockNoCorpus-Strategie. Die entsprechenden Konfigurationsdateien befinden sich in den entsprechenden Unterordnern, und wir erklären nun, wie das Fuzzing auf den vier Entwicklungsboards eingerichtet wird.
GDBFuzz benötigt Zugriff auf einen GDB-Server. In diesem Fall werden das B-L4S5I-IOT01A und sein On-Board-Debugger verwendet. Dieser On-Board-Debugger richtet über das Programm 'st-util' einen GDB-Server ein und ermöglicht den Zugriff auf diesen GDB-Server über localhost:4242.
sudo apt-get install stlink-tools gdb-multiarch
Erstellen und flashen Sie eine Firmware für das STM32 B-L4S5I-IOT01A, zum Beispiel das arduinojson-Projekt.
Voraussetzung: Installieren Sie platformio (pio)
cd ./example_firmware/stm32_disco_arduinojson/
pio run --target upload
Zu Ihrer Information: Platformio hat eine .elf-Datei des SUT unter ./example_firmware/stm32_disco_arduinojson/.pio/build/disco_l4s5i_iot01a/firmware.elf gespeichert. Diese .elf-Datei wird später auch in der Benutzerkonfiguration für Ghidra verwendet.
Starten Sie ein neues Terminal und führen Sie Folgendes aus, um einen GDB-Server zu starten:
st-util
Führen Sie GDBFuzz mit einer Benutzerkonfiguration für arduinojson aus. Wir können Daten über den USB-Anschluss an den Mikrocontroller senden. Der Mikrocontroller leitet diese Daten seriell an das SUT weiter. In unserem Fall ist /dev/ttyACM0 das USB-Gerät zum Mikrocontroller-Board. Wenn Ihr System ein anderes Gerät dem Mikrocontroller-Board zugewiesen hat, ändern Sie /dev/ttyACM0 in der Konfigurationsdatei auf Ihr Gerät.
./src/GDBFuzz/main.py --config ./example_firmware/stm32_disco_arduinojson/fuzz_serial_json.cfg
Fuzzer-Statistiken und Logs befinden sich im Verzeichnis ./output/...
Installieren Sie pyocd:
pip install --upgrade pip 'mbed-ls>=1.7.1' 'pyocd>=0.16'
Stellen Sie sicher, dass 'KitProg v3' auf dem Gerät ist und versetzen Sie das Board in den 'Arm DAPLink'-Modus, indem Sie den entsprechenden Knopf drücken. Starten Sie den GDB-Server:
pyocd gdbserver --persist
Flashen Sie eine Firmware und starten Sie das Fuzzing z.B. mit
gdb-multiarch
target remote :3333
load ./example_firmware/CY8CKIT_json/mtb-example-psoc6-uart-transmit-receive.elf
monitor reset
./src/GDBFuzz/main.py --config ./example_firmware/CY8CKIT_json/fuzz_serial_json.cfg
Erstellen und flashen Sie eine Firmware für den ESP32, zum Beispiel das arduinojson-Beispiel mit platformio.
cd ./example_firmware/esp32_arduinojson/
pio run --target upload
Fügen Sie die folgende Zeile zur openocd-Konfigurationsdatei für den J-Link-Debugger hinzu: jlink.cfg
adapter speed 10000
Starten Sie ein neues Terminal und führen Sie Folgendes aus, um den GDB-Server zu starten:
get_idf
openocd -f interface/jlink.cfg -f target/esp32.cfg -c "telnet_port 7777" -c "gdb_port 8888"
Führen Sie GDBFuzz mit einer Benutzerkonfiguration für arduinojson aus. Wir können Daten über den USB-Anschluss an den Mikrocontroller senden. Der Mikrocontroller leitet diese Daten seriell an das SUT weiter. In unserem Fall ist /dev/ttyUSB0 das USB-Gerät zum Mikrocontroller-Board. Wenn Ihr System ein anderes Gerät dem Mikrocontroller-Board zugewiesen hat, ändern Sie /dev/ttyUSB0 in der Konfigurationsdatei auf Ihr Gerät.
./src/GDBFuzz/main.py --config ./example_firmware/esp32_arduinojson/fuzz_serial.cfg
Fuzzer-Statistiken und Logs befinden sich im Verzeichnis ./output/...
Installieren Sie TI MSP430 GCC von https://www.ti.com/tool/MSP430-GCC-OPENSOURCE
Starten Sie den GDB-Server
./gdb_agent_console libmsp430.so
oder (stabiler). Bauen Sie mspdebug von https://github.com/dlbeer/mspdebug/ und verwenden Sie:
until mspdebug --fet-skip-close --force-reset tilib "opt gdb_loop True" gdb ; do sleep 1 ; done
Ghidra kann Binärdateien für den TI MSP430-Controller standardmäßig nicht analysieren. Um das zu beheben, importieren wir die Datei in der Ghidra-GUI, wählen MSP430X als Architektur und überspringen die Auto-Analyse. Als nächstes öffnen wir die 'Symboltabelle', sortieren sie nach Namen und löschen alle Symbole mit Namen wie $C$L*. Jetzt kann die Auto-Analyse ausgeführt werden. Nach der Analyse starten Sie die ghidra bridge manuell von der Ghidra-GUI aus und starten dann GDBFuzz.
./src/GDBFuzz/main.py --config ./example_firmware/msp430_arduinojson/fuzz_serial.cfg
Um USB-Geräte als Nicht-Root-Benutzer mit pyusb zugreifen zu können, fügen wir entsprechende Regeln zu udev hinzu. Fügen Sie folgende Zeilen in /etc/udev/rules.d/50-myusb.rules ein:
SUBSYSTEM=="usb", ATTRS{idVendor}=="1234", ATTRS{idProduct}=="5678" GROUP="usbusers", MODE="666"
udev neu laden:
sudo udevadm control --reload
sudo udevadm trigger
In RQ2 aus dem Paper vergleichen wir GDBFuzz mit dem emulationsbasierten Ansatz Fuzzware. Zuerst führen wir GDBFuzz und Fuzzware wie zuvor beschrieben auf den mitgelieferten Firmware-Dateien aus. Für jedes GDBFuzz-Experiment erstellen wir eine Datei mit gültigen Basisblöcken aus den Kontrollflussgraphen-Dateien wie folgt:
cut -d " " -f1 ./cfg > valid_bbs.txt
Jetzt können wir die Coverage gegen das Fuzzware-Ergebnis abspielen fuzzware genstats --valid-bb-file valid_bbs.txt
Wenn abstürzende oder hängende Eingaben gefunden werden, werden sie im Ordner crashes gespeichert. Während der Evaluierung haben wir die folgenden drei Fehler gefunden:
GDBFuzz kann auch auf einem Raspberry-Pi-Host mit geringfügigen Änderungen ausgeführt werden:
In der Datei ./dependencies/ghidra/support/launch.sh:125 muss die Variable JAVA_HOME daher fest codiert werden, z.B. auf JAVA_HOME="/usr/lib/jvm/default-java"
Um Software auf anderen Boards zu fuzzen, benötigt GDBFuzz
src/GDBFuzz/connections), die die Ausführung des Codes am Einstiegspunkt auslöst, z.B. serielle VerbindungAlle diese Eigenschaften müssen in der Konfigurationsdatei angegeben werden.
Für die RQs 4-8 führen wir einen groß angelegten Benchmark durch.
Erstellen Sie zunächst das Docker-Image wie zuvor beschrieben und kompilieren Sie Anwendungen aus Googles Fuzzer Test Suite mit unserem Fuzzing-Harness in benchmark/benchSUTs/GDBFuzz_wrapper/common.
cd ./benchmark/benchSUTs
chmod a+x setup_benchmark_SUTs.py
make dockerbenchmarkimage
Passen Sie als nächstes die Benchmark-Einstellungen in benchmark/scripts/benchmark.py und benchmark/scripts/benchmark_aflpp.py an Ihre Anforderungen an (insbesondere number_of_cores, trials und seconds_per_trial) und starten Sie den Benchmark mit:
cd ./benchmark/scripts
./benchmark.py $(pwd)/../benchSUTs/SUTs/ SUTs.json
./benchmark_aflpp.py $(pwd)/../benchSUTs/SUTs/ SUTs.json
Ein Ordner erscheint in ./benchmark/scripts, der Plot-Dateien (Coverage über Zeit), Fuzzer-Statistik-Dateien und Kontrollflussgraph-Dateien für jedes Experiment enthält, wie in evaluation/fuzzer_test_suite_qemu_runs.
GDBFuzz hat eine optionale Funktion, bei der der Kontrollflussgraph der abgedeckten Knoten dargestellt wird. Diese ist standardmäßig deaktiviert. Sie können sie aktivieren, indem Sie die Anweisungen in diesem Abschnitt befolgen und 'enable_UI' in der Benutzerkonfiguration auf 'True' setzen.
Auf dem Host:
Installieren
sudo apt-get install graphviz
Installieren Sie eine aktuelle Version von Node, zum Beispiel Option 2 von hier. Verwenden Sie Option 2 und nicht Option 1. Dies sollte sowohl Node als auch npm installieren. Als Referenz: Unsere Versionsnummern sind (aber neuere Versionen sollten auch funktionieren):
➜ node --version
v16.9.1
➜ npm --version
7.21.1
Installieren Sie die Web-UI-Abhängigkeiten:
cd ./src/webui
npm install
Installieren Sie den Mosquitto-MQTT-Broker, siehe z.B. hier
Aktualisieren Sie die Mosquitto-Broker-Konfiguration: Ersetzen Sie die Datei /etc/mosquitto/conf.d/mosquitto.conf durch den folgenden Inhalt:
listener 1883
allow_anonymous true
listener 9001
protocol websockets
Starten Sie den Mosquitto-Broker neu:
sudo service mosquitto restart
Überprüfen Sie, ob der Mosquitto-Broker läuft:
sudo service mosquitto status
Die Ausgabe sollte den Text 'Active: active (running)' enthalten
Starten Sie die Web-UI:
cd ./src/webui
npm start
Ihr Webbrowser sollte sich automatisch unter 'http://localhost:3000/' öffnen.
Starten Sie GDBFuzz und verwenden Sie eine Benutzerkonfigurationsdatei, in der enable_UI auf True gesetzt ist. Sie können den Docker-Container und das arduinojson-SUT von oben verwenden. Stellen Sie jedoch sicher, dass 'enable_UI' auf 'True' gesetzt ist.
Die in 'blau' abgedeckten Knoten sind abgedeckt. Weiße Knoten sind nicht abgedeckt. Wir zeigen nur nicht abgedeckte Knoten, wenn ihr Elternteil abgedeckt ist (das Zeichnen des vollständigen Kontrollflussgraphen dauert zu lange, wenn der Kontrollflussgraph groß ist).
GDBFuzz ist unter der AGPL-3.0-Lizenz als Open Source verfügbar. Einzelheiten finden Sie in der Datei LICENSE.
Eine Liste der anderen Open-Source-Komponenten, die in GDBFuzz enthalten sind, finden Sie in der Datei 3rd-party-licenses.txt.