# Présentation de LibVMI : lire la mémoire d'une machine virtuelle depuis l'hyperviseur

**Catégorie :** LibVMI

---

## Résumé

[LibVMI](https://github.com/libvmi/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](index.html?w=libvmi-support-windows-11-server-32bits.md) et [la liste de ce que j'ai envoyé au projet](index.html?w=libvmi-contributions.md).

## 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](https://github.com/libvmi/libvmi) sur GitHub, le code de `os/windows/` (`core.c`, `kdbg.c`, `kpcr.c`) et `libvmi.conf.example`.
- Pour le décodage du KDBG tel que le fait Volatility : les modules `windows.kdbg` de Volatility 3.
