
Fuzzer de noyau Windows guidé par la couverture et basé sur des instantanés
Rewind est un fuzzer orienté couverture basé sur des snapshots ciblant les composants du noyau Windows.
L'idée est de partir d'un snapshot d'un système en cours d'exécution. Ce snapshot est composé des pages de mémoire physique ainsi que de l'état du cpu.
Cet état est utilisé pour configurer l'état initial d'un cpu virtuel. En exploitant la pagination à la demande, seules les pages nécessaires à l'exécution de la fonction cible sont lues depuis le snapshot.
Comme nous utilisons une machine virtuelle dédiée avec uniquement les pages de mémoire physique utiles à l'exécution de la fonction cible, la restauration d'un snapshot est rapide.
À l'heure actuelle, 2 backends sont disponibles :
WHVP backend utilise l'API WHVP (Windows Hypervisor Platform) pour
fournir un accès à une partition Hyper-V. Voir
https://docs.microsoft.com/en-us/virtualization/api/hypervisor-platform/hypervisor-platform
pour plus de détails.Bochs backend utilise l'émulateur Bochs
(https://bochs.sourceforge.io/)Un backend KVM est en cours de développement et devrait bientôt être disponible.
Rewind fournit 2 fonctionnalités principales :
Il fournit également une TUI (interface utilisateur terminal) de base pour rapporter des informations utiles concernant le fuzzing.
Il a été testé sur Windows et Linux (uniquement le backend bochs pour linux pour l'instant).
J'ai toujours aimé faire de la recherche de vulnérabilités dans le noyau, en particulier le noyau Windows. Le processus implique toujours un mélange d'analyse statique et dynamique. Faire de l'analyse dynamique peut rapidement devenir fastidieux. Le cycle debug / crash / reboot / reset de tous les points d'arrêt est lent et pénible. Quand vous voulez faire du fuzzing, cela nécessite souvent de configurer une ou plusieurs machines virtuelles plus un débogueur noyau et d'écrire des scripts bricolés pour gérer la détection de crash...
Faire des snapshots avec des machines virtuelles aide, mais c'est lent.
En 2018, Microsoft a introduit un nouvel ensemble d'API nommé Windows Hypervisor Platform (WHVP). Ces API permettent de configurer une partition (VM dans le langage hyper-V) avec des processeurs virtuels et d'avoir un contrôle sur les VM exits qui se produisent dans la machine virtuelle. C'est presque comme avoir votre propre gestionnaire de VM-exit en espace utilisateur. Très pratique pour faire des choses utiles, par exemple Simpleator ou applepie.
J'ai donc commencé à jouer avec WHVP et j'ai fait un premier PoC me permettant d'exécuter du shellcode dans une partition Hyper-V. Il était écrit en Python et assez lent. Ce premier PoC a assez rapidement évolué vers une sorte de traceur basé sur des snapshots. Je voulais avoir quelque chose pour amorcer le CPU virtuel et assez facile à configurer. Comme j'utilisais déjà un débogueur noyau pour jouer avec ma cible, j'ai décidé d'utiliser des kernel dumps réalisés avec WinDbg comme snapshot. Avec cela, j'avais juste besoin de configurer une partition avec un cpu virtuel. Le contexte du cpu virtuel est défini avec le contexte extrait du dump. Chaque fois que le cpu virtuel a besoin d'une page physique, j'utilise celles du dump.
Avec cela, j'ai pu forker l'état du dump dans une partition puis reprendre l'exécution. Cela m'a permis de tracer facilement l'exécution de ma fonction cible. En modifiant les arguments et en restaurant l'état mémoire de la partition, il était aussi très facile de fuzzer la cible.
L'outil implémente 2 possibilités pour obtenir la couverture. La première exploite le TF (Trap Flag) classique pour avoir des interruptions INT1 sur chaque instruction. Cela nécessite de modifier la cible et c'est lent. J'aurais préféré utiliser le trap flag MONITOR. Mais WHVP n'offre pas cette possibilité.
Afin d'avoir des performances correctes (requises pour le fuzzing), j'ai décidé de réduire la précision de la couverture et d'ajouter un mode où l'on sait uniquement quand une instruction est exécutée pour la première fois.
Pour cela, je patche les pages extraites du snapshot avec des octets 0xcc (uniquement pour les pages exécutables). Quand le cpu exécutera ces instructions patchées, l'hyperviseur interceptera l'exception et réécrira les instructions avec le code original.
C'est comme avoir un point d'arrêt logiciel unique posé sur chaque instruction. Cela fonctionne 95% du temps, mais dans certains morceaux de code particuliers (ceux avec des jump tables par exemple), cela échouera car des données seront remplacées.
Pour contourner cela, une option serait de désassembler le code avant de le mapper et de ne patcher que ce qui est nécessaire (peut-être la prochaine fois).
Au cours de mon expérience, j'ai rencontré plusieurs limitations lors de l'utilisation de WHVP. C'est lent, vraiment lent. Le code source de VirtualBox contient des commentaires intéressants :)
Donc pour avoir des performances correctes, il faut vraiment limiter les VM exits et c'est incompatible si vous voulez utiliser Hyper-V comme hyperviseur de traçage (car cela nécessite beaucoup de VM exits).
Au même moment, j'ai commencé à utiliser bochs (en particulier la partie instrumentation) pour vérifier si les traces obtenues par l'outil étaient correctes. Bochs était une sorte d'oracle pour voir si j'avais des traces divergentes.
Bochs est plus rapide que WHVP lors d'un traçage complet et vous avez aussi l'avantage d'avoir les accès mémoire plus d'autres bonnes choses utiles.
J'ai décidé d'ajouter bochs comme autre backend. whvp n'était plus un nom approprié et j'ai opté pour rewind.

rewind a été conçu autour de mon propre flux de travail lorsque je mène des évaluations de sécurité pour des pilotes noyau sur la plateforme Windows.
La première étape consiste à installer le logiciel cible dans une machine virtuelle. Comme j'utilise un mélange d'analyse statique et dynamique, je configure aussi un débogueur noyau.
Après avoir ouvert quelques pilotes au hasard dans IDA, je commence rapidement à cibler certaines fonctions. Pour cela, je place habituellement des points d'arrêt avec windbg et combiné avec ret-sync, je peux commencer à jouer.
C'est là que rewind entre en jeu. Au lieu de modifier des buffers au hasard en mémoire, de faire du pas à pas et d'annoter l'IDB pour avoir une idée approximative de ce qui se passe, je prends un snapshot avec windbg et j'utilise rewind à la place.
Cela facilitera grandement le processus. Avoir un snapshot offre beaucoup d'avantages. Tout est déterministe. Vous pouvez rejouer ad nauseum un appel de fonction. Vous pouvez lancer un fuzzer si la fonction cible semble intéressante. Vous pouvez même fermer la VM car elle n'est plus nécessaire.
Évidemment, vous avez besoin de Rust (installation testée sur Windows et Linux avec Rust 1.50). CMake est également nécessaire pour certaines dépendances.
Commencez par cloner le dépôt :
$ git clone [email protected]:quarkslab/rewind.git
Continuez avec l'installation du backend bochs
Clonez le dépôt bochscpu (https://github.com/yrp604/bochscpu) dans le répertoire vendor :
$ cd vendor
$ git clone https://github.com/yrp604/bochscpu
Téléchargez les artefacts bochs précompilés depuis bochscpu-build (https://github.com/yrp604/bochscpu-build)
$ curl.exe -L --output bochs-x64-win.zip [artifact_url]
Extrayez les dossiers lib et bochs dans le checkout bochscpu.
$ Expand-Archive -Path .\bochs-x64-win.zip -DestinationPath .\
$ copy -Recurse .\bochs-x64-win\msvc\* .\bochscpu\
Sous Windows, WHVP sera également compilé comme backend.
Dans une session PowerShell avec élévation de privilèges, utilisez la commande suivante pour vérifier si WHVP est activé :
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 :
S'il n'est pas activé, vous pouvez utiliser l'applet de commande Set-WindowsOptionalFeature pour l'activer. Vous devrez également activer Hyper-V.
Vous devez également avoir un Windows SDK (10.0.19041.0) installé. Vous pouvez le télécharger depuis https://developer.microsoft.com/fr-fr/windows/downloads/windows-10-sdk/.
Vous devez installer LLVM et définir la variable d'environnement LIBCLANG_PATH (requise par bindgen). Voir https://rust-lang.github.io/rust-bindgen/requirements.html pour une explication détaillée.
$ $env:LIBCLANG_PATH="C:\Program Files\LLVM\bin"
À partir de là, vous devriez pouvoir compiler rewind (nightly requis à cause de unwind_attributes dans la crate bochscpu) :
$ cd rewind_cli
$ cargo +nightly build --release
Le binaire rewind sera disponible dans le répertoire target/release.
Vous pouvez également utiliser cargo pour installer localement :
$ cd rewind_cli
$ cargo +nightly install --path .
> error: failed to run custom build command for `zydis v3.1.1`
whvp-sys échouera à la compilationUn tutoriel de base exploitant CVE-2020-17087 est fourni dans le répertoire examples
Voir TODO.md
hit, le traceur se comporte mal sur certaines fonctions (c'est le cas avec certaines switch tables). La raison est que chaque octet est remplacé par des points d'arrêt logiciels (y compris les données si elles sont présentes dans une page exécutable). Une meilleure façon de faire serait d'obtenir la liste de tous les blocs de base à partir d'un désassembleur par exemple.Cet outil est actuellement développé et sponsorisé par Quarkslab sous la licence Apache 2.0.
Salutations à @yrp604, @0verclk0, Alexandre Gazet pour leur aide, leurs retours et leurs réflexions. Merci aussi à tous mes collègues chez Quarkslab !