Debug ton Windows comme un pro

Written by aaSSfxxx -

Yolo les saloupiauds, aujourd'hui on va parler de debugging de drivers Windows (et donc de WinDBG). Pour se remettre dans le contexte, j'ai décidé de convertir une image disque de mon vieux PC sous Windows XP de mon adolescence en machine virtuelle sous libvirt (Qemu avec l'hyperviseur KVM), et l'opération s'est bien passée. Cependant, lorsque j'ai voulu installer le driver vidéo QXL pour avoir le redimensionnement automatique de l'écran de la VM, le driver refusait mystérieusement de démarrer. J'ai donc décidé de sortir l'artillerie lourde, et analyser le chargement du driver avec WinDBG.

Avant de rentrer dans le vif du sujet, j'en profite pour mentionner ma chaîne Vimeo qui contient quelques vidéo troll avec des bouts de hack puissant dedans.

Maintenant, un peu de musique pour se donner du courage:

Un peu de préparation

Pour faire du kernel debugging sous Windows, nous aurons besoin d'une autre VM Windows munie de WinDBG (comme l'implémentation du protocole KD de radare2/rizin est complètement non fonctionnelle). Si vous êtes sous Windows 10, je recommande d'installer "WinDBG Preview" depuis le Microsoft Store, qui offre une interface beaucoup plus moderne que le WinDBG classique des "Debugging Tools for Windows".

Nous devons également configurer les deux machines virtuelles pour communiquer entre elles via le port série. La procédure étant différente selon le logiciel de virtualisation utilisé, je ne détaillerai pas la procédure dans cet article, votre moteur de recherche préféré est votre ami. Côté Windows XP, on duplique la dernière ligne du fichier boot.ini, puis on rajoute "/debug /debugport=COM1". Le boot.ini ressemblera donc à ceci:

[boot loader]
timeout=30
default=multi(0)disk(0)rdisk(0)partition(1)\WINDOWS
[operating systems]
multi(0)disk(0)rdisk(0)partition(1)\WINDOWS="Microsoft Windows XP Professionnel" /noexecute=optin /fastdetect
multi(0)disk(0)rdisk(0)partition(1)\WINDOWS="Microsoft Windows XP Professionnel" /noexecute=optin /fastdetect /debug /debugport=COM1

ATTENTION !

Cette commande ne s'applique que pour Windows 2000/XP, à partir de Windows Vista, il faut passer par bcdedit en utilisant la commande bcdedit /debug on puis bcdedit /dbgsettings serial debugport:1 baudrate:115200. La suite de l'article ne devrait pas énormément changer ensuite.

Côté Windows 10, on peut désormais lancer WinDBG (je détaillerai les manipulations sur la version "Preview" uniquement). Puis on clique sur l'onglet "Fichier" du ruban, et on sélectionne l'option "Attach to kernel". Sur la page de configuration qui apparaît à côté, on clique sur l'onglet "COM", et on coche "Break on connect" en laissant les autres options telles quelles, comme sur la capture ci-dessous:

On peut maintenant redémarrer la VM Windows XP que l'on va déboguer. Normalement, le menu de démarrage s'affiche et on peut choisir entre "Windows XP" et "Windows XP [débogage activé]". Sélectionnons l'entrée "débogage activé".

Après quelques secondes (ou minutes, le temps de télécharger les PDB du noyau Windows XP), WinDBG devrait nous redonner la main, en affichant quelque chose comme ceci:

Cliquons sur la flèche verte "Go". Un deuxième point d'arrêt est atteint, cliquons à nouveau sur la flèche verte. Windows XP continue de se lancer comme si de rien était, mais nous pouvons désormais l'interrompre et examiner son état à tout moment. Mais dans un premier temps, nous observerons simplement les messages envoyés par le driver. Cependant, les drivers officiels n'incluant ni les symboles de débogage (les fameux PDB), ni les messages, nous devrons recompiler le driver nous-même en mode "checked build" avec le DDK.

Debug logs go brrr

Pour compiler notre driver, nous aurons besoin d'installer Git, afin de cloner le dépôt des sources. Vous pourrez le trouver sur ce lien, et l'installer. N'oubliez pas de cocher l'option pour ajouter Git au PATH de Windows, afin de pouvoir l'utiliser sans problème avec PowerShell ou l'invite de commandes.

Une fois ceci fait, il faudra cloner le dépôt qxl, avec les commandes suivantes:

mkdir c:\sources
cd c:\sources
git clone https://gitlab.freedesktop.org/spice/win32/qxl
cd qxl
git submodule init
git submodule update
cd spice-protocol
git checkout origin/master

Puis, dans le code de xddm\miniport\qxl.c, il faudra remplacer la fonction "DebugPrintV" par celle-ci afin d'avoir des messages de debug propres.

void DebugPrintV(char *log_buf, PUCHAR log_port, const char *message, const char *func, va_list ap)
{
    int n, n_strlen;
    char buffer[1024];

    snprintf(buffer, 1024, QXL_MINIPORT_DEBUG_PREFIX);
    vsnprintf(buffer + 7, 1017, message, ap);
    VideoDebugPrint((0, "%s", buffer));
}

Puis, nous devons nous munir du Windows DDK pour Windows 7, que l'on peut trouver sur le site de Microsoft, pour compiler notre driver. Une fois le DDK téléchargé et installé, nous pouvons exécuter les commandes suivantes:

cd c:\sources\qxl\xddm
set DDKVER=7600.16385.1
scripts\buildAll.bat chk XP

La compilation devrait s'effectuer sans problème, et un répertoire "install_chk_wxp_x86" devrait être apparu dans c:\sources\qxl\xddm. Une fois notre driver "checked" fraîchement recompilé, on place le PDB généré par la compilation dans le dossier de symboles sur la machine sur laquelle WinDBG est lancé (C:\ProgramData\Dbg\sym sur mon système). Puis on désinstalle la version "officielle" pour la remplacer par la version "compilée" via le gestionnaire de périphériques. Et là c'est la catastrophe, mais on voit apparaître ces messages dans WinDBG:

qxlmp: InitRam
qxlmp: InitRam: map ram filed
qxlmp: FindAdapter: findAdapter failed
qxlmp: FindAdapter: write QXL ID failed
qxlmp: FindAdapter: exit 87

La fonction "InitRAM" échoue lors de l'appel à VideoPortMapMemory, et nous devons désormais comprendre pourquoi. C'est là où notre cher ami WinDBG entre en jeu.

Debug ur kernelz like a t4pz

Maintenant que nous avons identifié le coupable, nous pouvons poser un breakpoint sur la fonction InitRam. Cliquons sur le bouton "Break" de WinDBG, puis entrons les commandes suivantes dans la fenêtre "Command" de WinDbg (celle où s'affichent les messages de debug):

bp qxl!InitRam
g

On peut maintenant désactiver puis réactiver le driver QXL, et notre breakpoint sera atteint. Normalement, une fenêtre avec le code source du fichier qxl.c devrait s'ouvrir, avec la définition de fonction surlignée en rouge, comme sur la capture ci-dessous.

On peut supprimer ce breakpoint (en cliquant sur la pastille rouge), puis en définir un autre sur l'appel à VideoPortMapMemory en cliquant sur la zone vide à gauche du code source. Puis on clique sur "Go" pour arriver à ce point d'arrêt. On s'arrête à nouveau, et maintenant on clique sur le bouton "Step Into" pour rentrer dans cette fonction.

Une fois dans la fonction VideoPortMapMemory, nous allons faire un "step-over" (exécution pas à pas sans rentrer dans les appels), afin de localiser plus précisément où les carabistouilles se produisent.

On remarque assez vite que c'est la fonction VIDEOPRT!pVideoPortGetDeviceBase qui échoue, et nous allons devoir l'examiner plus en détail. Nous voilà donc repartis à faire un "step over" dans VideoPortMapMemory, puis un "step into" dans pVideoPortGetDeviceBase. Dans cette fonction, on s'aperçoit que c'est MmMapVideoDisplay qui échoue. On en déduit déjà que le souci est sûrement un problème impliquant le memory manager. Continuons à creuser cette piste en exécutant pas à pas cette fonction, qui ne fait qu'appeler MmMapIoSpace.

En exécutant pas à pas MmMapIoSpace, on se rend compte que la fonction problématique est nt!MiReserveSystemPtes, qui renvoie NULL au lieu de renvoyer un pointeur vers un MMPTE décrivant la zone réservée par le kernel, cf l'implémentation de MmMapIoSpace de ReactOS.

On peut ainsi se douter que notre cher Windows n'arrive pas à réserver assez de "SystemPtes" pour notre driver, pour une raison inconnue. Ces SystemPtes jouent un rôle crucial dans la gestion de la mémoire sous Windows, mais je ne détaillerai pas plus dans cet article par manque de connaissances sur le sujet (mais peut-être que j'en parlerai dans un futur article, qui sait :þ). Notre objectif principal sera de faire en sorte qu'il y ait assez de ces SystemPtes pour que la fonction InitRam aille jusqu'au bout et que le driver se charge.

En fouillant un peu dans les internets, je finis par découvrir une clé de registre bien sympathique: HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management\SystemPages, qui contenait une valeur sortie de nulle part. J'essaie donc de réinitialiser cette valeur à sa valeur par défaut, c'est-à-dire 0, puis redémarrer ma VM XP, et pas de bol, le driver ne se charge toujours pas. Ayant flairé le bon filon, j'inspecte les autres valeurs dans HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management\, et je trouve une autre valeur intéressante, "DisablePagingExecutive" qui était mise à 1, au lieu de 0.

Je repasse donc cette valeur à 0, redémarre ma VM et ô miracle, le driver QXL tant espéré se charge enfin correctement !

En effet, quand la clé "DisablePagingExecutive" est à 0, Windows écrit les pages des sections "pageables" des drivers ou du kernel non utilisées dans le pagefile.sys, pour libérer de la RAM physique et donc des précieux SystemPtes (qui servent à décrire la mémoire virtuelle kernel-land). Cependant, lorsque cette valeur est à 1, Windows ne fait aucun nettoyage, et les pages inutilisées encombrent la RAM pour rien sans pouvoir être "swappées", et on court le risque de manquer de SystemPtes.

Cet article arrive à sa fin, et comme vous pouvez le constater, il est le premier d'une série d'articles (je l'espère, si la Sainte Flemme ne prend pas le dessus) sur le ring0/Windows Internals, en essayant de taper sur des Windows récents cette fois-ci.

That's all folks ! :þ