
Un hyperviseur pour le fuzzing construit avec WHVP et Bochs
Bonjour ! Bienvenue sur applepie ! C'est un outil conçu pour le fuzzing, l'introspection et la recherche de bugs ! C'est un hyperviseur utilisant l'API Windows Hypervisor Platform présente dans les versions récentes de Windows (plus précisément, cela a été développé et testé sur Windows 10 17763). Bochs est utilisé pour fournir une introspection approfondie et l'émulation de périphériques.
L'API Windows Hypervisor Platform (WHVP) est un ensemble d'API pour accéder aux capacités d'hyperviseur de Hyper-V. Cette API nous permet d'implémenter facilement une machine virtuelle entièrement en espace utilisateur sans avoir besoin de pilotes spéciaux ou d'autorisations particulières.
Ce projet se développe rapidement. Je tweeterai probablement lorsque de nouvelles fonctionnalités sortiront avant qu'elles ne soient documentées.
J'aime avoir des objets physiques pour mes projets :
C'est un outil conçu pour le fuzzing et l'introspection lors de recherches en sécurité. En utilisant un hyperviseur, les techniques courantes de fuzzing peuvent être appliquées à n'importe quelle cible, qu'elle soit noyau ou espace utilisateur. Cet environnement permet le fuzzing de systèmes entiers sans nécessiter l'accès au code source de la cible. Au niveau de l'hyperviseur, on peut collecter la couverture de code, et si nécessaire, l'émulation Bochs peut être utilisée pour fournir une introspection arbitraire dans un environnement d'émulation. Ces informations de couverture peuvent être utilisées pour déterminer l'efficacité des cas de fuzzing. Un cas de fuzzing qui provoque une augmentation de la couverture peut être sauvegardé car il s'agit d'un cas intéressant. Cette entrée peut être réutilisée ultérieurement, enrichie par de nouvelles corruptions.
Le fuzzing par snapshot est l'utilisation principale de cet outil. On prend un snapshot d'un système dans un certain état et on le sauvegarde. Ce snapshot peut ensuite être chargé pour le fuzzing, où un cas de fuzzing est injecté, puis on le reprend. Comme la machine virtuelle peut être réinitialisée très facilement, on peut la réinitialiser souvent. Si Word met 5 secondes à démarrer, mais que vous pouvez le capturer juste au moment où il lit votre fichier, vous pouvez réduire le cas de fuzzing à ce qui est pertinent pour une entrée. Cela permet une boucle de fuzzing très serrée sans avoir besoin d'accéder au code source. Comme les machines virtuelles sont totalement séparées, plusieurs peuvent être exécutées en parallèle pour passer à l'échelle sur tous les cœurs.
Actuellement, cet outil prend uniquement en charge la collecte de couverture de code, le téléchargement dynamique de symboles pour Windows et l'analyse des symboles/modules pour les cibles Windows également. Le support du fuzzing arrivera très bientôt.
Étant donné que j'ai déjà écrit presque toutes ces fonctionnalités auparavant (couverture, fuzzing, réinitialisations rapides, etc.), je m'attends à ce que ce projet devienne assez rapidement prêt pour le fuzzing, sauf si je suis distrait :D
Je vise la fin janvier pour la couverture (fait !), les retours, la liste des modules (fait !), les listes de processus, les réinitialisations rapides et le support des symboles (fait !). Cela en ferait un fuzzer très performant.
La cible principale supportée est Windows 10 moderne. Les cibles Windows permettent de télécharger des symboles depuis le store de symboles. Cela permet une couverture symbolique pour les cibles Windows dès le départ. Cependant, le code est écrit de manière à ce qu'un « enlightenment » Linux puisse être facilement ajouté.
Sans aucun « enlightenment », tout système d'exploitation qui démarre peut toujours être fuzzé et une couverture de base peut être collectée.
Avant de signaler des problèmes de support d'OS, veuillez vérifier que le problème vient de l'hyperviseur / des modifications apportées à Bochs en essayant de démarrer votre cible avec un Bochs pré-construit standard sans hyperviseur. Bochs n'est pas couramment utilisé et peut fréquemment avoir des bugs bloquants même pour des choses courantes comme le démarrage de Linux. Surtout avec les changements internes rapides concernant l'utilisation de CPUID/MSR suite aux atténuations Spectre/Meltdown intégrées dans les OS.
Consultez la page des problèmes sur Github pour une liste des problèmes. J'en ai déjà ajouté quelques-uns. Certains doivent être résolus rapidement avant de commencer le développement du fuzzing.
Pour construire ce projet, vous avez besoin de quelques éléments :
Installez Visual Studio 2017 et assurez-vous qu'il est à jour. Nous utilisons des API, en-têtes et bibliothèques de pointe.
J'utilisais la version de cl.exe : Microsoft (R) C/C++ Optimizing Compiler Version 19.16.27025.1 for x64
Et la version du SDK 10.0.17763.0
Installez Rust via https://rustup.rs/. J'utilisais rustc 1.32.0-nightly (b3af09205 2018-12-04)
Assurez-vous d'installer la chaîne d'outils x86_64-pc-windows-msvc car seul le 64 bits est supporté pour ce projet.
Assurez-vous que cargo est dans votre PATH. Cela devrait être le cas par défaut.
Allez chercher Python https://www.python.org/ et assurez-vous qu'il est dans votre PATH afin que python puisse être invoqué.
Installez Cygwin 64 bits (https://www.cygwin.com/setup-x86_64.exe) spécifiquement dans C:\cygwin64. Lors de l'installation de Cygwin, assurez-vous d'installer les paquets autoconf et make.
Allez dans « Activer ou désactiver des fonctionnalités Windows » et cochez la case à côté de « Hyper-V » et « Windows Hypervisor Platform ». Cela nécessite bien sûr que votre ordinateur supporte Hyper-V.
Ce guide d'installation a été vérifié sur la configuration suivante :
Clean install of Windows 10, Build 17763
rustc 1.33.0-nightly (8e2063d02 2019-01-07)
Microsoft (R) C/C++ Optimizing Compiler Version 19.16.27025.1 for x64
Visual Studio Community 2017 version 15.9.4
applepie commit `f84c084feb487e2e7f31f9052a4ab0addd2c4cf9`
Python 3.7.2 x64
git version 2.20.1.windows.1



autoconf)make)git clone https://github.com/gamozolabs/applepiepython build.py
Ce processus de construction initial peut prendre environ 2 minutes, sur une machine moderne c'est probablement 20 à 30 secondes.
Exécutez simplement python build.py depuis le répertoire racine de ce projet. Il devrait vérifier la cohérence de l'environnement et tout devrait « fonctionner ».
Exécutez python build.py clean pour nettoyer les binaires Bochs et Rust.
Exécutez python build.py deepclean pour supprimer complètement tous les binaires Bochs et Rust, ainsi que toute la configuration de Bochs. Utilisez ceci si vous reconfigurez Bochs d'une manière ou d'une autre.
Renseignez-vous sur la configuration de Bochs pour savoir comment configurer votre environnement. Nous avons quelques exigences, comme sync=none, ips=1000000, et actuellement le support d'un seul processeur uniquement. Celles-ci sont appliquées dans le code lui-même pour vous éviter de vous tirer une balle dans le pied.
Utilisez les configurations incluses bochservisor_test\bochsrc.bxrc et bochservisor_test_real\bochsrc.bxrc comme exemples. bochservisor_test_real est probablement la configuration la plus à jour que vous devriez consulter comme référence.
Les cibles Windows bénéficient d'un « enlightenment » de liste de modules, ce qui nous permet de voir les listes de tous les modules dans le contexte où nous exécutons. Grâce à cela, nous pouvons convertir les adresses d'instructions en module + décalage. Ce module + décalage aide à conserver les informations de couverture entre les cas de fuzzing lorsque l'état ASLR change. Cela permet également de colorer le module dans un outil comme IDA pour voir visuellement le code qui a été atteint.
Pour les cibles Windows, les symboles seront téléchargés dynamiquement depuis le store de symboles en utilisant votre _NT_SYMBOL_PATH et symchk. Sans symchk dans le chemin, cela échouera silencieusement. Avec les symboles, une version lisible de la couverture peut être sauvegardée pour consultation. De plus, avec les symboles privés, la couverture peut être convertie en source:ligne de manière à pouvoir colorer le code source.
Bon, il n'y a pas vraiment de tests, mais il y a bochservisor_test qui est un tout petit OS qui vérifie simplement que tout démarre avec l'hyperviseur.
Il y a ensuite bochservisor_test_real qui est une configuration que j'utilise pour des choses comme Windows/Linux. C'est celle qui sera probablement mise à jour le plus fréquemment.
Cette base de code introduit une petite quantité de code dans Bochs pour permettre un accès modulaire au contexte CPU, à la traduction de la mémoire physique invitée vers sa mémoire de sauvegarde, et à l'avancement pas à pas de l'état du périphérique et du CPU.
Le code principal que vous voulez consulter se trouve dans lib.rs du projet Rust bochservisor.
Dans la boucle CPU principale de Bochs, nous chargeons plutôt la DLL bochservisor via LoadLibrary(). Cette DLL exporte une routine qui est la boucle CPU Rust qui sera invoquée.
Bochs passera une structure à cette routine bochs_cpu_loop qui contiendra des pointeurs de fonction pour obtenir des informations de Bochs et pour avancer pas à pas l'état du périphérique et du CPU.
Lorsqu'une MMIO ou une E/S se produit, l'hyperviseur sort avec une erreur mémoire ou une erreur d'instruction d'E/S. Bien que WHVP fournisse une API d'émulation, elle est vraiment insuffisante et pas assez performante.
Nous utilisons plutôt Bochs, qui est déjà là, et nous avançons de quelques instructions. En maintenant l'état CPU de l'hyperviseur synchronisé avec Bochs, nous pouvons basculer dynamiquement entre l'hyperviseur et l'émulation à tout moment (du moins en théorie).
Cela signifie que l'état complet de l'hyperviseur est toujours synchronisé avec Bochs, donc des choses comme les snapshots Bochs devraient fonctionner normalement et pourraient être démarrées sans l'hyperviseur (sauf peut-être pour certains états CPUID qui doivent être stockés dans les informations du snapshot).
Lorsqu'une MMIO ou une E/S se produit, nous exécutons un certain nombre d'instructions sous émulation plutôt que d'en émuler une seule. En raison du coût API d'entrée et de sortie de l'hyperviseur, et de la probabilité que des opérations MMIO similaires se produisent les unes à côté des autres, nous avançons de quelques instructions. Cela nous permet de réduire la surcharge de l'API et de diminuer la fréquence des VMEXIT. C'est un nombre ajustable, mais celui présent dans le codebase a probablement une raison d'être.
Nous traitons les interruptions d'une manière vraiment intéressante. Plutôt que de planifier les interruptions pour qu'elles soient délivrées à l'hyperviseur, nous traitons toutes les interruptions dans l'émulation Bochs elle-même. Les choses comme les exceptions qui se produisent entièrement à l'intérieur de l'hyperviseur ne sont bien sûr pas gérées par Bochs.
Cela nous donne également des fonctionnalités que WHVP ne supporte pas, comme les SMI (pour SMM). Le BIOS de Bochs utilise SMM par défaut et sans support SMI, un BIOS personnalisé doit être construit. J'ai fait cela dans ma première itération... je ne le recommande pas.
Ce projet est conçu pour le fuzzing, mais il est si récent (seulement quelques jours) qu'il ne comporte encore aucune de ces fonctionnalités.
Parmi les premières choses à venir :
Nous pourrions potentiellement faire fonctionner les périphériques Bochs dans un thread en boucle en temps réel, et un autre thread exécuterait l'hyperviseur. Les événements asynchrones seraient communiqués via IPC et permettraient de mettre à jour les périphériques pendant l'exécution dans l'invité.
Actuellement, tout se fait dans un seul thread, ce qui signifie que l'hyperviseur doit sortir à intervalles réguliers pour s'assurer que nous pouvons faire avancer les périphériques. C'est comme si nous avions écrit notre propre ordonnanceur.
Cela pourrait être un peu plus rapide, mais cela augmente aussi la complexité et ajoute des risques de problèmes de concurrence. Il est difficile de dire si cela arrivera un jour.
Je ne sais pas encore quelle méthode j'utiliserai pour collecter la couverture de code, mais il y aura au moins quelques options. Allant de précise à rapide, etc. Tous ces mécanismes de couverture seront au niveau système et ne nécessiteront pas de source ou de symboles des cibles.
Analyse des structures du système d'exploitation pour obtenir des informations primitives telles que les listes de processus, les listes de modules, etc. Cela serait ensuite utilisé pour interroger les PDB afin d'obtenir des informations sur les symboles.
Signaler les crashs de manière significative. Idéalement, des minidumps seraient intéressants car ils pourraient être chargés et traités dans WinDbg. Cela pourrait être assez facile car les DMP ne sont que de la mémoire physique et un contexte processeur, ce que nous avons déjà.
J'ai quelques techniques amusantes pour l'analyse des causes racines qui ont historiquement été couronnées de succès. Je prévois de les intégrer ici.
En suivant les pages modifiées et en restaurant uniquement les éléments modifiés, nous devrions pouvoir réinitialiser les machines virtuelles très rapidement. Cela nous donne la capacité de fuzzer à des vitesses maximales sur tous les cœurs d'une cible système. C'est similaire à ce que j'ai fait dans falkervisor, donc c'est déjà réfléchi et conçu. Il suffit de le porter ici.
Fuzzing extrêmement rapide qui annule l'exécution lorsqu'une MMIO ou une E/S se produit. Cela permet de passer tout le temps CPU dans l'hyperviseur et aucun temps d'émulation. Cela a l'inconvénient de ne pas supporter des choses comme les E/S disque pendant un cas de fuzzing, mais c'est intéressant.
Certains des concepts fondamentaux de ce projet sont d'apporter des modifications minimales à Bochs. Cela nous permet de maintenir la partie Bochs de ce dépôt à jour.
L'objectif est également de déplacer autant de code que possible dans Rust et les DLL pour rendre le système beaucoup plus modulaire et sûr. Cela devrait, espérons-le, réduire les risques de bugs de corruption stupides dans l'hyperviseur lui-même, ce qui entraînerait des résultats de fuzzing invalides.
Actuellement, l'hyperviseur est une DLL et peut être échangé sans modification de Bochs (sauf si l'API FFI change).
Les modifications ultérieures de Bochs lui-même doivent être documentées clairement, et je vais créer prochainement un document pour suivre les modifications de Bochs qui doivent être portées et réévaluées avec les mises à jour de Bochs.