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 (hidevm Windows 11, fusionnée le 30/09), drakvuf#1882 (course ntdll, fusionnée le 30/09), issue drakvuf#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) 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 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, 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 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_ThermalZoneTemperaturen'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.