# hidevm sur Windows 11 et Windows 10 : un chiffre hexa manquant, les offsets fastprox.dll et une course sur ntdll.dll

**Catégorie :** DRAKVUF

---

## Résumé

Le plugin `hidevm` de DRAKVUF masque la VM au malware, entre autres en réécrivant les requêtes WMI `Win32_PerfFormattedData*` au passage dans `fastprox.dll`. Sur Windows 11, il ne produisait aucun événement, sur les six builds, sans erreur. Le billet suit l'enquête dans l'ordre, avec ses fausses pistes : un scénario de test qui ne pouvait pas marcher, LibVMI qui classe Windows 11 en Windows 10, un hook posé sur du code jamais exécuté, un soupçon de course dans la pose des hooks, et finalement une constante `0xFFFFF7800000000` à 15 chiffres au lieu de 16. Ce bug était dans `main` depuis le début, sous `--hidevm-delay`. Au passage : les offsets `ExecQuery` et `ExecQueryAsync` de `fastprox.dll` pour les six builds, une vraie course dans `libusermode` qui abandonnait un processus pour toujours, et la suite sur 14 builds Windows 10.

> Suivi upstream : [drakvuf#1881](https://github.com/tklengyel/drakvuf/pull/1881) (hidevm Windows 11, fusionnée le 30/09), [drakvuf#1882](https://github.com/tklengyel/drakvuf/pull/1882) (course ntdll, fusionnée le 30/09), issue [drakvuf#1883](https://github.com/tklengyel/drakvuf/issues/1883) (commentaire Windows 10 du 06/10). La PR Windows 10 n'est pas ouverte.

---

## Environnement

Hôte habituel (Xeon E5-2683 v4, 32 Go, Debian 13, Xen 4.21.0, `altp2m=1`, `viridian=0`, `cpuid="host,hypervisor=0"`, SMBIOS Dell OptiPlex 7090, réseau isolé). DRAKVUF `main` `071b0f2`, puis `b2f64d8` pour Windows 10. Six VM Windows 11 : 21H2 (22000), 22H2 (22621), 23H2 (22631), 24H2 (26100.1742), 25H2 (26200), 26H1 (28000), snapshot `after-install-cold` chacune.

## 1. Le symptôme

`-a hidevm` : 0 événement sur les six builds, quelle que soit la requête WMI injectée. Pas d'erreur, pas de crash, rien.

## 2. L'enquête, dans l'ordre

**Le scénario de test ne pouvait pas marcher.** Première requête injectée : `Get-WmiObject -Namespace root/wmi -Class MSAcpi_ThermalZoneTemperature`. Résultat écrit dans `C:\Users\Public\wmiresult.txt`, relu avec guestfish : `Not supported`. Le matériel QEMU/Xen n'expose aucune zone thermique ACPI, la requête échoue avant d'atteindre l'IOCTL que `hidevm` surveille. Ça disculpe le scénario, ça ne valide rien.

**Relire la PR d'origine.** `hidevm` ([#1499](https://github.com/tklengyel/drakvuf/pull/1499)) a trois mécanismes indépendants : l'avance de `TickCount` via `--hidevm-delay`, le hook de `IWbemServices::ExecQuery` dans `fastprox.dll` qui réécrit les requêtes `Win32_PerfFormattedData*` et `Win32_PerfRawData*` vers `Win32_BIOS`, et le hook de `NtDeviceIoControlFile` en trois étapes pour la zone thermique. J'avais confondu les deux derniers.

**LibVMI classe Windows 11 en Windows 10.** gdb sur `win_ver` avec 22H2 : `VMI_OS_WINDOWS_10`. `pe2version()` ne voit que 10.0 et `ntbuild2version()` s'arrête à 19041. Le `switch` prend la branche Windows 10 et pose le hook à `0x2b280`, l'offset d'un `fastprox.dll` de Windows 10. Le hook s'installe sans erreur sur du code que Windows 11 n'exécute jamais.

**Premier correctif, et toujours rien.** Lecture de `NtBuildNumber` à `KUSER_SHARED_DATA+0x260`, et une table build vers offset. gdb montre `on_dll_hooked` qui se déclenche (« DLL hooked - done »), mais `ExecQuery_handler` jamais.

**Fausse piste : la course.** Le hook s'installait dans PowerShell, jamais dans `WmiPrvSE.exe`, où la requête est réellement traitée. Hypothèse de course dans la pose des hooks (voir section 5, il y en avait bien une, mais ce n'était pas la cause ici). Découverte au passage de `--userhook-injection-mode` : le mode `pf` par défaut est une machine à états qui a besoin d'environ 27 appels système du même processus pour converger, et sur un processus éphémère ça peut ne jamais finir. Avec ce flag, le hook s'installe dans `WmiPrvSE` (confirmé deux fois). Toujours 0 déclenchement.

**La vraie cause.** En traçant quel offset est réellement utilisé : encore `0x2b280`. La constante `kuser = 0xFFFFF7800000000` a 15 chiffres hexadécimaux au lieu de 16 (`0xFFFFF78000000000`), un facteur 16 d'écart. `vmi_translate_kv2p` échouait, `nt_build_number` restait à 0, et on retombait dans la branche Windows 10. Ce bug était déjà dans `main`, utilisé par `--hidevm-delay`, et personne ne l'avait vu. Corrigé aux deux endroits.

**Deux maillons de plus.** PowerShell (`Get-WmiObject`, `Get-CimInstance`) appelle `IWbemServices::ExecQueryAsync`, une fonction distincte, avec `strQuery` au même rang (troisième argument). Hookée aussi. Et `check_object_name()` ne réagit qu'à `win32_perfformatteddata` et `win32_perfrawdata` : le scénario de test devait donc être `Get-WmiObject -Query "SELECT * FROM Win32_PerfFormattedData_PerfOS_Processor"`.

## 3. Les offsets

PDB de `fastprox.dll` récupérés sur `msdl.microsoft.com` avec le GUID exact de chaque build, lus avec `llvm-pdbutil dump -publics`. x64 seulement.

| Build | `ExecQuery` | `ExecQueryAsync` |
|---|---|---|
| 21H2 (22000) | `0x26FB0` | `0x26E20` |
| 22H2 (22621) | `0x1A6A0` | `0x1A290` |
| 23H2 (22631) | `0x1A6A0` | `0x1A290` (même binaire que 22H2) |
| 24H2 (26100) | `0x363C0` | `0x37000` |
| 25H2 (26200) | `0x36560` | `0x371A0` |
| 26H1 (28000) | `0x3C020` | `0x3B550` |

Build inconnu à partir de 22000 : pas de hook et un message de debug, plutôt qu'un offset faux.

Détail d'extraction sur 24H2 : `ntoskrnl.exe` y est stocké compressé (WOF, CompactOS, tag de reparse `0x80000017`) et guestfish n'arrive pas à le lire. Extraction par `qemu-nbd` et `dissect.ntfs` : les chunks de 8192 octets sont annoncés XPRESS8K mais sont en réalité en LZXPRESS Huffman (vérifié par le `MZ` en tête), et 8 chunks sur 1554 sortent un octet trop courts, bien après le répertoire de debug, sans effet sur le GUID.

## 4. La preuve

Build release par défaut, sans gdb, injection `-m createproc` dans `explorer.exe`, `-a hidevm -a procmon`. Événement `"Reason":"WMI query spoofed"` sur 6 builds sur 6 (25H2 à la deuxième tentative). Bonus inattendu : `WMIADAP.exe`, un composant natif de Windows, a déclenché le masquage tout seul sur sa requête interne `win32_perfrawdata` (22H2, PID 5772).

La PR [#1881](https://github.com/tklengyel/drakvuf/pull/1881) a d'abord eu une CI rouge sur la complexité cognitive (le constructeur passait de 49 à 70). Extraction en trois fonctions nommées, retour à 49, somme à 3783 comme `main`, 34 checks verts. Retest après ce remaniement sur les six builds : 21H2, 22H2, 23H2 et 26H1 à la première tentative, 24H2 à la deuxième, 25H2 à la cinquième (il a fallu `--userhook-injection-mode` et un `-t` plus long). Vérifié en build debug que les échecs n'étaient pas une branche d'erreur (zéro ligne `[HIDEVM]`) : `fastprox.dll` n'était simplement pas chargée dans la fenêtre. Fusionnée le 30 septembre, sans commentaire écrit, juste le déclenchement de Jenkins et le merge.

## 5. La course sur `ntdll.dll`, qui touchait tout le monde

`try_hook_ntdll()` dans `libusermode` cherche le VAD de `ntdll.dll` une seule fois par PID, au premier `NtMapViewOfSection` ou `NtProtectVirtualMemory` observé. Si le VAD n'est pas encore visible, l'échec est mis en cache (`proc_ntdll[pid] = {nullopt, false}`) pour toute la vie du processus. Plus aucun hook, en silence. Ça touche n'importe quel plugin userhook sur n'importe quel processus éphémère, pas seulement `hidevm` ni seulement Windows 11.

Correctif : un champ `ntdll_lookup_retries` dans `module_context_t`, et on relance la recherche tant qu'il est inférieur à 20, en incrémentant sur place (réinitialiser l'entrée ferait boucler). Après 20 échecs, abandon comme avant. Avec un build debug instrumenté sur 22H2 : avant, `WmiPrvSE` reste à `is_hooked=0` sans nouvelle tentative ; après, le VAD est trouvé en quelques essais ; le processus `Registry` (PID 112, sans ntdll) plafonne à 20. La CI a de nouveau tiqué sur la complexité (25 vers 26 à cause d'une lambda), extraite en fonction. PR [#1882](https://github.com/tklengyel/drakvuf/pull/1882), fusionnée le 30 septembre. Je n'ai pas mesuré la fréquence de la course, la PR dit « some fraction of runs », sans compter.

## 6. La suite : Windows 10, 14 builds

Windows 10 n'utilise qu'une paire d'offsets dans `hidevm` : `0x2b280` en x64 et `0x30690` en x86. Ce sont exactement ceux de 1803 (17134.1). Extraction hors ligne sur 14 builds (`qemu-nbd` sur chaque qcow2 éteint, le script WOF ci-dessus, PDB Microsoft, symboles `?ExecQuery@XWbemServices@CWbemSvcWrapper@@`), x64 et SysWOW64, SHA256 archivés. 1903 et 1909 partagent un binaire, 2004 à 22H2 aussi (même sha256, 19041.1).

| `NtBuildNumber` | x64 `ExecQuery` / `Async` | x86 `ExecQuery` / `Async` |
|---|---|---|
| 10240 | `0x2f640` / `0x2f500` | `0xa4b0` / `0xa3c0` |
| 10586 | `0x305b0` / `0x30470` | `0xa3c0` / `0xa2d0` |
| 14393 | `0x2d0f0` / `0x2cfb0` | `0x2dba0` / `0x2dab0` |
| 15063 | `0x4040` / `0x3f00` | `0x30570` / `0x30490` |
| 16299 | `0x2b140` / `0x2b000` | `0x2ff30` / `0x2fe50` |
| 17134 | `0x2b280` / `0x2b140` | `0x30690` / `0x305b0` |
| 17763 | `0x2aae0` / `0x5fa0` | `0x2c8d0` / `0x2c7f0` |
| 18362, 18363 | `0xc8f0` / `0x7210` | `0x2d3e0` / `0x2d2a0` |
| 19041 à 19045 | `0xd740` / `0xd8c0` | `0xc7e0` / `0x9fb0` |

Test avec une branche locale sur `b2f64d8`, `ept=ad=0`, injection de six clients WMI : `wmic` 64 bits, PowerShell `Get-WmiObject` puis `ManagementObjectSearcher.Get(observer)` en asynchrone, et les trois mêmes depuis un `cmd.exe` 32 bits. Résultat : 1803, 1809, 1903, 1909, 2004, 20H2, 21H1, 21H2 et 22H2, 6 clients masqués sur 6 (à partir de 19041, deux événements de plus venant de `WmiApSrv.exe`). `main` non patché sur 1803 : seulement les 4 clients synchrones. 1809 : « Process startup failed » deux fois en release, aussi avec `main`, 6 sur 6 en debug, non déterministe.

Non établi : 1507, 1511, 1607, 1703 et 1709 (`Plugin hidevm startup failed!`, `InitFail`, 0 événement, ou pas d'`explorer.exe` trouvé). Je l'attribue à l'attache instable de DRAKVUF sur ces vieux builds, pas aux offsets, sans l'avoir prouvé. Résultats postés sur [#1883](https://github.com/tklengyel/drakvuf/issues/1883) le 6 octobre. La PR Windows 10 attend un rejeu de la CI et une réponse sur ces cinq builds.

## Ce que je n'ai PAS fait

- Le mécanisme `MSAcpi_ThermalZoneTemperature` n'a jamais été validé positivement : impossible sur ce matériel virtuel.
- WOW64 sur Windows 11.
- Je n'ai pas vérifié si les mises à jour cumulatives remplacent `fastprox.dll`, ce qui déplacerait les offsets.

## Sources

- PR [#1881](https://github.com/tklengyel/drakvuf/pull/1881) et [#1882](https://github.com/tklengyel/drakvuf/pull/1882), issue [#1883](https://github.com/tklengyel/drakvuf/issues/1883).
- Journal d'enquête (`PROFILES_LOG.md`), tables d'offsets et SHA256 des `fastprox.dll`, script d'extraction WOF. En local, fournis sur demande.
- Les chiffres de ce billet ont été recontrôlés contre ce journal.
