Objectif
Ce cours regroupe les techniques essentielles de debugging Windows en user mode et en kernel mode, l’utilisation de WinDbg, la validation de drivers avec Driver Verifier, ainsi que les mécanismes de logging et de tracing comme DbgPrintEx et ETW.
L’objectif est de disposer d’un workflow cohérent pour diagnostiquer un programme natif ou un driver : observer, instrumenter, provoquer les erreurs latentes, analyser les crashes et confirmer la correction.
1. Debugging user mode et kernel mode
Le debugging user mode agit sur un processus utilisateur.
Le debugging kernel agit sur le noyau, les drivers et les objets système.
La différence principale est l’impact d’une erreur :
- en user mode, un bug provoque généralement le crash d’un processus ;
- en kernel mode, une erreur peut provoquer un bugcheck et arrêter tout le système.
Pour l’apprentissage et les tests de drivers, une VM avec snapshot est fortement recommandée.
2. Outils principaux
Les outils importants sont :
- : debugger principal pour Windows ;
Code: Select all
WinDbg - : vérifie dynamiquement le comportement d’un driver ;
Code: Select all
Driver Verifier - : observation des processus, handles, modules et threads ;
Code: Select all
Process Explorer - : observation filesystem, registre et processus ;
Code: Select all
Process Monitor - : activation de paramètres de debug ;
Code: Select all
GFlags - : vérification de dumps ;
Code: Select all
DumpChk - : analyse de fuites mémoire user mode ;
Code: Select all
UMDH - : visualisation de traces ETW.
Code: Select all
TraceView
Une session de debugging contient généralement :
Code: Select all
Host
|
| connexion de debug
v
Target
Le target est la machine ou le processus observé.
En user mode, host et target peuvent être la même machine.
En kernel mode complet, il est courant d’utiliser une VM comme target et la machine principale comme host.
4. Types de commandes WinDbg
WinDbg utilise plusieurs familles de commandes.
Les commandes natives du debugger :
Code: Select all
r
u
k
lm
bp
bl
Code: Select all
.symfix
.sympath
.reload
Code: Select all
!
Code: Select all
!process
!thread
!irp
!pool
!locks
!verifier
!analyze -v
Les symboles sont essentiels pour obtenir des noms de fonctions, types, structures et variables.
Sans symboles, une stack ressemble davantage à :
Code: Select all
fffff800`12345678
fffff800`1234abcd
Code: Select all
nt!KeBugCheckEx
MyDriver!DispatchDeviceControl
MyDriver!HandleRequest
Code: Select all
.symfix
.reload
Code: Select all
.symfixPuis
Code: Select all
.reload6. Modules
La commande
Code: Select all
lmPour obtenir davantage de détails :
Code: Select all
lmv
lmvm MyDriver
- adresse de base ;
- taille ;
- nom ;
- chemin ;
- symboles ;
- timestamp ;
- version.
La commande
Code: Select all
rLes plus importants sont :
Code: Select all
RAX
RBX
RCX
RDX
R8
R9
RSP
RBP
RIP
Code: Select all
RIPCode: Select all
RSPCode: Select all
RAX8. Convention d’appel x64 Windows
Les quatre premiers arguments d’une fonction sont généralement passés dans :
Code: Select all
RCX = argument 1
RDX = argument 2
R8 = argument 3
R9 = argument 4
La valeur de retour se trouve dans
Code: Select all
RAXExemple :
Code: Select all
CreateFileW(
lpFileName,
dwDesiredAccess,
dwShareMode,
...
);
Code: Select all
RCX = lpFileName
RDX = dwDesiredAccess
R8 = dwShareMode
Les commandes
Code: Select all
d*Exemples :
Code: Select all
db adresse
dw adresse
dd adresse
dq adresse
da adresse
du adresse
Code: Select all
dbCode: Select all
dqCode: Select all
daCode: Select all
duExemple :
Code: Select all
du @rcx
Code: Select all
RCX10. Désassemblage
La commande
Code: Select all
uExemple :
Code: Select all
u ntdll!NtCreateFile
Code: Select all
mov
call
jmp
cmp
test
jne
syscall
ret
11. Call stack
Les commandes :
Code: Select all
k
kb
kv
Code: Select all
kCode: Select all
kvUne stack permet de répondre à la question :
comment le programme est-il arrivé jusqu’à cette fonction ?
C’est souvent l’information la plus importante lors d’un crash.
12. Threads
La commande
Code: Select all
~On peut changer de thread puis analyser sa stack.
L’objectif est de repérer :
- un thread bloqué ;
- un thread en attente ;
- un thread fautif ;
- un thread qui détient un verrou.
En kernel debugging, le contexte processus est important.
La commande
Code: Select all
!processElle peut révéler :
- EPROCESS ;
- PID ;
- PEB ;
- threads ;
- token ;
- handle count ;
- working set ;
- temps CPU.
14. PEB et EPROCESS
Le
Code: Select all
PEBIl contient notamment :
- informations de loader ;
- liste des modules ;
- paramètres du processus ;
- adresse de base de l’image.
Code: Select all
EPROCESSIl contient des informations beaucoup plus larges sur le processus du point de vue du noyau.
Le schéma mental :
Code: Select all
User mode -> PEB
Kernel mode -> EPROCESS
Deux méthodes principales :
- lancer directement un programme avec WinDbg ;
- attacher WinDbg à un processus déjà lancé.
On peut poser des breakpoints, inspecter les registres, la mémoire et les stacks.
16. Breakpoints
Les commandes essentielles sont :
Code: Select all
bp
bu
bl
bc
bd
be
Code: Select all
bpCode: Select all
buCode: Select all
blCode: Select all
bcCode: Select all
bdCode: Select all
be17. Breakpoint sur CreateFileW
Exemple :
Code: Select all
bp kernel32!CreateFileW
g
Code: Select all
r rcx
du @rcx
La stack :
Code: Select all
k
Code: Select all
CreateFileW18. De CreateFileW à NtCreateFile
Les API Win32 finissent souvent par appeler une API native.
Exemple conceptuel :
Code: Select all
CreateFileW
|
v
KernelBase
|
v
ntdll!NtCreateFile
|
v
syscall
|
v
kernel
Code: Select all
ntdll!NtCreateFile19. Stub syscall
Une fonction native x64 ressemble conceptuellement à :
Code: Select all
mov r10, rcx
mov eax, ServiceNumber
syscall
ret
Code: Select all
EAXCes numéros ne doivent pas être codés en dur comme une interface stable.
20. Retour NTSTATUS
Une fonction native retourne généralement un
Code: Select all
NTSTATUSCode: Select all
RAXExemple :
Code: Select all
r rax
Code: Select all
if (!NT_SUCCESS(status))
{
...
}
Le kernel debugging complet permet :
- breakpoints kernel ;
- breakpoints dans un driver ;
- inspection des objets kernel ;
- analyse d’IRP ;
- inspection de mémoire kernel ;
- pas à pas dans DriverEntry ;
- analyse de bugchecks.
Le local kernel debugging permet certaines inspections sans utiliser de deuxième machine.
Il est utile pour observer certaines structures, mais il est plus limité que le debugging kernel complet.
Pour développer un driver, un vrai host/target reste préférable.
23. Full kernel debugging
Le full kernel debugging permet au host de suspendre complètement le target.
Lorsque le target est cassé dans le debugger :
la machine cible est gelée jusqu’à reprise de l’exécution.
C’est ce qui permet d’inspecter précisément l’état du noyau.
24. Connexion host / target
Plusieurs transports existent :
- réseau ;
- port série ;
- named pipe avec VM ;
- USB dans certains scénarios.
Pour des environnements modernes, le debugging réseau est également fréquent.
25. VM de debug
Un environnement pratique :
Code: Select all
PC principal
|
| WinDbg
|
+----> VM Windows
|
+-> driver testé
- snapshot ;
- reboot rapide ;
- isolation ;
- récupération après BSOD ;
- possibilité de tester Driver Verifier sans risquer l’hôte.
Si le driver n’est pas encore chargé :
Code: Select all
bu MyDriver!DriverEntry
Code: Select all
g
Code: Select all
DriverEntry27. Breakpoints dans un driver
Exemples :
Code: Select all
bu MyDriver!DispatchDeviceControl
bu MyDriver!Unload
bu MyDriver!MyFunction
28. !process
La commande :
Code: Select all
!process 0 0
Un niveau de détail plus élevé permet d’afficher les threads et stacks.
Cette commande est essentielle pour comprendre le contexte d’exécution kernel.
29. !thread
La commande :
Code: Select all
!thread adresse
Informations utiles :
- état ;
- raison d’attente ;
- priorité ;
- processus propriétaire ;
- stack ;
- IRP associés.
La commande :
Code: Select all
!irp adresse
Elle permet de voir :
- major function ;
- minor function ;
- stack locations ;
- driver courant ;
- completion routine ;
- état de l’IRP.
31. Logging kernel simple
Les macros classiques sont :
Code: Select all
KdPrint
DbgPrint
Code: Select all
KdPrint(("Driver loaded\n"));
Code: Select all
KdPrint(("Status = 0x%08X\n", status));
Le logging simple a plusieurs limites :
- beaucoup de bruit ;
- filtrage limité ;
- coût potentiel ;
- peu adapté à la production ;
- messages difficiles à structurer.
Code: Select all
DbgPrintEx33. DbgPrintEx et KdPrintEx
Code: Select all
DbgPrintEx- ComponentId ;
- Level ;
- filtrage.
Code: Select all
DbgPrintEx(
DPFLTR_IHVDRIVER_ID,
DPFLTR_INFO_LEVEL,
"DriverEntry called\n"
);
Code: Select all
DPFLTR_ERROR_LEVEL
DPFLTR_WARNING_LEVEL
DPFLTR_TRACE_LEVEL
DPFLTR_INFO_LEVEL
Un driver peut créer des wrappers :
Code: Select all
LogInfo(...)
LogWarning(...)
LogError(...)
Code: Select all
LogInfo("DriverEntry called");
LogError("IoCreateDevice failed: 0x%08X", status);
35. ASSERT
Une assertion vérifie qu’une condition censée être toujours vraie l’est effectivement.
Exemple :
Code: Select all
ASSERT(DeviceExtension != NULL);
Code: Select all
ASSERT(KeGetCurrentIrql() == PASSIVE_LEVEL);
36. NT_ASSERT
Code: Select all
NT_ASSERTExemple :
Code: Select all
NT_ASSERT(NT_SUCCESS(status));
37. Checked et Free builds
Historiquement, Windows proposait notamment :
- Free build ;
- Checked build.
Cette distinction est aujourd’hui surtout historique.
Pour le développement moderne, on utilise plutôt symboles, WinDbg, Driver Verifier, assertions et instrumentation ciblée.
38. Driver Verifier
Driver Verifier est un outil qui surveille le comportement des drivers et force certains bugs latents à devenir visibles.
Il peut provoquer volontairement un bugcheck lorsqu’une violation grave est détectée.
Le but est de transformer un bug aléatoire en erreur déterministe et analysable.
39. Configuration de Driver Verifier
L’outil graphique peut être lancé avec :
Code: Select all
verifier
- options standard ;
- options personnalisées ;
- drivers spécifiques.
40. Désactivation
Commande utile :
Code: Select all
verifier /reset
Il faut connaître cette commande avant d’activer Verifier.
En cas de boucle de BSOD, le mode sans échec peut être nécessaire.
41. Special Pool
Special Pool aide à détecter :
- buffer overflow ;
- buffer underrun ;
- use-after-free ;
- corruption de pool.
42. Pool Tracking
Pool Tracking surveille les allocations du driver.
Il aide à détecter :
- fuites de pool ;
- allocations jamais libérées ;
- mauvaise gestion des pool tags.
43. Force IRQL Checking
Cette option détecte des violations liées à l’IRQL.
Exemple :
Code: Select all
KeAcquireSpinLock(...);
// IRQL = DISPATCH_LEVEL
// accès paged memory -> dangereux
44. I/O Verification
I/O Verification vérifie la gestion des IRP.
Erreurs typiques :
- double completion ;
- IRP jamais complété ;
- mauvaise stack location ;
- mauvais forwarding ;
- mauvaise completion routine ;
- mauvais .
Code: Select all
IoStatus.Information
Deadlock Detection surveille l’ordre d’acquisition des locks.
Exemple classique :
Code: Select all
Thread A :
Lock1
Lock2
Thread B :
Lock2
Lock1
46. Low Resources Simulation
Cette option provoque volontairement des échecs d’allocation.
Exemple :
Code: Select all
PVOID p = ExAllocatePool2(...);
if (p == NULL)
{
...
}
47. DMA Verification
DMA Verification concerne surtout les drivers qui utilisent directement le DMA.
Elle est moins importante pour un driver logiciel simple sans DMA.
48. Security Checks
Les vérifications de sécurité peuvent détecter certaines mauvaises pratiques :
- mauvais paramètres ;
- mauvaise gestion de handles ;
- violations de règles d’accès ;
- mauvaises hypothèses dans le code.
Après un bugcheck, la première commande importante est :
Code: Select all
!analyze -v
- bugcheck ;
- arguments ;
- module suspect ;
- stack ;
- contexte probable.
50. Analyse d’une corruption mémoire
Workflow typique :
Code: Select all
Driver Verifier
|
+-> Special Pool
|
+-> crash au moment de la violation
|
+-> !analyze -v
|
+-> !pool
|
+-> stack
51. Analyse d’une fuite
Une fuite de pool peut être révélée par :
- Pool Tracking ;
- pool tags ;
- augmentation progressive de mémoire ;
- inspection avec les extensions WinDbg.
Code: Select all
allocation -> utilisation -> libération
Lorsqu’un bugcheck indique une erreur IRQL :
- lire la stack ;
- identifier l’API appelée ;
- identifier le niveau IRQL ;
- vérifier si la mémoire utilisée est paginée ;
- corriger l’architecture.
Code: Select all
PASSIVE_LEVEL53. Analyse d’un deadlock
Commandes utiles :
Code: Select all
!locks
!thread
k
- quel thread attend ;
- quel verrou il attend ;
- quel thread détient ce verrou ;
- quel autre verrou ce second thread attend.
Commandes :
Code: Select all
!irp adresse
!thread adresse
k
55. ETW
ETW signifie :
Code: Select all
Event Tracing for Windows
Concepts :
Code: Select all
Provider
Session
Event
Consumer
Une session collecte.
Un consumer lit ou affiche.
56. Pourquoi ETW
ETW est préférable à
Code: Select all
DbgPrint- beaucoup d’événements ;
- faible coût ;
- traces structurées ;
- filtrage ;
- analyse temporelle ;
- utilisation hors debugger ;
- journalisation en production.
Un provider est identifié par un GUID.
Le driver :
- enregistre le provider ;
- émet des événements ;
- désenregistre le provider au cleanup.
58. Sessions ETW
Une session peut fonctionner :
- en temps réel ;
- vers un fichier ETL.
Le fichier ETL est utile pour une analyse ultérieure.
59. TraceView
TraceView peut :
- créer une session ;
- ajouter un provider ;
- utiliser un GUID ;
- afficher les événements ;
- enregistrer dans un fichier.
60. Comparaison logging / debugging / tracing
Code: Select all
KdPrint / DbgPrint
Code: Select all
DbgPrintEx
Code: Select all
WinDbg
Code: Select all
Driver Verifier
Code: Select all
ETW
61. Workflow de debug conseillé
- reproduire le bug ;
- ajouter des logs ciblés ;
- observer la stack et l’état ;
- activer Driver Verifier ;
- reproduire sous Verifier ;
- analyser le dump ;
- identifier la vraie cause ;
- corriger ;
- retenter avec Verifier ;
- désactiver Verifier quand les tests sont terminés.
Avant de considérer un driver comme stable :
- tester ;
Code: Select all
DriverEntry - tester unload ;
- tester plusieurs load/unload ;
- tester les IOCTL valides ;
- tester les IOCTL invalides ;
- tester erreurs d’allocation ;
- tester concurrence ;
- tester IRQL ;
- tester Driver Verifier ;
- tester crash dump ;
- tester les traces.
- Toujours utiliser une VM pour les tests risqués.
- Garder un snapshot propre.
- Utiliser des pool tags compréhensibles.
- Vérifier chaque .
Code: Select all
NTSTATUS - Éviter les logs excessifs.
- Utiliser les symboles correspondant au binaire.
- Ne pas ignorer un bugcheck provoqué par Verifier.
- Tester les chemins d’erreur.
- Documenter les invariants avec des assertions.
Symboles incorrects
La stack devient trompeuse ou incomplète.
Mauvais contexte processus
Une adresse user mode peut être interprétée dans le mauvais processus.
Breakpoint non résolu
Le module n’est peut-être pas encore chargé : utiliser
Code: Select all
buTrop de logs
Le signal utile est perdu dans le bruit.
Verifier sur tous les drivers
Cela complique inutilement le diagnostic.
Tester sur la machine principale
Un mauvais driver peut rendre le système instable.
65. Historique
Certaines notions anciennes restent utiles comme culture générale :
- checked builds ;
- Windows 98/Me ;
- anciens transports de debug ;
- anciens outils secondaires.
66. Commandes WinDbg essentielles
À connaître en priorité :
Code: Select all
r
u
db
du
k
kv
lm
lmvm
bp
bu
bl
bc
~
.symfix
.reload
!process
!thread
!irp
!pool
!locks
!verifier
!analyze -v
À retenir :
Code: Select all
KdPrint
DbgPrint
DbgPrintEx
KdPrintEx
ASSERT
NT_ASSERT
Code: Select all
WinDbg
Driver Verifier
TraceView
ETW
Code: Select all
Bug
|
v
Logs
|
v
WinDbg
|
v
Driver Verifier
|
v
Crash déterministe
|
v
!analyze -v
|
+-> stack
+-> IRP
+-> pool
+-> locks
+-> IRQL
|
v
Correction
|
v
Re-test
|
v
ETW / validation finale
Exercice 1 :
Code: Select all
bp kernel32!CreateFileW
Code: Select all
du @rcx
Code: Select all
bp ntdll!NtCreateFile
Code: Select all
syscallExercice 3 :
Code: Select all
bu MyDriver!DriverEntry
Exercice 4 :
Code: Select all
!process 0 0
Exercice 5 :
Créer volontairement une fuite de pool dans une VM et utiliser Driver Verifier pour la détecter.
Exercice 6 :
Créer une trace ETW minimale et la visualiser dans TraceView.
70. À retenir en priorité
- WinDbg sert à comprendre l’état réel du programme ou du noyau.
- Les symboles sont indispensables.
- La convention d’appel x64 permet de retrouver rapidement les arguments.
- ,
Code: Select all
k,Code: Select all
r,Code: Select all
uetCode: Select all
dusont des commandes de base.Code: Select all
lm - ,
Code: Select all
!process,Code: Select all
!thread,Code: Select all
!irpetCode: Select all
!poolsont essentielles en kernel.Code: Select all
!locks - est utile pour les fonctions de drivers non encore chargés.
Code: Select all
bu - et
Code: Select all
KdPrintservent à instrumenter rapidement.Code: Select all
DbgPrintEx - et
Code: Select all
ASSERTservent à vérifier des invariants.Code: Select all
NT_ASSERT - Driver Verifier détecte des violations difficiles à reproduire autrement.
- Special Pool aide pour les corruptions mémoire.
- Pool Tracking aide pour les fuites.
- Force IRQL Checking aide pour les erreurs d’IRQL.
- I/O Verification aide pour les erreurs d’IRP.
- Deadlock Detection aide pour les problèmes de locks.
- Low Resources Simulation teste les chemins d’échec.
- ETW est adapté au tracing structuré et performant.
Le debugging Windows bas niveau ne repose pas sur un seul outil.
WinDbg permet d’observer et d’analyser.
Driver Verifier transforme de nombreux bugs silencieux en erreurs explicites.
Les assertions permettent de détecter immédiatement les états impossibles pendant le développement.
Les fonctions de debug permettent une instrumentation rapide.
ETW permet un tracing structuré et performant.
Un workflow solide consiste à combiner ces outils plutôt qu’à les utiliser séparément.
