# Tester DRAKVUF sur six builds Windows 11, et les correctifs qui en sont sortis

**Catégorie :** DRAKVUF

---

## Résumé

Fin septembre j'ai passé DRAKVUF sur six builds Windows 11, de 21H2 à 26H1 Insider, avec le même scénario à chaque fois, et posté le résultat dans l'issue [drakvuf#1883](https://github.com/tklengyel/drakvuf/issues/1883). Verdict : aucune incompatibilité propre à Windows 11. Tout ce que j'ai cru être un bug était autre chose : un plugin que je n'avais pas instancié, des redémarrages Windows Update, un repli KDBG lent mais déterministe. La première partie donne le protocole, les faux positifs et les pièges d'outillage. La seconde raconte les correctifs sortis de cette période et des campagnes LibVMI qui ont suivi : `--userhook-target-process` ([#1876](https://github.com/tklengyel/drakvuf/pull/1876)), six traps nuls déréférencés, les return hooks vérifiés dans `libhook` ([#1879](https://github.com/tklengyel/drakvuf/pull/1879), [#1880](https://github.com/tklengyel/drakvuf/pull/1880)), `repl` ([#1877](https://github.com/tklengyel/drakvuf/pull/1877)), une macro de debug jamais compilée ([#1878](https://github.com/tklengyel/drakvuf/pull/1878)) et `dkommon` sur 24H2 ([#1884](https://github.com/tklengyel/drakvuf/issues/1884)).

> Suivi upstream : issue [#1883](https://github.com/tklengyel/drakvuf/issues/1883) (ouverte le 28/09, réponse de Tamas le 30/09) ; PR [#1876](https://github.com/tklengyel/drakvuf/pull/1876), [#1877](https://github.com/tklengyel/drakvuf/pull/1877), [#1878](https://github.com/tklengyel/drakvuf/pull/1878), [#1879](https://github.com/tklengyel/drakvuf/pull/1879) fusionnées le 26/09, [#1880](https://github.com/tklengyel/drakvuf/pull/1880) le 27/09 ; issue [#1884](https://github.com/tklengyel/drakvuf/issues/1884) ouverte ; traps nuls en commit local.

---

## Environnement

Hôte habituel (Xeon E5-2683 v4, 32 Go, Debian 13, Xen 4.21.0). VM : `altp2m=1`, `viridian=0`, `cpuid="host,hypervisor=0"`, `xen_platform_pci=0`, SMBIOS Dell OptiPlex 7090, disque IDE, carte e1000, bridge isolé sans sortie. Six builds : 21H2 (22000), 22H2 (22621), 23H2 (22631), 24H2 (26100.1742), 25H2 (26200), 26H1 (28000). TPM et Secure Boot contournés à l'installation (`LabConfig`). Un profil JSON par build (guestfish, `pdbguid.py`, `pdbconv.py` : 40 007 symboles et 1653 types pour un build). Snapshot `after-install-cold` pour chaque VM. Pour les correctifs de la seconde partie : Windows 10 22H2 (4 vCPU, 8 Go), Debian 12, et jusqu'à 13 invités de Windows 7 à Server 2019.

## Partie 1 : six builds Windows 11

### 1. Pourquoi Windows 11 est à part

Deux raisons. Le KVA shadowing, renforcé par VBS et HVCI activés par défaut : si le vCPU que LibVMI choisit exécute du code utilisateur à l'instant de l'attache, la traduction par son CR3 échoue et LibVMI scanne la RAM pendant des minutes (c'est [libvmi#1123](https://github.com/libvmi/libvmi/pull/1123)). Et le nombre de builds, chacun avec son profil.

### 2. Le protocole

1. **Prérequis.** Le profil de ce build précis. Le snapshot propre. Une seule VM allumée.
2. **Fumée.** `-a procmon -t 30`. On attend une centaine de processus. Si 0 événement, ne pas conclure : lire d'abord `xl dmesg` et `xl-<vm>.log`.
3. **Blocage au démarrage.** Reconnaître le domaine en pause (`p` dans `xl list`), Ctrl+C inopérant, `kill -9`. Répéter 10 à 15 attaches pour mesurer la fréquence. `-k <kpgd>` comme filet.
4. **Injection.** `--write-file` seul d'abord, puis `createproc`, avant de combiner avec des plugins.
5. **Plugins.** Un par un, puis groupés. Chaque plugin a son déclencheur et il faut le connaître. Pour `fileextractor`, c'est `FILE_DELETE_ON_CLOSE`, obtenu avec un `.NET FileStream` depuis PowerShell, passé en `-EncodedCommand` (Base64 d'UTF-16LE) pour survivre à la chaîne shell, SSH, injecteur, `cmd`, PowerShell, qui mange les guillemets.
6. **Contrôle Windows 10.** Même scénario sur un invité connu, pour savoir si le problème est Windows 11 ou moi.
7. **Robustesse.** Jamais de `xl destroy` répétés. Lire le code du plugin avant d'appeler « 0 événement » un bug.
8. **Rapport.** What, Test environment, Method, Results, Log excerpts, une colonne par build.

### 3. Ce que la campagne a donné

Fumée, injection, `procmon`, `filetracer`, `regmon`, `syscalls`, `apimon`, `rpcmon`, `codemon` : OK sur les six builds. Le blocage de libvmi#1123 ne s'est jamais produit (environ 15 attaches sur 21H2, 3 sur chacun des autres, avant et après le correctif). `hidevm` : 0 événement, mais c'est un vrai bug, traité dans [son billet](index.html?w=drakvuf-hidevm-windows-11-10.md).

### 4. Deux faux positifs

**`fileextractor` : 0 événement sur six builds.** Je n'avais pas passé le flag `-D <dossier>`. Sans lui, le plugin n'est pas instancié du tout : gdb le prouve, même le constructeur ne break pas. Une fois corrigé, extraction de `test2.tmp` (Size 9) et plus de cent extractions venues de l'activité normale de Windows.

**Un segfault 3 s après `xl create`.** Code retour 139, gdb pointe un pointeur nul dans `find_kdbg_address_fast` (`kdbg.c:767`). C'est mon script qui attachait trop tôt, mais un pointeur nul reste un vrai manque de robustesse, noté dans l'issue.

### 5. Deux non-bugs

**Des redémarrages spontanés** sur 21H2, 23H2 et 26H1, reason code 1, très probablement Windows Update. Pendant le redémarrage, DRAKVUF sort une rafale de `xc_altp2m_change_gfn rc -1` et « Critical error in removing int3 », le même schéma que [drakvuf#1860](https://github.com/tklengyel/drakvuf/issues/1860). Réponse de Tamas le 30 septembre : DRAKVUF devrait détecter le redémarrage de l'invité et s'arrêter proprement, en lisant la même source que `xl`, pas par de la logique VMI. Je ne l'ai pas traité.

**Un repli KDBG lent.** Sur 24H2, 25H2 et 26H1, un démarrage sur 3 à 10 donne une attache à environ 150 s. Ce n'est pas un blocage et c'est déterministe par démarrage : domid 74, 3 attaches lentes sur 3 ; domid 75, 3 rapides sur 3. Mais avec `-t 20`, le timeout est consommé par l'init, DRAKVUF sort rc 0 et 0 événement. La cause est dans [le billet LibVMI](index.html?w=libvmi-support-windows-11-server-32bits.md).

Une dernière découverte : trois injections `createproc` successives dans le même `explorer.exe` le font planter (bureau noir). Une injection par démarrage.

### 6. Les pièges d'outillage

- `drakvuf` sans `-a` active **tous** les plugins.
- Un binaire de build incrémental invalide toute bissection.
- Trois lignes de diff suffisent à déplacer un débordement de pile sous LTO `-O3` : le symptôme bouge, la cause reste.
- ASan et `stdbuf` sont incompatibles.
- `pkill -f` se tue lui-même quand le motif est dans sa propre ligne de commande.
- Pas d'arithmétique bash sur des adresses 64 bits.
- Un seul DRAKVUF par LibVMI : [libvmi#1074](https://github.com/libvmi/libvmi/pull/1074) fait passer `page_info_t` de 128 à 136 octets sans changer la version `0.15.0`, et le mélange donne un canari de pile qui saute au hasard.
- « Plugin dkommon startup failed! » sort avant la boucle principale sur 24H2 et plus (section 12). `-x dkommon` en attendant.
- Un premier `xl create` sur un build neuf peut se figer (deux fois sur 1709, une fois sur 26H1) : la colonne `Time(s)` de `xl list` ne bouge pas. `destroy` puis `create`.
- `vncdo capture` est fiable ; le screendump QMP sort toujours noir sur cet hôte.
- `drakvuf ... | head` : le SIGPIPE tue DRAKVUF sans `drakvuf_close()`, le domaine reste en pause avec le ring `vm_event` actif, et `xl unpause` ne suffit pas.
- `-i <pid>` avec un PID périmé donne un `InitFail` sans plus d'explication.

## Partie 2 : les correctifs

### 7. `--userhook-target-process` (#1876)

L'issue [#1828](https://github.com/tklengyel/drakvuf/issues/1828) demandait d'intégrer un patch de 2019 dont dépend SNIPER, un traceur d'API. En relisant le patch contre `main` `45c7b26`, presque tout était déjà en amont (`drakvuf_find_mmvad`, `get_user_stack32/64`, `get_wow_context`, les return hooks, le chargement des DLL par page fault). Il manquait le filtre : ne hooker que le malware et ses enfants, pas les cent processus de Windows. Le plugin `persmon` de SNIPER n'a pas de licence, je n'ai repris aucun code.

Option répétable. La vérification est dans `perform_hooking()` : les autres processus ne sont pas hookés du tout, ce n'est pas un filtre sur la sortie. Comparaison insensible à la casse sur le nom de base (Windows donne le chemin NT complet). Ascendance par la liste de processus vivante (`drakvuf_find_process`, `drakvuf_get_process_ppid`, 32 niveaux), donc un parent lancé avant l'attache est reconnu. Six fichiers, 89 lignes. Le cache a eu une première version par PID seul, qui a hooké `mscorsvw.exe` par réutilisation de PID : cache par couple (PID, PPID).

Validation : `apimon` avec un seul hook (`ntdll.dll,LdrLoadDll`), `-t 45`, injection de `cmd.exe /c ping -n 3 127.0.0.1` dans `explorer.exe`, un script qui reconstruit la chaîne d'ancêtres de chaque `api_called`. **35 runs, 0 violation, 0 échec.** Sans l'option : 2428 à 4373 événements sur `main`. Cible `explorer.exe` : 54 à 616 événements, 4 à 5 processus, bimodal parce que dans les runs à 610 `explorer.exe` avait planté (`WerFault.exe`, enfant de la cible, correctement suivi) puis redémarré. Cible `cmd.exe` : 43 événements, 3 processus (`cmd`, `conhost`, `ping`). Nom inexistant : 0. Linux : sans effet, 53 000 à 58 000 lignes dans les trois variantes.

La deuxième PR, « hooker un processus déjà en cours », a été mesurée avant d'être écrite. Un patch jetable appelle `drakvuf_request_userhook_on_running_process` N fois sur un `notepad.exe` inactif pendant qu'un PowerShell fait 800 000 `Thread.Sleep(0)`, 3 runs par N : N = 0, 52,8 à 60,4 s ; N = 1, 85,1 à plus de 100 s ; CPU de DRAKVUF de 50 à 90 s. Le trap est sur une adresse physique, donc il tire dans tout processus qui mappe la même page de `ntdll` : des centaines de milliers de hits venus du workload, aucun de `notepad`. Réponse de Tamas le 26 septembre : cible et descendants, pas de victimes déjà en cours, DRAKVUF part d'une ardoise propre. Sujet clos.

### 8. Six traps nuls (correctif local)

`pluginex::register_trap()` renvoie `nullptr` quand le trap ne peut pas être posé (`Could not find EPROCESS struct for pid = 4`, `No DTB found for pid 4`, `Failed to add a trap for NtCreateUserProcess`). Douze appelants vérifiaient, six non : quatre dans `procmon/win.cpp`, deux dans `dkommon.cpp`. `get_trap_params(nullptr)` suivi de `set_result_call_params()` écrit `target_pid` à `this+8`, d'où `segfault at 8`. Vu deux fois sur environ 270 attaches pendant la campagne de [libvmi#1128](https://github.com/libvmi/libvmi/pull/1128), sur Windows 10 21H2 et 2004, backtrace avec `this=0x0` dans `open_process_hook_cb`.

Correctif : `if (!trap) return VMI_EVENT_RESPONSE_NONE;` aux six endroits, 18 lignes, commit local `ac96c35`. La preuve a été le plus long : 360 attaches naturelles n'ont jamais reproduit le cas (avec les deux vCPU saturés en mode utilisateur, presque toute attache se dégrade, mais pas jusqu'au crash). Donc un patch jetable, `DRK_FORCE_TRAP_FAIL=1`, qui force l'échec : sur 9 invités (Windows 7 SP1 x64 et x86, 8.1, Windows 10 1507, 1809 x86, 2004, 22H2, Windows 11 23H2, Server 2019), non corrigé, rc 139 à la même instruction (`0x2625c7`) ; corrigé, rc 0 avec la nouvelle branche prise de 31 à 6 506 fois. `dkommon` avec un vrai `sc create` et `sc delete` injectés : même crash sans, OK avec. Non-régression : 13 invités, 104 runs `procmon` et 52 `dkommon`, 4 binaires, 0 échec. ASan, UBSan et LSan sur 4 invités, 5 runs chacun : 0. Vu à côté, non traité : 15 `createReturnHook()` qui déréférencent sans test (par lecture, pas reproduit), `dkommon::srv_trap[2]` non initialisé (crash dans `fprintf` en debug, 4 runs sur 5 sur 1803), un faux « Hidden Service » quand le trap retour de `delete_service_cb` échoue. Pas poussé : j'attendais la fin de la campagne LibVMI.

### 9. Les return hooks vérifiés dans `libhook` (#1879, #1880)

Un `ReturnHook` est un breakpoint sur l'adresse de retour ; un autre thread, un autre processus ou un appel récursif peut le toucher. Chaque plugin devait appeler `verifyResultCallParams()` à la main, rien ne l'imposait, et `libhooktest` ne le faisait pas (issue [#1584](https://github.com/tklengyel/drakvuf/issues/1584)). `ReturnHook::create()` enregistre PID, TID et RSP, et le callback de trap vérifie avant d'appeler le plugin : deux fichiers, +6/-3.

Tests en alternant référence et nouveau, en build debug, sur Windows 10 22H2 et Debian 12 : `filetracer` 107, `rpcmon` 22, `apimon` 43 événements pour le processus injecté dans les 6 runs (un run de référence à 210, isolé) ; Linux, 27 événements fichiers et 34 lignes `procmon` dans 6 runs sur 6. Sur `libhooktest`, la part des retours appelés passe de 89 à 90 % à 83 à 84 %, cohérent : ce sont les retours d'autres appels, maintenant filtrés. ASan et UBSan à 0. Non couvert : `fileextractor` et `hidevm` (callbacks jamais déclenchés, et pour cause, voir la section 4), `codemon` en fumée.

#1880 supprime ensuite les appels devenus morts dans 10 fichiers (+114/-153), `hidevm` dé-indenté d'environ 190 lignes, vérifié mécaniquement. Résultats identiques avant et après (`rpcmon` 22, `apimon` 43, Linux 27 et 34, `procmon` 160 dans 4 runs sur 4, `codemon` 6 643 lignes). Combiné avec libvmi#1123, sans interaction.

### 10. `repl` (#1877)

Trois problèmes en build meson. `repl` cherche `libdrakvuf.py` à côté de l'exécutable, dans `librepl/`, le layout autotools ; meson le génère dans `src/librepl/`. L'échec se produisait **après** l'attache, avec un `exit(1)` sans `drakvuf_close()` : domaine laissé en pause, et le run suivant disait `vm_event is (or was) active`. Et impossible d'arrêter le REPL proprement : un trap sur chaque écriture de CR3, `exit()` qui rouvre un prompt, Ctrl+C qui est une touche IPython en mode brut. Deux commits fusionnés. Un troisième, Ctrl+C sur ligne vide (`InteractiveShellEmbed`, repli sur `IPython.embed()`), est testé sur IPython 7.34 à 9.17, tmux, `ssh -tt`, Windows 10 et Debian 12 (arrêt en 0,1 à 0,3 s, domaine jamais laissé en pause), et pas publié.

### 11. Une macro qui n'a jamais compilé (#1878)

Trois blocs de `vmi.c` (`_int3_cb`, `_debug_cb`, `_msr_cb`) sous `#ifdef DEBUG`, alors que la macro s'appelle `DRAKVUF_DEBUG` depuis des années. Les traces INT3, debug et MSR n'étaient jamais compilées, même en build debug avec `-v`. Trois lignes. Avant : 0 ligne `INT3 event` ; après : 18 787 puis 2 863 lignes en 25 s. Cinq variantes de CI, 0 avertissement. Les traces Debug et MSR, je ne les ai jamais vues s'imprimer : aucun plugin ne les déclenche ici.

### 12. `dkommon` sur 24H2 et plus (#1884)

`MiProcessLoaderEntry` n'existe plus dans le `ntoskrnl.exe` de 24H2, et `register_trap` dessus est dans le bloc des traps critiques (`throw -1`). 10 runs sur 10 en échec, 3 sur 3 après le correctif, qui sort ce trap du bloc critique avec un message « MiProcessLoaderEntry not found, driver load/unload monitoring disabled ». Issue ouverte le 29 septembre, patch prêt, PR pas ouverte.

## Ce que je n'ai PAS fait

- La détection des redémarrages demandée par Tamas.
- Le pointeur nul de `kdbg.c:767` côté LibVMI.
- Ouvrir les PR des traps nuls et de `dkommon`, et publier le troisième commit de `repl`.
- APC, détournement de thread, hollowing : pas couverts par l'arbre de processus de #1876.

## Sources

- Issue [#1883](https://github.com/tklengyel/drakvuf/issues/1883) ; PR [#1876](https://github.com/tklengyel/drakvuf/pull/1876), [#1877](https://github.com/tklengyel/drakvuf/pull/1877), [#1878](https://github.com/tklengyel/drakvuf/pull/1878), [#1879](https://github.com/tklengyel/drakvuf/pull/1879), [#1880](https://github.com/tklengyel/drakvuf/pull/1880) ; issues [#1584](https://github.com/tklengyel/drakvuf/issues/1584), [#1828](https://github.com/tklengyel/drakvuf/issues/1828), [#1884](https://github.com/tklengyel/drakvuf/issues/1884).
- Le protocole, le journal des profils, les rapports de test par PR, les backtraces gdb. En local, fournis sur demande.
- Les chiffres de ce billet ont été recontrôlés contre ces rapports.
