
Snapshot-basierter, coverage-gesteuerter Windows-Kernel-Fuzzer
Rewind ist ein Snapshot-basierter, coverage-gesteuerter Fuzzer, der auf Windows-Kernel-Komponenten abzielt.
Die Idee ist, von einem Snapshot eines live laufenden Systems auszugehen. Dieser Snapshot besteht aus den physischen Speicherseiten zusammen mit dem Zustand der CPU.
Dieser Zustand wird verwendet, um den Anfangszustand einer virtuellen CPU einzurichten. Durch die Nutzung von On-Demand-Paging werden nur die Seiten aus dem Snapshot gelesen, die für die Ausführung der Zielfunktion benötigt werden.
Da wir eine dedizierte virtuelle Maschine verwenden, die nur die physischen Speicherseiten enthält, die für die Ausführung der Zielfunktion nützlich sind, ist das Wiederherstellen eines Snapshots schnell.
Derzeit sind 2 Backends verfügbar:
WHVP-Backend nutzt die WHVP-API (Windows Hypervisor Platform), um Zugriff auf eine Hyper-V-Partition zu bieten. Siehe
https://docs.microsoft.com/en-us/virtualization/api/hypervisor-platform/hypervisor-platform
für weitere Details.Bochs-Backend nutzt den Bochs-Emulator
(https://bochs.sourceforge.io/)Ein KVM-Backend befindet sich in Entwicklung und sollte bald verfügbar sein.
Rewind bietet 2 Hauptfunktionen:
Es bietet außerdem eine einfache TUI (Terminal User Interface), um nützliche Informationen zum Fuzzing zu melden.
Es wurde unter Windows und Linux getestet (derzeit nur Bochs-Backend für Linux).
Ich hatte immer Freude an der Forschung zu Kernel-Sicherheitslücken, insbesondere am Windows-Kernel. Der Prozess umfasst immer eine Mischung aus statischer und dynamischer Analyse. Dynamische Analyse kann schnell mühsam werden. Der Zyklus Debuggen / Absturz / Neustart / Zurücksetzen aller Breakpoints ist langsam und schmerzhaft. Wenn man fuzzen möchte, muss man oft eine oder mehrere virtuelle Maschinen plus einen Kernel-Debugger einrichten und ein paar improvisierte Skripte für die Absturzerkennung basteln ...
Snapshots mit virtuellen Maschinen zu erstellen hilft, aber es ist langsam.
Im Jahr 2018 führte Microsoft eine neue API-Gruppe namens Windows Hypervisor Platform (WHVP) ein. Diese APIs ermöglichen es, eine Partition (VM in der Hyper-V-Sprache) mit einigen virtuellen Prozessoren einzurichten und die in der virtuellen Maschine auftretenden VM-Exits zu kontrollieren. Es ist fast so, als hätte man seinen eigenen VM-Exit-Handler im Userland. Ziemlich praktisch, um nützliche Dinge zu tun, zum Beispiel Simpleator oder applepie.
Also begann ich, mit WHVP zu spielen, und erstellte einen ersten PoC, der es mir ermöglichte, Shellcode in einer Hyper-V-Partition auszuführen. Er war in Python geschrieben und ziemlich langsam. Dieser erste PoC entwickelte sich ziemlich schnell zu einer Art Snapshot-basiertem Tracer. Ich wollte etwas, das die virtuelle CPU bootstrapt und ziemlich einfach einzurichten ist. Da ich bereits einen Kernel-Debugger für mein Ziel verwendete, beschloss ich, mit WinDbg erstellte Kernel-Dumps als meinen Snapshot zu verwenden. Damit musste ich nur eine Partition mit einer virtuellen CPU einrichten. Der Kontext der virtuellen CPU wird mit dem Kontext aus dem Dump gesetzt. Wann immer die virtuelle CPU eine physische Seite benötigt, verwende ich die aus dem Dump.
Damit konnte ich den Zustand des Dumps in eine Partition forken und dann die Ausführung fortsetzen. Es ermöglichte mir, die Ausführung meiner Zielfunktion einfach zu verfolgen. Durch das Ändern der Argumente und das Zurücksetzen des Speicherzustands der Partition war es auch wirklich einfach, das Ziel zu fuzzen.
Das Tool implementiert 2 Möglichkeiten, um die Coverage zu erhalten. Die erste nutzt das klassische TF (Trap Flag), um bei jeder Anweisung INT1-Unterbrechungen zu erhalten. Es erfordert, das Ziel zu modifizieren, und es ist langsam. Ich hätte das MONITOR-Trap-Flag bevorzugt. Aber WHVP bietet diese Möglichkeit nicht.
Um eine angemessene Leistung zu erzielen (die für das Fuzzing erforderlich ist), habe ich beschlossen, die Präzision der Coverage zu reduzieren und einen Modus hinzuzufügen, bei dem man nur weiß, wann eine Anweisung zum ersten Mal ausgeführt wird.
Dazu patche ich die aus dem Snapshot geholten Seiten mit 0xcc-Bytes (nur für ausführbare Seiten). Wenn die CPU diese gepatchten Anweisungen ausführt, fängt der Hypervisor die Ausnahme ab und schreibt die Anweisungen mit dem ursprünglichen Code zurück.
Es ist, als ob auf jeder Anweisung ein einzelner Software-Breakpoint gesetzt wäre. Es funktioniert 95 % der Zeit, aber bei bestimmten Codestücken (z. B. solchen mit Sprungtabellen) schlägt es fehl, weil Daten ersetzt werden.
Um dies zu überwinden, wäre eine Option, den Code zu disassemblieren, bevor man ihn mappt, und nur das zu patchen, was benötigt wird (vielleicht beim nächsten Mal).
Während meines Experiments bin ich bei der Verwendung von WHVP auf mehrere Einschränkungen gestoßen. Es ist langsam, wirklich langsam. Der Quellcode von VirtualBox enthält einige interessante Kommentare :)
Um also eine angemessene Leistung zu erzielen, muss man VM-Exits wirklich begrenzen, und das ist inkompatibel, wenn man Hyper-V als Tracing-Hypervisor verwenden möchte (da dies viele VM-Exits erfordert).
Zur gleichen Zeit begann ich, bochs (insbesondere den Instrumentierungsteil) zu verwenden, um zu überprüfen, ob die vom Tool erhaltenen Traces korrekt waren. Bochs war eine Art Orakel, um zu sehen, ob ich divergierende Traces hatte.
Bochs ist schneller als WHVP, wenn man einen vollständigen Trace durchführt, und man hat außerdem den Vorteil, Speicherzugriffe und andere nützliche Dinge zu erhalten.
Ich habe beschlossen, Bochs als weiteres Backend hinzuzufügen. whvp war kein passender Name mehr, und ich entschied mich für rewind.

rewind wurde um meinen eigenen Arbeitsablauf herum entwickelt, wenn ich Sicherheitsbewertungen für Kernel-Treiber auf der Windows-Plattform durchführe.
Der erste Schritt ist, die Zielsoftware in einer virtuellen Maschine zu installieren. Da ich eine Mischung aus statischer und dynamischer Analyse verwende, werde ich auch einen Kernel-Debugger einrichten.
Nachdem ich einige zufällige Treiber in IDA geöffnet habe, beginne ich schnell, einige Funktionen ins Visier zu nehmen. Dazu setze ich normalerweise einige Breakpoints mit windbg und kann, kombiniert mit ret-sync, loslegen.
Genau hier kommt rewind ins Spiel. Statt zufällige Puffer im Speicher zu bearbeiten, einzelne Schritte auszuführen und die IDB zu annotieren, um eine grobe Vorstellung davon zu bekommen, was vor sich geht, mache ich einen Snapshot mit windbg und verwende stattdessen rewind.
Das wird den Prozess erheblich erleichtern. Ein Snapshot bietet viele Vorteile. Alles ist deterministisch. Man kann einen Funktionsaufruf beliebig oft wiederholen. Man kann einen Fuzzer starten, wenn die Zielfunktion interessant aussieht. Man kann sogar die VM schließen, da sie nicht mehr benötigt wird.
Offensichtlich benötigst du Rust (die Installation wurde unter Windows und Linux mit Rust 1.50 getestet). CMake wird ebenfalls von einigen Abhängigkeiten benötigt.
Klone zuerst das Repository:
$ git clone [email protected]:quarkslab/rewind.git
Fahre mit der Installation des Bochs-Backends fort
Klone das Repository bochscpu (https://github.com/yrp604/bochscpu) in das vendor-Verzeichnis:
$ cd vendor
$ git clone https://github.com/yrp604/bochscpu
Lade die vorgebauten Bochs-Artefakte von bochscpu-build (https://github.com/yrp604/bochscpu-build) herunter
$ curl.exe -L --output bochs-x64-win.zip [artifact_url]
Entpacke die Ordner lib und bochs in das Bochscpu-Checkout.
$ Expand-Archive -Path .\bochs-x64-win.zip -DestinationPath .\
$ copy -Recurse .\bochs-x64-win\msvc\* .\bochscpu\
Unter Windows wird WHVP ebenfalls als Backend gebaut.
Führe in einer PowerShell-Sitzung mit Administratorrechten den folgenden Befehl aus, um zu prüfen, ob WHVP aktiviert ist:
Get-WindowsOptionalFeature -FeatureName HypervisorPlatform -Online
FeatureName : HypervisorPlatform
DisplayName : Windows Hypervisor Platform
Description : Enables virtualization software to run on the Windows hypervisor
RestartRequired : Possible
State : Enabled
CustomProperties :
Falls es nicht aktiviert ist, kannst du das Cmdlet Set-WindowsOptionalFeature verwenden, um es zu aktivieren. Du musst außerdem Hyper-V aktivieren.
Du benötigst außerdem ein installiertes Windows SDK (10.0.19041.0). Du kannst es von https://developer.microsoft.com/fr-fr/windows/downloads/windows-10-sdk/ herunterladen.
Du musst LLVM installieren und die Umgebungsvariable LIBCLANG_PATH setzen (von bindgen benötigt). Eine detaillierte Erklärung findest du unter https://rust-lang.github.io/rust-bindgen/requirements.html.
$ $env:LIBCLANG_PATH="C:\Program Files\LLVM\bin"
Von dort aus solltest du rewind bauen können (nightly erforderlich wegen unwind_attributes in der bochscpu-Crate):
$ cd rewind_cli
$ cargo +nightly build --release
Die rewind-Binärdatei wird im Verzeichnis target/release verfügbar sein.
Du kannst cargo auch verwenden, um lokal zu installieren:
$ cd rewind_cli
$ cargo +nightly install --path .
> error: failed to run custom build command for `zydis v3.1.1`
whvp-sys fehlEin grundlegendes Tutorial, das CVE-2020-17087 nutzt, findest du im Verzeichnis examples
Siehe TODO.md
hit verhält sich der Tracer bei einigen Funktionen fehlerhaft (das ist bei einigen Switch-Tabellen der Fall). Der Grund ist, dass jedes Byte durch Software-Breakpoints ersetzt wird (einschließlich Daten, wenn sie in einer ausführbaren Seite vorhanden sind). Ein besserer Weg wäre, die Liste aller Basisblöcke beispielsweise von einem Disassembler zu erhalten.Dieses Tool wird derzeit von Quarkslab unter der Apache-2.0-Lizenz entwickelt und gesponsert.
Ein Gruß an @yrp604, @0verclk0, Alexandre Gazet für ihre Hilfe, ihr Feedback und ihre Gedanken. Danke auch an alle meine Kollegen bei Quarkslab!