Bienvenue dans le quatrième article de la série sur le développement de KDAMonitor !
Dans cet article, je vais couvrir la version v0.4 de ce projet ainsi que le premier crash rencontré dans ce dernier. La version v0.4 est consacrée avant tout à l’implémentation de la journalisation via des fichiers .jsonl. Cette journalisation permettra de donner un sens à la file implémentée précédemment.
Voici les fichiers concernés par cet article, et la section qui les explique :
| Fichier | Rôle | Section |
|---|---|---|
kdamon_config.h | Nouvelles constantes centralisées (chemins de log) | Pourquoi un thread dédié à l’écriture ? |
log_writer.h | Interface publique du log writer (Start/Stop) | Le thread dédié à l’écriture (log_writer.c) |
log_writer.c | Ouverture/fermeture du fichier, sérialisation JSONL, thread, contrôleurs start/stop | Le thread dédié à l’écriture (log_writer.c) / Journalisation au format JSONL |
event_queue.c | Ajout de WakeEvent dans KDAMON_EVENT_QUEUE, signalisation dans Push, nouveau getter | Événements de réveil |
driver_entry.c | Retrait du code de test, câblage KdaMonLogWriterStart/Stop | Intégration dans driver_entry.c |
docs/crashes.md | Documentation du crash #1 | Le premier crash : IRQL_NOT_LESS_OR_EQUAL (0xA) |
Le projet peut être retrouvé dans ce dépôt : KDAMonitor.
Pourquoi un thread dédié à l’écriture ?#
Il y a deux raisons à faire un thread dédié à l’écriture et ne pas compter sur les callbacks :
- Cela permet de répartir clairement ce que chaque objet créé (device, queue, …) doit faire, sans mettre trop de responsabilité sur un seul.
- Éviter les parties lentes et bloquantes concernant le fichier (ouverture puis écriture). Plus de détails dans la partie suivante.
Le problème du couplage producteur/consommateur#
Ce que j’appelle un producteur (ou fournisseur) seront les callbacks qui seront bientôt implémentés. Chacun de ces callbacks va fournir des événements à la file, lorsqu’il vont rajouter les événements à la file ils vont s’exécuter à DISPATCH_LEVEL. Cependant, à DISPATCH_LEVEL le scheduler ne peut intervenir, c’est-à-dire qu’aucune I/O bloquante n’est autorisée (pas de lecture/écriture disque, pas d’attente sur un objet qui peut dormir).
Pour résoudre ce problème, il faut un nouvel object qui sera dédié à cela. Ainsi, oute la partie lente et bloquante (ouvrir un fichier, écrire dedans) est déportée dans un thread système séparé, qui lui tourne à PASSIVE_LEVEL.
Nouvelles constantes centralisées (kdamon_config.h)#
C’est le bon moment pour introduire les nouvelles constantes :
KDAMON_DIR: Le chemin du dossier attribué au driver (tout le projet)L"\\??\\C:\\KDAMonitor\\".KDAMON_LOG_DIR: Le chemin du dossier attribué au journaux du driverL"\\??\\C:\\KDAMonitor\\logs\\".KDAMON_LOG_FILE_PREFIXetKDAMON_LOG_FILE_EXTENSION: Respectivement, le préfixe et suffixe donnés au fichier de log.
Pourquoi cette notation \??\C:\ ?
Ce lien symbolique appartient à l’espace de noms objets du noyau (Object Manager namespace) ; il permet de faire le pont vers le disque C:, une notion propre à l’espace Win32 que cet espace de noms objets ne connaît pas nativement.
\??\est aussi appelé\DosDevices\.
Dans cette version, kdamon_config.h reçoit aussi une correction de compilation à cette occasion : les identifiants STATUS_* (utilisés entre autres par ZwCreateFile) n’étaient pas déclarés. Il faut inclure <ntstatus.h> avant <ntddk.h>, en définissant WIN32_NO_STATUS entre les deux pour éviter les conflits de macros, voici le fichier final :
#pragma once
#include <ntstatus.h>
#define WIN32_NO_STATUS
#include <ntddk.h>
#undef WIN32_NO_STATUS
#define DRIVER_TAG "[KDAMonitor]"
#define KDAMON_DEVICE_NAME L"\\Device\\KDAMonitor"
#define KDAMON_SYMLINK_NAME L"\\DosDevices\\KDAMonitor"
#define KDAMON_DIR L"\\??\\C:\\KDAMonitor\\"
#define KDAMON_LOG_DIR L"\\??\\C:\\KDAMonitor\\logs\\"
#define KDAMON_LOG_FILE_PREFIX L"kdamon_"
#define KDAMON_LOG_FILE_EXTENSION L".jsonl"Événements de réveil#
Le problème du polling#
- Sans mécanisme de réveil, le thread devrait interroger la file en boucle pour savoir si de nouveaux événements sont arrivés → CPU gaspillé, latence.
Le WakeEvent ajouté à KDAMON_EVENT_QUEUE#
- Nouveau champ
KEVENT WakeEventdans la structure de la file (event_queue.c), de typeSynchronizationEvent. - Dans
KdaMonEventQueuePush, juste avant de relâcher le spinlock :KeSetEvent(&g_EventQueue.WakeEvent, IO_NO_INCREMENT, FALSE)— donc signalé à chaque push réussi, sous spinlock, àDISPATCH_LEVEL.- Point à expliciter :
KeSetEventest justement conçu pour pouvoir être appelé jusqu’àDISPATCH_LEVEL, contrairement à des primitives d’attente côté appelant — cohérent avec le choix du spinlock de l’article 03.
- Point à expliciter :
- Nouveau getter
KdaMonEventQueueGetWakeEventexposé dansevent_queue.h, utilisé parlog_writer.cpour récupérer le pointeur vers cet événement sans exposer toute la structure de la file.
Distinction avec l’événement d’arrêt#
- Bien préciser qu’il y a deux événements distincts :
WakeEvent(porté par la file, signalé à chaque nouvel événement) etg_StopEvent(local àlog_writer.c, signalé une seule fois à l’arrêt). Le thread attend les deux en même temps viaKeWaitForMultipleObjects, ce qui lui permet de réagir immédiatement à l’arrêt sans attendre un hypothétique prochain événement.
Le thread dédié à l’écriture (log_writer.c)#
Avant de continuer dans cette partie, je vais présenter certaines fonctions qui ne seront pas détaillées dans cet article :
KdaMonLogWriterOpenFiles’occupe de créer (ou vérifier l’existence de)C:\KDAMonitor\, puisC:\KDAMonitor\logs\, puis construire un nom de fichier horodaté (kdamon_YYYYMMDD_HHMMSS.jsonl) et l’ouvrir en écriture.KdaMonLogWriterCloseFileferme le handle du fichier de log s’il est ouvert (ZwClose), puis le remet àNULL.
La fonction KdaMonLogWriterWriteEvent écrit une ligne dans le fichier .jsonl représentant un événement et sera détaillée dans la section Journalisation au format JSONL.
Sans callbacks implémentés, cette fonction va écrire un événement basique, sans informations pertinentes. L’événement contiendra : l’ID, le type d’événement et le timestamp.
De plus, voici l’état global maintenu par ce module :
g_ThreadObject: le pointeur vers l’objet thread noyau, conservé pour pouvoir l’attendre à l’arrêt (voir plus bas).g_StopEvent: l’événement signalé pour demander l’arrêt du thread.g_LogFileHandle: le handle vers le fichier de log actuellement ouvert.
Un dernier objet est utilisé mais ne fait pas partie de ce module : le WakeEvent, dans la structure de la file d’événements elle-même (event_queue.c) et signalé à chaque Push. Son fonctionnement complet a été détaillé dans la section Événements de réveil, juste avant.
Création du thread : IoCreateSystemThread#
Commençons par la fonction qui va nous permettre de lancer le thread dédié au journaliseur, KdaMonLogWriterStart.
Elle commence par initialiser g_StopEvent en tant que NotificationEvent, qui va nous permettre de réveiller le thread en attente :
KeInitializeEvent(&g_StopEvent, NotificationEvent, FALSE);NotificationEvent signifie qu’une fois signalé, l’événement reste signalé indéfiniment. Une fois l’arrêt demandé, il ne doit pas “s’auto-consommer”, il doit rester signalé de façon permanente. Le dernier paramètre, FALSE, fixe l’état initial à non-signalé.
Ensuite, la fonction vérifie que le fichier de log existe puis l’ouvre (KdaMonLogWriterOpenFile). Juste après, on crée le thread système avec IoCreateSystemThread :
status = IoCreateSystemThread(
DriverObject, // Objet driver auquel associer le thread
&threadHandle, // Un handle vers le thread (en sortie)
THREAD_ALL_ACCESS, // Le masque de droits demandé sur ce handle
NULL, // ObjectAttributes
NULL, // ProcessHandle
NULL, // ClientId
KdaMonLogWriterThread, // La routine d'entrée du thread
NULL // StartContext
);Le premier paramètre, DriverObject, est ce qui distingue cette fonction de PsCreateSystemThread. En le passant ici, l’I/O Manager associe le thread créé au driver et incrémente un compteur interne de threads actifs pour ce driver. Concrètement, cela empêche Windows de décharger le driver tant que ce compteur n’est pas revenu à zéro, ou autrement dit, tant que le thread tourne encore. Cela rajoute une couche de protection sur un déchargement prématuré que PsCreateSystemThread n’offre pas nativement.
Les paramètres suivants sont laissés à NULL :
ObjectAttributes: pas de nom d’objet à associerProcessHandle: le thread est créé dans le contexte du processus System par défautClientId: on n’a pas besoin de récupérer son PID/TID puisqu’on va travailler directement avec un pointeur d’objet (voir plus bas).
KdaMonLogWriterThread est la routine d’entrée du thread (voir Boucle principale du thread (KdaMonLogWriterThread)). StartContext reste NULL : elle n’a besoin de rien recevoir en paramètre, elle récupère elle-même le WakeEvent de la file.
En cas d’échec à cette étape, la fonction appelle KdaMonLogWriterCloseFile.
Voici le code final de cette fonction :
BOOLEAN KdaMonLogWriterStart(_In_ PDRIVER_OBJECT DriverObject)
{
NTSTATUS status;
HANDLE threadHandle;
KeInitializeEvent(&g_StopEvent, NotificationEvent, FALSE);
status = KdaMonLogWriterOpenFile();
if (!NT_SUCCESS(status))
{
KdPrint((DRIVER_TAG " [ERROR]: KdaMonLogWriterStart: failed to open log file (0x%08X)\n", status));
return FALSE;
}
status = IoCreateSystemThread(
DriverObject,
&threadHandle,
THREAD_ALL_ACCESS,
NULL,
NULL,
NULL,
KdaMonLogWriterThread,
NULL
);
if (!NT_SUCCESS(status))
{
KdPrint((DRIVER_TAG " [ERROR]: IoCreateSystemThread failed (0x%08X)\n", status));
KdaMonLogWriterCloseFile();
return FALSE;
}
status = ObReferenceObjectByHandle(
threadHandle,
THREAD_ALL_ACCESS,
NULL,
KernelMode,
&g_ThreadObject,
NULL
);
ZwClose(threadHandle);
if (!NT_SUCCESS(status))
{
KdPrint((DRIVER_TAG " [ERROR]: ObReferenceObjectByHandle failed (0x%08X)\n", status));
return FALSE;
}
KdPrint((DRIVER_TAG " [SUCCESS]: Log writer started\n"));
return TRUE;
}Boucle principale du thread (KdaMonLogWriterThread)#
Une fois lancé par IoCreateSystemThread, le thread exécute KdaMonLogWriterThread en boucle jusqu’à ce qu’on lui demande de s’arrêter :
static VOID KdaMonLogWriterThread(_In_ PVOID StartContext)
{
UNREFERENCED_PARAMETER(StartContext);
PRKEVENT WakeEvent = KdaMonEventQueueGetWakeEvent();
PVOID WaitObjects[WAIT_OBJECT_COUNT];
NTSTATUS WaitStatus;
KDAMON_EVENT Event;
WaitObjects[0] = &g_StopEvent;
WaitObjects[1] = WakeEvent;
KdPrint((DRIVER_TAG " [SUCCESS]: Log writer thread started\n"));
for (;;) {
WaitStatus = KeWaitForMultipleObjects(
WAIT_OBJECT_COUNT,
WaitObjects,
WaitAny,
Executive,
KernelMode,
FALSE,
NULL,
NULL
);
if (WaitStatus == STATUS_WAIT_0)
{
break;
}
while (KdaMonEventQueuePop(&Event))
{
KdaMonLogWriterWriteEvent(&Event);
}
}
KdPrint((DRIVER_TAG " [SUCCESS]: Log writer thread exiting\n"));
PsTerminateSystemThread(STATUS_SUCCESS);
}Le thread récupère d’abord le champ WakeEvent de la file via KdaMonEventQueueGetWakeEvent (voir Événements de réveil). Ensuite, il place ces deux objets dans un tableau (WaitObjects) : g_StopEvent en index 0 et le WakeEvent en index 1.
La boucle for (;;) attend ensuite sur ces deux objets simultanément avec KeWaitForMultipleObjects en mode WaitAny. Ce mode permet au thread de se mettre en sommeil et de se réveiller dès que l’un des deux objets est signalé.
La valeur de retour est utilisée pour déterminer le comportement que doit adopter le code :
- Si la valeur de retour est
STATUS_WAIT_0, alors cela correspond à l’index 0 deWaitObjectset donc l’objet signalé estg_StopEvent. Si c’est le cas, la boucle est immédiatement interrompue. - Dans tous les autres cas, le thread vide entièrement la file via la boucle
while (KdaMonEventQueuePop(&Event))qui retire et écrit, viaKdaMonLogWriterWriteEvent, les événements un par un, jusqu’à ce que la file soit vide, avant de retourner attendre le prochain réveil.
Une fois sorti de la boucle principale, PsTerminateSystemThread(STATUS_SUCCESS) termine le thread.
Cycle de vie et arrêt propre#
L’arrêt du thread se fait via KdaMonLogWriterStop, appelée depuis DriverUnload, avant KdaMonEventQueueDestroy :
VOID KdaMonLogWriterStop(VOID)
{
if (g_ThreadObject == NULL) {
return;
}
KeSetEvent(&g_StopEvent, IO_NO_INCREMENT, FALSE);
KeWaitForSingleObject(g_ThreadObject, Executive, KernelMode, FALSE, NULL);
ObDereferenceObject(g_ThreadObject);
g_ThreadObject = NULL;
KdaMonLogWriterCloseFile();
KdPrint((DRIVER_TAG " [SUCCESS]: Log writer stopped\n"));
}KeSetEvent(&g_StopEvent, ...) signale l’arrêt, le thread en attente dans KeWaitForMultipleObjects se réveille avec STATUS_WAIT_0 et sort de sa boucle.
En utilisant KeWaitForSingleObject(g_ThreadObject, ...), un objet thread passe à l’état signalé exactement quand le thread se termine réellement (au moment du PsTerminateSystemThread). Cet appel bloque donc jusqu’à ce que le thread ait fini de s’exécuter, pas seulement jusqu’à ce qu’on le lui ait demandé. Sans cette attente, DriverUnload pourrait continuer, et le driver être déchargé, pendant que le thread tourne encore.
Une fois le thread garanti terminé, ObDereferenceObject relâche la référence prise dans KdaMonLogWriterStart, et g_ThreadObject est remis à NULL. Enfin, KdaMonLogWriterCloseFile ferme le fichier de log.
Journalisation au format JSONL#
Pourquoi JSONL ?#
Le format JSONL (JSON Lines) consiste à écrire un objet JSON valide par ligne, plutôt qu’un unique tableau JSON englobant l’ensemble des événements. J’ai choisi ce format pour deux raisons :
- chaque événement peut être écrit indépendamment en simple append, sans avoir à réécrire ou refermer une structure englobante
- la correspondance stricte “une ligne = un événement” rend le fichier trivial à parser ensuite.
Un autre point qui a justifié ce choix que j’ai découvert après est que si le processus est interrompu brutalement (crash, arrêt forcé), les lignes déjà écrites restent exploitables telles quelles.
Sérialisation d’un événement (KdaMonLogWriterWriteEvent)#
Dans cette partie, le format donné sera la sérialisation de base commune à tous les types d’événements. La construction de la ligne JSON est faite avec RtlStringCbPrintfA :
{"id":...,"type":"...","timestamp":...}\nDans le code cela donne :
NTSTATUS status = RtlStringCbPrintfA(
EventBuffer,
sizeof(EventBuffer),
"{\"id\":%lu,\"type\":\"%s\",\"timestamp\":%lld}\n",
Event->Id,
KdaMonEventTypeToString(Event->Type),
Event->Timestamp.QuadPart
);
KdaMonEventTypeToString: petite fonction de mapping de l’enumKDAMON_EVENT_TYPEvers une chaîne lisible ("Process","Network", etc.).
RtlStringCbPrintfA est utilisée à la place d’un sprintf classique. C’est une fonction de la bibliothèque safe strings du noyau (ntstrsafe.h), qui prend explicitement la taille du buffer de destination (sizeof(EventBuffer)) et garantit de ne jamais écrire au-delà.
Une fois la ligne construite, sa longueur exacte est récupérée avec RtlStringCbLengthA :
status = RtlStringCbLengthA(EventBuffer, sizeof(EventBuffer), &Length);Cette longueur (sans le \0 final) est nécessaire pour indiquer à ZwWriteFile combien d’octets écrire précisément :
status = ZwWriteFile(
g_LogFileHandle,
NULL, // Event
NULL, // ApcRoutine
NULL, // ApcContext
&IoStatusBlock,
EventBuffer,
(ULONG)Length,
NULL, // ByteOffset
NULL // Key
);ZwWriteFile est l’équivalent noyau de WriteFile. Elle écrit dans le fichier, on utilise le handle du fichier de log : g_LogFileHandle. Les paramètres Event et ApcRoutine laissés à NULL ne sont pas utilisés ici. L’appel est synchrone, grâce au flag FILE_SYNCHRONOUS_IO_NONALERT posé lors de l’ouverture du fichier. IoStatusBlock reçoit en sortie le nombre d’octets réellement écrits ainsi que le statut de l’opération.
Le code de cette fonction ne sera pas donné en entier car ce n’est qu’un prototype qui sera remplacé par les fonctions de remplissage attribuées aux callbacks. Il peut cependant être trouvé dans la release v0.4 du projet : v0.4 - Log Writer.
Intégration dans driver_entry.c#
Tout d’abord, il faut rajouter le lancement du log writer (KdaMonLogWriterStart) dans DriverEntry et son arrêt (KdaMonLogWriterStop) dans DriverUnload :
void DriverUnload(_In_ PDRIVER_OBJECT DriverObject)
{
UNREFERENCED_PARAMETER(DriverObject);
KdaMonLogWriterStop();
KdaMonEventQueueDestroy();
KdaMonDeleteDevice(g_DeviceObject);
KdPrint((DRIVER_TAG " [SUCCESS]: Driver Unload called\n"));
}
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath)
{
...
if (!KdaMonEventQueueInitialize())
{
KdPrint((DRIVER_TAG " [ERROR]: EventQueueInitialize failed\n"));
return STATUS_UNSUCCESSFUL;
}
if (!KdaMonLogWriterStart(DriverObject))
{
KdPrint((DRIVER_TAG " [ERROR]: KdaMonLogWriterStart failed\n"));
return STATUS_UNSUCCESSFUL;
}
KdPrint((DRIVER_TAG " [SUCCESS]: Initialized successfully\n"));
...
return STATUS_SUCCESS;
}Ensuite, on va rajouter dans DriverEntry un test tout simple : on va créer 2 événements et les ajouter à la file. Si notre log writer fonctionne parfaitement, les événements devraient être retirés de la file dans l’ordre d’arrivée et écrits dans un fichier .jsonl. Ci-dessous le code final de DriverEntry :
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);
DriverObject->DriverUnload = DriverUnload;
DriverObject->MajorFunction[IRP_MJ_CREATE] = KdaMonCreateClose;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = KdaMonCreateClose;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = KdaMonDeviceControl;
NTSTATUS status = KdaMonCreateDevice(DriverObject, &g_DeviceObject);
if (!NT_SUCCESS(status))
{
return status;
}
if (!KdaMonEventQueueInitialize())
{
KdPrint((DRIVER_TAG " [ERROR]: EventQueueInitialize failed\n"));
return STATUS_UNSUCCESSFUL;
}
if (!KdaMonLogWriterStart(DriverObject))
{
KdPrint((DRIVER_TAG " [ERROR]: KdaMonLogWriterStart failed\n"));
return STATUS_UNSUCCESSFUL;
}
KdPrint((DRIVER_TAG " [SUCCESS]: Initialized successfully\n"));
// --- BEGIN TEST QUEUE ---
KDAMON_EVENT testEvent1 = { 0 };
testEvent1.Type = KdaMonEventProcess;
KeQuerySystemTimePrecise(&testEvent1.Timestamp);
KdaMonEventQueuePush(&testEvent1);
KDAMON_EVENT testEvent2 = { 0 };
testEvent2.Type = KdaMonEventNetwork;
KeQuerySystemTimePrecise(&testEvent2.Timestamp);
KdaMonEventQueuePush(&testEvent2);
// --- END TEST QUEUE ---
return STATUS_SUCCESS;
}Contrairement au test de l’article 03, on ne dépile plus manuellement : c’est le thread de log qui va consommer ces deux événements de lui-même, dès qu’il sera réveillé par le
WakeEvent.
Voici une démonstration de ce test en exécution :
Le premier crash : IRQL_NOT_LESS_OR_EQUAL (0xA)#
Contexte#
La VM a produit un BSOD (Blue Screen Of Death). Le dump généré à la suite du crash (trouvé dans C:\Windows\Minidump\) a été conservé dans le dossier docs/dumps/ du dépôt et analysé avec WinDbg (!analyze -v).
Le bugcheck relevé est IRQL_NOT_LESS_OR_EQUAL (0xA) :
IRQL_NOT_LESS_OR_EQUAL (a)
An attempt was made to access a pageable (or completely invalid) address at an
interrupt request level (IRQL) that is too high. This is usually
caused by drivers using improper addresses.
If a kernel debugger is available get the stack backtrace.
Arguments:
Arg1: 0000000000000000, memory referenced
Arg2: 0000000000000002, IRQL
Arg3: 0000000000000000, bitfield :
bit 0 : value 0 = read operation, 1 = write operation
bit 3 : value 0 = not an execute operation, 1 = execute operation (only on chips which support this level of status)
Arg4: fffff807cdc7274f, address which referenced memoryArg1 confirme que l’adresse mémoire référencée était NULL, et Arg2 confirme un IRQL de 2, soit DISPATCH_LEVEL.
L’instruction fautive elle-même se trouve dans nt!KeSetEvent :
IP_IN_PAGED_CODE:
nt!KeSetEvent+1af
fffff807`cdc7274f 4d8b2424 mov r12,qword ptr [r12]Et la pile d’appel confirme le point d’entrée dans le driver :
STACK_TEXT:
fffffb8a`2cac8338 fffff807`ce0bece9 : 00000000`0000000a 00000000`00000000 00000000`00000002 00000000`00000000 : nt!KeBugCheckEx
fffffb8a`2cac8340 fffff807`ce0b9fa8 : 00000000`00000000 00000000`00000000 fffff807`6496d0c0 00000000`00000000 : nt!KiBugCheckDispatch+0x69
fffffb8a`2cac8480 fffff807`cdc7274f : fffffb8a`00000003 fffff807`cdccb2ba 00000000`00000000 00000000`00000000 : nt!KiPageFault+0x468
fffffb8a`2cac8610 fffff807`6496162c : ffffbf82`00000000 00000000`00000000 00000001`89e45800 00000001`8521b4e3 : nt!KeSetEvent+0x1af
fffffb8a`2cac86a0 ffffbf82`00000000 : 00000000`00000000 00000001`89e45800 00000001`8521b4e3 ffffffff`80003500 : KDAMonitor+0x162c
fffffb8a`2cac86a8 00000000`00000000 : 00000001`89e45800 00000001`8521b4e3 ffffffff`80003500 ffffbf82`ee180000 : 0xffffbf82`00000000KDAMonitor+0x162c correspond à l’appel à KeSetEvent fait dans KdaMonEventQueuePush (event_queue.c), donc juste après l’ajout d’un nouvel événement à la file.
Diagnostic#
Le code fautif se trouvait dans KdaMonEventQueueInitialize :
BOOLEAN KdaMonEventQueueInitialize(VOID)
{
// Initialisé AVANT le zéro-out
KeInitializeEvent(&g_EventQueue.WakeEvent, SynchronizationEvent, FALSE);
KeInitializeSpinLock(&g_EventQueue.Lock);
RtlZeroMemory(&g_EventQueue, sizeof(g_EventQueue)); // <- FAUTIF
return TRUE;
}Le RtlZeroMemory qui suivait KeInitializeEvent écrasait toute la structure g_EventQueue, y compris ce WakeEvent tout juste initialisé, le pointeur devenait NULL au lieu de continuer à pointer sur lui-même. Ainsi, l’objet event était corrompu avant même d’avoir servi.
Le crash se produit au premier Push. C’est KeSetEvent qui tente de parcourir cette liste d’attente interne, déréférence le pointeur NULL laissé par le zéro-out, et provoque le bugcheck.
Résolution#
Le correctif consiste simplement à inverser l’ordre des opérations : RtlZeroMemory d’abord, initialiser les objets noyau ensuite.
BOOLEAN KdaMonEventQueueInitialize(VOID)
{
RtlZeroMemory(&g_EventQueue, sizeof(g_EventQueue));
KeInitializeEvent(&g_EventQueue.WakeEvent, SynchronizationEvent, FALSE);
KeInitializeSpinLock(&g_EventQueue.Lock);
return TRUE;
}Conclusion#
La v0.4 donne enfin un vrai débouché à la file construite en v0.3 : le thread du log writer va maintenant vider automatiquement la file et écrire les événements récupérés directement dans un fichier .jsonl.
La prochaine version, v0.5, viendra enfin remplir la file pour de vrai avec le premier capteur : la création et la destruction de processus.
Merci d’avoir lu jusqu’au bout et à bientôt pour le prochain et cinquième article de cette série : Premier capteur : surveillance de la création et de la terminaison des processus.