Présentation de LibVMI : lire la mémoire d'une machine virtuelle depuis l'hyperviseur
Catégorie : LibVMI
Résumé
LibVMI est une bibliothèque C qui lit la mémoire et les registres d'une machine virtuelle depuis l'extérieur, sans rien installer dedans. C'est la brique sur laquelle repose DRAKVUF, et c'est elle qui décide si une analyse démarre en 300 ms ou reste bloquée dix minutes. Ce billet explique ce qu'elle fait, comment elle traduit une adresse, comment elle retrouve le noyau Windows avec ou sans profil, ce qu'est le KdDebuggerDataBlock et pourquoi il est encodé sur les Windows récents, et ce qui se passe pendant l'initialisation. C'est le vocabulaire nécessaire pour lire le billet sur le support de Windows 11, Server et 32 bits et la liste de ce que j'ai envoyé au projet.
1. Ce que fait LibVMI
Une VM est, vue de l'hôte, un gros tableau d'octets (sa mémoire physique) et quelques registres par vCPU. LibVMI donne accès aux deux, et surtout il sait leur donner un sens : traduire une adresse virtuelle du guest en adresse physique, lire une structure du noyau à partir de son nom de champ, parcourir la liste des processus, trouver les modules chargés. Un outil comme DRAKVUF n'a pas à savoir où est EPROCESS sur ce build de Windows : il demande à LibVMI.
Trois usages typiques : l'introspection en direct d'une VM Xen ou KVM (le cas DRAKVUF), la lecture d'une image mémoire sur disque (le pilote « file »), et le débogage ou l'enseignement, avec des petits outils en ligne de commande livrés avec la bibliothèque : vmi-process-list, vmi-module-list, vmi-dump-memory, vmi-win-guid.
2. Les pilotes
LibVMI parle à l'hyperviseur par un pilote. Sous Xen, c'est libxc et le vm_event pour les événements ; LibVMI s'exécute dans le dom0 et mappe les pages du guest à la demande. Sous KVM, ça passe par une interface d'introspection côté QEMU. Le pilote « file » lit une image brute, ce qui permet d'exécuter le même code sur une capture mémoire. Dans ce lab, tout est sous Xen 4.21.
Deux fonctions de base : vmi_read_pa() lit à une adresse physique, vmi_get_vcpureg() lit un registre d'un vCPU (CR3, RIP, GS_BASE, MSR_LSTAR, l'IDTR). Tout le reste est construit dessus.
3. Traduire une adresse
Le noyau et les processus travaillent en adresses virtuelles. Pour en lire une, LibVMI doit faire ce que fait le processeur : partir du CR3 (l'adresse physique de la table de pages de niveau supérieur), descendre les quatre niveaux de tables (ou trois en PAE 32 bits, ou deux en mode legacy), et tomber sur la page physique. vmi_translate_kv2p() fait ça pour une adresse du noyau, vmi_translate_uv2p() pour une adresse d'un processus donné. Le mode de pagination (IA-32e, PAE, legacy) est détecté au départ d'après CR0, CR4 et EFER, et stocké dans page_mode. S'il est périmé, par exemple parce qu'on a relu les registres pendant que le guest basculait entre deux modes au démarrage, toutes les traductions suivantes sont fausses.
Point important pour la suite : depuis Windows 10 1709 et le KVA shadowing (la mitigation de Meltdown), un processus en mode utilisateur a une table de pages qui ne mappe presque rien du noyau. Si on prend le CR3 d'un vCPU qui exécute du code utilisateur et qu'on tente de lire une structure du noyau avec, la traduction échoue. Le CR3 du noyau est celui d'un vCPU qui exécute du code noyau, ou celui du processus System.
4. Trouver le noyau Windows
Tout commence par deux inconnues : où est le noyau (KernBase) et quelle table de pages utiliser (kpgd, en pratique le DTB du processus System). Deux chemins.
Avec profil. Un fichier JSON, généré depuis le PDB du build de Windows (le même mécanisme que les symboles de Volatility), donne les offsets de chaque champ et les RVA de chaque symbole du noyau. LibVMI cherche alors la base du noyau par le KPCR : chaque CPU a une structure _KPCR dont l'adresse est dans GS_BASE en mode noyau ; connaissant la RVA de KiInitialPCR dans le PDB, on en déduit KernBase. Si ça ne marche pas, LibVMI essaie MSR_LSTAR (l'adresse du gestionnaire d'appels système, dont il connaît la RVA) puis l'entrée 0 de l'IDT. Ensuite, PsInitialSystemProcess donne l'EPROCESS de System, dont le champ DirectoryTableBase est le kpgd.
Sans profil. LibVMI ne connaît que quelques offsets d'EPROCESS (donnés dans la configuration). Il doit retrouver le reste en mémoire, et pour ça il cherche le KdDebuggerDataBlock.
5. Le KDBG
Le KdDebuggerDataBlock (KDBG) est une structure que le noyau Windows maintient pour le débogueur noyau. Elle commence par un en-tête avec le tag KDBG, et contient les pointeurs dont un débogueur a besoin : KernBase, PsActiveProcessHead (la tête de la liste des processus), PsLoadedModuleList, et une centaine d'autres. Pour un outil d'introspection, c'est une mine : avec PsActiveProcessHead et les offsets d'EPROCESS, on liste les processus sans aucun symbole.
Le bloc est dans la section .data du noyau. LibVMI le cherche de deux façons : à partir du KPCR si un vCPU est en mode noyau (lecture de KernBase, puis scan de .data), sinon par un scan de la mémoire physique à la recherche de l'en-tête PE de ntoskrnl, par pas de 2 Mo, puisque le noyau est normalement chargé en grandes pages alignées.
Depuis Windows 8 en 64 bits, quand le débogueur noyau n'est pas activé, le bloc est encodé : chaque qword est transformé par une rotation, un échange d'octets et un XOR avec une constante propre au démarrage. Le tag KDBG en clair n'existe plus en mémoire. Volatility sait le décoder depuis longtemps, avec les symboles KiWaitNever et KiWaitAlways. LibVMI, jusqu'à octobre 2026, ne savait pas : sans profil, il cherchait le tag en clair, ne le trouvait jamais, et finissait par « All KdDebuggerDataBlock search methods failed » après avoir lu toute la RAM. En 32 bits, le bloc est resté en clair sur tous les builds que j'ai vus.
6. L'initialisation, pas à pas
vmi_init_complete() enchaîne : ouverture du pilote, lecture des registres du vCPU 0, détection du mode de pagination, puis windows_init() qui essaie dans l'ordre le chemin avec profil, puis le chemin KDBG. Chaque étape a un repli, et chaque repli est plus lent que l'étape qu'il remplace. C'est ce qui explique les comportements vus en pratique : une attache qui réussit en 300 ms quand tout est en place, et la même qui prend 200 à 700 s quand un seul maillon manque, parce que LibVMI tombe dans un scan exhaustif de la mémoire, sans message, sans possibilité de l'interrompre.
Le moment de l'attache compte autant que le code. Pendant le démarrage de Windows, bootmgr tourne avec la pagination désactivée, winload l'active et la désactive des dizaines de fois (ses appels au BIOS), le noyau se lance avec PsInitialSystemProcess encore à zéro pendant quelques centaines de millisecondes. Chaque état donne un échec différent, et certains ressemblent à des succès.
7. Configuration et usage
Un fichier /etc/libvmi.conf décrit chaque VM : type d'OS, chemin du profil JSON, ou les offsets d'EPROCESS si on travaille sans profil (win_tasks, win_pdbase, win_pid, win_pname). Côté code, vmi_init_complete() avec le nom de la VM, puis vmi_get_offset(), vmi_read_addr_va(), vmi_pid_to_dtb() et compagnie. vmi_init_paging() doit être rappelé si on réessaie après un échec, sinon page_mode reste à la valeur du premier essai.
Pour générer un profil : récupérer le ntoskrnl.exe du disque de la VM (guestfish, ou qemu-nbd sur les builds où le fichier est compressé), lire le GUID du PDB dans son répertoire de debug, télécharger le PDB chez Microsoft, et le convertir en JSON. Un profil ne vaut que pour ce build exact.
8. Ce que LibVMI ne fait pas
Il ne pose pas de points d'arrêt et ne suit pas l'exécution, c'est le rôle de DRAKVUF au-dessus. Il ne sait pas tout seul que le guest n'a pas fini de démarrer. Il ne gère pas les redémarrages du guest. Et il n'a pas de test d'intégration sur de vrais Windows dans sa CI : ce que la CI vérifie, c'est la compilation sur plusieurs distributions, les avertissements et la complexité du code. Les tests sur 50 invités que je décris dans les autres billets, il a fallu les faire à la main.
Sources
- LibVMI sur GitHub, le code de
os/windows/(core.c,kdbg.c,kpcr.c) etlibvmi.conf.example. - Pour le décodage du KDBG tel que le fait Volatility : les modules
windows.kdbgde Volatility 3.