Debuguer en Kernel mode via WinDbg

Ce cours dédiée au développement de drivers en kernel mode sans Framework

Moderator: Rick

Post Reply
Hydraxx
Site Admin
Posts: 46
Joined: Mon Jan 12, 2026 4:04 pm
Location: France
Contact:

Debuguer en Kernel mode via WinDbg

Post by Hydraxx »

WinDbg, debugging kernel et tracing Windows

Salut l'équipe 8-)

Aujourd’hui on attaque un sujet fondamental pour tout développeur système Windows : WinDbg, le kernel debugging et le tracing.

Ce cours n’est pas juste une liste de commandes. Le but est de comprendre comment raisonner devant un programme, un driver, une pile d’appels, un registre, un IRP ou un message de debug.

Quand on commence le développement Win32 bas niveau, on peut souvent s’en sortir avec Visual Studio, des

Code: Select all

printf
, ou quelques logs.

Mais dès qu’on touche à :
  • la Native API
  • les transitions user mode vers kernel mode
  • les drivers
  • les IRP
  • les threads bloqués
  • les BSOD
  • les dumps mémoire
  • les symboles
  • les traces système
il faut un outil plus sérieux.

Cet outil, c’est WinDbg.

1) Pourquoi WinDbg est important

WinDbg est le debugger système de Windows.

Il permet de travailler à plusieurs niveaux :
  • debug user-mode
  • debug kernel-mode
  • analyse de crash dump
  • analyse de mémoire
  • inspection des registres CPU
  • inspection des threads
  • inspection des stacks
  • inspection des modules chargés
  • debug de drivers
  • utilisation d’extensions spécialisées
Ce qui rend WinDbg très puissant, ce n’est pas seulement son interface.
C’est surtout son accès direct à la structure du système.

Avec WinDbg, on peut passer d’une vision simple :

Code: Select all

Mon programme plante
à une vision beaucoup plus basse :

Code: Select all

Quel thread exécute quoi ?
Quelle fonction a été appelée ?
Quels arguments ont été passés ?
Quel registre contient quelle valeur ?
Quel module contient cette adresse ?
Quel processus possède ce thread ?
Quel driver a reçu cet IRP ?
2) WinDbg vs x64dbg vs Visual Studio

Chaque outil a son domaine.

Visual Studio

Visual Studio est très confortable pour développer et déboguer du code applicatif.

Il est utile pour :
  • compiler
  • mettre des breakpoints classiques
  • inspecter des variables
  • déboguer du code source
  • faire du développement C/C++ classique
Mais il devient vite limité quand on veut comprendre Windows en profondeur.

x64dbg

x64dbg est très bon pour le reverse user-mode.

Il est pratique pour :
  • analyse d’exécutables
  • patching
  • breakpoints user-mode simples
  • interface graphique confortable
  • inspection rapide du désassemblage
Mais x64dbg n’est pas l’outil naturel pour le kernel debugging ou l’analyse système avancée.

WinDbg

WinDbg est plus austère, mais plus puissant pour le développement système.

Il est meilleur pour :
  • les symboles Microsoft
  • les PDB
  • les extensions système
  • les stacks fiables
  • les dumps mémoire
  • le kernel debugging
  • les drivers
  • les structures Windows
  • les commandes comme

    Code: Select all

    !process
    ,

    Code: Select all

    !thread
    ,

    Code: Select all

    !handle
    ,

    Code: Select all

    !peb
Résumé simple :

Code: Select all

x64dbg  = reverse user-mode confortable
VS      = développement et debug source classique
WinDbg  = debugger système Windows sérieux
Pour apprendre Win32 bas niveau, Native API, kernel et drivers, WinDbg doit devenir un outil principal.

3) Les trois familles de commandes WinDbg

WinDbg se pilote énormément par commandes.

Il faut distinguer plusieurs familles.

3.1) Commandes classiques

Exemples :

Code: Select all

r
k
lm
db
dq
du
bp
g
p
t
Elles permettent de lire les registres, afficher la pile, lister les modules, lire la mémoire ou contrôler l’exécution.

3.2) Commandes avec point

Exemples :

Code: Select all

.symfix
.reload
.process
.detach
.cls
Ces commandes contrôlent plutôt l’état du debugger, les symboles, le contexte ou la session.

3.3) Commandes d’extension

Exemples :

Code: Select all

!process
!thread
!handle
!peb
!teb
!analyze
Le préfixe

Code: Select all

!
indique une commande fournie par une extension de debug.

Ces commandes sont extrêmement importantes en kernel debugging, car elles savent interpréter des structures internes de Windows.

4) Les symboles : la base absolue

Sans symboles, WinDbg affiche surtout des adresses.

Exemple sans symboles :

Code: Select all

00007ff7`23cc9c28
fffff804`12345678
Avec symboles :

Code: Select all

ntdll!NtCreateFile
kernel32!CreateFileW
MyCounter!DriverEntry
MyCounter!DispatchDeviceControl
Les symboles permettent d’associer des adresses mémoire à :
  • des noms de fonctions
  • des noms de modules
  • des variables globales
  • des types
  • des offsets
  • des informations de debug
4.1) Chemin de symboles classique

Un chemin de symboles courant est :

Code: Select all

srv*C:\Symbols*https://msdl.microsoft.com/download/symbols
Cela signifie :
  • utiliser

    Code: Select all

    C:\Symbols
    comme cache local
  • télécharger les symboles nécessaires depuis le serveur Microsoft
4.2) Commandes symboles à connaître

Initialiser un chemin de symboles Microsoft :

Code: Select all

.symfix
Recharger les symboles :

Code: Select all

.reload
Forcer le rechargement :

Code: Select all

.reload /f
Lister les modules :

Code: Select all

lm
Lister un module précis :

Code: Select all

lm m ntdll
lm m kernel32
lm m MyCounter
Chercher des symboles :

Code: Select all

x module!*
x module!fonction*
x ntdll!Nt*
x MyCounter!*
4.3) Symboles pour ses propres drivers

Quand on développe un driver, il faut garder le fichier

Code: Select all

.pdb
correspondant au fichier

Code: Select all

.sys
.

Si le

Code: Select all

.pdb
ne correspond pas exactement à la build du driver, WinDbg peut afficher :
  • de mauvais noms
  • de mauvais offsets
  • des stacks incomplètes
  • des breakpoints qui ne se résolvent pas
Règle simple :

Code: Select all

.sys chargé + .pdb correspondant = debug propre
5) Lire les registres

La commande principale est :

Code: Select all

r
Elle affiche les registres du thread courant.

Pour afficher un registre précis :

Code: Select all

r rcx
r rdx
r r8
r r9
r rax
r rsp
r rip
5.1) Registres importants

En x64 Windows, certains registres sont particulièrement importants :
5.2) Convention d’appel x64 Windows

Sur Windows x64, les quatre premiers arguments d’une fonction sont passés dans :

Code: Select all

RCX = 1er argument
RDX = 2e argument
R8  = 3e argument
R9  = 4e argument
La valeur de retour est généralement dans :

Code: Select all

RAX
C’est une notion essentielle pour le debug et le reverse.

Exemple avec

Code: Select all

MessageBoxW
:

Code: Select all

MessageBoxW(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType)
Au moment de l’appel :

Code: Select all

RCX = hWnd
RDX = lpText
R8  = lpCaption
R9  = uType
Donc si on est arrêté au début de

Code: Select all

MessageBoxW
, on peut afficher le texte avec :

Code: Select all

du rdx
et le titre avec :

Code: Select all

du r8
6) Lire la mémoire

WinDbg permet de lire la mémoire de plusieurs façons.

6.1) Affichage brut

Afficher des bytes :

Code: Select all

db adresse
Afficher des DWORD 32 bits :

Code: Select all

dd adresse
Afficher des QWORD 64 bits :

Code: Select all

dq adresse
6.2) Affichage de chaînes

Afficher une chaîne ANSI :

Code: Select all

da adresse
Afficher une chaîne Unicode UTF-16 :

Code: Select all

du adresse
Important :

Code: Select all

du = Display Unicode
Ce n’est pas seulement un dump brut.
C’est un affichage de mémoire interprétée comme une chaîne Unicode.

Exemple :

Code: Select all

du rcx
Si

Code: Select all

RCX
pointe vers une chaîne wide string, WinDbg affiche le texte.

6.3) Exemple pratique

Si un programme appelle :

Code: Select all

MessageBoxW(NULL, L"Appuie sur OK", L"Test", MB_OK);
et qu’on est arrêté dans

Code: Select all

MessageBoxW
, alors :

Code: Select all

du rdx
peut afficher :

Code: Select all

Appuie sur OK
et :

Code: Select all

du r8
peut afficher :

Code: Select all

Test
7) Rechercher en mémoire

La commande de recherche est :

Code: Select all

s
Chercher une chaîne Unicode :

Code: Select all

s -u debut taille "texte"
Exemple :

Code: Select all

s -u 0 L?80000000 "TOP_SECRET"
Cela cherche la chaîne Unicode

Code: Select all

TOP_SECRET
dans une zone mémoire.

Si on connaît déjà la base du module, on peut limiter la recherche :

Code: Select all

s -u 00007ff7`23cb0000 L?200000 "TOP_SECRET"
Quand WinDbg retourne une adresse, on peut afficher la chaîne avec :

Code: Select all

du adresse
8) Les breakpoints

Un breakpoint permet d’arrêter l’exécution à un endroit précis.

8.1) Breakpoint classique

Code: Select all

bp module!fonction
Exemple :

Code: Select all

bp user32!MessageBoxW
8.2) Breakpoint différé

Code: Select all

bu module!fonction
Le breakpoint différé est important quand le module n’est pas encore chargé.

Exemple pour un driver :

Code: Select all

bu MyCounter!DriverEntry
WinDbg garde le breakpoint en attente. Quand

Code: Select all

MyCounter.sys
est chargé, le breakpoint est résolu.

Règle mentale :

Code: Select all

bp = module déjà chargé
bu = module pas encore chargé ou breakpoint symbolique durable
8.3) Gérer les breakpoints

Lister les breakpoints :

Code: Select all

bl
Désactiver un breakpoint :

Code: Select all

bd numéro
Activer un breakpoint :

Code: Select all

be numéro
Supprimer un breakpoint :

Code: Select all

bc numéro
Supprimer tous les breakpoints :

Code: Select all

bc *
8.4) Contrôler l’exécution

Continuer :

Code: Select all

g
Step into :

Code: Select all

t
Step over :

Code: Select all

p
Sortir de la fonction courante :

Code: Select all

gu
Revenir au debugger / interrompre :

Code: Select all

Break
ou via l’interface de WinDbg.

9) La stack : voir la vérité de l’exécution

La stack est l’un des éléments les plus importants en debug.

Afficher la stack :

Code: Select all

k
Variantes utiles :

Code: Select all

kb
kp
kv
Différence simplifiée :
La stack montre le chemin d’appel.

Exemple user-mode :

Code: Select all

user32!MessageBoxW
secret!main
ucrtbase!invoke_main
kernel32!BaseThreadInitThunk
ntdll!RtlUserThreadStart
En kernel / driver, on peut voir :

Code: Select all

MyCounter!DispatchDeviceControl
nt!IofCallDriver
nt!NtDeviceIoControlFile
ntdll!NtDeviceIoControlFile
KERNELBASE!DeviceIoControl
La stack est fondamentale parce qu’elle montre comment on est arrivé là.

10) Les threads en user-mode

Afficher les threads :

Code: Select all

~
Changer de thread :

Code: Select all

~0s
~1s
~2s
Afficher la stack d’un thread spécifique :

Code: Select all

~1k
~2k
Chaque thread a sa propre stack, ses propres registres, son propre contexte d’exécution.

Règle mentale :

Code: Select all

un processus contient des threads
un thread contient une exécution
une stack montre le chemin de cette exécution
11) TEB et PEB

11.1) TEB

Le TEB est le Thread Environment Block.

Il contient des informations user-mode liées à un thread :
  • informations de stack
  • TLS
  • données spécifiques au thread
  • liens vers certaines structures user-mode
Commande utile :

Code: Select all

!teb
11.2) PEB

Le PEB est le Process Environment Block.

Il contient des informations user-mode liées au processus :
  • liste des modules chargés
  • informations du loader
  • chemin image
  • paramètres processus
  • informations d’environnement
Commande utile :

Code: Select all

!peb
12) Modules chargés

Afficher les modules :

Code: Select all

lm
Afficher un module précis :

Code: Select all

lm m kernel32
lm m ntdll
lm m user32
lm m MyCounter
Pourquoi c’est important ?

Parce que chaque adresse appartient à un module.

Si on voit une adresse :

Code: Select all

00007fff`a9bbec60
WinDbg peut l’associer à :

Code: Select all

user32!MessageBoxW
si les symboles sont bons.

13) De Win32 à la Native API

Une application appelle souvent des API Win32 :

Code: Select all

CreateFileW
ReadFile
WriteFile
DeviceIoControl
VirtualAlloc
CreateProcessW
Mais ces API finissent souvent par appeler des fonctions Native API dans

Code: Select all

ntdll.dll
.

Exemple :

Code: Select all

CreateFileW
↓
KernelBase / Kernel32
↓
ntdll!NtCreateFile
↓
syscall
↓
kernel
À retenir :

Code: Select all

Win32 API = couche pratique user-mode
Native API = interface plus proche du noyau
syscall = transition user-mode vers kernel-mode
14) NtCreateFile et syscall

Quand on observe

Code: Select all

CreateFileW
ou

Code: Select all

NtCreateFile
dans WinDbg, on peut voir la transition vers le système.

En x64, les arguments suivent la convention d’appel Windows :

Code: Select all

RCX, RDX, R8, R9
Le retour d’une fonction native est souvent un

Code: Select all

NTSTATUS
placé dans :

Code: Select all

RAX
Exemples de retours :

Code: Select all

STATUS_SUCCESS
STATUS_OBJECT_NAME_NOT_FOUND
STATUS_ACCESS_DENIED
Quand

Code: Select all

RAX
contient une valeur négative en représentation signée, c’est souvent un échec NTSTATUS.

15) Debug user-mode : exercice mental

Exemple :

Code: Select all

const wchar_t* secret = L"TOP_SECRET_123";

MessageBoxW(NULL, L"Appuie sur OK", L"Test", MB_OK);
On place un breakpoint :

Code: Select all

bp user32!MessageBoxW
g
Une fois stoppé :

Code: Select all

r
du rdx
du r8
Puis on cherche le secret en mémoire :

Code: Select all

s -u 0 L?80000000 "TOP_SECRET"
Quand l’adresse est trouvée :

Code: Select all

du adresse
Ce petit exercice apprend déjà beaucoup de choses :
  • breakpoint
  • registres
  • calling convention x64
  • chaînes Unicode
  • recherche mémoire
  • lecture mémoire
16) Kernel debugging : principe général

Le kernel debugging ressemble au user-mode debugging, mais avec une différence majeure :

Code: Select all

User-mode debug  = un processus
Kernel debug     = tout le système
En kernel debugging, le debugger tourne généralement sur une machine host, et le système à déboguer est la machine target.

Schéma :

Code: Select all

Host avec WinDbg
↓
connexion de debug
↓
Target Windows / VM
Pourquoi cette séparation ?

Parce que si le kernel de la target se bloque, crashe ou atteint un breakpoint, le système cible est suspendu.
Il faut donc que le debugger soit ailleurs.

17) Host et target

Host

La machine host exécute WinDbg.

Elle sert à :
  • contrôler la session
  • recevoir les breakpoints
  • afficher les stacks
  • analyser les structures kernel
  • envoyer les commandes
Target

La machine target est le Windows débogué.

Elle peut être :
  • une autre machine physique
  • une VM Hyper-V
  • une VM VMware
  • une VM VirtualBox selon configuration
Pour le développement driver, le meilleur modèle est souvent :

Code: Select all

PC principal = host
VM Windows = target
18) Local kernel debugging

Le local kernel debugging consiste à déboguer le kernel de la même machine.

Il est utile pour certaines observations, mais il a des limites.

Avantages :
  • mise en place plus simple
  • utile pour explorer
  • pas besoin de deuxième machine
Limites :
  • moins adapté au debug driver sérieux
  • certaines actions ne sont pas possibles
  • si le système est bloqué, le debugger est aussi affecté
  • Secure Boot peut gêner
Commande fréquemment utilisée pour activer le debug :

Code: Select all

bcdedit /debug on
Mais pour du vrai développement driver, il vaut mieux utiliser une VM target.

19) Full kernel debugging avec VM

Le modèle recommandé :

Code: Select all

Host Windows
↓
WinDbg
↓
connexion réseau ou pipe
↓
VM target Windows
↓
driver à déboguer
Ce setup permet :
  • de stopper le kernel cible
  • de garder le host actif
  • de charger et décharger des drivers
  • de voir les BSOD
  • d’analyser les crash dumps
  • de mettre des breakpoints dans DriverEntry
20) Connexion kernel : réseau

Le kernel debugging réseau est moderne et pratique.

Côté target :

Code: Select all

bcdedit /debug on
bcdedit /dbgsettings net hostip:<IP_HOST> port:<PORT> key:<KEY>
Exemple :

Code: Select all

bcdedit /debug on
bcdedit /dbgsettings net hostip:192.168.1.10 port:50000 key:1.2.3.4
Puis redémarrer la target.

Côté host :

Code: Select all

WinDbg
File
Attach to kernel
NET
Il faut que :
  • l’adresse IP soit correcte
  • le port soit correct
  • la key soit correcte
  • le firewall ne bloque pas
  • host et target puissent communiquer
21) Connexion kernel : COM / named pipe

En VM, on peut aussi utiliser un port série virtuel exposé comme un named pipe.

Exemple de pipe :

Code: Select all

\\.\pipe\debug
Dans WinDbg :

Code: Select all

File
Attach to kernel
COM
Port: \\.\pipe\debug
Baud: 115200
Ce mode est ancien mais encore utile, surtout avec certaines configurations de VM.

22) Première commande kernel : !process

La commande centrale :

Code: Select all

!process 0 0
Elle liste les processus du système.

Signification :
  • premier

    Code: Select all

    0
    : tous les processus
  • deuxième

    Code: Select all

    0
    : niveau de détail minimal
Variantes :

Code: Select all

!process 0 1
!process 0 2
!process 0 7
Inspecter un processus précis :

Code: Select all

!process <adresse_EPROCESS> 1
!process <adresse_EPROCESS> 7
23) EPROCESS

En kernel, un processus est représenté par une structure interne souvent appelée EPROCESS.

Quand WinDbg affiche un processus avec

Code: Select all

!process
, on peut voir des champs comme :
  • adresse du processus
  • PID / Cid
  • ParentCid
  • Peb
  • ObjectTable
  • HandleCount
  • Image
  • SessionId
À retenir :

Code: Select all

EPROCESS = représentation kernel du processus
PEB      = représentation user-mode importante du processus
24) Changer de contexte processus en kernel debug

En kernel debug, on n’est pas automatiquement dans le bon contexte user-mode.

Si on veut lire correctement la mémoire user-mode d’un processus, il faut souvent changer de contexte :

Code: Select all

.process /r /p <adresse_EPROCESS>
.reload /user
Puis on peut utiliser :

Code: Select all

!peb
Règle mentale :

Code: Select all

Kernel debug ≠ accès automatique correct à toute la mémoire user-mode
Il faut dire explicitement :

Code: Select all

je veux travailler dans ce processus
25) Threads kernel : !thread

Afficher le thread courant :

Code: Select all

!thread
Afficher un thread précis :

Code: Select all

!thread <adresse_ETHREAD>
Un thread côté kernel est représenté par des structures comme ETHREAD et KTHREAD.

Dans les sorties WinDbg, on peut voir :
  • TID
  • TEB
  • état
  • priorité
  • wait reason
  • objet attendu
  • stack
À retenir :

Code: Select all

ETHREAD = représentation kernel d’un thread
TEB     = structure user-mode du thread
26) États d’attente des threads

Un thread qui ne s’exécute pas attend souvent quelque chose.

WinDbg peut montrer des états comme :

Code: Select all

WAIT
UserRequest
WrQueue
WrLpcReply
Executive
KernelMode
UserMode
Non-Alertable
Le thread peut attendre :
  • un event
  • un mutex
  • une queue
  • un message GUI
  • une réponse LPC/ALPC
  • une opération d’I/O
  • un objet kernel
Pour le debug driver, c’est crucial.
Un mauvais driver peut bloquer un thread si :
  • un IRP n’est jamais complété
  • un verrou n’est pas relâché
  • une attente est faite au mauvais IRQL
  • un événement n’est jamais signalé
27) Debug driver : objectif

Quand on débogue un driver, on veut souvent observer :
  • le chargement du driver
  • DriverEntry
  • DriverUnload
  • les dispatch routines
  • les IRP reçus
  • les IOCTL
  • les buffers
  • les statuts retournés
  • la complétion des IRP
Le driver n’a pas un

Code: Select all

main
classique.
Il est appelé par le système.

Donc les breakpoints importants sont souvent dans :

Code: Select all

DriverEntry
DriverUnload
DispatchCreateClose
DispatchRead
DispatchWrite
DispatchDeviceControl
28) Breakpoint sur DriverEntry

Pour arrêter au chargement d’un driver :

Code: Select all

bu MyCounter!DriverEntry
g
Puis on charge le driver dans la target.

Quand le driver est chargé, WinDbg s’arrête dans

Code: Select all

DriverEntry
.

Pourquoi

Code: Select all

bu
?

Parce que le driver n’est souvent pas encore chargé au moment où on place le breakpoint.

29) Breakpoints sur dispatch routines

Exemples :

Code: Select all

bu MyCounter!DispatchCreateClose
bu MyCounter!DispatchDeviceControl
bu MyCounter!DispatchRead
bu MyCounter!DispatchWrite
Quand le programme user-mode fait :

Code: Select all

CreateFileW(L"\\\\.\\MyCounter", ...)
DeviceIoControl(...)
ReadFile(...)
WriteFile(...)
CloseHandle(...)
le driver reçoit des IRP majeurs :

Code: Select all

IRP_MJ_CREATE
IRP_MJ_DEVICE_CONTROL
IRP_MJ_READ
IRP_MJ_WRITE
IRP_MJ_CLOSE
30) Le lien user-mode vers driver

Côté user-mode, l’application parle souvent au driver avec :

Code: Select all

CreateFileW
DeviceIoControl
ReadFile
WriteFile
CloseHandle
Exemple mental :

Code: Select all

CreateFileW("\\\\.\\MyCounter")
↓
I/O Manager
↓
IRP_MJ_CREATE
↓
Driver
Puis :

Code: Select all

DeviceIoControl(...)
↓
I/O Manager
↓
IRP_MJ_DEVICE_CONTROL
↓
Driver
Donc quand on debug un driver, il faut aussi comprendre ce que fait le programme user-mode.

31) IoGetCurrentIrpStackLocation

Dans une dispatch routine, on récupère les paramètres de l’IRP avec :

Code: Select all

PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
C’est une fonction absolument essentielle.

Mentalité :

Code: Select all

IRP = requête d’I/O
IO_STACK_LOCATION = paramètres de cette requête à ce niveau de la pile
Pour un IOCTL :

Code: Select all

stack->Parameters.DeviceIoControl.IoControlCode
stack->Parameters.DeviceIoControl.InputBufferLength
stack->Parameters.DeviceIoControl.OutputBufferLength
Pour une écriture :

Code: Select all

stack->Parameters.Write.Length
Pour une lecture :

Code: Select all

stack->Parameters.Read.Length
32) Compléter un IRP

Un driver doit terminer correctement les requêtes qu’il traite.

Forme classique :

Code: Select all

Irp->IoStatus.Status = status;
Irp->IoStatus.Information = bytes;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return status;
Champs importants :
  • Code: Select all

    IoStatus.Status
    : statut final
  • Code: Select all

    IoStatus.Information
    : nombre d’octets utiles retournés ou traités selon le cas
Si un driver oublie de compléter un IRP, un thread user-mode peut rester bloqué.

33) API kernel importantes vues dans ce contexte

Création du device :

Code: Select all

IoCreateDevice
Création du lien symbolique :

Code: Select all

IoCreateSymbolicLink
Suppression du lien :

Code: Select all

IoDeleteSymbolicLink
Suppression du device :

Code: Select all

IoDeleteDevice
Récupération de la stack location :

Code: Select all

IoGetCurrentIrpStackLocation
Complétion de l’IRP :

Code: Select all

IoCompleteRequest
Fonctions de log / debug :

Code: Select all

DbgPrint
DbgPrintEx
KdPrint
KdPrintEx
Assertions :

Code: Select all

ASSERT
NT_ASSERT
34) DriverEntry : rôle

Code: Select all

DriverEntry
est le point d’entrée du driver.

Il sert généralement à :
  • initialiser le driver
  • enregistrer les dispatch routines
  • créer un device object
  • créer un symbolic link si nécessaire
  • préparer les structures globales
  • définir la routine de déchargement
Exemple de choses qu’on retrouve :

Code: Select all

DriverObject->DriverUnload = MyUnload;

DriverObject->MajorFunction[IRP_MJ_CREATE] = MyCreateClose;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = MyCreateClose;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = MyDeviceControl;
35) DriverUnload : rôle

Code: Select all

DriverUnload
sert au nettoyage.

Il doit libérer ce que

Code: Select all

DriverEntry
ou le driver a créé.

Exemples :

Code: Select all

IoDeleteSymbolicLink(&symLink);
IoDeleteDevice(DeviceObject);
Règle simple :

Code: Select all

ce qui est créé doit être nettoyé
36) ASSERT et NT_ASSERT

Les assertions servent à vérifier des hypothèses internes.

Exemple :

Code: Select all

ASSERT(DeviceObject != NULL);
ASSERT(Irp != NULL);
En build debug, une assertion qui échoue peut déclencher un break dans le debugger.

En release, certaines assertions peuvent disparaître selon les macros et la configuration.

Utilité :
  • attraper les bugs tôt
  • documenter les hypothèses du code
  • éviter de continuer avec un état incohérent
Mais attention :
  • une assertion ne remplace pas une vraie validation d’entrée
  • une assertion sert surtout à vérifier un invariant interne
37) DbgPrint et KdPrint

Code: Select all

DbgPrint
permet d’écrire un message de debug depuis le kernel.

Exemple :

Code: Select all

DbgPrint("MyCounter loaded\n");

Code: Select all

KdPrint
est souvent utilisé sous forme de macro :

Code: Select all

KdPrint(("MyCounter loaded\n"));
Attention à la double parenthèse avec

Code: Select all

KdPrint
.

Ces messages peuvent être visibles dans :
  • WinDbg kernel
  • DebugView selon configuration
  • certains outils de capture debug
38) DbgPrintEx

Code: Select all

DbgPrintEx
est plus propre que

Code: Select all

DbgPrint
car il permet le filtrage.

Forme :

Code: Select all

DbgPrintEx(ComponentId, Level, Format, ...);
Exemple :

Code: Select all

DbgPrintEx(DPFLTR_IHVDRIVER_ID,
           DPFLTR_INFO_LEVEL,
           "MyCounter: Driver loaded\n");
38.1) ComponentId

Le

Code: Select all

ComponentId
indique le composant ou la catégorie.

Pour un driver tiers, on utilise souvent :

Code: Select all

DPFLTR_IHVDRIVER_ID
38.2) Level

Niveaux courants :

Code: Select all

DPFLTR_ERROR_LEVEL
DPFLTR_WARNING_LEVEL
DPFLTR_INFO_LEVEL
DPFLTR_TRACE_LEVEL
Idée :
  • ERROR : problème sérieux
  • WARNING : problème potentiel
  • INFO : information normale
  • TRACE : détails très verbeux
39) Macros de logging personnelles

Pour éviter de répéter

Code: Select all

DbgPrintEx
, on peut créer des macros ou fonctions.

Exemple :

Code: Select all

#define LOG_COMPONENT DPFLTR_IHVDRIVER_ID

#define LogInfo(fmt, ...) \
    DbgPrintEx(LOG_COMPONENT, DPFLTR_INFO_LEVEL, "[MyDriver] " fmt, __VA_ARGS__)

#define LogError(fmt, ...) \
    DbgPrintEx(LOG_COMPONENT, DPFLTR_ERROR_LEVEL, "[MyDriver] " fmt, __VA_ARGS__)
But :
  • avoir un format uniforme
  • filtrer plus facilement
  • rendre le code plus lisible
Attention : selon le compilateur et le standard C/C++, la gestion de

Code: Select all

__VA_ARGS__
vide peut demander une variante.

40) DebugView

DebugView est un outil Sysinternals qui peut afficher des sorties debug.

Il est utile pour :
  • voir rapidement des messages

    Code: Select all

    DbgPrint
  • déboguer sans garder WinDbg ouvert en permanence
  • observer des traces simples
Mais pour du debug kernel sérieux, WinDbg reste indispensable.

41) Filtres DbgPrintEx

Un point important : tous les messages

Code: Select all

DbgPrintEx
ne s’affichent pas forcément.

L’affichage dépend notamment :
  • du ComponentId
  • du Level
  • des filtres configurés
  • du debugger attaché
  • de la configuration système
Donc si un message ne s’affiche pas, ce n’est pas forcément que le code ne passe pas.

Il faut se demander :
  • est-ce que le breakpoint est atteint ?
  • est-ce que la fonction de log est appelée ?
  • est-ce que le filtre autorise ce niveau ?
  • est-ce que le debugger capture bien la sortie ?
42) ETW — Event Tracing for Windows

ETW est le système de tracing avancé de Windows.

C’est plus sérieux, plus structuré et plus puissant que de simples

Code: Select all

DbgPrint
.

Schéma :

Code: Select all

Provider ETW
↓
Session ETW
↓
Consumer / outil d’analyse
Dans le cas d’un driver :

Code: Select all

Driver
↓
événements ETW
↓
session de trace
↓
TraceView / WPA / outil de lecture
43) Concepts ETW

Provider

Le provider est le composant qui produit les événements.

Cela peut être :
  • un driver
  • un service
  • un composant système
  • une application
Session

La session est la capture active.

Elle décide :
  • quels providers sont activés
  • quels niveaux sont capturés
  • quels mots-clés sont activés
  • où les traces sont écrites
Consumer

Le consumer lit ou affiche les événements.

Exemples :
  • TraceView
  • Windows Performance Analyzer
  • outils personnalisés
GUID

Un provider ETW est identifié par un GUID.

Le GUID permet à l’outil de trace de savoir quel provider activer.

44) Niveaux ETW

Les événements peuvent avoir des niveaux :
  • Critical
  • Error
  • Warning
  • Information
  • Verbose
Le niveau permet de filtrer la quantité d’informations.

Exemple :

Code: Select all

Information = événements normaux utiles
Verbose     = détails nombreux, utiles en diagnostic profond
Error       = événements d’échec
45) Keywords ETW

Les keywords permettent de catégoriser les événements.

Exemple mental :

Code: Select all

KEYWORD_IO
KEYWORD_INIT
KEYWORD_POWER
KEYWORD_REGISTRY
Une session peut choisir de capturer seulement certaines catégories.

C’est ce qui rend ETW beaucoup plus propre que du logging brut.

46) TraceView

TraceView est un outil graphique du WDK permettant de configurer et visualiser des traces ETW.

Il permet :
  • de créer une session de trace
  • d’ajouter un provider
  • de choisir un GUID
  • de démarrer la capture
  • de voir les événements
  • d’écrire dans un fichier de log
TraceView est surtout utile pour comprendre ETW au début.

Pour un usage plus avancé, on peut aussi rencontrer :
  • Code: Select all

    logman
  • Code: Select all

    tracelog
  • Windows Performance Recorder
  • Windows Performance Analyzer
47) DbgPrintEx vs ETW

Résumé :

Code: Select all

DbgPrintEx = simple, pratique, rapide pour debug
ETW        = structuré, filtrable, plus adapté au diagnostic sérieux
DbgPrintEx est utile pendant le développement.

ETW est meilleur pour :
  • tracing structuré
  • diagnostic long
  • production
  • analyse de performance
  • capture sélective
  • corrélation d’événements
48) Commandes WinDbg à connaître par cœur

48.1) Symboles

Code: Select all

.symfix
.reload
.reload /f
lm
lm m module
x module!*
48.2) Registres et mémoire

Code: Select all

r
r rcx
r rdx
r r8
r r9
r rax
db adresse
dd adresse
dq adresse
da adresse
du adresse
s -u debut taille "texte"
48.3) Stack et threads user-mode

Code: Select all

k
kb
kp
kv
~
~0s
~1s
~2k
48.4) Breakpoints

Code: Select all

bp module!fonction
bu module!fonction
bl
bd numéro
be numéro
bc numéro
bc *
g
p
t
gu
48.5) Processus et threads kernel

Code: Select all

!process 0 0
!process 0 1
!process 0 7
!process <EPROCESS> 7
!thread
!thread <ETHREAD>
.process /r /p <EPROCESS>
.reload /user
!peb
!teb
49) APIs et routines à retenir côté driver

Point d’entrée et déchargement :

Code: Select all

DriverEntry
DriverUnload
Création / suppression d’objets :

Code: Select all

IoCreateDevice
IoCreateSymbolicLink
IoDeleteSymbolicLink
IoDeleteDevice
Dispatch routines :

Code: Select all

IRP_MJ_CREATE
IRP_MJ_CLOSE
IRP_MJ_READ
IRP_MJ_WRITE
IRP_MJ_DEVICE_CONTROL
IRP :

Code: Select all

IoGetCurrentIrpStackLocation
IoCompleteRequest
Logging / assertions :

Code: Select all

DbgPrint
DbgPrintEx
KdPrint
ASSERT
NT_ASSERT
Côté user-mode pour parler au driver :

Code: Select all

CreateFileW
DeviceIoControl
ReadFile
WriteFile
CloseHandle
50) Ce qu’il faut savoir faire après ce chapitre

Après ce chapitre, il faut savoir :
  • lancer WinDbg sur un programme user-mode
  • mettre un breakpoint sur une API Win32
  • lire les registres
  • comprendre

    Code: Select all

    RCX/RDX/R8/R9
  • afficher une chaîne Unicode avec

    Code: Select all

    du
  • chercher une chaîne en mémoire avec

    Code: Select all

    s -u
  • lire une stack avec

    Code: Select all

    k
  • lister les modules avec

    Code: Select all

    lm
  • configurer les symboles
  • comprendre le passage Win32 vers Native API
  • connecter WinDbg à une VM en kernel debug
  • utiliser

    Code: Select all

    !process
    et

    Code: Select all

    !thread
  • mettre un breakpoint différé sur

    Code: Select all

    DriverEntry
  • déboguer une dispatch routine
  • utiliser

    Code: Select all

    DbgPrintEx
  • comprendre l’intérêt d’ETW
51) Modèle mental global

Pour le user-mode :

Code: Select all

Programme
↓
API Win32
↓
Native API
↓
syscall
↓
kernel
Pour le driver :

Code: Select all

Application user-mode
↓
CreateFile / DeviceIoControl
↓
I/O Manager
↓
IRP
↓
Dispatch routine du driver
↓
IoStatus
↓
IoCompleteRequest
↓
retour user-mode
Pour le debug :

Code: Select all

Breakpoint
↓
Registres
↓
Mémoire
↓
Stack
↓
Modules
↓
Symboles
↓
Structures système
52) Checklist debug user-mode

Code: Select all

1. Ouvrir l'exe dans WinDbg
2. Vérifier les symboles
3. Mettre un breakpoint sur l'API intéressante
4. g
5. r
6. Lire RCX/RDX/R8/R9
7. du sur les pointeurs string
8. k pour voir la stack
9. lm pour voir les modules
10. chercher en mémoire si nécessaire
53) Checklist debug driver

Code: Select all

1. Compiler le driver en Debug
2. Garder le .pdb correspondant
3. Préparer une VM target
4. Activer le kernel debugging
5. Connecter WinDbg depuis le host
6. Configurer les symboles
7. bu Driver!DriverEntry
8. Charger le driver
9. Vérifier DriverEntry
10. Placer des breakpoints sur les dispatch routines
11. Lancer le programme user-mode
12. Observer IRP, stack, registres et logs
13. Corriger le driver
14. Rebuild, reload, retester
54) Erreurs classiques
  • oublier les symboles
  • charger un mauvais PDB
  • utiliser

    Code: Select all

    bp
    alors que le driver n’est pas encore chargé
  • oublier

    Code: Select all

    .reload /f
  • ne pas comprendre

    Code: Select all

    RCX/RDX/R8/R9
  • confondre adresse et contenu pointé
  • utiliser

    Code: Select all

    db
    au lieu de

    Code: Select all

    du
    pour une chaîne Unicode
  • faire

    Code: Select all

    !peb
    sans bon contexte process en kernel debug
  • ne pas compléter un IRP
  • mettre trop de

    Code: Select all

    DbgPrint
    sans filtrage
  • oublier que le système entier est figé quand le kernel est stoppé
55) Résumé ultra condensé

WinDbg est l’outil central du debug système Windows.

Les symboles sont indispensables.
La stack montre le chemin réel d’exécution.
Les registres donnent les arguments et les retours.
En x64 Windows, les quatre premiers arguments sont dans

Code: Select all

RCX
,

Code: Select all

RDX
,

Code: Select all

R8
et

Code: Select all

R9
.
Les chaînes Unicode se lisent avec

Code: Select all

du
.
Les modules se listent avec

Code: Select all

lm
.
Les processus kernel se listent avec

Code: Select all

!process 0 0
.
Les threads kernel s’inspectent avec

Code: Select all

!thread
.
Un driver se débogue souvent avec un breakpoint différé sur

Code: Select all

DriverEntry
via

Code: Select all

bu
.
Les IRP se comprennent avec

Code: Select all

IoGetCurrentIrpStackLocation
et se terminent avec

Code: Select all

IoCompleteRequest
.

Code: Select all

DbgPrintEx
sert au logging filtrable.
ETW sert au tracing propre, structuré et plus professionnel.

Conclusion

Ce chapitre donne les bases nécessaires pour passer d’un développeur Win32 classique à quelqu’un qui commence à raisonner comme un développeur système Windows.

Le point important n’est pas seulement de connaître des commandes.

Le vrai objectif est de savoir répondre à ces questions :
  • où suis-je dans l’exécution ?
  • quel thread exécute ce code ?
  • quelle fonction m’a amené ici ?
  • quels arguments ont été passés ?
  • quelle mémoire est pointée ?
  • quel module contient cette adresse ?
  • quel processus possède ce contexte ?
  • quel driver reçoit cette requête ?
  • est-ce que l’IRP est correctement terminé ?
  • est-ce que mes logs sont visibles et filtrables ?
Quand on sait faire ça, WinDbg devient beaucoup plus qu’un debugger.

Il devient une fenêtre directe sur Windows.

Who is online

Users browsing this forum: No registered users and 1 guest