[{"content":"Bienvenue dans le neuvième article de la série sur le développement de KDAMonitor !\nDans cet article, je vais couvrir la version v0.9 de ce projet. Dans cette version, j\u0026rsquo;implémente le quatrième capteur du projet : le capteur d\u0026rsquo;activité du registre.\nVoici les fichiers concernés par cet article, et la section qui les explique :\nFichier Rôle Section event_types.h Nouveau type KDAMON_REGISTRY_EVENT_DATA + enum KDAMON_REGISTRY_ACTION + union dans KDAMON_EVENT Que récupérer lors d\u0026rsquo;un événement registre ? kdamon_config.h Constantes de configuration du capteur registre Que récupérer lors d\u0026rsquo;un événement registre ? registry_callback.h Déclaration du register/unregister Implémenter le callback registry_callback.c Le callback lui-même Implémenter le callback log_writer.c Sérialisation JSONL spécifique aux événements de registre Sérialiser les événements registre en JSONL driver_entry.c Enregistrement/désenregistrement du callback au chargement/déchargement Intégration dans driver_entry.c Le projet peut être retrouvé dans ce dépôt : KDAMonitor.\nQue récupérer lors d\u0026rsquo;un événement registre ? # Comme pour chaque article qui concerne un capteur, on commence par lister ce que notre événement doit contenir, voici ce qui a été retenu :\nl\u0026rsquo;ID du processus intéragissant avec un registre (HANDLE ProcessId)\nle chemin vers le processus intéragissant avec un registre (WCHAR ProcessPath[KDAMON_REG_PATH_MAX];)\nl\u0026rsquo;action effectuée (KDAMON_REGISTRY_ACTION Action), c\u0026rsquo;est-à-dire ce que le processus a fait :\ndéfini (créé ou modifié) une valeur dans une clef du registre supprimé une valeur d\u0026rsquo;une clef du registre créé ou ouvert une clef dans le registre L\u0026rsquo;action effectuée est représentée par une enum :\ntypedef enum _KDAMON_REGISTRY_ACTION { KDAMON_REGISTRY_ACTION_SET_VALUE, KDAMON_REGISTRY_ACTION_DELETE_VALUE, KDAMON_REGISTRY_ACTION_CREATE_KEY, } KDAMON_REGISTRY_ACTION; le chemin de la clef concernée (WCHAR KeyPath[KDAMON_REG_PATH_MAX];) le nom de la valeur concernée (WCHAR ValueName[KDAMON_REG_VALUENAME_MAX];) le type de la valeur (ULONG ValueType;) le contenu de la valeur (UCHAR ValueData[KDAMON_REG_VALUEDATA_MAX];) la taille du contenu de la valeur (ULONG ValueDataSize;) le statut à l\u0026rsquo;issue de l\u0026rsquo;opération (NTSTATUS Status;), uniquement renseigné pour la création de clef Implémenter le callback # Tout le code présenté dans cette partie est dans le fichier registry_callback.c. Contrairement aux capteurs précédents, on n\u0026rsquo;enregistre pas une routine par type d\u0026rsquo;événement. On va utiliser CmRegisterCallbackEx qui enregistre une unique routine qui reçoit toutes les opérations du registre. Il faut ensuite qu\u0026rsquo;on décide à quelle(s) opération(s) on souhaite réagir.\nDeux fonctions sont exposées dans le header :\nNTSTATUS KdaMonRegistryCallbackRegister(_In_ PDRIVER_OBJECT DriverObject); : enregistre le callback. Elle prend en plus le DriverObject, exigé par CmRegisterCallbackEx VOID KdaMonRegistryCallbackUnregister(VOID); : désenregistre le callback Le reste est privé :\nKdaMonRegistryCallback : la routine enregistrée, qui joue le rôle de dispatcher KdaMonRegistryHandleSetValueKey, KdaMonRegistryHandleDeleteValueKey, KdaMonRegistryHandlePostCreateKeyEx : un handler par action KdaMonRegistryResolveKeyPath, KdaMonRegistryResolveProcessPath : deux helpers qui résolvent les chemins Voir la documentation Microsoft pour EX_CALLBACK_FUNCTION et CmRegisterCallbackEx.\nEnregistrer et désenregistrer le callback # LARGE_INTEGER g_RegistryCookie = { 0 }; ... NTSTATUS KdaMonRegistryCallbackRegister(_In_ PDRIVER_OBJECT DriverObject) { NTSTATUS status; UNICODE_STRING altitude; RtlInitUnicodeString(\u0026amp;altitude, KDAMON_REG_ALTITUDE); status = CmRegisterCallbackEx( KdaMonRegistryCallback, \u0026amp;altitude, DriverObject, NULL, \u0026amp;g_RegistryCookie, NULL ); if (!NT_SUCCESS(status)) { KdPrint((DRIVER_TAG \u0026#34; [ERROR]: CmRegisterCallbackEx failed: 0x%X\\n\u0026#34;, status)); return status; } KdPrint((DRIVER_TAG \u0026#34; [SUCCESS]: Registry callback registered\\n\u0026#34;)); return STATUS_SUCCESS; } KdaMonRegistryCallback : la routine appelée à chaque opération sur le registre altitude : une chaîne qui définit la position du callback dans la chaîne des filtres registre (KDAMON_REG_ALTITUDE, 360000) \u0026amp;g_RegistryCookie : un identifiant retourné par le noyau pour cet enregistrement Le cookie est global car il sert au désenregistrement et à la résolution du chemin des clefs (voir plus bas).\nVOID KdaMonRegistryCallbackUnregister(VOID) { if (g_RegistryCookie.QuadPart != 0) { CmUnRegisterCallback(g_RegistryCookie); g_RegistryCookie.QuadPart = 0; KdPrint((DRIVER_TAG \u0026#34; [SUCCESS]: Registry callback unregistered\\n\u0026#34;)); } } Le test sur QuadPart évite de désenregistrer un callback qui n\u0026rsquo;a jamais été enregistré.\nLe dispatcher # static NTSTATUS KdaMonRegistryCallback(_In_ PVOID CallbackContext, _In_opt_ PVOID Argument1, _In_opt_ PVOID Argument2) { UNREFERENCED_PARAMETER(CallbackContext); REG_NOTIFY_CLASS notifyClass = (REG_NOTIFY_CLASS)(ULONG_PTR)Argument1; switch (notifyClass) { case RegNtPreSetValueKey: KdaMonRegistryHandleSetValueKey((PREG_SET_VALUE_KEY_INFORMATION)Argument2); break; case RegNtPreDeleteValueKey: KdaMonRegistryHandleDeleteValueKey((PREG_DELETE_VALUE_KEY_INFORMATION)Argument2); break; case RegNtPostCreateKeyEx: KdaMonRegistryHandlePostCreateKeyEx((PREG_POST_OPERATION_INFORMATION)Argument2); break; default: break; } return STATUS_SUCCESS; } Argument1 contient la classe de l\u0026rsquo;opération (REG_NOTIFY_CLASS). Argument2 pointe vers une structure dont le type dépend de cette classe, on la cast donc dans chaque case. Les trois classes gérées sont :\nClasse Structure Moment RegNtPreSetValueKey REG_SET_VALUE_KEY_INFORMATION Avant l\u0026rsquo;écriture de la valeur RegNtPreDeleteValueKey REG_DELETE_VALUE_KEY_INFORMATION Avant la suppression de la valeur RegNtPostCreateKeyEx REG_POST_OPERATION_INFORMATION Après la création de la clef Les deux premières sont des notifications Pre, ce qui signifie que le callback est appelé avant l\u0026rsquo;opération, avec les informations de la valeur, mais sans connaître son résultat. La dernière est une notification Post, l\u0026rsquo;opération est terminée, ce qui donne accès à son Status.\nLe callback retourne toujours STATUS_SUCCESS. Retourner une erreur depuis une notification Pre bloquerait l\u0026rsquo;opération.\nRegNtPostCreateKeyEx est notifié pour tout appel à ZwCreateKey, qui crée la clef ou ouvre une clef déjà existante.\nRésolution du chemin de la clef # static NTSTATUS KdaMonRegistryResolveKeyPath(_In_ PVOID Object, _Out_writes_bytes_(KeyPathBufferSize) PWCHAR KeyPathBuffer, _In_ ULONG KeyPathBufferSize) { NTSTATUS status; PCUNICODE_STRING ObjectName = NULL; KeyPathBuffer[0] = L\u0026#39;\\0\u0026#39;; if (Object == NULL) { return STATUS_INVALID_PARAMETER; } status = CmCallbackGetKeyObjectIDEx( \u0026amp;g_RegistryCookie, Object, NULL, \u0026amp;ObjectName, 0 ); if (!NT_SUCCESS(status) || ObjectName == NULL) { return status; } ULONG charsToCopy = min(ObjectName-\u0026gt;Length / sizeof(WCHAR), KeyPathBufferSize - 1); RtlCopyMemory(KeyPathBuffer, ObjectName-\u0026gt;Buffer, charsToCopy * sizeof(WCHAR)); KeyPathBuffer[charsToCopy] = L\u0026#39;\\0\u0026#39;; CmCallbackReleaseKeyObjectIDEx(ObjectName); return STATUS_SUCCESS; } Les structures reçues contiennent un pointeur vers l\u0026rsquo;objet clef mais pas son chemin. CmCallbackGetKeyObjectIDEx retourne ce chemin (de la forme \\REGISTRY\\MACHINE\\...) dans un UNICODE_STRING alloué par le système, qu\u0026rsquo;il faut libérer avec CmCallbackReleaseKeyObjectIDEx une fois la copie faite.\nLa copie utilise la même troncature sécurisée que dans les articles précédents. Le buffer est mis à vide dès le départ et si la résolution échoue, l\u0026rsquo;événement obtient un chemin vide.\nRésolution du chemin du processus # NTKERNELAPI NTSTATUS SeLocateProcessImageName(_In_ PEPROCESS Process, _Out_ PUNICODE_STRING* pImageFileName); SeLocateProcessImageName est exportée par le noyau mais n\u0026rsquo;est pas déclarée dans les en-têtes du WDK : on écrit donc le prototype à la main en haut du fichier.\nstatic NTSTATUS KdaMonRegistryResolveProcessPath(_Out_writes_z_(Length) PWCHAR Buffer, _In_ ULONG Length) { NTSTATUS status; PUNICODE_STRING imageName = NULL; Buffer[0] = L\u0026#39;\\0\u0026#39;; status = SeLocateProcessImageName(PsGetCurrentProcess(), \u0026amp;imageName); if (!NT_SUCCESS(status) || imageName == NULL) { return status; } ULONG charsToCopy = min(imageName-\u0026gt;Length / sizeof(WCHAR), Length - 1); RtlCopyMemory(Buffer, imageName-\u0026gt;Buffer, charsToCopy * sizeof(WCHAR)); Buffer[charsToCopy] = L\u0026#39;\\0\u0026#39;; ExFreePool(imageName); return STATUS_SUCCESS; } Le callback s\u0026rsquo;exécute dans le contexte du thread qui effectue l\u0026rsquo;opération, PsGetCurrentProcess() est donc le processus qui touche au registre.\nLes trois handlers # Les trois handlers suivent le même squelette :\ncréer l\u0026rsquo;événement et son timestamp remplir le PID (PsGetCurrentProcessId()) et le chemin du processus remplir l\u0026rsquo;action et le chemin de la clef remplir les champs propres à l\u0026rsquo;action pousser l\u0026rsquo;événement dans la file Par exemple, voici KdaMonRegistryHandleSetValueKey en entier :\nstatic VOID KdaMonRegistryHandleSetValueKey(_In_opt_ PREG_SET_VALUE_KEY_INFORMATION Info) { if (Info == NULL || Info-\u0026gt;ValueName == NULL) { return; } KDAMON_EVENT Event = { 0 }; Event.Type = KdaMonEventRegistry; KeQuerySystemTimePrecise(\u0026amp;Event.Timestamp); Event.Data.Registry.ProcessId = PsGetCurrentProcessId(); KdaMonRegistryResolveProcessPath( Event.Data.Registry.ProcessPath, RTL_NUMBER_OF(Event.Data.Registry.ProcessPath) ); Event.Data.Registry.Action = KDAMON_REGISTRY_ACTION_SET_VALUE; KdaMonRegistryResolveKeyPath( Info-\u0026gt;Object, Event.Data.Registry.KeyPath, RTL_NUMBER_OF(Event.Data.Registry.KeyPath) ); ULONG nameChars = min( Info-\u0026gt;ValueName-\u0026gt;Length / sizeof(WCHAR), RTL_NUMBER_OF(Event.Data.Registry.ValueName) - 1 ); RtlCopyMemory(Event.Data.Registry.ValueName, Info-\u0026gt;ValueName-\u0026gt;Buffer, nameChars * sizeof(WCHAR)); Event.Data.Registry.ValueName[nameChars] = L\u0026#39;\\0\u0026#39;; Event.Data.Registry.ValueType = Info-\u0026gt;Type; ULONG dataSize = min(Info-\u0026gt;DataSize, KDAMON_REG_VALUEDATA_MAX); if (Info-\u0026gt;Data != NULL \u0026amp;\u0026amp; dataSize \u0026gt; 0) { RtlCopyMemory(Event.Data.Registry.ValueData, Info-\u0026gt;Data, dataSize); } Event.Data.Registry.ValueDataSize = dataSize; KdaMonEventQueuePush(\u0026amp;Event); } Les données de la valeur sont tronquées à KDAMON_REG_VALUEDATA_MAX octets, et ValueDataSize contient la taille copiée.\nLes deux autres handlers ne diffèrent que sur l\u0026rsquo;étape 4 :\nKdaMonRegistryHandleDeleteValueKey (PREG_DELETE_VALUE_KEY_INFORMATION) : seul le nom de la valeur est copié, ValueType et ValueDataSize valent 0 KdaMonRegistryHandlePostCreateKeyEx (PREG_POST_OPERATION_INFORMATION) : pas de nom de valeur et c\u0026rsquo;est le seul handler qui récupère le résultat de l\u0026rsquo;opération avec Event.Data.Registry.Status = Info-\u0026gt;Status; Les codes complets de KdaMonRegistryHandleDeleteValueKey et KdaMonRegistryHandlePostCreateKeyEx peuvent être trouvés dans le fichier KDAMonitor/driver/src/registry_callback.c.\nSérialiser les événements registre en JSONL # Voici la fonction dédiée à la sérialisation des événements concernant les registres :\nstatic NTSTATUS KdaMonLogWriterWriteRegistryEvent(_In_ const KDAMON_EVENT* Event, _Out_writes_z_(BufferSize) PSTR EventBuffer, _In_ SIZE_T BufferSize) { CHAR EscapedKeyPath[520]; CHAR EscapedValueName[520]; CHAR FormattedValueData[600]; CHAR StatusField[16]; const char* action; if (!KdaMonJsonEscapeW(Event-\u0026gt;Data.Registry.KeyPath, EscapedKeyPath, sizeof(EscapedKeyPath))) { KdPrint((DRIVER_TAG \u0026#34; [WARNING]: KeyPath truncated during JSON escape (event %lu)\\n\u0026#34;, Event-\u0026gt;Id)); } switch (Event-\u0026gt;Data.Registry.Action) { case KDAMON_REGISTRY_ACTION_SET_VALUE: { action = \u0026#34;set_value\u0026#34;; break; } case KDAMON_REGISTRY_ACTION_DELETE_VALUE: { action = \u0026#34;delete_value\u0026#34;; break; } case KDAMON_REGISTRY_ACTION_CREATE_KEY: { action = \u0026#34;create_key\u0026#34;; break; } default: { action = \u0026#34;unknown\u0026#34;; break; } } if (Event-\u0026gt;Data.Registry.Action == KDAMON_REGISTRY_ACTION_CREATE_KEY) { RtlStringCbCopyA(EscapedValueName, sizeof(EscapedValueName), \u0026#34;\u0026#34;); RtlStringCbCopyA(FormattedValueData, sizeof(FormattedValueData), \u0026#34;null\u0026#34;); } else { if (!KdaMonJsonEscapeW(Event-\u0026gt;Data.Registry.ValueName, EscapedValueName, sizeof(EscapedValueName))) { KdPrint((DRIVER_TAG \u0026#34; [WARNING]: ValueName truncated during JSON escape (event %lu)\\n\u0026#34;, Event-\u0026gt;Id)); } if (Event-\u0026gt;Data.Registry.Action == KDAMON_REGISTRY_ACTION_SET_VALUE) { KdaMonRegistryFormatValueData(\u0026amp;Event-\u0026gt;Data.Registry, FormattedValueData, sizeof(FormattedValueData)); } else { RtlStringCbCopyA(FormattedValueData, sizeof(FormattedValueData), \u0026#34;null\u0026#34;); } } if (Event-\u0026gt;Data.Registry.Action == KDAMON_REGISTRY_ACTION_CREATE_KEY) { RtlStringCbPrintfA(StatusField, sizeof(StatusField), \u0026#34;\\\u0026#34;0x%08X\\\u0026#34;\u0026#34;, (ULONG)Event-\u0026gt;Data.Registry.Status); } else { RtlStringCbCopyA(StatusField, sizeof(StatusField), \u0026#34;null\u0026#34;); } return RtlStringCbPrintfA( EventBuffer, BufferSize, \u0026#34;{\\\u0026#34;id\\\u0026#34;:%lu,\\\u0026#34;type\\\u0026#34;:\\\u0026#34;%s\\\u0026#34;,\\\u0026#34;timestamp\\\u0026#34;:%lld,\u0026#34; \u0026#34;\\\u0026#34;pid\\\u0026#34;:%lu,\\\u0026#34;action\\\u0026#34;:\\\u0026#34;%s\\\u0026#34;,\u0026#34; \u0026#34;\\\u0026#34;key_path\\\u0026#34;:\\\u0026#34;%s\\\u0026#34;,\\\u0026#34;value_name\\\u0026#34;:\\\u0026#34;%s\\\u0026#34;,\\\u0026#34;value_data\\\u0026#34;:%s,\u0026#34; \u0026#34;\\\u0026#34;status\\\u0026#34;:%s}\\n\u0026#34;, Event-\u0026gt;Id, KdaMonEventTypeToString(Event-\u0026gt;Type), Event-\u0026gt;Timestamp.QuadPart, (ULONG)(ULONG_PTR)Event-\u0026gt;Data.Registry.ProcessId, action, EscapedKeyPath, EscapedValueName, FormattedValueData, StatusField ); } Voici, brièvement, le déroulement de la fonction KdaMonLogWriterWriteRegistryEvent :\nOn échappe le chemin de la clef avec KdaMonJsonEscapeW. On convertit l\u0026rsquo;action en chaîne (set_value, delete_value ou create_key). On prépare les champs qui dépendent de l\u0026rsquo;action : create_key : pas de nom ni de contenu de valeur (value_data vaut null) set_value : le nom de la valeur est échappé et son contenu formaté par KdaMonRegistryFormatValueData delete_value : le nom de la valeur est échappé, value_data vaut null Le statut n\u0026rsquo;a de sens que pour create_key (notification Post), pour les autres actions, il vaut null. On remplit le buffer EventBuffer avec les informations de l\u0026rsquo;événement. KdaMonRegistryFormatValueData choisit le format du champ value_data d\u0026rsquo;après ValueType (le détail de cette fonction ne sera pas montré ici, voir log_writer.c) :\nType Format dans le JSON REG_SZ, REG_EXPAND_SZ chaîne échappée entre guillemets REG_DWORD, REG_QWORD nombre décimal tous les autres (REG_BINARY, REG_MULTI_SZ, \u0026hellip;) chaîne hexadécimale entre guillemets Les contenus binaires sont tronqués à KDAMON_REG_VALUEDATA_MAX octets, comme à la capture.\nIl ne reste plus qu\u0026rsquo;à appeler cette fonction dans le switch de KdaMonLogWriterWriteEvent :\ncase KdaMonEventRegistry: status = KdaMonLogWriterWriteRegistryEvent(Event, EventBuffer, sizeof(EventBuffer)); break; Les lignes d\u0026rsquo;événement registre auront les formes suivantes :\n{\u0026#34;id\u0026#34;:20,\u0026#34;type\u0026#34;:\u0026#34;registry\u0026#34;,\u0026#34;timestamp\u0026#34;:134303322704667968,\u0026#34;pid\u0026#34;:4836,\u0026#34;action\u0026#34;:\u0026#34;create_key\u0026#34;,\u0026#34;key_path\u0026#34;:\u0026#34;\\\\REGISTRY\\\\MACHINE\\\\SOFTWARE\\\\KDAMonitorTest\u0026#34;,\u0026#34;value_name\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;value_data\u0026#34;:null,\u0026#34;status\u0026#34;:\u0026#34;0x00000000\u0026#34;} {\u0026#34;id\u0026#34;:21,\u0026#34;type\u0026#34;:\u0026#34;registry\u0026#34;,\u0026#34;timestamp\u0026#34;:134303322704668353,\u0026#34;pid\u0026#34;:4836,\u0026#34;action\u0026#34;:\u0026#34;set_value\u0026#34;,\u0026#34;key_path\u0026#34;:\u0026#34;\\\\REGISTRY\\\\MACHINE\\\\SOFTWARE\\\\KDAMonitorTest\u0026#34;,\u0026#34;value_name\u0026#34;:\u0026#34;StringValue\u0026#34;,\u0026#34;value_data\u0026#34;:\u0026#34;hello\u0026#34;,\u0026#34;status\u0026#34;:null} {\u0026#34;id\u0026#34;:109,\u0026#34;type\u0026#34;:\u0026#34;registry\u0026#34;,\u0026#34;timestamp\u0026#34;:134303322707340091,\u0026#34;pid\u0026#34;:3584,\u0026#34;action\u0026#34;:\u0026#34;delete_value\u0026#34;,\u0026#34;key_path\u0026#34;:\u0026#34;\\\\REGISTRY\\\\MACHINE\\\\SOFTWARE\\\\KDAMonitorTest\u0026#34;,\u0026#34;value_name\u0026#34;:\u0026#34;StringValue\u0026#34;,\u0026#34;value_data\u0026#34;:null,\u0026#34;status\u0026#34;:null} Au passage, le ProcessId des événements réseau est passé de ULONG à HANDLE dans cette version, pour rester cohérent avec les autres capteurs.\nIntégration dans driver_entry.c # Ce capteur est le dernier enregistré, et le premier désenregistré.\nDonc dans DriverUnload :\nvoid DriverUnload(_In_ PDRIVER_OBJECT DriverObject) { UNREFERENCED_PARAMETER(DriverObject); KdPrint((DRIVER_TAG \u0026#34; [INFO]: Driver Unload begin\\n\u0026#34;)); // --- Unregister callbacks --- KdaMonRegistryCallbackUnregister(); KdaMonImageCallbackUnregister(); KdaMonProcessCallbackUnregister(); // --- Stop the log writer --- KdaMonLogWriterStop(); // --- Destroy the event queue --- KdaMonEventQueueDestroy(); // --- Cleanup WFP callout --- KdaMonWfpCalloutUnregister(); // --- Cleanup WFP session --- KdaMonWfpSessionCleanup(); // --- Delete device object --- KdaMonDeleteDevice(\u0026amp;g_DeviceObject); KdPrint((DRIVER_TAG \u0026#34; [INFO]: Driver Unload complete\\n\u0026#34;)); } Dans DriverEntry, on l\u0026rsquo;enregistre après l\u0026rsquo;enregistrement du callback des images :\n// --- Register image load callback --- ... // --- Register registry callback --- status = KdaMonRegistryCallbackRegister(DriverObject); if (!NT_SUCCESS(status)) { KdPrint((DRIVER_TAG \u0026#34; [ERROR]: KdaMonRegistryCallbackRegister failed\\n\u0026#34;)); goto cleanup_image; } KdPrint((DRIVER_TAG \u0026#34; [INFO]: Initialized successfully\\n\u0026#34;)); return STATUS_SUCCESS; Validation # Le test doit couvrir les trois actions et les principaux types de valeur. Voici test_v09.ps1 :\n# test_v09.ps1 $Driver = \u0026#34;KDAMonitor\u0026#34; $DriverPath = \u0026#34;$env:USERPROFILE\\Desktop\\$Driver.sys\u0026#34; $TestKey = \u0026#34;HKLM\\SOFTWARE\\KDAMonitorTest\u0026#34; sc.exe stop $Driver sc.exe delete $Driver sc.exe create $Driver type= kernel binPath= $DriverPath sc.exe start $Driver reg add $TestKey /f reg add $TestKey /v StringValue /t REG_SZ /d \u0026#34;hello\u0026#34; /f reg add $TestKey /v ExpandValue /t REG_EXPAND_SZ /d \u0026#34;%TEMP%\u0026#34; /f reg add $TestKey /v DwordValue /t REG_DWORD /d 42 /f reg add $TestKey /v QwordValue /t REG_QWORD /d 42 /f reg add $TestKey /v BinaryValue /t REG_BINARY /d deadbeef /f reg delete $TestKey /v StringValue /f reg delete $TestKey /f sc.exe stop $Driver sc.exe delete $Driver Le premier reg add sur la clef seule déclenche create_key ; les cinq suivants déclenchent set_value, un par type de valeur géré par KdaMonRegistryFormatValueData ; les deux reg delete couvrent delete_value puis la suppression de la clef elle-même, cette dernière n\u0026rsquo;est pas capturée par le capteur car il ne suit que les valeurs.\nVoici une démonstration de ce test en exécution :\nEn filtrant le journal sur le nom de la clef de test, on retrouve les douze événements produits par le script : six create_key (un par commande reg add, la clef étant déjà présente dès la deuxième), cinq set_value avec le format attendu pour chaque type testé, et un delete_value pour la valeur explicitement supprimée en fin de script.\nSelect-String -Path \u0026#34;C:\\KDAMonitor\\logs\\*.jsonl\u0026#34; -Pattern \u0026#34;KDAMonitorTest\u0026#34; -SimpleMatch C:\\KDAMonitor\\logs\\kdamon_20260804_155110.jsonl:10:{\u0026#34;id\u0026#34;:9,\u0026#34;type\u0026#34;:\u0026#34;Registry\u0026#34;,\u0026#34;timestamp\u0026#34;:134303322704282678,\u0026#34;pid\u0026#34;:11028,\u0026#34;action\u0026#34;:\u0026#34;create_key\u0026#34;,\u0026#34;key_path\u0026#34;:\u0026#34;\\REGISTRY\\MACHINE\\SOFTWARE\\KDAMonitorTest\u0026#34;,\u0026#34;value_name\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;value_data\u0026#34;:null,\u0026#34;status\u0026#34;:\u0026#34;0x00000000\u0026#34;} C:\\KDAMonitor\\logs\\kdamon_20260804_155110.jsonl:11:{\u0026#34;id\u0026#34;:10,\u0026#34;type\u0026#34;:\u0026#34;Registry\u0026#34;,\u0026#34;timestamp\u0026#34;:134303322704283042,\u0026#34;pid\u0026#34;:11028,\u0026#34;action\u0026#34;:\u0026#34;set_value\u0026#34;,\u0026#34;key_path\u0026#34;:\u0026#34;\\REGISTRY\\MACHINE\\SOFTWARE\\KDAMonitorTest\u0026#34;,\u0026#34;value_name\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;value_data\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;status\u0026#34;:null} C:\\KDAMonitor\\logs\\kdamon_20260804_155110.jsonl:21:{\u0026#34;id\u0026#34;:20,\u0026#34;type\u0026#34;:\u0026#34;Registry\u0026#34;,\u0026#34;timestamp\u0026#34;:134303322704667968,\u0026#34;pid\u0026#34;:4836,\u0026#34;action\u0026#34;:\u0026#34;create_key\u0026#34;,\u0026#34;key_path\u0026#34;:\u0026#34;\\REGISTRY\\MACHINE\\SOFTWARE\\KDAMonitorTest\u0026#34;,\u0026#34;value_name\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;value_data\u0026#34;:null,\u0026#34;status\u0026#34;:\u0026#34;0x00000000\u0026#34;} C:\\KDAMonitor\\logs\\kdamon_20260804_155110.jsonl:22:{\u0026#34;id\u0026#34;:21,\u0026#34;type\u0026#34;:\u0026#34;Registry\u0026#34;,\u0026#34;timestamp\u0026#34;:134303322704668353,\u0026#34;pid\u0026#34;:4836,\u0026#34;action\u0026#34;:\u0026#34;set_value\u0026#34;,\u0026#34;key_path\u0026#34;:\u0026#34;\\REGISTRY\\MACHINE\\SOFTWARE\\KDAMonitorTest\u0026#34;,\u0026#34;value_name\u0026#34;:\u0026#34;StringValue\u0026#34;,\u0026#34;value_data\u0026#34;:\u0026#34;hello\u0026#34;,\u0026#34;status\u0026#34;:null} C:\\KDAMonitor\\logs\\kdamon_20260804_155110.jsonl:32:{\u0026#34;id\u0026#34;:31,\u0026#34;type\u0026#34;:\u0026#34;Registry\u0026#34;,\u0026#34;timestamp\u0026#34;:134303322705186483,\u0026#34;pid\u0026#34;:3796,\u0026#34;action\u0026#34;:\u0026#34;create_key\u0026#34;,\u0026#34;key_path\u0026#34;:\u0026#34;\\REGISTRY\\MACHINE\\SOFTWARE\\KDAMonitorTest\u0026#34;,\u0026#34;value_name\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;value_data\u0026#34;:null,\u0026#34;status\u0026#34;:\u0026#34;0x00000000\u0026#34;} C:\\KDAMonitor\\logs\\kdamon_20260804_155110.jsonl:33:{\u0026#34;id\u0026#34;:32,\u0026#34;type\u0026#34;:\u0026#34;Registry\u0026#34;,\u0026#34;timestamp\u0026#34;:134303322705186841,\u0026#34;pid\u0026#34;:3796,\u0026#34;action\u0026#34;:\u0026#34;set_value\u0026#34;,\u0026#34;key_path\u0026#34;:\u0026#34;\\REGISTRY\\MACHINE\\SOFTWARE\\KDAMonitorTest\u0026#34;,\u0026#34;value_name\u0026#34;:\u0026#34;ExpandValue\u0026#34;,\u0026#34;value_data\u0026#34;:\u0026#34;%TEMP%\u0026#34;,\u0026#34;status\u0026#34;:null} C:\\KDAMonitor\\logs\\kdamon_20260804_155110.jsonl:77:{\u0026#34;id\u0026#34;:76,\u0026#34;type\u0026#34;:\u0026#34;Registry\u0026#34;,\u0026#34;timestamp\u0026#34;:134303322706094293,\u0026#34;pid\u0026#34;:1048,\u0026#34;action\u0026#34;:\u0026#34;create_key\u0026#34;,\u0026#34;key_path\u0026#34;:\u0026#34;\\REGISTRY\\MACHINE\\SOFTWARE\\KDAMonitorTest\u0026#34;,\u0026#34;value_name\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;value_data\u0026#34;:null,\u0026#34;status\u0026#34;:\u0026#34;0x00000000\u0026#34;} C:\\KDAMonitor\\logs\\kdamon_20260804_155110.jsonl:78:{\u0026#34;id\u0026#34;:77,\u0026#34;type\u0026#34;:\u0026#34;Registry\u0026#34;,\u0026#34;timestamp\u0026#34;:134303322706095212,\u0026#34;pid\u0026#34;:1048,\u0026#34;action\u0026#34;:\u0026#34;set_value\u0026#34;,\u0026#34;key_path\u0026#34;:\u0026#34;\\REGISTRY\\MACHINE\\SOFTWARE\\KDAMonitorTest\u0026#34;,\u0026#34;value_name\u0026#34;:\u0026#34;DwordValue\u0026#34;,\u0026#34;value_data\u0026#34;:42,\u0026#34;status\u0026#34;:null} C:\\KDAMonitor\\logs\\kdamon_20260804_155110.jsonl:88:{\u0026#34;id\u0026#34;:87,\u0026#34;type\u0026#34;:\u0026#34;Registry\u0026#34;,\u0026#34;timestamp\u0026#34;:134303322706480125,\u0026#34;pid\u0026#34;:608,\u0026#34;action\u0026#34;:\u0026#34;create_key\u0026#34;,\u0026#34;key_path\u0026#34;:\u0026#34;\\REGISTRY\\MACHINE\\SOFTWARE\\KDAMonitorTest\u0026#34;,\u0026#34;value_name\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;value_data\u0026#34;:null,\u0026#34;status\u0026#34;:\u0026#34;0x00000000\u0026#34;} C:\\KDAMonitor\\logs\\kdamon_20260804_155110.jsonl:89:{\u0026#34;id\u0026#34;:88,\u0026#34;type\u0026#34;:\u0026#34;Registry\u0026#34;,\u0026#34;timestamp\u0026#34;:134303322706481009,\u0026#34;pid\u0026#34;:608,\u0026#34;action\u0026#34;:\u0026#34;set_value\u0026#34;,\u0026#34;key_path\u0026#34;:\u0026#34;\\REGISTRY\\MACHINE\\SOFTWARE\\KDAMonitorTest\u0026#34;,\u0026#34;value_name\u0026#34;:\u0026#34;QwordValue\u0026#34;,\u0026#34;value_data\u0026#34;:42,\u0026#34;status\u0026#34;:null} C:\\KDAMonitor\\logs\\kdamon_20260804_155110.jsonl:99:{\u0026#34;id\u0026#34;:98,\u0026#34;type\u0026#34;:\u0026#34;Registry\u0026#34;,\u0026#34;timestamp\u0026#34;:134303322706957224,\u0026#34;pid\u0026#34;:7816,\u0026#34;action\u0026#34;:\u0026#34;create_key\u0026#34;,\u0026#34;key_path\u0026#34;:\u0026#34;\\REGISTRY\\MACHINE\\SOFTWARE\\KDAMonitorTest\u0026#34;,\u0026#34;value_name\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;value_data\u0026#34;:null,\u0026#34;status\u0026#34;:\u0026#34;0x00000000\u0026#34;} C:\\KDAMonitor\\logs\\kdamon_20260804_155110.jsonl:100:{\u0026#34;id\u0026#34;:99,\u0026#34;type\u0026#34;:\u0026#34;Registry\u0026#34;,\u0026#34;timestamp\u0026#34;:134303322706958245,\u0026#34;pid\u0026#34;:7816,\u0026#34;action\u0026#34;:\u0026#34;set_value\u0026#34;,\u0026#34;key_path\u0026#34;:\u0026#34;\\REGISTRY\\MACHINE\\SOFTWARE\\KDAMonitorTest\u0026#34;,\u0026#34;value_name\u0026#34;:\u0026#34;BinaryValue\u0026#34;,\u0026#34;value_data\u0026#34;:\u0026#34;deadbeef\u0026#34;,\u0026#34;status\u0026#34;:null} C:\\KDAMonitor\\logs\\kdamon_20260804_155110.jsonl:110:{\u0026#34;id\u0026#34;:109,\u0026#34;type\u0026#34;:\u0026#34;Registry\u0026#34;,\u0026#34;timestamp\u0026#34;:134303322707340091,\u0026#34;pid\u0026#34;:3584,\u0026#34;action\u0026#34;:\u0026#34;delete_value\u0026#34;,\u0026#34;key_path\u0026#34;:\u0026#34;\\REGISTRY\\MACHINE\\SOFTWARE\\KDAMonitorTest\u0026#34;,\u0026#34;value_name\u0026#34;:\u0026#34;StringValue\u0026#34;,\u0026#34;value_data\u0026#34;:null,\u0026#34;status\u0026#34;:null} Conclusion # L\u0026rsquo;avant-dernier capteur est fait ! KDAMonitor journalise maintenant les processus, les chargements d\u0026rsquo;images, les connexions réseau et l\u0026rsquo;activité du registre. On approche de la fin de ce projet petit à petit\u0026hellip;\nVoici l\u0026rsquo;architecture mise à jour :\nMerci d\u0026rsquo;avoir lu jusqu\u0026rsquo;au bout et à bientôt pour le prochain et dixième article de cette série : Surveillance de la création et de la terminaison des threads.\n","date":"28 septembre 2026","externalUrl":null,"permalink":"/posts/09-registry-sensor/","section":"Blog","summary":"Implémentation du quatrième capteur de KDAMonitor : surveillance de la création, modification et suppression de valeurs de registre.","title":"09 - Surveillance de l'activité du registre","type":"posts"},{"content":"","date":"28 septembre 2026","externalUrl":null,"permalink":"/","section":"Accueil","summary":"","title":"Accueil","type":"page"},{"content":"Articles, notes techniques et recherches personnelles.\n","date":"28 septembre 2026","externalUrl":null,"permalink":"/posts/","section":"Blog","summary":"","title":"Blog","type":"posts"},{"content":"","date":"28 septembre 2026","externalUrl":null,"permalink":"/tags/c/","section":"Tags","summary":"","title":"C","type":"tags"},{"content":"","date":"28 septembre 2026","externalUrl":null,"permalink":"/series/kdamonitor/","section":"Series","summary":"Développement d’un driver Windows Kernel pour surveiller l’activité système : processus, images, réseau, registre et threads.","title":"KDAMonitor","type":"series"},{"content":"","date":"28 septembre 2026","externalUrl":null,"permalink":"/tags/kdamonitor/","section":"Tags","summary":"","title":"KDAMonitor","type":"tags"},{"content":"","date":"28 septembre 2026","externalUrl":null,"permalink":"/tags/kernel-driver/","section":"Tags","summary":"","title":"Kernel Driver","type":"tags"},{"content":"","date":"28 septembre 2026","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"},{"content":"","date":"28 septembre 2026","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"","date":"28 septembre 2026","externalUrl":null,"permalink":"/tags/windows-kernel/","section":"Tags","summary":"","title":"Windows Kernel","type":"tags"},{"content":"Bienvenue dans le huitième article de la série sur le développement de KDAMonitor !\nDans cet article qui reprend la v0.8 du projet, je vais implémenter le troisième capteur de KDAMonitor : le capteur des connexions réseau. Pour rappel, dans l\u0026rsquo;article précédent (sur la v0.7), j\u0026rsquo;avais implémenté la session WFP (moteur, provider et sublayer) sans rajouter la partie observation.\nLe capteur devra inspecter les paquets IPv4 qui passent, sans les bloquer, et en extraire les données importantes. Contrairement aux autres capteurs, il n\u0026rsquo;y a pas de routine ou de callback cette fois : on va parler d\u0026rsquo;un callout appelé par un filtre qu\u0026rsquo;on ajoute au sublayer.\nVoici les fichiers concernés par cet article, et la section qui les explique :\nFichier Rôle Section event_types.h Nouveau type KDAMON_NETWORK_EVENT_DATA, enum KDAMON_NETWORK_DIRECTION + union dans KDAMON_EVENT Que récupérer lors d\u0026rsquo;une connexion réseau ? guids.c Centralisation des DEFINE_GUID (session + callouts) Centraliser les GUID wfp_callout.h Déclaration du register/unregister Implémenter le callout wfp_callout.c Les callouts, leurs filtres, l\u0026rsquo;enregistrement Implémenter le callout wfp_session.c / wfp_session.h Handle du moteur partagé, poids du sublayer Adapter la session WFP log_writer.c Sérialisation JSONL des événements réseau Sérialiser les événements réseau en JSONL driver_entry.c Enregistrement/désenregistrement du callout Intégration dans driver_entry.c Le projet peut être retrouvé dans ce dépôt : KDAMonitor.\nQue récupérer lors d\u0026rsquo;une connexion réseau ? # Comme pour chaque capteur, il faut d\u0026rsquo;abord décider quelles informations retenir d\u0026rsquo;une connexion réseau. J\u0026rsquo;en ai retenu six :\nle PID du processus à l\u0026rsquo;origine de la connexion le chemin de ce processus le protocole utilisé l\u0026rsquo;adresse IP et le port locaux l\u0026rsquo;adresse IP et le port distants la direction : la connexion est-elle entrante ou sortante ? On a donc les structures suivantes :\ntypedef enum _KDAMON_NETWORK_DIRECTION { KDAMON_NETWORK_DIRECTION_INBOUND, KDAMON_NETWORK_DIRECTION_OUTBOUND, } KDAMON_NETWORK_DIRECTION; typedef struct _KDAMON_NETWORK_EVENT_DATA { ULONG ProcessId; WCHAR ProcessPath[260]; UINT8 Protocol; ULONG LocalIp; USHORT LocalPort; ULONG RemoteIp; USHORT RemotePort; KDAMON_NETWORK_DIRECTION Direction; } KDAMON_NETWORK_EVENT_DATA; Pour les adresses IP et les ports, j\u0026rsquo;ai choisi ULONG et USHORT par simplicité, ce qui limite cette version à IPv4.\nIl faut bien sûr l\u0026rsquo;ajouter dans l\u0026rsquo;union de la structure finale :\ntypedef struct _KDAMON_EVENT { ... union { KDAMON_PROCESS_EVENT_DATA Process; KDAMON_IMAGE_LOAD_EVENT_DATA ImageLoad; KDAMON_NETWORK_EVENT_DATA Network; } Data; } KDAMON_EVENT, * PKDAMON_EVENT; Le magic number 260 correspond à la longueur maximale des chemins dans Windows et sera changé en une macro dans une version ultérieure :)\nComment fonctionne un callout WFP ? # Jusqu\u0026rsquo;ici, tous les capteurs fonctionnaient sur le même principe de callback : on donnait à Windows une fonction, via une routine, et il l\u0026rsquo;exécutait quand l\u0026rsquo;événement se produisait. Le moteur WFP n\u0026rsquo;appelle pas directement nos fonctions. Il applique des filtres au trafic, et si un paquet passe le filtre, celui-ci déclenche notre callout.\nDans l\u0026rsquo;article précédent, on a ouvert la session et enregistré le provider et le sublayer. Il manque maintenant ce qui va réellement regarder le trafic.\nLes couches ALE # WFP découpe le traitement réseau en plusieurs couches. Celles qui nous intéressent sont les couches ALE (Application Layer Enforcement, documentation), qui suivent les connexions au niveau applicatif et fournissent le PID et le chemin du processus dans les métadonnées. J\u0026rsquo;en utilise deux :\nCouche Direction Se déclenche pour FWPM_LAYER_ALE_AUTH_CONNECT_V4 sortant un connect() TCP, le premier paquet UDP vers un couple adresse/port distant, le premier message ICMP sortant FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4 entrant une connexion TCP entrante, le premier paquet UDP entrant depuis un couple adresse/port, le premier message ICMP entrant On obtient donc une notification par connexion (ou par flux UDP/ICMP). Pour chaque direction, il y a trois objets à mettre en place :\nLe callout noyau, avec FwpsCalloutRegister2. C\u0026rsquo;est ici qu\u0026rsquo;on fournit nos fonctions dans une structure FWPS_CALLOUT2 : classifyFn (appelée quand du trafic correspond à un filtre), notifyFn (notifications sur les filtres ajoutés ou retirés) et flowDeleteFn (inutile ici, laissée à NULL). L\u0026rsquo;ajout du callout, avec FwpmCalloutAdd, qui annonce au moteur qu\u0026rsquo;un callout existe pour telle couche. Le filtre, avec FwpmFilterAdd, qui se place dans notre sublayer et désigne le callout. Observer sans bloquer # Dans notre cas, le filtre ne sert pas à filtrer : il sert à déclencher le callout sur tout le trafic IPv4 de la couche, pour en extraire des informations. On lui donne l\u0026rsquo;action FWP_ACTION_CALLOUT_INSPECTION, qui indique que le callout observe sans décider, donc sans bloquer. En retour, classifyFn doit rendre FWP_ACTION_CONTINUE pour que le trafic poursuive son chemin.\nCentraliser les GUID # Le callout, le filtre, le provider et le sublayer sont tous identifiés par des GUID. Lors de la v0.7, les deux GUID de la session étaient définis en haut de wfp_session.c. Avec les deux GUID des callouts, qui sont utilisés dans wfp_callout.c, il y en a maintenant quatre répartis sur plusieurs fichiers. Je les ai donc regroupés dans un fichier dédié, guids.c.\nOn définit INITGUID dans guids.c uniquement, et les autres fichiers ne voient que les déclarations extern const GUID de leurs headers :\n// guids.c #define INITGUID #include \u0026lt;guiddef.h\u0026gt; #include \u0026lt;ntddk.h\u0026gt; #define NDIS630 #include \u0026lt;ndis.h\u0026gt; #include \u0026lt;fwpmk.h\u0026gt; // --- Session GUIDs --- DEFINE_GUID(KDAMON_WFP_PROVIDER_GUID, ...); DEFINE_GUID(KDAMON_WFP_SUBLAYER_GUID, ...); // --- Callout GUIDs --- DEFINE_GUID(KDAMON_WFP_CALLOUT_OUTBOUND_GUID, ...); DEFINE_GUID(KDAMON_WFP_CALLOUT_INBOUND_GUID, ...); // wfp_callout.h extern const GUID KDAMON_WFP_CALLOUT_OUTBOUND_GUID; extern const GUID KDAMON_WFP_CALLOUT_INBOUND_GUID; Ainsi, wfp_session.c n\u0026rsquo;a plus besoin de définir INITGUID ni ses propres GUID, et si un GUID change, il n\u0026rsquo;y a qu\u0026rsquo;un seul endroit à modifier.\nImplémenter le callout # Tout est dans le fichier wfp_callout.c. Deux fonctions sont exposées dans le header, et tout le reste est privé :\nNTSTATUS KdaMonWfpCalloutRegister(PDEVICE_OBJECT DeviceObject); : enregistre les callouts et leurs filtres VOID KdaMonWfpCalloutUnregister(VOID); : les retire Le fichier garde en variables globales les identifiants renvoyés par WFP, nécessaires pour tout retirer plus tard :\nstatic UINT32 g_WpsCalloutIdOutbound = 0; static UINT32 g_WpsCalloutIdInbound = 0; static UINT64 g_FilterIdOutbound = 0; static UINT64 g_FilterIdInbound = 0; Le traitement des paquets # Quand un paquet correspond au filtre, WFP appelle classifyFn avec trois paramètres :\ninFixedValues : les champs de la couche filtrée (protocole, adresses, ports) inMetaValues : les métadonnées (PID, chemin du processus) classifyOut : où on indique au moteur ce qu\u0026rsquo;on décide Les champs de inFixedValues sont accessibles par un indice FWPS_FIELD_* qui dépend de la couche. Le traitement est identique pour les connexions sortantes et entrantes, on a donc une fonction commune qui reçoit ces indices en paramètres :\nstatic VOID KdaMonWfpClassifyCommon( _In_ const FWPS_INCOMING_VALUES0* inFixedValues, _In_ const FWPS_INCOMING_METADATA_VALUES0* inMetaValues, _Inout_ FWPS_CLASSIFY_OUT0* classifyOut, _In_ KDAMON_NETWORK_DIRECTION Direction, _In_ UINT32 FieldProtocol, _In_ UINT32 FieldLocalIp, _In_ UINT32 FieldLocalPort, _In_ UINT32 FieldRemoteIp, _In_ UINT32 FieldRemotePort ) { KDAMON_EVENT Event = { 0 }; Event.Type = KdaMonEventNetwork; KeQuerySystemTimePrecise(\u0026amp;Event.Timestamp); // --- PID --- Event.Data.Network.ProcessId = (ULONG)inMetaValues-\u0026gt;processId; // --- Process path --- if (inMetaValues-\u0026gt;processPath \u0026amp;\u0026amp; inMetaValues-\u0026gt;processPath-\u0026gt;data \u0026amp;\u0026amp; inMetaValues-\u0026gt;processPath-\u0026gt;size \u0026gt; 0) { SIZE_T bytesToCopy = min( inMetaValues-\u0026gt;processPath-\u0026gt;size, (RTL_NUMBER_OF(Event.Data.Network.ProcessPath) - 1) * sizeof(WCHAR) ); RtlCopyMemory(Event.Data.Network.ProcessPath, inMetaValues-\u0026gt;processPath-\u0026gt;data, bytesToCopy); Event.Data.Network.ProcessPath[bytesToCopy / sizeof(WCHAR)] = L\u0026#39;\\0\u0026#39;; } // --- Protocol, IPs, ports --- Event.Data.Network.Protocol = (UINT8)inFixedValues-\u0026gt;incomingValue[FieldProtocol].value.uint8; Event.Data.Network.LocalIp = inFixedValues-\u0026gt;incomingValue[FieldLocalIp].value.uint32; Event.Data.Network.RemoteIp = inFixedValues-\u0026gt;incomingValue[FieldRemoteIp].value.uint32; Event.Data.Network.LocalPort = inFixedValues-\u0026gt;incomingValue[FieldLocalPort].value.uint16; Event.Data.Network.RemotePort = inFixedValues-\u0026gt;incomingValue[FieldRemotePort].value.uint16; Event.Data.Network.Direction = Direction; KdaMonEventQueuePush(\u0026amp;Event); classifyOut-\u0026gt;actionType = FWP_ACTION_CONTINUE; } Voici un résumé du déroulement de la fonction :\nOn crée l\u0026rsquo;événement comme pour les autres capteurs. Le PID vient des métadonnées et WFP le fournit sur 64 bits, on le réduit à un ULONG. Le chemin du processus est fourni sous forme de blob (FWP_BYTE_BLOB) dont la taille est en octets. Protocole, adresses et ports sont lus dans inFixedValues et ajoutés à l\u0026rsquo;événement. On ajoute l\u0026rsquo;événement dans la file et on rend FWP_ACTION_CONTINUE. On garde le chemin au format NT (\\Device\\HarddiskVolume3\\...) et non au format Win32 (C:\\...).\nLes deux classifyFn # Ce que WFP appelle, ce sont deux fonctions, une pour chaque sens. Elles se contentent d\u0026rsquo;indiquer la direction et les indices de champs de leur couche :\nstatic VOID KdaMonWfpClassifyFnOutbound( _In_ const FWPS_INCOMING_VALUES0* inFixedValues, _In_ const FWPS_INCOMING_METADATA_VALUES0* inMetaValues, _Inout_opt_ VOID* layerData, _In_opt_ const VOID* classifyContext, _In_ const FWPS_FILTER2* filter, _In_ UINT64 flowContext, _Inout_ FWPS_CLASSIFY_OUT0* classifyOut ) { UNREFERENCED_PARAMETER(layerData); UNREFERENCED_PARAMETER(classifyContext); UNREFERENCED_PARAMETER(filter); UNREFERENCED_PARAMETER(flowContext); KdaMonWfpClassifyCommon( inFixedValues, inMetaValues, classifyOut, KDAMON_NETWORK_DIRECTION_OUTBOUND, FWPS_FIELD_ALE_AUTH_CONNECT_V4_IP_PROTOCOL, FWPS_FIELD_ALE_AUTH_CONNECT_V4_IP_LOCAL_ADDRESS, FWPS_FIELD_ALE_AUTH_CONNECT_V4_IP_LOCAL_PORT, FWPS_FIELD_ALE_AUTH_CONNECT_V4_IP_REMOTE_ADDRESS, FWPS_FIELD_ALE_AUTH_CONNECT_V4_IP_REMOTE_PORT ); } La version entrante, KdaMonWfpClassifyFnInbound, est identique : elle passe KDAMON_NETWORK_DIRECTION_INBOUND et les indices FWPS_FIELD_ALE_AUTH_RECV_ACCEPT_V4_*. La notifyFn est aussi demandée à l\u0026rsquo;enregistrement, mais on ne s\u0026rsquo;en sert pas. Donc, KdaMonWfpNotifyFn est un simple stub qui retourne STATUS_SUCCESS.\nEnregistrer les callouts # KdaMonWfpCalloutRegister fait, pour chaque direction, les trois étapes vues plus haut. Pour la connexion sortante, on a :\nNTSTATUS KdaMonWfpCalloutRegister(_In_ PDEVICE_OBJECT DeviceObject) { NTSTATUS status; FWPS_CALLOUT2 callout_s = { 0 }; FWPM_CALLOUT callout_m = { 0 }; FWPM_FILTER filter = { 0 }; // ========================================================= // OUTBOUND — FWPM_LAYER_ALE_AUTH_CONNECT_V4 // ========================================================= RtlCopyMemory(\u0026amp;callout_s.calloutKey, \u0026amp;KDAMON_WFP_CALLOUT_OUTBOUND_GUID, sizeof(GUID)); callout_s.flags = 0; callout_s.classifyFn = KdaMonWfpClassifyFnOutbound; callout_s.notifyFn = KdaMonWfpNotifyFn; callout_s.flowDeleteFn = NULL; status = FwpsCalloutRegister2(DeviceObject, \u0026amp;callout_s, \u0026amp;g_WpsCalloutIdOutbound); if (!NT_SUCCESS(status)) { KdPrint((DRIVER_TAG \u0026#34; [ERROR]: FwpsCalloutRegister2 (outbound) failed: 0x%X\\n\u0026#34;, status)); return status; } On enregistre d\u0026rsquo;abord le callout côté noyau avec ses fonctions.\nRtlCopyMemory(\u0026amp;callout_m.calloutKey, \u0026amp;KDAMON_WFP_CALLOUT_OUTBOUND_GUID, sizeof(GUID)); RtlCopyMemory(\u0026amp;callout_m.applicableLayer, \u0026amp;FWPM_LAYER_ALE_AUTH_CONNECT_V4, sizeof(GUID)); callout_m.displayData.name = KDAMON_WFP_CALLOUT_OUTBOUND_NAME; callout_m.displayData.description = KDAMON_WFP_CALLOUT_OUTBOUND_DESCRIPTION; callout_m.flags = 0; status = FwpmCalloutAdd(g_EngineHandle, \u0026amp;callout_m, NULL, NULL); if (!NT_SUCCESS(status)) { KdPrint((DRIVER_TAG \u0026#34; [ERROR]: FwpmCalloutAdd (outbound) failed: 0x%X\\n\u0026#34;, status)); KdaMonWfpCalloutUnregister(); return status; } Ensuite, on ajoute le callout au moteur pour la couche FWPM_LAYER_ALE_AUTH_CONNECT_V4.\nRtlZeroMemory(\u0026amp;filter, sizeof(filter)); filter.displayData.name = KDAMON_WFP_FILTER_OUTBOUND_NAME; filter.displayData.description = KDAMON_WFP_FILTER_OUTBOUND_DESCRIPTION; filter.providerKey = (GUID*)\u0026amp;KDAMON_WFP_PROVIDER_GUID; filter.numFilterConditions = 0; filter.filterCondition = NULL; filter.action.type = FWP_ACTION_CALLOUT_INSPECTION; RtlCopyMemory(\u0026amp;filter.layerKey, \u0026amp;FWPM_LAYER_ALE_AUTH_CONNECT_V4, sizeof(GUID)); RtlCopyMemory(\u0026amp;filter.subLayerKey, \u0026amp;KDAMON_WFP_SUBLAYER_GUID, sizeof(GUID)); RtlCopyMemory(\u0026amp;filter.action.calloutKey, \u0026amp;KDAMON_WFP_CALLOUT_OUTBOUND_GUID, sizeof(GUID)); status = FwpmFilterAdd(g_EngineHandle, \u0026amp;filter, NULL, \u0026amp;g_FilterIdOutbound); if (!NT_SUCCESS(status)) { KdPrint((DRIVER_TAG \u0026#34; [ERROR]: FwpmFilterAdd (outbound) failed: 0x%X\\n\u0026#34;, status)); KdaMonWfpCalloutUnregister(); return status; } KdPrint((DRIVER_TAG \u0026#34; [SUCCESS]: WFP outbound callout registered\\n\u0026#34;)); Enfin, le filtre est rattaché à notre provider et à notre sublayer, sur la même couche.\nLa partie entrante suit exactement les mêmes étapes, avec KDAMON_WFP_CALLOUT_INBOUND_GUID, KdaMonWfpClassifyFnInbound et la couche FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4.\nDésenregistrer les callouts # VOID KdaMonWfpCalloutUnregister(VOID) { if (g_FilterIdInbound != 0) { FwpmFilterDeleteById(g_EngineHandle, g_FilterIdInbound); g_FilterIdInbound = 0; } if (g_FilterIdOutbound != 0) { FwpmFilterDeleteById(g_EngineHandle, g_FilterIdOutbound); g_FilterIdOutbound = 0; } if (g_WpsCalloutIdInbound != 0) { FwpmCalloutDeleteByKey(g_EngineHandle, \u0026amp;KDAMON_WFP_CALLOUT_INBOUND_GUID); FwpsCalloutUnregisterById(g_WpsCalloutIdInbound); g_WpsCalloutIdInbound = 0; } if (g_WpsCalloutIdOutbound != 0) { FwpmCalloutDeleteByKey(g_EngineHandle, \u0026amp;KDAMON_WFP_CALLOUT_OUTBOUND_GUID); FwpsCalloutUnregisterById(g_WpsCalloutIdOutbound); g_WpsCalloutIdOutbound = 0; } } On retire d\u0026rsquo;abord les deux filtres puis chaque callout, côté gestion avec FwpmCalloutDeleteByKey et côté noyau avec FwpsCalloutUnregisterById.\nAdapter la session WFP # Il faut faire deux modifications dans wfp_session.c pour que le callout fonctionne avec la session de l\u0026rsquo;article précédent.\nPartager le handle du moteur # Le callout a besoin du handle de la session pour appeler FwpmCalloutAdd et FwpmFilterAdd. Jusqu\u0026rsquo;ici, g_EngineHandle était static et donc privé à wfp_session.c. Il devient donc global, et le header le déclare :\n// wfp_session.h extern HANDLE g_EngineHandle; // wfp_session.c HANDLE g_EngineHandle = NULL; Le callout réutilise ainsi la session ouverte par KdaMonWfpSessionInit.\nLa priorité du sublayer # Le poids du sublayer passe de 0 à 0xFFFF, la valeur maximale :\nsubLayer.weight = (UINT16)0xFFFF; Plus le poids d\u0026rsquo;un sublayer est élevé, plus tôt il est évalué. Notre sublayer est donc appelé avant les autres. Comme le capteur ne fait qu\u0026rsquo;observer, il est préférable qu\u0026rsquo;il soit appelé en premier plutôt que de dépendre de l\u0026rsquo;ordre des autres sublayers.\nSérialiser les événements réseau en JSONL # Voici la fonction dédiée à la sérialisation des événements réseau :\nstatic NTSTATUS KdaMonLogWriterWriteNetworkEvent( _In_ const KDAMON_EVENT* Event, _Out_writes_z_(BufferSize) PSTR EventBuffer, _In_ SIZE_T BufferSize) { CHAR EscapedPath[520]; ULONG localIp = Event-\u0026gt;Data.Network.LocalIp; ULONG remoteIp = Event-\u0026gt;Data.Network.RemoteIp; if (!KdaMonJsonEscapeW(Event-\u0026gt;Data.Network.ProcessPath, EscapedPath, sizeof(EscapedPath))) { KdPrint((DRIVER_TAG \u0026#34; [WARNING]: Process path truncated during JSON escape (event %lu)\\n\u0026#34;, Event-\u0026gt;Id)); } const char* direction = (Event-\u0026gt;Data.Network.Direction == KDAMON_NETWORK_DIRECTION_OUTBOUND) ? \u0026#34;outbound\u0026#34; : \u0026#34;inbound\u0026#34;; const char* protocol; switch (Event-\u0026gt;Data.Network.Protocol) { case 1: protocol = \u0026#34;ICMP\u0026#34;; break; case 6: protocol = \u0026#34;TCP\u0026#34;; break; case 17: protocol = \u0026#34;UDP\u0026#34;; break; default: protocol = \u0026#34;UNKNOWN\u0026#34;; break; } return RtlStringCbPrintfA( EventBuffer, BufferSize, \u0026#34;{\\\u0026#34;id\\\u0026#34;:%lu,\\\u0026#34;type\\\u0026#34;:\\\u0026#34;%s\\\u0026#34;,\\\u0026#34;timestamp\\\u0026#34;:%lld,\u0026#34; \u0026#34;\\\u0026#34;pid\\\u0026#34;:%lu,\\\u0026#34;process\\\u0026#34;:\\\u0026#34;%s\\\u0026#34;,\u0026#34; \u0026#34;\\\u0026#34;direction\\\u0026#34;:\\\u0026#34;%s\\\u0026#34;,\\\u0026#34;protocol\\\u0026#34;:\\\u0026#34;%s\\\u0026#34;,\u0026#34; \u0026#34;\\\u0026#34;local_ip\\\u0026#34;:\\\u0026#34;%u.%u.%u.%u\\\u0026#34;,\\\u0026#34;local_port\\\u0026#34;:%u,\u0026#34; \u0026#34;\\\u0026#34;remote_ip\\\u0026#34;:\\\u0026#34;%u.%u.%u.%u\\\u0026#34;,\\\u0026#34;remote_port\\\u0026#34;:%u}\\n\u0026#34;, Event-\u0026gt;Id, KdaMonEventTypeToString(Event-\u0026gt;Type), Event-\u0026gt;Timestamp.QuadPart, Event-\u0026gt;Data.Network.ProcessId, EscapedPath, direction, protocol, (localIp \u0026gt;\u0026gt; 24) \u0026amp; 0xFF, (localIp \u0026gt;\u0026gt; 16) \u0026amp; 0xFF, (localIp \u0026gt;\u0026gt; 8) \u0026amp; 0xFF, localIp \u0026amp; 0xFF, Event-\u0026gt;Data.Network.LocalPort, (remoteIp \u0026gt;\u0026gt; 24) \u0026amp; 0xFF, (remoteIp \u0026gt;\u0026gt; 16) \u0026amp; 0xFF, (remoteIp \u0026gt;\u0026gt; 8) \u0026amp; 0xFF, remoteIp \u0026amp; 0xFF, Event-\u0026gt;Data.Network.RemotePort ); } Voici le déroulement de la fonction :\nOn échappe les caractères spéciaux du chemin du processus avec KdaMonJsonEscapeW. On convertit la direction en texte (outbound ou inbound). On convertit le numéro de protocole en nom : 1 pour ICMP, 6 pour TCP, 17 pour UDP, et UNKNOWN pour le reste. On découpe chaque adresse IPv4 en quatre octets par décalages de bits pour l\u0026rsquo;écrire sous la forme a.b.c.d. On remplit le buffer EventBuffer au format JSON. La ligne d\u0026rsquo;événement d\u0026rsquo;une connexion réseau aura la forme suivante :\n{\u0026#34;id\u0026#34;:151,\u0026#34;type\u0026#34;:\u0026#34;Network\u0026#34;,\u0026#34;timestamp\u0026#34;:134303323937637081,\u0026#34;pid\u0026#34;:10548,\u0026#34;process\u0026#34;:\u0026#34;\\\\device\\\\harddiskvolume3\\\\windows\\\\system32\\\\curl.exe\u0026#34;,\u0026#34;direction\u0026#34;:\u0026#34;outbound\u0026#34;,\u0026#34;protocol\u0026#34;:\u0026#34;TCP\u0026#34;,\u0026#34;local_ip\u0026#34;:\u0026#34;192.168.158.130\u0026#34;,\u0026#34;local_port\u0026#34;:57977,\u0026#34;remote_ip\u0026#34;:\u0026#34;104.20.23.154\u0026#34;,\u0026#34;remote_port\u0026#34;:80} Il ne reste qu\u0026rsquo;à rajouter cette fonction dans le switch de KdaMonLogWriterWriteEvent :\ncase KdaMonEventNetwork: status = KdaMonLogWriterWriteNetworkEvent(Event, EventBuffer, sizeof(EventBuffer)); break; Intégration dans driver_entry.c # Le callout est le premier producteur enregistré, et le dernier désenregistré.\nDonc dans DriverUnload :\nvoid DriverUnload(_In_ PDRIVER_OBJECT DriverObject) { UNREFERENCED_PARAMETER(DriverObject); KdPrint((DRIVER_TAG \u0026#34; [INFO]: Driver Unload begin\\n\u0026#34;)); // --- Unregister callbacks --- KdaMonImageCallbackUnregister(); KdaMonProcessCallbackUnregister(); // --- Stop the log writer --- KdaMonLogWriterStop(); // --- Destroy the event queue --- KdaMonEventQueueDestroy(); // --- Cleanup WFP callout --- KdaMonWfpCalloutUnregister(); // --- Cleanup WFP session --- KdaMonWfpSessionCleanup(); // --- Delete device object --- KdaMonDeleteDevice(\u0026amp;g_DeviceObject); KdPrint((DRIVER_TAG \u0026#34; [INFO]: Driver Unload complete\\n\u0026#34;)); } Dans DriverEntry, on l\u0026rsquo;enregistre après l\u0026rsquo;initialisation de la session WFP, avant la file :\nstatus = KdaMonWfpCalloutRegister(g_DeviceObject); if (!NT_SUCCESS(status)) { KdPrint((DRIVER_TAG \u0026#34; [ERROR]: KdaMonWfpCalloutRegister failed\\n\u0026#34;)); goto cleanup_wfp; } Cet ordre n\u0026rsquo;est pas idéal car classifyFn pousse dans la file d\u0026rsquo;événements, or le callout est enregistré avant sa création et désenregistré après sa destruction. Je le corrigerai lors du refactor de la v0.11.\nValidation # Pour cette version, il faut vérifier :\nque les objets WFP de KDAMonitor sont bien enregistrés puis retirés que des événements réseau arrivent bien jusque dans le fichier de log Comme pour la v0.7, j\u0026rsquo;utilise netsh wfp show state avant, pendant, et après le cycle de vie du driver. Entre le chargement et le déchargement, le script génère du trafic sortant et entrant, puis lit le journal une fois le driver arrêté (test_v08.ps1) :\n# test_v08.ps1 $Driver = \u0026#34;KDAMonitor\u0026#34; $DriverPath = \u0026#34;$env:USERPROFILE\\Desktop\\$Driver.sys\u0026#34; $LogPath = \u0026#34;C:\\KDAMonitor\\logs\\*.jsonl\u0026#34; netsh wfp show state file=wfpstate_before.xml Write-Host \u0026#34;--- Before load ---\u0026#34; (Select-String -Path wfpstate_before.xml -Pattern \u0026#34;\u0026lt;name\u0026gt;KDAMonitor\u0026#34;).Line sc.exe stop $Driver | Out-Null sc.exe delete $Driver | Out-Null sc.exe create $Driver type= kernel binPath= $DriverPath sc.exe start $Driver netsh wfp show state file=wfpstate_after_load.xml Write-Host \u0026#34;--- After load ---\u0026#34; (Select-String -Path wfpstate_after_load.xml -Pattern \u0026#34;\u0026lt;name\u0026gt;KDAMonitor\u0026#34;).Line # --- Outbound traffic: TCP, ICMP and UDP --- curl.exe -s -o NUL http://example.com ping.exe -n 1 1.1.1.1 nslookup.exe example.com 1.1.1.1 # --- Inbound traffic: local listener and a connection to it --- $listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Any, 8080) $listener.Start() $client = [System.Net.Sockets.TcpClient]::new(\u0026#34;127.0.0.1\u0026#34;, 8080) $client.Close() $listener.Stop() Start-Sleep -Seconds 2 sc.exe stop $Driver netsh wfp show state file=wfpstate_after_unload.xml Write-Host \u0026#34;--- After unload ---\u0026#34; (Select-String -Path wfpstate_after_unload.xml -Pattern \u0026#34;\u0026lt;name\u0026gt;KDAMonitor\u0026#34;).Line sc.exe delete $Driver Write-Host \u0026#34;--- Network events ---\u0026#34; $Log = Get-ChildItem $LogPath | Sort-Object LastWriteTime | Select-Object -Last 1 (Select-String -Path $Log.FullName -Pattern \u0026#39;\u0026#34;type\u0026#34;:\u0026#34;Network\u0026#34;\u0026#39;).Line | Where-Object { $_ -match \u0026#39;curl\\.exe|ping\\.exe|nslookup\\.exe|_port\u0026#34;:8080\u0026#39; } Avant chargement, il n\u0026rsquo;y a aucune entrée KDAMonitor. Après chargement, on retrouve les six objets : le provider, le sublayer, les deux callouts et les deux filtres. Et enfin, après déchargement, plus aucune trace.\nCôté journal, on retrouve la connexion TCP sortante de curl.exe vers example.com, les requêtes DNS de nslookup.exe en UDP, et la connexion vers notre listener vue des deux côtés, en sortant puis en entrant.\nLe ping n\u0026rsquo;apparaît pas dans la sortie du script, car celle-ci filtre sur le nom des processus et que l\u0026rsquo;écho ICMP est attribué au processus System (PID 4) et non à ping.exe. On le retrouve en cherchant directement le protocole dans le journal :\nGet-ChildItem C:\\KDAMonitor\\logs\\*.jsonl | Sort-Object LastWriteTime | Select-Object -Last 1 | Select-String -Pattern \u0026#39;\u0026#34;protocol\u0026#34;:\u0026#34;ICMP\u0026#34;\u0026#39; {\u0026#34;id\u0026#34;:179,\u0026#34;type\u0026#34;:\u0026#34;Network\u0026#34;,\u0026#34;timestamp\u0026#34;:134303323940213004,\u0026#34;pid\u0026#34;:4,\u0026#34;process\u0026#34;:\u0026#34;System\u0026#34;,\u0026#34;direction\u0026#34;:\u0026#34;outbound\u0026#34;,\u0026#34;protocol\u0026#34;:\u0026#34;ICMP\u0026#34;,\u0026#34;local_ip\u0026#34;:\u0026#34;192.168.158.130\u0026#34;,\u0026#34;local_port\u0026#34;:8,\u0026#34;remote_ip\u0026#34;:\u0026#34;1.1.1.1\u0026#34;,\u0026#34;remote_port\u0026#34;:0} Conclusion # Un troisième capteur de fait ! KDAMonitor journalise maintenant les processus, les chargements d\u0026rsquo;images et les connexions réseau. Il existe cependant des limites :\nIPv6 n\u0026rsquo;est pas couvert. Seules les couches _V4 sont enregistrées. Le filtre n\u0026rsquo;a aucune condition. Tout le trafic IPv4 est capturé, ce qui produit beaucoup d\u0026rsquo;événements. Le chemin du processus est au format NT (\\Device\\HarddiskVolume3\\...), tel que WFP le fournit. Mais en soit je suis assez satisfait de l\u0026rsquo;état du projet jusqu\u0026rsquo;à aujourd\u0026rsquo;hui !\nVoici l\u0026rsquo;architecture mise à jour :\nMerci d\u0026rsquo;avoir lu jusqu\u0026rsquo;au bout et à bientôt pour le prochain et neuvième article de cette série : Surveillance de l\u0026rsquo;activité du registre.\n","date":"24 septembre 2026","externalUrl":null,"permalink":"/posts/08-network-sensor/","section":"Blog","summary":"Troisième capteur de KDAMonitor : capture des connexions IPv4 entrantes et sortantes.","title":"08 - Surveillance des connexions réseau avec la Windows Filtering Platform","type":"posts"},{"content":"Bienvenue dans le troisième article de la série sur le développement de mon noyau AArch64 bare-metal !\nDans cet article, je vais présenter la version v0.2 de ce projet. Le but de cette version est d\u0026rsquo;implémenter les exceptions du noyau : obtenir un noyau capable de détecter et de gérer une exception synchrone (via svc) ainsi qu\u0026rsquo;une interruption matérielle asynchrone (IRQ), grâce à une table de vecteurs, un handler synchrone, un handler IRQ et le GIC (contrôleur d\u0026rsquo;interruptions).\nVoici les fichiers concernés par cet article, et la section qui les explique :\nFichier Rôle Section src/exceptions/vectors.s Table de vecteurs d\u0026rsquo;exception Implémenter la table de vecteurs src/exceptions/handlers.s Handlers synchrone et IRQ Le handler synchrone et Le handler IRQ src/gic/gic.s, src/gic/gic.inc Driver GIC (gic_init) Le GIC et le handler IRQ src/uart/uart.s Ajout de uart_put_hex uart_put_hex src/boot/boot.s Configuration VBAR_EL1, gic_init, déclenchement svc/SGI Déclencher l\u0026rsquo;exception synchrone et Le GIC et le handler IRQ Le projet peut être retrouvé dans ce dépôt : aarch64-baremetal-kernel.\nModèle d\u0026rsquo;exception AArch64 # Cette section est fortement inspirée de la documentation officielle d\u0026rsquo;ARM : Learn the architecture - AArch64 Exception Model.\nExceptions synchrones et asynchrones # En AArch64, il existe deux types d\u0026rsquo;exceptions :\nles exceptions synchrones qui sont causées (ou liées) à l\u0026rsquo;instruction actuellement exécutée les exceptions asynchrones qui sont causées par un élément extérieur au flux d\u0026rsquo;instruction. Une exception synchrone est directement provoquée par l\u0026rsquo;instruction en cours d\u0026rsquo;exécution : un appel système (svc), une instruction invalide, ou une faute mémoire par exemple. L\u0026rsquo;adresse de retour a une relation définie par l\u0026rsquo;architecture avec l\u0026rsquo;instruction fautive et ce type d\u0026rsquo;exception ne peut pas être masqué, autrement dit elle interrompt le flux d\u0026rsquo;instruction.\nUne exception asynchrone provient d\u0026rsquo;un événement externe, par exemple : un timer, un périphérique ou un autre cœur. On parle alors d\u0026rsquo;interruption (IRQ, FIQ ou SError). Contrairement aux exceptions synchrones, les interruptions peuvent être masquées via le registre PSTATE.DAIF, et sont gérées via le GIC (Generic Interrupt Controller), qu\u0026rsquo;on détaillera plus loin dans cet article.\nDans cet article, on va provoquer et gérer un exemple de chaque type : un svc pour la partie synchrone, et un SGI (Software-Generated Interrupt) via le GIC pour la partie asynchrone.\nVoici les registres utilisés pour gérer les exceptions :\nRegistre Rôle ELR_ELx Adresse de retour après l\u0026rsquo;exception ESR_ELx Cause de l\u0026rsquo;exception (synchrone/SError uniquement) SPSR_ELx PSTATE sauvegardé au moment de l\u0026rsquo;exception VBAR_ELx Adresse de base de la table de vecteurs Le suffixe x dépend de l\u0026rsquo;EL courant. Ce noyau ne tournant qu\u0026rsquo;à EL1, on utilisera uniquement ELR_EL1, ESR_EL1, SPSR_EL1 et VBAR_EL1.\neret restaure PSTATE depuis SPSR_ELx et fait sauter le CPU à l\u0026rsquo;adresse contenue dans ELR_ELx, de manière atomique. Pour un svc, ELR_EL1 contient l\u0026rsquo;adresse de l\u0026rsquo;instruction suivant le svc.\nLa table de vecteurs # Chaque EL possède sa propre table de vecteurs, dont l\u0026rsquo;adresse est indiquée par VBAR_ELx. Cette table contient 16 entrées de 128 octets (ce qui donne 32 instructions) chacune, et doit être alignée sur 2 Ko.\nOffset Type Source +0x000 Synchronous current EL, SP_EL0 +0x080 IRQ current EL, SP_EL0 +0x180 SError current EL, SP_EL0 +0x200 Synchronous current EL, SP_ELx +0x280 IRQ current EL, SP_ELx +0x380 SError current EL, SP_ELx +0x400 à +0x780 (lower EL, AArch64/AArch32) non utilisées Ce noyau ne tourne qu\u0026rsquo;à EL1, sans EL0. Seules les deux entrées en gras (+0x200, +0x280) nous concernent, le reste sera composé de stubs.\nImplémenter la table de vecteurs # Pour implémenter la table de vecteurs, je me suis inspiré du fichier 11_exceptions_part1_groundwork/src/_arch/aarch64/exception.s dans le dépôt rust-raspberrypi-OS-tutorials.\nLes macros CALL_HANDLER et UNUSED_VECTOR # Dans un nouveau fichier src/exceptions/vectors.s, on va créer plusieurs composants :\nTout d\u0026rsquo;abord, une macro qui va appeler le handler correct lié à l\u0026rsquo;exception demandée : .macro CALL_HANDLER handler vector_\\handler: mrs x19, ELR_EL1 mrs x20, SPSR_EL1 mrs x21, ESR_EL1 mov x0, sp bl \\handler .endm Cette macro CALL_HANDLER sauvegarde ELR/SPSR/ESR dans des registres callee-saved (x19-x21) avant d\u0026rsquo;appeler le vrai handler, avec sp passé en x0.\nEnsuite, il faut une macro qui va servir de handler pour toutes les autres exceptions non implémentées : .macro UNUSED_VECTOR 1: wfe b 1b .endm La macro UNUSED_VECTOR utilise un label numérique local pour pouvoir être expansée plusieurs fois sans collision de symboles.\nAlignement et .org # Ensuite, on va établir la table de vecteurs :\n.align 11 _exception_vector_table: .org 0x000 UNUSED_VECTOR .org 0x080 UNUSED_VECTOR ... .org 0x200 CALL_HANDLER el_synchronous .org 0x280 CALL_HANDLER el_irq ... .org 0x800 Au début, on aligne la table sur 2048 octets (2 KiB), comme l\u0026rsquo;exige VBAR_EL1.\nEnsuite, .org permet de placer chaque entrée à l\u0026rsquo;offset exact attendu dans la table, en avançant le compteur de position de l\u0026rsquo;assembleur si nécessaire.\nLa plupart des entrées de cette table renvoient vers UNUSED_VECTOR. Dans notre cas, les deux entrées qui nous intéressent sont celles aux offsets 0x200 et 0x280, où l\u0026rsquo;on place respectivement CALL_HANDLER el_synchronous et CALL_HANDLER el_irq.\nDéclencher l\u0026rsquo;exception synchrone # uart_put_hex # Avant de rajouter l\u0026rsquo;exception synchrone, il faut rajouter dans src/uart/uart.s, une fonction permettant d\u0026rsquo;afficher (via l\u0026rsquo;UART) une chaîne hexadécimale de 64 bits :\nuart_put_hex: stp x19, x20, [sp, #-32]! stp x30, xzr, [sp, #16] mov x19, x0 mov x20, #16 hex_loop: lsr x3, x19, #60 and x3, x3, #0xF ldr x2, =hex_chars ldrb w0, [x2, x3] bl uart_putc lsl x19, x19, #4 subs x20, x20, #1 b.ne hex_loop ldp x30, xzr, [sp, #16] ldp x19, x20, [sp], #32 ret hex_chars: .asciz \u0026#34;0123456789ABCDEF\u0026#34; Cette fonction est nécessaire car uart_puts ne gère que de l\u0026rsquo;ASCII, or l\u0026rsquo;idée est d\u0026rsquo;afficher la valeur des registres lors de chaque exception.\nLe handler synchrone # Dans src/exceptions/handlers.s, on définit el_synchronous, la fonction appelée par CALL_HANDLER. Le handler ne doit pour l\u0026rsquo;instant faire qu\u0026rsquo;afficher des informations. Il commence par afficher Synchronous exception caught!\\n lorsqu\u0026rsquo;il est appelé puis il va afficher les valeurs dans ELR_EL1, SPSR_EL1 et ESR_EL1.\nel_synchronous: stp x29, x30, [sp, #-16]! ldr x0, =message_synchronous bl uart_puts ldr x0, =message_prefix_elr_el1 bl uart_puts mov x0, x19 bl uart_put_hex ldr x0, =message_prefix_spsr_el1 bl uart_puts mov x0, x20 bl uart_put_hex ldr x0, =message_prefix_esr_el1 bl uart_puts mov x0, x21 bl uart_put_hex ldp x29, x30, [sp], #16 eret x19, x20 et x21, remplis par CALL_HANDLER, survivent aux appels uart_puts/uart_put_hex puisque ce sont des registres callee-saved. Le eret final permet de reprendre l\u0026rsquo;exécution juste après l\u0026rsquo;instruction ayant déclenché l\u0026rsquo;exception.\nIl ne reste plus qu\u0026rsquo;à configurer VBAR_EL1 et à déclencher l\u0026rsquo;exception. Dans src/boot/boot.s :\nldr x0, =_exception_vector_table msr vbar_el1, x0 ... svc #0 VBAR_EL1 doit être configuré avant toute instruction pouvant lever une exception. Le svc #0 provoque ensuite volontairement une exception synchrone, qui va être interceptée à l\u0026rsquo;offset +0x200 de la table de vecteurs.\nRésultat # qemu-system-aarch64 -M virt -cpu cortex-a57 -nographic -kernel build/kernel.elf Hello, AArch64! Synchronous exception caught! ELR_EL1: 0x0000000040000024 SPSR_EL1: 0x0000000040000345 ESR_EL1: 0x0000000056000000 ESR_EL1, bits [31:26] (champ EC) = 0x15, la classe d\u0026rsquo;exception correspondant à un SVC en AArch64. Voir ARM A64 Instruction Set - SVC.\nSPSR_EL1, bits [3:0] = 0x5 = EL1h (EL1, SP_ELx). Voir ARM AArch64 System Registers - SPSR_EL1.\nELR_EL1 (0x40000024) correspond bien à l\u0026rsquo;adresse de l\u0026rsquo;instruction suivant le svc #0 (0x40000020) dans boot.s, vérifiable via aarch64-none-elf-objdump -d build/kernel.elf : 0000000040000000 \u0026lt;_start\u0026gt;: ... 4000001c: d50342ff msr daifclr, #0x2 40000020: d4000001 svc #0x0 40000024: 58000160 ldr x0, 40000050 \u0026lt;_start+0x50\u0026gt; ... Le GIC et le handler IRQ # Encore une fois, pour cette partie, je me suis basé sur la documentation officielle d\u0026rsquo;ARM : Arm Generic Interrupt Controller Architecture Specification (GICv2). QEMU virt avec cortex-a57 implémente un GICv2.\nDistributeur et interface CPU # Le GIC (Generic Interrupt Controller) est le contrôleur d\u0026rsquo;interruptions standard d\u0026rsquo;ARM : une ressource centralisée entre toutes les sources d\u0026rsquo;interruptions possibles et le CPU.\nLe GIC se découpe en deux blocs :\nBloc Rôle Préfixe Distributeur Centralise les sources, priorise GICD_* Interface CPU Masquage de priorité par processeur GICC_* Le cycle de vie d\u0026rsquo;une interruption suit trois étapes :\nAcknowledge : lecture de GICC_IAR, qui retourne l\u0026rsquo;ID de l\u0026rsquo;interruption en attente et la passe à l\u0026rsquo;état actif. Handle : le handler s\u0026rsquo;exécute. Complete : écriture de la même valeur dans GICC_EOIR. Trouver GICD_BASE / GICC_BASE # Comme pour l\u0026rsquo;UART, il faut trouver l\u0026rsquo;adresse de base de ces deux composants (le distributeur et l\u0026rsquo;interface) :\nqemu-system-aarch64 -M virt -cpu cortex-a57 -machine dumpdtb=virt.dtb -nographic dtc -I dtb -O dts virt.dtb | grep -A 5 intc On obtient :\nintc@8000000 { phandle = \u0026lt;0x8002\u0026gt;; reg = \u0026lt;0x00 0x8000000 0x00 0x10000 0x00 0x8010000 0x00 0x10000\u0026gt;; compatible = \u0026#34;arm,cortex-a15-gic\u0026#34;; ranges; #size-cells = \u0026lt;0x02\u0026gt;; D\u0026rsquo;après le device tree standard arm,gic, le Distributeur est listé en premier dans reg :\nRegistre Adresse GICD_BASE 0x08000000 GICC_BASE 0x08010000 Ces adresses sont confirmées directement dans le code source de QEMU : hw/arm/virt.c définit VIRT_GIC_DIST à 0x08000000 et VIRT_GIC_CPU à 0x08010000.\ngic_init # Les offsets des registres utilisés sont regroupés dans un fichier séparé, src/gic/gic.inc, afin d\u0026rsquo;être partagés entre plusieurs fichiers .s :\n.equ GICD_BASE, 0x08000000 .equ GICC_BASE, 0x08010000 .equ GICD_CTLR, 0x000 .equ GICD_ISENABLER0, 0x100 .equ GICD_SGIR, 0xF00 .equ GICC_CTLR, 0x000 .equ GICC_PMR, 0x004 .equ GICC_IAR, 0x00C .equ GICC_EOIR, 0x010 Dans src/gic/gic.s :\ngic_init: ldr x0, =GICD_BASE mov w1, #0x1 str w1, [x0, #GICD_CTLR] // enable Group 0 forwarding ldr x0, =GICD_BASE mov w2, #0x1 str w2, [x0, #GICD_ISENABLER0] // enable SGI 0 ldr x0, =GICC_BASE mov w3, #0xFF str w3, [x0, #GICC_PMR] // let all priorities through ldr x0, =GICC_BASE mov w4, #0x1 str w4, [x0, #GICC_CTLR] // enable Group 0 signaling ret .include colle littéralement le contenu de gic.inc dans le fichier au moment de l\u0026rsquo;assemblage. C\u0026rsquo;est nécessaire ici car un offset utilisé en immédiat ([x0, #GICD_CTLR]) doit être connu de l\u0026rsquo;assembleur dès l\u0026rsquo;assemblage, pas seulement au link. Le Makefile passe -Isrc pour que .include \u0026quot;gic/gic.inc\u0026quot; se résolve relativement à src/.\ngic_init configure donc quatre choses :\nl\u0026rsquo;activation du forwarding des interruptions Group 0 au niveau du Distributeur l\u0026rsquo;activation du SGI 0 le masque de priorité de l\u0026rsquo;interface CPU (0xFF = tout laisse passer) et l\u0026rsquo;activation de la signalisation Group 0 côté interface CPU. Cette fonction est appelée une fois, tôt dans _start, dans src/boot/boot.s :\nbl gic_init Le handler IRQ et déclenchement du SGI # Le handler IRQ va faire à peu près la même chose que le handler synchrone (afficher un message et un registre en hexadécimal, puis eret). Cependant, il n\u0026rsquo;y a qu\u0026rsquo;un seul registre à afficher (GICC_IAR) et il faut gérer le cycle Acknowledge/Complete du GIC vu plus haut :\nel_irq: stp x22, x30, [sp, #-16]! ldr x0, =GICC_BASE ldr w22, [x0, #GICC_IAR] // moves the interrupt to active ldr x0, =message_irq bl uart_puts ldr x0, =message_prefix_iar bl uart_puts mov x0, x22 bl uart_put_hex ldr x0, =GICC_BASE str w22, [x0, #GICC_EOIR] // write back the exact value read ldp x22, x30, [sp], #16 eret x22 est utilisé plutôt que x19-x21 (déjà réservés par CALL_HANDLER pour ELR_EL1/SPSR_EL1/ESR_EL1) afin de conserver la valeur de GICC_IAR à travers les appels à uart_puts/uart_put_hex.\nOn commence par démasquer les IRQ, masquées par défaut au reset. Toujours dans src/boot/boot.s :\nmsr daifclr, #2 Voir ARM AArch64 System Registers - DAIF.\nPuis, dans src/boot/boot.s, écrire dans GICD_SGIR pour déclencher le SGI :\nldr x0, =GICD_BASE mov w1, #0x2000000 str w1, [x0, #GICD_SGIR] 0x2000000 encode TargetListFilter = 0b10 (bits [25:24], cible le processeur courant uniquement) combiné à l\u0026rsquo;ID de SGI 0 (bits [3:0]). Cette écriture déclenche immédiatement l\u0026rsquo;interruption, qui est prise en charge à l\u0026rsquo;offset +0x280 de la table de vecteurs.\nVoir GICv2 Architecture Specification pour l\u0026rsquo;encodage de GICD_SGIR (TargetListFilter en bits [25:24], ID de SGI en bits [3:0]).\nCompiler et vérifier # On commence par compiler et vérifier que les symboles attendus sont bien présents :\nmake clean \u0026amp;\u0026amp; make aarch64-none-elf-nm build/kernel.elf | grep -E \u0026#34;exception_vector_table|el_irq|el_synchronous|gic_init\u0026#34; 0000000040000800 T _exception_vector_table 00000000400000a4 T el_irq 0000000040000058 T el_synchronous 0000000040001000 T gic_init 0000000040000a80 t vector_el_irq 0000000040000a00 t vector_el_synchronous _exception_vector_table se trouve à l\u0026rsquo;adresse 0x40000800, un multiple de 0x800 (2048), ce qui confirme que l\u0026rsquo;alignement sur 2 Ko demandé par VBAR_EL1 a bien été respecté par le linker.\nOn retrouve aussi vector_el_synchronous et vector_el_irq, les labels locaux générés respectivement par CALL_HANDLER el_synchronous et CALL_HANDLER el_irq, à 0x40000800 + 0x200 = 0x40000a00 et 0x40000800 + 0x280 = 0x40000a80. Les deux entrées étant bien routées à leurs offsets attendus dans la table, on en conclut que tout est bon.\nEnfin, on lance le kernel avec QEMU :\nConclusion # Le noyau a maintenant sa propre table de vecteurs (qu\u0026rsquo;il faut encore peupler) ainsi que des handlers rudimentaires pour les vecteurs implémentés.\nMerci d\u0026rsquo;avoir lu jusqu\u0026rsquo;au bout, et à bientôt pour le prochain article : Mettre en place le timer ARM.\n","date":"22 septembre 2026","externalUrl":null,"permalink":"/posts/03-exceptions/","section":"Blog","summary":"Mise en place de la table de vecteurs d’exception AArch64, du handler synchrone, du GIC et du handler IRQ.","title":"03 - Les exceptions : construire la table de vecteurs","type":"posts"},{"content":"","date":"22 septembre 2026","externalUrl":null,"permalink":"/tags/aarch64/","section":"Tags","summary":"","title":"AArch64","type":"tags"},{"content":"","date":"22 septembre 2026","externalUrl":null,"permalink":"/series/aarch64-bare-metal-kernel/","section":"Series","summary":"Construction d’un petit kernel bare-metal en assembleur AArch64, du boot minimal jusqu’à un système d’exploitation minimal fonctionnel.","title":"AArch64 Bare-Metal Kernel","type":"series"},{"content":"","date":"22 septembre 2026","externalUrl":null,"permalink":"/tags/arm/","section":"Tags","summary":"","title":"ARM","type":"tags"},{"content":"","date":"22 septembre 2026","externalUrl":null,"permalink":"/tags/assembly/","section":"Tags","summary":"","title":"Assembly","type":"tags"},{"content":"","date":"22 septembre 2026","externalUrl":null,"permalink":"/tags/kernel/","section":"Tags","summary":"","title":"Kernel","type":"tags"},{"content":"Bienvenue dans le septième article de la série sur le développement de KDAMonitor !\nDans cet article, je vais couvrir la version v0.7 de ce projet. Cette version ne filtre encore aucun trafic réseau, elle met en place l\u0026rsquo;infrastructure WFP (provider, sublayer) sur laquelle viendra s\u0026rsquo;accrocher le capteur réseau de la v0.8. Une section de cet article va contenir le deuxième crash que j\u0026rsquo;ai rencontré.\nVoici les fichiers concernés par cet article, et la section qui les explique :\nFichier Rôle Section wfp_session.h GUIDs provider/sublayer, déclarations Implémenter la session WFP wfp_session.c Ouverture de l\u0026rsquo;engine, ajout provider/sublayer, cleanup Implémenter la session WFP driver_entry.c Intégration dans DriverEntry/DriverUnload Intégration dans driver_entry.c device.c / driver_entry.c Crash #2 (PAGE_FAULT_IN_NONPAGED_AREA) et son fix Crash #2 : le device object supprimé deux fois Le projet peut être retrouvé dans ce dépôt : KDAMonitor.\nWFP en bref # Comme indiqué par la documentation microsoft, la Windows Filtering Platform est un ensemble d\u0026rsquo;API et de services permettant de filtrer le trafic réseau.\nPour ouvrir une session de filtrage, on utilise FwpmEngineOpen qui établit la connexion avec le moteur WFP. Une fois la session ouverte, on peut déclarer un provider avec FwpmProviderAdd. Un provider est l\u0026rsquo;identité de l\u0026rsquo;application (ou driver) auprès du moteur WFP. Il permet, par exemple, d\u0026rsquo;interagir directement avec les objets, tels que les sublayers, enregistrés par KDAMonitor.\nLes sublayers sont des espaces reservés qui permettent de stocker les filtres et le callout réseau de KDAMonitor (prévu pour la v0.8). Pour cette version, on se contente d\u0026rsquo;ouvrir la session et d\u0026rsquo;enregistrer le provider et le sublayer. Donc pour l\u0026rsquo;instant, aucun callout ou filtre.\nImplémenter la session WFP # Avant de commencer la partie concrète du code, il faut d\u0026rsquo;abord définir des GUIDs, leurs noms et descriptions, nous en avons besoin de deux :\npour le provider : DEFINE_GUID(KDAMON_WFP_PROVIDER_GUID, 0x16821234, 0xd300, 0x42f1, 0xbc, 0xe8, 0xd2, 0x23, 0x1a, 0xf3, 0x25, 0xc3); #define KDAMON_WFP_PROVIDER_NAME L\u0026#34;KDAMonitor Provider\u0026#34; #define KDAMON_WFP_PROVIDER_DESCRIPTION L\u0026#34;KDAMonitor - Kernel Driver Activity Monitor\u0026#34; et pour le sublayer : DEFINE_GUID(KDAMON_WFP_SUBLAYER_GUID, 0xa141444c, 0x7f15, 0x4a05, 0xa2, 0x95, 0x07, 0xca, 0x38, 0xc2, 0x3c, 0xb1); #define KDAMON_WFP_SUBLAYER_NAME L\u0026#34;KDAMonitor Sublayer\u0026#34; #define KDAMON_WFP_SUBLAYER_DESCRIPTION L\u0026#34;KDAMonitor sublayer for network event monitoring\u0026#34; Pour utiliser la macro DEFINE_GUID, il faut commencer le fichier par INITGUID.\nOn définit aussi un handle pour le moteur (HANDLE g_EngineHandle) qui représente la connexion avec le moteur WFP. La fonction NTSTATUS KdaMonWfpSessionInit(void) se charge d\u0026rsquo;ouvrir la connexion avec le moteur WFP, puis d\u0026rsquo;enregistrer le provider et le sublayer de KDAMonitor. On commence par déclarer les structures utilisées par les trois étapes avant d\u0026rsquo;ouvrir la session avec FwpmEngineOpen :\nNTSTATUS status; FWPM_SESSION wfpSession = { 0 }; FWPM_PROVIDER provider = { 0 }; FWPM_SUBLAYER subLayer = { 0 }; status = FwpmEngineOpen(NULL, RPC_C_AUTHN_WINNT, NULL, \u0026amp;wfpSession, \u0026amp;g_EngineHandle); if (status != STATUS_SUCCESS) { KdPrint((DRIVER_TAG \u0026#34; [ERROR]: FwpmEngineOpen failed with status 0x%X\\n\u0026#34;, status)); return STATUS_UNSUCCESSFUL; } On enregistre ensuite le provider avec FwpmProviderAdd:\nprovider.providerKey = KDAMON_WFP_PROVIDER_GUID; provider.displayData.name = KDAMON_WFP_PROVIDER_NAME; provider.displayData.description = KDAMON_WFP_PROVIDER_DESCRIPTION; provider.flags = 0; provider.serviceName = NULL; status = FwpmProviderAdd(g_EngineHandle, \u0026amp;provider, NULL); if (status != STATUS_SUCCESS) { KdPrint((DRIVER_TAG \u0026#34; [ERROR]: FwpmProviderAdd failed with status 0x%X\\n\u0026#34;, status)); FwpmEngineClose(g_EngineHandle); g_EngineHandle = NULL; return STATUS_UNSUCCESSFUL; } providerKey et displayData (nom + description) identifient le provider auprès du moteur WFP, et dans des outils comme netsh wfp show providers. serviceName reste à NULL car le provider n\u0026rsquo;est pas rattaché à un service Windows en particulier.\nEt enfin le sublayer avec FwpmSubLayerAdd :\nsubLayer.subLayerKey = KDAMON_WFP_SUBLAYER_GUID; GUID providerKey = KDAMON_WFP_PROVIDER_GUID; subLayer.providerKey = \u0026amp;providerKey; subLayer.displayData.name = KDAMON_WFP_SUBLAYER_NAME; subLayer.displayData.description = KDAMON_WFP_SUBLAYER_DESCRIPTION; subLayer.flags = 0; subLayer.weight = (UINT16)0; status = FwpmSubLayerAdd(g_EngineHandle, \u0026amp;subLayer, NULL); if (status != STATUS_SUCCESS) { KdPrint((DRIVER_TAG \u0026#34; [ERROR]: FwpmSubLayerAdd failed with status 0x%X\\n\u0026#34;, status)); FwpmProviderDeleteByKey(g_EngineHandle, \u0026amp;KDAMON_WFP_PROVIDER_GUID); FwpmEngineClose(g_EngineHandle); g_EngineHandle = NULL; return STATUS_UNSUCCESSFUL; } return STATUS_SUCCESS; subLayer.providerKey relie le sublayer au provider créé juste avant. weight définit la priorité relative entre sublayers, ce paramètre est sans importance tant qu\u0026rsquo;un seul sublayer existe.\nÀ chaque étape, un échec défait ce que l\u0026rsquo;étage précédent a construit, dans l\u0026rsquo;ordre inverse : si FwpmProviderAdd échoue, on referme simplement la session avec FwpmEngineClose ; si FwpmSubLayerAdd échoue, on va un cran plus loin en supprimant d\u0026rsquo;abord le provider fraîchement créé (FwpmProviderDeleteByKey) avant de refermer la session.\nPour nettoyer/supprimer la session WFP, on fait appel à VOID KdaMonWfpSessionCleanup(void) qui est le symétrique de KdaMonWfpSessionInit et va donc supprimer le sublayer (FwpmSubLayerDeleteByKey), supprimer le provider (FwpmProviderDeleteByKey), fermer le moteur WFP (FwpmEngineClose) et remettre le handle du moteur à NULL (g_EngineHandle) :\nVOID KdaMonWfpSessionCleanup(void) { NTSTATUS status; if (g_EngineHandle == NULL) return; status = FwpmSubLayerDeleteByKey(g_EngineHandle, \u0026amp;KDAMON_WFP_SUBLAYER_GUID); if (status != STATUS_SUCCESS) { KdPrint((DRIVER_TAG \u0026#34; [ERROR]: FwpmSubLayerDeleteByKey failed with status 0x%X\\n\u0026#34;, status)); } status = FwpmProviderDeleteByKey(g_EngineHandle, \u0026amp;KDAMON_WFP_PROVIDER_GUID); if (status != STATUS_SUCCESS) { KdPrint((DRIVER_TAG \u0026#34; [ERROR]: FwpmProviderDeleteByKey failed with status 0x%X\\n\u0026#34;, status)); } status = FwpmEngineClose(g_EngineHandle); if (status != STATUS_SUCCESS) { KdPrint((DRIVER_TAG \u0026#34; [ERROR]: FwpmEngineClose failed with status 0x%X\\n\u0026#34;, status)); } g_EngineHandle = NULL; KdPrint((DRIVER_TAG \u0026#34; [SUCCESS]: WFP session closed successfully\\n\u0026#34;)); } Intégration dans driver_entry.c # Dans le code, l\u0026rsquo;initialisation de la session WFP sera faite entre l\u0026rsquo;initialisation du device et de la file, la session WFP n\u0026rsquo;étant pas un capteur, elle est placée au début.\nL\u0026rsquo;ordre d\u0026rsquo;initialisation dans DriverEntry sera donc :\nle device la session WFP la file le log writer et les callbacks (processus et image) On ajoute donc cela à la fonction :\n// --- Initialize WFP session --- status = KdaMonWfpSessionInit(); if (!NT_SUCCESS(status)) { KdPrint((DRIVER_TAG \u0026#34; [ERROR]: KdaMonWfpSessionInit failed\\n\u0026#34;)); goto cleanup_device; } La destruction de ces éléments dans DriverUnload se fait dans l\u0026rsquo;ordre inverse. Voici donc les deux fonctions complètes :\nvoid DriverUnload(_In_ PDRIVER_OBJECT DriverObject) { UNREFERENCED_PARAMETER(DriverObject); KdPrint((DRIVER_TAG \u0026#34; [INFO]: Driver Unload begin\\n\u0026#34;)); // --- Unregister callbacks --- KdaMonImageCallbackUnregister(); KdaMonProcessCallbackUnregister(); // --- Stop the log writer --- KdaMonLogWriterStop(); // --- Destroy the event queue --- KdaMonEventQueueDestroy(); // --- Cleanup WFP session --- KdaMonWfpSessionCleanup(); // --- Delete device object --- KdaMonDeleteDevice(\u0026amp;g_DeviceObject); KdPrint((DRIVER_TAG \u0026#34; [INFO]: Driver Unload complete\\n\u0026#34;)); } NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) { UNREFERENCED_PARAMETER(RegistryPath); KdPrint((DRIVER_TAG \u0026#34; [INFO]: DriverEntry begin\\n\u0026#34;)); NTSTATUS status; DriverObject-\u0026gt;DriverUnload = DriverUnload; DriverObject-\u0026gt;MajorFunction[IRP_MJ_CREATE] = KdaMonCreateClose; DriverObject-\u0026gt;MajorFunction[IRP_MJ_CLOSE] = KdaMonCreateClose; DriverObject-\u0026gt;MajorFunction[IRP_MJ_DEVICE_CONTROL] = KdaMonDeviceControl; // --- Create device object --- status = KdaMonCreateDevice(DriverObject, \u0026amp;g_DeviceObject); if (!NT_SUCCESS(status)) { goto cleanup_none; } // --- Initialize WFP session --- status = KdaMonWfpSessionInit(); if (!NT_SUCCESS(status)) { KdPrint((DRIVER_TAG \u0026#34; [ERROR]: KdaMonWfpSessionInit failed\\n\u0026#34;)); goto cleanup_device; } // --- Initialize the event queue --- if (!KdaMonEventQueueInitialize()) { KdPrint((DRIVER_TAG \u0026#34; [ERROR]: EventQueueInitialize failed\\n\u0026#34;)); status = STATUS_UNSUCCESSFUL; goto cleanup_wfp; } // --- Start the log writer thread --- if (!KdaMonLogWriterStart(DriverObject)) { KdPrint((DRIVER_TAG \u0026#34; [ERROR]: KdaMonLogWriterStart failed\\n\u0026#34;)); status = STATUS_UNSUCCESSFUL; goto cleanup_queue; } // --- Register process creation callback --- status = KdaMonProcessCallbackRegister(); if (!NT_SUCCESS(status)) { KdPrint((DRIVER_TAG \u0026#34; [ERROR]: KdaMonProcessCallbackRegister failed\\n\u0026#34;)); goto cleanup_logwriter; } // --- Register image load callback --- status = KdaMonImageCallbackRegister(); if (!NT_SUCCESS(status)) { KdPrint((DRIVER_TAG \u0026#34; [ERROR]: KdaMonImageCallbackRegister failed\\n\u0026#34;)); goto cleanup_process; } KdPrint((DRIVER_TAG \u0026#34; [SUCCESS]: Initialized successfully\\n\u0026#34;)); return STATUS_SUCCESS; cleanup_process: KdaMonProcessCallbackUnregister(); cleanup_logwriter: KdaMonLogWriterStop(); cleanup_queue: KdaMonEventQueueDestroy(); cleanup_wfp: KdaMonWfpSessionCleanup(); cleanup_device: KdaMonDeleteDevice(\u0026amp;g_DeviceObject); cleanup_none: return status; } La structure de nettoyage des objets en cas d\u0026rsquo;échec d\u0026rsquo;initialisation dans DriverEntry de la version précédente a été modifiée afin d\u0026rsquo;utiliser des goto qui rend le code bien plus propre.\nCrash #2 : le device object supprimé deux fois # Contexte # Ce crash est apparu en testant le refactor de DriverEntry présenté plus haut. Le dump est conservé dans le repo au sein du fichier docs/dumps/2_PAGE_FAULT_IN_NONPAGED_AREA.dmp.\nLe bugcheck relevé est PAGE_FAULT_IN_NONPAGED_AREA (0x50), avec un accès en lecture (Arg2 = 0) à une adresse invalide. L\u0026rsquo;instruction fautive se trouve dans nt!ObQueryNameStringMode, appelée depuis nt!IoDeleteDevice, elle-même appelée depuis KdaMonDeleteDevice (device.c) dans DriverUnload (driver_entry.c) :\nnt!ObQueryNameStringMode+a8 fffff802`7a369ad8 488b81a0000000 mov rax,qword ptr [rcx+0A0h] IoDeleteDevice appelle en interne ObQueryNameString pour résoudre le nom de l\u0026rsquo;objet avant de le supprimer — ici, sur un DEVICE_OBJECT déjà libéré.\nDiagnostic # Le code fautif se trouvait dans DriverEntry, à qui il manquait un return STATUS_SUCCESS; juste après le log de succès :\nKdPrint((DRIVER_TAG \u0026#34; [SUCCESS]: Initialized successfully\\n\u0026#34;)); // il manque : return STATUS_SUCCESS; cleanup_process: KdaMonProcessCallbackUnregister(); cleanup_wfp: KdaMonWfpSessionCleanup(); cleanup_logwriter: KdaMonLogWriterStop(); cleanup_queue: KdaMonEventQueueDestroy(); cleanup_device: KdaMonDeleteDevice(g_DeviceObject); cleanup_none: return status; // le driver arrive là même sans erreur ! Sans ce return, l\u0026rsquo;exécution tombait directement dans la cascade de cleanup même après une initialisation réussie avant de renvoyer STATUS_SUCCESS.\nKdaMonDeleteDevice recevait g_DeviceObject par valeur. Elle pouvait donc libérer le DEVICE_OBJECT, mais pas mettre g_DeviceObject à NULL.\nAinsi, après le premier appel, g_DeviceObject contenait toujours l\u0026rsquo;adresse de l\u0026rsquo;objet désormais libéré (dangling pointer).\nLors du déchargement, DriverUnload appelait de nouveau KdaMonDeleteDevice(g_DeviceObject), qui tentait alors d\u0026rsquo;utiliser ce pointeur invalide, provoquant le crash.\nRésolution # Le correctif intervient à deux niveaux :\nle return status; manquant, ajouté juste après le log de succès et KdaMonDeleteDevice qui prend désormais un PDEVICE_OBJECT* et remet le pointeur de l\u0026rsquo;appelant à NULL après suppression, en renfort contre un futur double-cleanup : void KdaMonDeleteDevice(_Inout_ PDEVICE_OBJECT* DeviceObject) { UNICODE_STRING symLink = RTL_CONSTANT_STRING(KDAMON_SYMLINK_NAME); IoDeleteSymbolicLink(\u0026amp;symLink); if (*DeviceObject != NULL) { IoDeleteDevice(*DeviceObject); *DeviceObject = NULL; } ... } Les appelants passent désormais \u0026amp;g_DeviceObject plutôt que g_DeviceObject.\nValidation # Pour cette version, la validation consiste à vérifier que le provider et le sublayer de KDAMonitor apparaissent bien dans l\u0026rsquo;état du moteur WFP après le chargement du driver, et disparaissent après son déchargement. Pour cela, j\u0026rsquo;ai utilisé netsh wfp show state avant, pendant, et après le cycle de vie du driver, à l\u0026rsquo;aide du script suivant (test_v07.ps1) :\n# test_v07.ps1 $Driver = \u0026#34;KDAMonitor\u0026#34; $DriverPath = \u0026#34;$env:USERPROFILE\\Desktop\\$Driver.sys\u0026#34; netsh wfp show state file=wfpstate_before.xml sc.exe stop $Driver sc.exe delete $Driver sc.exe create $Driver type= kernel binPath= $DriverPath sc.exe start $Driver netsh wfp show state file=wfpstate_after_load.xml sc.exe stop $Driver netsh wfp show state file=wfpstate_after_unload.xml sc.exe delete $Driver Avant chargement, aucune entrée KDAMonitor :\nAprès chargement, le provider et le sublayer apparaissent bien dans l\u0026rsquo;état du moteur WFP :\nEt après déchargement, plus aucune trace des deux :\nConclusion # Dans cette version, la session WFP a été ouverte, le provider et le sublayer de KDAMonitor enregistrés, en attente du premier filtre.\nBien que cette version n\u0026rsquo;ait pas été très intéressante (un peu ennuyeuse :)), elle est nécessaire pour la partie que je trouve la plus intéressante : le capteur réseau.\nMerci d\u0026rsquo;avoir lu jusqu\u0026rsquo;au bout et à bientôt pour le prochain et huitième article de cette série : Surveillance des connexions réseau avec la Windows Filtering Platform.\n","date":"21 septembre 2026","externalUrl":null,"permalink":"/posts/07-wfp-session/","section":"Blog","summary":"Mise en place de la session WFP (provider, sublayer) de KDAMonitor.","title":"07 - Préparation de la surveillance réseau : mise en place de la session WFP","type":"posts"},{"content":"Bienvenue dans le sixième article de la série sur le développement de KDAMonitor !\nDans cet article, je vais couvrir la version v0.6 de ce projet. Dans cette version, j\u0026rsquo;implémente le deuxième capteur du projet : le capteur de chargement d\u0026rsquo;image.\nVoici les fichiers concernés par cet article, et la section qui les explique :\nFichier Rôle Section event_types.h Nouveau type KDAMON_IMAGE_LOAD_EVENT_DATA + union dans KDAMON_EVENT Que récupérer lors du chargement d\u0026rsquo;une image ? image_callback.h Déclaration du register/unregister Implémenter le callback image_callback.c Le callback lui-même Implémenter le callback log_writer.c Sérialisation JSONL spécifique aux événements de chargement d\u0026rsquo;image Sérialiser les événements de chargement d\u0026rsquo;image en JSONL driver_entry.c Enregistrement/désenregistrement du callback au chargement/déchargement Intégration dans driver_entry.c Le projet peut être retrouvé dans ce dépôt : KDAMonitor.\nQue récupérer lors du chargement d\u0026rsquo;une image ? # Lorsqu\u0026rsquo;une image est chargée, le callback va pouvoir récupérer un certain nombre d\u0026rsquo;informations. J\u0026rsquo;ai choisi de retenir les informations suivantes qui me semblent être les plus indicatives :\nl\u0026rsquo;ID du processus dans lequel l\u0026rsquo;image est mappée (ProcessId) l\u0026rsquo;adresse et la taille du mapping (ImageBase, ImageSize) les propriétés brutes de l\u0026rsquo;image (Properties) trois indicateurs extraits de ces propriétés : SystemModeImage, ImageMappedToAllPids, ImagePartialMap le niveau et le type de signature de l\u0026rsquo;image (SignatureLevel, SignatureType) le chemin complet de l\u0026rsquo;image (ImageFileName) On a donc la structure suivante dans event_types.h :\ntypedef struct _KDAMON_IMAGE_LOAD_EVENT_DATA { HANDLE ProcessId; PVOID ImageBase; SIZE_T ImageSize; ULONG Properties; ULONG SystemModeImage; ULONG ImageMappedToAllPids; ULONG ImagePartialMap; ULONG SignatureLevel; ULONG SignatureType; WCHAR ImageFileName[260]; } KDAMON_IMAGE_LOAD_EVENT_DATA; Le magic number 260 correspond à la longueur maximale des chemins dans Windows et sera changé en une macro dans une version ultérieure :)\nImplémenter le callback # Cette partie va expliquer l\u0026rsquo;implémentation dans le driver des callbacks, tout est dans le fichier image_callback.c. Tout comme le capteur de processus, il y a trois fonctions, dont deux sont exposées dans le header. Ces deux fonctions sont strictement identiques à celles décrites dans l\u0026rsquo;article précédent, à l\u0026rsquo;exception des noms :\nNTSTATUS KdaMonImageCallbackRegister(VOID); : la fonction utilisée dans driver_entry.c pour enregistrer le callback\nVOID KdaMonImageCallbackUnregister(VOID); : la fonction utilisée dans driver_entry.c pour désenregistrer le callback\nEt la fonction privée appelée lors du chargement d\u0026rsquo;une image :\nstatic VOID KdaMonImageNotifyRoutine( _In_opt_ PUNICODE_STRING FullImageName, _In_ HANDLE ProcessId, _In_ PIMAGE_INFO ImageInfo ); La signature de cette fonction suit la forme suivante :\nPLOAD_IMAGE_NOTIFY_ROUTINE LoadImageNotifyRoutine; VOID LoadImageNotifyRoutine( [in, optional] PUNICODE_STRING FullImageName, [in] HANDLE ProcessId, [in] PIMAGE_INFO ImageInfo ) {...} Voir la documentation Microsoft pour PLOAD_IMAGE_NOTIFY_ROUTINE, la routine utilisée par PsSetLoadImageNotifyRoutine.\nLa routine de notification # Commençons par définir la routine appelée lors du chargement d\u0026rsquo;une image. Comme montré ci-dessus, la signature de cette routine nous donne trois informations :\nPUNICODE_STRING FullImageName : Un pointeur vers la chaîne de caractères (Unicode) contenant le nom de l\u0026rsquo;image HANDLE ProcessId : L\u0026rsquo;ID du processus dans lequel l\u0026rsquo;image est mappée PIMAGE_INFO ImageInfo : Un pointeur vers la structure IMAGE_INFO qui donne des informations sur l\u0026rsquo;image Voici la documentation de IMAGE_INFO : IMAGE_INFO structure (filter.h).\nOn commence par créer l\u0026rsquo;événement :\nKDAMON_EVENT Event = { 0 }; Event.Type = KdaMonEventImageLoad; KeQuerySystemTimePrecise(\u0026amp;Event.Timestamp); // ProcessId est déjà donné dans la signature de la fonction ! Event.Data.ImageLoad.ProcessId = ProcessId; Contrairement à FullImageName, ImageInfo n\u0026rsquo;est pas documenté comme optionnel (annoté _In_ et non _In_opt_). On garde tout de même une vérification par précaution avant de l\u0026rsquo;utiliser :\nif (ImageInfo) { Event.Data.ImageLoad.ImageBase = ImageInfo-\u0026gt;ImageBase; Event.Data.ImageLoad.ImageSize = ImageInfo-\u0026gt;ImageSize; Event.Data.ImageLoad.Properties = ImageInfo-\u0026gt;Properties; Event.Data.ImageLoad.SystemModeImage = ImageInfo-\u0026gt;SystemModeImage; Event.Data.ImageLoad.ImageMappedToAllPids = ImageInfo-\u0026gt;ImageMappedToAllPids; Event.Data.ImageLoad.ImagePartialMap = ImageInfo-\u0026gt;ImagePartialMap; Event.Data.ImageLoad.SignatureLevel = ImageInfo-\u0026gt;ImageSignatureLevel; Event.Data.ImageLoad.SignatureType = ImageInfo-\u0026gt;ImageSignatureType; } FullImageName, lui, est explicitement documenté comme optionnel : il faut donc le tester avant de l\u0026rsquo;utiliser, au cas où il vaudrait NULL ou pointerait vers un buffer vide. La copie reprend le même principe de troncature sécurisée que pour ImageFileName dans l\u0026rsquo;article 05. Et enfin, on ajoute l\u0026rsquo;événement à la file :\nif (FullImageName \u0026amp;\u0026amp; FullImageName-\u0026gt;Buffer != NULL) { SIZE_T MaxCopyLength = sizeof(Event.Data.ImageLoad.ImageFileName) - sizeof(WCHAR); SIZE_T ImageFileNameLength = (FullImageName-\u0026gt;Length \u0026lt; MaxCopyLength) ? FullImageName-\u0026gt;Length : MaxCopyLength; RtlCopyMemory(Event.Data.ImageLoad.ImageFileName, FullImageName-\u0026gt;Buffer, ImageFileNameLength); Event.Data.ImageLoad.ImageFileName[ImageFileNameLength / sizeof(WCHAR)] = L\u0026#39;\\0\u0026#39;; } else { Event.Data.ImageLoad.ImageFileName[0] = L\u0026#39;\\0\u0026#39;; } KdaMonEventQueuePush(\u0026amp;Event); Ci-dessous, le code complet de KdaMonImageNotifyRoutine :\nstatic VOID KdaMonImageNotifyRoutine( _In_opt_ PUNICODE_STRING FullImageName, _In_ HANDLE ProcessId, _In_ PIMAGE_INFO ImageInfo ) { KDAMON_EVENT Event = { 0 }; Event.Type = KdaMonEventImageLoad; KeQuerySystemTimePrecise(\u0026amp;Event.Timestamp); Event.Data.ImageLoad.ProcessId = ProcessId; if (ImageInfo) { Event.Data.ImageLoad.ImageBase = ImageInfo-\u0026gt;ImageBase; Event.Data.ImageLoad.ImageSize = ImageInfo-\u0026gt;ImageSize; Event.Data.ImageLoad.Properties = ImageInfo-\u0026gt;Properties; Event.Data.ImageLoad.SystemModeImage = ImageInfo-\u0026gt;SystemModeImage; Event.Data.ImageLoad.ImageMappedToAllPids = ImageInfo-\u0026gt;ImageMappedToAllPids; Event.Data.ImageLoad.ImagePartialMap = ImageInfo-\u0026gt;ImagePartialMap; Event.Data.ImageLoad.SignatureLevel = ImageInfo-\u0026gt;ImageSignatureLevel; Event.Data.ImageLoad.SignatureType = ImageInfo-\u0026gt;ImageSignatureType; } if (FullImageName \u0026amp;\u0026amp; FullImageName-\u0026gt;Buffer != NULL) { SIZE_T MaxCopyLength = sizeof(Event.Data.ImageLoad.ImageFileName) - sizeof(WCHAR); SIZE_T ImageFileNameLength = (FullImageName-\u0026gt;Length \u0026lt; MaxCopyLength) ? FullImageName-\u0026gt;Length : MaxCopyLength; RtlCopyMemory(Event.Data.ImageLoad.ImageFileName, FullImageName-\u0026gt;Buffer, ImageFileNameLength); Event.Data.ImageLoad.ImageFileName[ImageFileNameLength / sizeof(WCHAR)] = L\u0026#39;\\0\u0026#39;; } else { Event.Data.ImageLoad.ImageFileName[0] = L\u0026#39;\\0\u0026#39;; } KdaMonEventQueuePush(\u0026amp;Event); } Enregistrer et désenregistrer le callback # Pour ce callback, on utilise PsSetLoadImageNotifyRoutine pour l\u0026rsquo;enregistrer et PsRemoveLoadImageNotifyRoutine pour le désenregistrer :\nNTSTATUS KdaMonImageCallbackRegister(VOID) { NTSTATUS status = PsSetLoadImageNotifyRoutine(KdaMonImageNotifyRoutine); if (!NT_SUCCESS(status)) { KdPrint((DRIVER_TAG \u0026#34;[ERROR] PsSetLoadImageNotifyRoutine failed: 0x%08X\\n\u0026#34;, status)); } return status; } VOID KdaMonImageCallbackUnregister(VOID) { NTSTATUS status = PsRemoveLoadImageNotifyRoutine(KdaMonImageNotifyRoutine); if (!NT_SUCCESS(status)) { KdPrint((DRIVER_TAG \u0026#34;[ERROR] PsRemoveLoadImageNotifyRoutine failed: 0x%08X\\n\u0026#34;, status)); } } Sérialiser les événements de chargement d\u0026rsquo;image en JSONL # Voici la fonction dédiée à la sérialisation des événements de chargement d\u0026rsquo;image :\nstatic NTSTATUS KdaMonLogWriterWriteImageEvent(_In_ const KDAMON_EVENT* Event, _Out_writes_z_(BufferSize) PSTR EventBuffer, _In_ SIZE_T BufferSize) { CHAR EscapedImage[520]; if (!KdaMonJsonEscapeW(Event-\u0026gt;Data.ImageLoad.ImageFileName, EscapedImage, sizeof(EscapedImage))) { KdPrint((DRIVER_TAG \u0026#34; [WARNING]: Image path truncated during JSON escape (event %lu)\\n\u0026#34;, Event-\u0026gt;Id)); } return RtlStringCbPrintfA( EventBuffer, BufferSize, \u0026#34;{\\\u0026#34;id\\\u0026#34;:%lu,\\\u0026#34;type\\\u0026#34;:\\\u0026#34;%s\\\u0026#34;,\\\u0026#34;timestamp\\\u0026#34;:%lld,\u0026#34; \u0026#34;\\\u0026#34;pid\\\u0026#34;:%lu,\\\u0026#34;image_base\\\u0026#34;:\\\u0026#34;%p\\\u0026#34;,\\\u0026#34;image_size\\\u0026#34;:%llu,\u0026#34; \u0026#34;\\\u0026#34;system_mode_image\\\u0026#34;:%s,\\\u0026#34;image_mapped_to_all_pids\\\u0026#34;:%s,\u0026#34; \u0026#34;\\\u0026#34;image_partial_map\\\u0026#34;:%s,\\\u0026#34;signature_level\\\u0026#34;:%u,\\\u0026#34;signature_type\\\u0026#34;:%u,\u0026#34; \u0026#34;\\\u0026#34;image\\\u0026#34;:\\\u0026#34;%s\\\u0026#34;}\\n\u0026#34;, Event-\u0026gt;Id, KdaMonEventTypeToString(Event-\u0026gt;Type), Event-\u0026gt;Timestamp.QuadPart, (ULONG)(ULONG_PTR)Event-\u0026gt;Data.ImageLoad.ProcessId, Event-\u0026gt;Data.ImageLoad.ImageBase, (unsigned long long)Event-\u0026gt;Data.ImageLoad.ImageSize, Event-\u0026gt;Data.ImageLoad.SystemModeImage ? \u0026#34;true\u0026#34; : \u0026#34;false\u0026#34;, Event-\u0026gt;Data.ImageLoad.ImageMappedToAllPids ? \u0026#34;true\u0026#34; : \u0026#34;false\u0026#34;, Event-\u0026gt;Data.ImageLoad.ImagePartialMap ? \u0026#34;true\u0026#34; : \u0026#34;false\u0026#34;, Event-\u0026gt;Data.ImageLoad.SignatureLevel, Event-\u0026gt;Data.ImageLoad.SignatureType, EscapedImage ); } Voici, brièvement, le déroulement de la fonction :\nOn échappe les caractères spéciaux du chemin de l\u0026rsquo;image avec la fonction KdaMonJsonEscapeW. Cette fonction ne sera pas détaillée ici pour des raisons de simplicité. Néanmoins, elle peut être consultée dans le code du fichier log_writer.c.\nOn remplit le buffer EventBuffer avec les informations récupérées par l\u0026rsquo;événement, au format JSON. La ligne d\u0026rsquo;événement d\u0026rsquo;un chargement d\u0026rsquo;image aura la forme suivante :\n{\u0026#34;id\u0026#34;:137,\u0026#34;type\u0026#34;:\u0026#34;image_load\u0026#34;,\u0026#34;timestamp\u0026#34;:134025123456789012,\u0026#34;pid\u0026#34;:1234,\u0026#34;image_base\u0026#34;:\u0026#34;0x00007FFA12340000\u0026#34;,\u0026#34;image_size\u0026#34;:45056,\u0026#34;system_mode_image\u0026#34;:false,\u0026#34;image_mapped_to_all_pids\u0026#34;:false,\u0026#34;image_partial_map\u0026#34;:false,\u0026#34;signature_level\u0026#34;:8,\u0026#34;signature_type\u0026#34;:1,\u0026#34;image\u0026#34;:\u0026#34;C:\\\\PATH\\\\TO\\\\DLL.dll\u0026#34;} Intégration dans driver_entry.c # On commence par rajouter le désenregistrement dans DriverUnload :\nvoid DriverUnload(_In_ PDRIVER_OBJECT DriverObject) { UNREFERENCED_PARAMETER(DriverObject); KdaMonImageCallbackUnregister(); // Désenregistrement du callback KdaMonProcessCallbackUnregister(); KdaMonLogWriterStop(); KdaMonEventQueueDestroy(); KdaMonDeleteDevice(g_DeviceObject); KdPrint((DRIVER_TAG \u0026#34; [SUCCESS]: Driver Unload called\\n\u0026#34;)); } Pour l\u0026rsquo;instant, le callback de chargement d\u0026rsquo;image est le dernier à avoir été enregistré, il est donc le premier à être désenregistré.\nDans DriverEntry, on rajoute l\u0026rsquo;enregistrement à la fin de la fonction juste après l\u0026rsquo;enregistrement du callback des processus :\nNTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) { UNREFERENCED_PARAMETER(RegistryPath); ... // --- Register process creation callback --- if (!NT_SUCCESS(KdaMonProcessCallbackRegister())) { KdPrint((DRIVER_TAG \u0026#34; [ERROR]: KdaMonProcessCallbackRegister failed\\n\u0026#34;)); return STATUS_UNSUCCESSFUL; } // --- Register image load callback --- if (!NT_SUCCESS(KdaMonImageCallbackRegister())) { KdPrint((DRIVER_TAG \u0026#34; [ERROR]: KdaMonImageCallbackRegister failed\\n\u0026#34;)); return STATUS_UNSUCCESSFUL; } KdPrint((DRIVER_TAG \u0026#34; [SUCCESS]: Initialized successfully\\n\u0026#34;)); return STATUS_SUCCESS; } Pour cette version, le test de validation va être de vérifier que le capteur capture bien les chargements d\u0026rsquo;images d\u0026rsquo;un processus courant (ses DLL de dépendance), ainsi qu\u0026rsquo;un chargement de DLL isolé et facilement identifiable. Pour cela, j\u0026rsquo;ai réalisé le script suivant (test_v06.ps1) :\n# test_v06.ps1 $Driver = \u0026#34;KDAMonitor\u0026#34; $DriverPath = \u0026#34;$env:USERPROFILE\\Desktop\\$Driver.sys\u0026#34; sc.exe stop $Driver sc.exe delete $Driver sc.exe create $Driver type= kernel binPath= $DriverPath sc.exe start $Driver Start-Process notepad.exe -Wait rundll32.exe user32.dll,MessageBeep sc.exe stop $Driver sc.exe delete $Driver Voici une démonstration de ce test en exécution :\nConclusion # Un deuxième capteur de fait ! Rien de très nouveau dans cet article qui ressemble beaucoup au précédent, le prochain sera cependant différent :).\nMerci d\u0026rsquo;avoir lu jusqu\u0026rsquo;au bout et à bientôt pour le prochain et septième article de cette série : Préparation du monitoring réseau : mise en place de la session WFP.\n","date":"17 septembre 2026","externalUrl":null,"permalink":"/posts/06-image-sensor/","section":"Blog","summary":"Implémentation du deuxième capteur de KDAMonitor : surveillance du chargement des images.","title":"06 - Deuxième capteur : suivi du chargement des images et des DLL","type":"posts"},{"content":"","date":"15 septembre 2026","externalUrl":"https://github.com/HalfTimeOfLife/mirai-arm64-analysis","permalink":"/projects/mirai-arm64-analysis/","section":"Projets","summary":"Analyse statique et dynamique d’un échantillon ELF ARM64 de la famille Mirai/Gafgyt.","title":"Analyse malware Mirai ARM64","type":"projects"},{"content":"","date":"15 septembre 2026","externalUrl":null,"permalink":"/tags/ghidra/","section":"Tags","summary":"","title":"Ghidra","type":"tags"},{"content":"","date":"15 septembre 2026","externalUrl":null,"permalink":"/tags/malware-analysis/","section":"Tags","summary":"","title":"Malware Analysis","type":"tags"},{"content":"","date":"15 septembre 2026","externalUrl":null,"permalink":"/tags/mirai/","section":"Tags","summary":"","title":"Mirai","type":"tags"},{"content":"Travaux, outils et analyses publiées.\n","date":"15 septembre 2026","externalUrl":null,"permalink":"/projects/","section":"Projets","summary":"","title":"Projets","type":"projects"},{"content":"","date":"15 septembre 2026","externalUrl":null,"permalink":"/tags/python/","section":"Tags","summary":"","title":"Python","type":"tags"},{"content":"Bienvenue dans le deuxième article de la série sur le développement de mon noyau AArch64 bare-metal !\nDans cet article, je vais présenter la version v0.1 de ce projet. Le but de cette version est de faire un noyau basique qui contiendra :\nle linker le boot et le code du driver UART À la fin, le noyau doit se lancer correctement dans QEMU et afficher une simple chaîne de caractères Hello, AArch64!.\nVoici les fichiers concernés par cet article, et la section qui les explique :\nFichier Rôle Section linker.ld Script de link : position du code en mémoire, définition de _stack_bottom et _stack_top Le linker script src/boot/boot.s Point d\u0026rsquo;entrée du noyau (_start), initialisation de la pile Le boot : initialiser la pile src/uart/uart.s Driver UART minimal : uart_putc et uart_puts Le driver UART Makefile Compilation des fichiers assembleur et génération de kernel.elf Compiler et lancer le noyau Le projet peut être retrouvé dans ce dépôt : aarch64-baremetal-kernel.\nQEMU virt et notions AArch64 de base # Qu\u0026rsquo;est-ce que QEMU virt ? # Tout d\u0026rsquo;abord, je vais expliquer rapidement ce qu\u0026rsquo;est QEMU et la machine virt.\nQEMU est un émulateur qui va nous permettre de lancer notre noyau AArch64 sans avoir de composant ARM.\nLa machine virt est une plateforme virtuelle générique conçue pour exécuter des systèmes invités. Elle permet de passer outre les contraintes d\u0026rsquo;un matériel spécifique.\nLayout mémoire simplifié # Voici la représentation de la mémoire simplifiée du projet :\n_______________ 0x00000000 | Flash | |_______________| | | | | | | | | | | |_______________|0x40000000 | RAM | |_______________| Registres et instructions utilisés dans cet article # Dans cette section, je ne fais que lister les instructions et registres utilisés, ils seront expliqués plus en détails lors de l\u0026rsquo;explication du code.\nInstructions # Instruction Type Rôle ldr Load Charge une valeur depuis la mémoire, ou charge une adresse avec ldr xN, =... ldrb Load Charge 1 octet depuis la mémoire str Store Écrit une valeur en mémoire stp Store Pair Sauvegarde deux registres en mémoire ldp Load Pair Restaure deux registres depuis la mémoire mov Data movement Copie une valeur d\u0026rsquo;un registre vers un autre tst Test Effectue un AND logique et met à jour les flags b Branch Effectue un saut inconditionnel b.ne Conditional branch Saute si le résultat précédent n\u0026rsquo;était pas zéro cbz Conditional branch Saute si un registre vaut zéro bl Branch with Link Appelle une fonction et sauvegarde l\u0026rsquo;adresse de retour dans x30 ret Return Retourne à l\u0026rsquo;adresse contenue dans x30 Registres # Registre Utilisation dans le code x0 / w0 Argument de fonction / caractère à envoyer à l\u0026rsquo;UART x1 Adresse de base de l\u0026rsquo;UART w2 Contenu du registre UART_FR x19 Pointeur vers la chaîne de caractères x30 Link Register (LR), adresse de retour après bl sp Stack Pointer, pointeur de pile Le linker script # Avant de pouvoir exécuter la moindre instruction, il faut dire au CPU où se trouve notre code en mémoire. C\u0026rsquo;est le rôle du linker script, écrit en linker command language.\nVoir : Linker Scripts\nPourquoi un linker script personnalisé ? # Sans linker script personnalisé, ld utiliserait son script par défaut, pensé pour un système hébergé. Le problème dans notre cas est qu\u0026rsquo;il n\u0026rsquo;a aucune idée de l\u0026rsquo;endroit où se trouve la RAM sur la machine QEMU virt, le code se retrouverait placé n\u0026rsquo;importe où, potentiellement à une adresse où le CPU ne peut même pas l\u0026rsquo;exécuter au démarrage.\nVoici le linker script utilisé :\nENTRY(_start) SECTIONS { . = 0x40000000; .text : { *(.text*) } .rodata : { *(.rodata*) } .data : { *(.data*) } .bss : { *(.bss*) } . = ALIGN(16); .stack : { _stack_bottom = .; . += 0x10000; _stack_top = .; } } Il faut donc lui dire explicitement où poser notre code. D\u0026rsquo;après la documentation de la machine QEMU virt, la RAM commence à l\u0026rsquo;adresse 0x40000000. C\u0026rsquo;est pour ça que le linker script démarre avec . = 0x40000000;.\n. représente l\u0026rsquo;adresse mémoire courante.\nVoir : ‘virt’ generic virtual platform (virt) - Hardware configuration information for bare-metal programming\nLes sections et l\u0026rsquo;alignement # Une fois l\u0026rsquo;adresse de départ fixée, le linker script organise le binaire en plusieurs sections :\n.text : le code exécutable .rodata : les données en lecture seule .data : les données initialisées .bss : les données non initialisées Chaque bloc utilise un caractère *, par exemple *(.text*) pour .text. C\u0026rsquo;est nécessaire car les fichiers sources ne déclarent pas toujours une section .text brute. Par exemple boot.s utilise .section .text.boot. Le * après .text permet donc de regrouper dans une seule section finale toutes les sous-sections dont le nom commence par .text, quel que soit le fichier source d\u0026rsquo;origine.\nJuste avant .stack, on trouve . = ALIGN(16);. La convention d\u0026rsquo;appel AArch64 impose que le pointeur de pile (sp) soit toujours aligné sur 16 octets. En alignant explicitement le curseur du linker avant de définir la zone de pile, on garantit que _stack_top respectera cette contrainte dès la première instruction dans boot.s.\n_stack_bottom et _stack_top # Le bloc .stack : { ... } du linker script définit la zone mémoire réservée à la pile. Il pose d\u0026rsquo;abord le symbole _stack_bottom à l\u0026rsquo;adresse courante (le bas de la pile) puis avance le \u0026ldquo;curseur\u0026rdquo; du linker de 0x10000 (64 Ko) avec . += 0x10000;, avant de poser _stack_top à la nouvelle position (le haut de la pile).\nLa taille de 64 Ko est totalement arbitraire, mais largement suffisante à ce stade du projet.\nLe boot : initialiser la pile # Une fois le linker script en place, on peut écrire le tout premier code exécuté par le CPU au démarrage : _start.\n.section .text.boot .global _start _start: ldr x0, =_stack_top mov sp, x0 b . .section .text.boot place ce code dans une sous-section de .text, ce qui lui permet d\u0026rsquo;être récupéré par le *(.text*) du linker script vu précédemment. .global _start rend le symbole visible en dehors de ce fichier, ce qui est nécessaire puisque le linker script référence ce même symbole via ENTRY(_start). La commande ENTRY() indique là où le CPU doit commencer.\nldr x0, =_stack_top / mov sp, x0 # Ces deux instructions initialisent la pile :\nldr x0, =_stack_top mov sp, x0 ldr x0, =_stack_top charge dans x0 l\u0026rsquo;adresse du symbole _stack_top qui est défini par le linker script (le haut de la zone de pile réservée). mov sp, x0 copie ensuite cette adresse dans le registre sp, le pointeur de pile.\nPourquoi cette forme précise ? # Pourquoi ldr x0, =_stack_top et pas un simple mov ? L\u0026rsquo;instruction mov avec une valeur immédiate ne peut encoder qu\u0026rsquo;un nombre limité de bits directement dans l\u0026rsquo;instruction. L\u0026rsquo;adresse de _stack_top étant une adresse 64 bits complète, elle ne peut pas toujours tenir dans cet espace. La pseudo-instruction ldr x0, =... demande à l\u0026rsquo;assembleur de générer le code nécessaire pour charger l\u0026rsquo;adresse complète.\nPourquoi passer par x0 plutôt que d\u0026rsquo;écrire directement dans sp ? Le jeu d\u0026rsquo;instructions AArch64 n\u0026rsquo;autorise pas sp comme registre de destination pour cette forme de ldr. On passe donc par x0 comme intermédiaire.\nLe driver UART # Maintenant que notre noyau boot, il est temps de faire quelque chose avec. L\u0026rsquo;objectif maintenant est de permettre à notre noyau d\u0026rsquo;afficher des caractères à l\u0026rsquo;écran. Pour cela nous allons utiliser un UART.\nTout le code décrit dans cette partie peut se trouver dans le fichier src/uart/uart.s.\nQu\u0026rsquo;est-ce que l\u0026rsquo;UART, et pourquoi le MMIO ? # Tout d\u0026rsquo;abord, un UART (Universal Asynchronous Receiver Transmitter) est un périphérique matériel utilisé pour la communication série.\nLe MMIO (Memory-mapped I/O) fait en sorte que les registres de l\u0026rsquo;UART soient mappés à des adresses mémoire précises. Donc lire et écrire à ces adresses permet de dialoguer directement avec le matériel. Cela permet donc d\u0026rsquo;utiliser directement les instructions ldr et str.\nUART_BASE, UART_FR, UART_DR # L\u0026rsquo;adresse de base de l\u0026rsquo;UART (UART_BASE) est 0x09000000, elle est fixée par QEMU virt. UART_FR correspond au flag register, autrement dit, à l\u0026rsquo;état du périphérique : si dans ce registre le bit TXFF (mask 0x20) est set alors la file de transmission est pleine et donc on ne peut envoyer des données au périphérique. Il est à l\u0026rsquo;offset 0x18.\nUART_DR correspond au data register, autrement dit, à l\u0026rsquo;endroit où le périphérique reçoit des données, c\u0026rsquo;est donc ici que l\u0026rsquo;on va écrire un caractère. Il est à l\u0026rsquo;offset 0x00.\nOn va donc ajouter cela dans notre fichier :\n.equ UART_BASE, 0x09000000 .equ UART_FR, 0x18 .equ UART_DR, 0x00 .equ UART_TXFF, 0x20 uart_putc # Tout d\u0026rsquo;abord nous allons écrire un seul caractère, pour cela on fait une boucle wait: qui charge le contenu de UART_FR dans w2, teste le bit TXFF avec tst et reboucle tant que la file est pleine (b.ne wait).\nUne fois la file libre, on écrit le caractère dans UART_DR (str w0, [x1, #UART_DR]), d\u0026rsquo;après la convention d\u0026rsquo;appel AArch64 x0 est le premier argument et w0 est la partie inférieure de x0 (32 premiers bits).\nEnfin, on fait un retour à l\u0026rsquo;appelant avec ret.\nVoici le code complet de la fonction :\nuart_putc: ldr x1, =UART_BASE wait: ldr w2, [x1, #UART_FR] tst w2, #UART_TXFF b.ne wait str w0, [x1, #UART_DR] ret uart_puts # Maintenant il faut être capable d\u0026rsquo;afficher une chaîne de caractères. On va donc écrire une fonction uart_puts qui reçoit un pointeur vers une chaîne terminée par \\0 dans x0 et qui va boucler sur cette chaîne, en appelant uart_putc pour chaque caractère.\nVoici le code de la fonction :\nuart_puts: stp x19, x30, [sp, #-16]! mov x19, x0 loop: ldrb w0, [x19], #1 cbz w0, done bl uart_putc b loop done: ldp x19, x30, [sp], #16 ret Tout d\u0026rsquo;abord, on sauvegarde x19 et x30 sur la pile (voir Pourquoi sauver x19 ?). On copie le contenu de x0 dans x19 puis on charge le caractère courant de x19 dans w0 et ensuite on incrémente x19 d\u0026rsquo;un octet.\nÀ chaque caractère, on teste si ce dernier est un caractère de fin de chaîne : cbz w0, done. Si c\u0026rsquo;est le cas, on sort de la boucle (branche vers done) sinon on appelle uart_putc sur le caractère et on revient au début de la boucle.\nSi la chaîne est entièrement affichée, on restaure x19 et x30 puis on fait un retour à l\u0026rsquo;appelant avec ret.\nPourquoi sauver x19 ? # En AArch64, x19 est un registre callee-saved, ainsi uart_puts doit le sauvegarder avant de l\u0026rsquo;utiliser, car uart_putc pourrait le modifier, puis le restaurer avant de retourner. L\u0026rsquo;instruction stp x19, x30, [sp, #-16]! sauvegarde à la fois x19 et l\u0026rsquo;adresse de retour x30 en 16 octets, tout en respectant l\u0026rsquo;alignement de la pile sur 16 octets.\nCompiler et lancer le noyau # Maintenant que le code est écrit, on doit compiler et lancer le noyau.\nLe Makefile # Dans cette partie, je ne vais pas détailler tout le Makefile, juste les composantes importantes.\nLe Makefile complet du projet peut être trouvé dans le dépôt : aarch64-baremetal-kernel.\nJe vais expliquer ce bout du Makefile :\nSRC := $(wildcard src/*/*.s) OUTPUT_DIR := build OBJ := $(patsubst src/%.s,$(OUTPUT_DIR)/%.o,$(SRC)) $(OUTPUT_DIR)/%.o: src/%.s mkdir -p $(@D) $(AS) -c $\u0026lt; -o $@ $(wildcard src/*/*.s) : Cherche tous les fichiers .s dans n\u0026rsquo;importe quel sous-dossier de src/ (donc src/boot/boot.s, src/uart/uart.s, etc.) $(patsubst src/%.s,$(OUTPUT_DIR)/%.o,$(SRC)) : Reconstruit le même chemin sous build/ pour chaque fichier objet correspondant mkdir -p $(@D) crée les sous-dossiers nécessaires dans build/ à la volée Le Makefile va donc créer l\u0026rsquo;arborescence suivante :\n├── Makefile └── build/ ├── boot/ │ └── boot.o ├── uart/ │ └── uart.o └── kernel.elf Vérifier les symboles avec nm # Avant de lancer le noyau avec QEMU, on vérifie que le binaire contient bien ce qu\u0026rsquo;on attend. Pour cela, on utilise la commande aarch64-none-elf-nm build/kernel.elf qui permet de vérifier que les symboles existent et sont résolus, on doit donc retrouver les symboles suivants :\n_start uart_putc uart_puts _stack_bottom _stack_top On peut aussi utiliser objdump -d build/kernel.elf pour voir le désassemblage du binaire.\nLancer avec QEMU # Voici la commande complète pour lancer notre noyau avec QEMU :\nqemu-system-aarch64 \\ -M virt \\ -cpu cortex-a57 \\ -nographic \\ -kernel build/kernel.elf Voici en détail ce que fait chaque option :\n-M virt : demande à utiliser la machine virtuelle virt -cpu cortex-a57 : modèle de CPU AArch64 émulé -nographic : pas de fenêtre graphique, l\u0026rsquo;UART redirigé vers stdout -kernel build/kernel.elf : charge directement notre ELF Résultat # On obtient donc cela :\nConclusion # À l\u0026rsquo;issue de cette première version, ce projet a maintenant un linker script qui place le code exactement où le CPU s\u0026rsquo;attend à le trouver, une pile correctement initialisée, et un premier driver matériel qui fonctionne.\nMerci d\u0026rsquo;avoir lu jusqu\u0026rsquo;au bout, et à bientôt pour le prochain article : Les exceptions : construire la table de vecteurs.\n","date":"11 septembre 2026","externalUrl":null,"permalink":"/posts/02-boot-uart/","section":"Blog","summary":"Mise en place du linker script, du code de boot et du premier driver UART.","title":"02 - Premier boot : linker script, pile et premier message UART","type":"posts"},{"content":"Bienvenue dans cette nouvelle série de mon blog ! Elle va me servir de journal de développement pour un petit kernel bare-metal en AArch64, écrit intégralement en assembleur et exécuté sur la machine virt de QEMU.\nLe projet peut être retrouvé dans ce dépôt : aarch64-baremetal-kernel.\nEn quoi consiste ce projet ? # Ce projet est censé être simple, je dois construire un petit kernel capable de :\ndémarrer sur une carte virtuelle AArch64 (QEMU virt) communiquer avec le monde extérieur via UART gérer les exceptions et les interruptions mettre en place un timer et du multitâche et, à terme, ressembler à un (tout petit) système d\u0026rsquo;exploitation fonctionnel Je modère mes attentes concernant ce système d\u0026rsquo;exploitation, je me suis lancé dans ce projet sans vraiment savoir où cela va finir :)\nDe plus, je ne vais utiliser que de l\u0026rsquo;assembleur (pas de C).\nPourquoi ce projet ? # La raison principale qui m\u0026rsquo;a poussé à faire ce projet est d\u0026rsquo;apprendre le langage assembleur ARM ainsi que de comprendre en profondeur l\u0026rsquo;architecture AArch64 et son fonctionnement bas niveau (registres, niveaux d\u0026rsquo;exception, MMU, etc.).\nÀ noter que la v0.1 est déjà terminée au moment où j\u0026rsquo;écris cet article : le kernel boot et affiche déjà un message via UART.\nEnvironnement cible # Émulateur QEMU Machine virt Architecture AArch64 (ARMv8-A) CPU cortex-a57 Langage Assembleur AArch64 Toolchain aarch64-none-elf QEMU virt expose une carte minimale mais suffisante pour ce projet : de la RAM, un CPU AArch64, et un UART compatible PL011, mappé en MMIO à l\u0026rsquo;adresse 0x09000000.\nPrérequis # Pour suivre cette série dans de bonnes conditions, je conseille d\u0026rsquo;avoir déjà quelques notions de base en assembleur (peu importe l\u0026rsquo;architecture). Aucune connaissance préalable d\u0026rsquo;AArch64 n\u0026rsquo;est nécessaire : je pars moi-même de zéro et j\u0026rsquo;expliquerai chaque instruction utilisée au fur et à mesure.\nPlan détaillé de développement # Voici le roadmap actuel du projet, version par version :\nVersion Fonctionnalité v0.1 Boot et UART v0.2 Exceptions et interruptions v0.3 Timer ARM v0.4 Gestion mémoire et MMU v0.5 Niveaux d\u0026rsquo;exception et mode utilisateur v0.6 Multitâche et ordonnanceur v0.7 Pilotes et périphériques v1.0 Système d\u0026rsquo;exploitation minimal Programme des articles à venir # Voici, dans l\u0026rsquo;ordre, les articles prévus pour cette série :\nArticle Version(s) Titre Contenu principal 01 - Introduction Présentation du projet, de ses objectifs et de son environnement cible. 02 v0.1 Premier boot : linker script, pile et premier message UART Mise en place du linker script, du code de boot, initialisation de la pile et premier driver UART pour afficher un message. 03 v0.2 Les exceptions : construire la table de vecteurs Construction de la table de vecteurs d\u0026rsquo;exception, configuration de VBAR_EL1, gestion des exceptions synchrones et des IRQ. 04 v0.3 Mettre en place le timer ARM Configuration du timer générique ARM, mise en place des interruptions périodiques et bases du timekeeping. 05 v0.4 Gérer la mémoire physique et virtuelle Mise en place d\u0026rsquo;un allocateur physique simple, des tables de pages, configuration de la MMU et passage en adressage virtuel. 06 v0.5 Premier programme en mode utilisateur Détection et gestion des niveaux d\u0026rsquo;exception, transition vers EL0 et premier programme en mode utilisateur. 07 v0.6 Faire tourner plusieurs tâches Sauvegarde/restauration de contexte, structure de tâche, ordonnanceur round-robin et exécution de plusieurs tâches noyau. 08 v0.7 Nouveaux pilotes et périphériques Amélioration du driver UART, ajout du support GPIO et de périphériques supplémentaires de la machine virt. 09 v1.0 Bilan Consolidation de tous les composants précédents en un petit système d\u0026rsquo;exploitation utilisable. Merci d\u0026rsquo;avance à toutes celles et ceux qui suivront cette série. Si vous avez des questions, des retours ou simplement envie d\u0026rsquo;échanger sur le projet, n\u0026rsquo;hésitez pas à me contacter.\nÀ bientôt pour le prochain article, où l\u0026rsquo;on plongera vraiment dans le code : le boot et le premier driver UART !\n","date":"8 septembre 2026","externalUrl":null,"permalink":"/posts/01-introduction-aarch64-kernel/","section":"Blog","summary":"Introduction au projet de kernel bare-metal AArch64 et à ses objectifs.","title":"01 - Introduction à mon kernel AArch64 bare-metal sur QEMU virt","type":"posts"},{"content":"Bienvenue dans le cinquième article de la série sur le développement de KDAMonitor !\nDans cet article, je vais couvrir la version v0.5 de ce projet. Dans cette version, j\u0026rsquo;implémente le premier des 5 capteurs/callbacks du projet : le capteur des processus.\nVoici les fichiers concernés par cet article, et la section qui les explique :\nFichier Rôle Section event_types.h Nouveau type KDAMON_PROCESS_EVENT_DATA + union dans KDAMON_EVENT Que récupérer lors de la création ou terminaison d\u0026rsquo;un processus ? process_callback.h Déclaration du register/unregister Implémenter le callback process_callback.c Le callback lui-même + logique création/terminaison Implémenter le callback log_writer.c Sérialisation JSONL spécifique aux événements de processus Sérialiser les événements processus en JSONL driver_entry.c Enregistrement/désenregistrement du callback au chargement/déchargement Intégration dans driver_entry.c Le projet peut être retrouvé dans ce dépôt : KDAMonitor.\nQue récupérer lors de la création ou terminaison d\u0026rsquo;un processus ? # Comme expliqué dans l\u0026rsquo;article 04, le driver utilisera une structure générique pour les événements avec une union qui donnera les informations spécifiques à chaque type d\u0026rsquo;événement. La structure de chaque événement est dans event_types.h et suivra la forme suivante :\ntypedef struct _KDAMON_\u0026lt;TYPE_OF_EVENT\u0026gt;_EVENT_DATA { // The event\u0026#39;s data } KDAMON_\u0026lt;TYPE_OF_EVENT\u0026gt;_EVENT_DATA; Qu\u0026rsquo;est ce qui est intéressant dans un événement concernant un processus ?\nJ\u0026rsquo;ai choisi de retenir quatre informations :\nl\u0026rsquo;ID du processus (ProcessId) : un HANDLE du processus qui est créé ou terminé l\u0026rsquo;ID du parent du processus (ParentProcessId) : un HANDLE du processus qui a créé ou terminé le processus actuel le nom de l\u0026rsquo;image du processus (ImageFileName) : une chaîne de caractères (de WCHAR) contenant le nom de l\u0026rsquo;image complète du processus actuel le statut du processus (IsCreate) : un BOOLEAN déterminant si le processus est créé (TRUE) ou terminé (FALSE) Ces quatre paramètres sont suffisants pour décrire et différencier un processus.\nVoici donc la structure représentant un événement de processus dans le driver :\ntypedef struct _KDAMON_PROCESS_EVENT_DATA { HANDLE ProcessId; HANDLE ParentProcessId; BOOLEAN IsCreate; WCHAR ImageFileName[260]; } KDAMON_PROCESS_EVENT_DATA; Le magic number 260 correspond à la longueur maximale des chemins dans Windows et sera changé en une macro dans une version ultérieure :)\nQu\u0026rsquo;est-ce qu\u0026rsquo;un callback ? # Dans l\u0026rsquo;article 2, j\u0026rsquo;ai implémenté le client ainsi que la communication avec le driver. Mais ici ce qu\u0026rsquo;on cherche à faire c\u0026rsquo;est que le driver détecte automatiquement certains comportements, ici la création et la terminaison de processus.\nPour faire cela, le noyau Windows propose les callbacks. Le principe est le suivant : on donne au noyau un pointeur vers une fonction, en lui demandant de l\u0026rsquo;exécuter automatiquement quand une certaine condition survient.\nPour la création/terminaison de processus, la fonction dédiée est PsSetCreateProcessNotifyRoutineEx, qui sera détaillée dans la section suivante.\nImplémenter le callback # Cette partie va expliquer l\u0026rsquo;implémentation dans le driver des callbacks, tout est dans le fichier process_callback.c. Il y a trois fonctions dans ce fichier, dont deux sont exposées dans le header. Commençons par ces deux-là :\nNTSTATUS KdaMonProcessCallbackRegister(VOID); : la fonction utilisée dans driver_entry.c pour enregistrer le callback\nVOID KdaMonProcessCallbackUnregister(VOID); : la fonction utilisée dans driver_entry.c pour désenregistrer le callback\nEt la fonction privée appelée lors de la création/terminaison d\u0026rsquo;un nouveau process :\nstatic VOID KdaMonProcessNotifyRoutine( _Inout_ PEPROCESS Process, _In_ HANDLE ProcessId, _Inout_opt_ PPS_CREATE_NOTIFY_INFO CreateInfo ); La signature de cette fonction suit la forme suivante :\nPCREATE_PROCESS_NOTIFY_ROUTINE_EX PcreateProcessNotifyRoutineEx; VOID PcreateProcessNotifyRoutineEx( [_Inout_] PEPROCESS Process, [in] HANDLE ProcessId, [in, out, optional] PPS_CREATE_NOTIFY_INFO CreateInfo ) {...} Voir la documentation microsoft pour PCREATE_PROCESS_NOTIFY_ROUTINE_EX, la routine utilisée par PsSetCreateProcessNotifyRoutineEx\nLa routine de notification # Tout d\u0026rsquo;abord, il faut définir la routine appelée lors de la création et de la terminaison d\u0026rsquo;un processus. La signature de cette routine nous donne trois informations :\nPEPROCESS Process : Un pointeur vers la structure représentant le processus HANDLE ProcessId : L\u0026rsquo;ID du processus PPS_CREATE_NOTIFY_INFO CreateInfo : Un pointeur vers la structure PS_CREATE_NOTIFY_INFO qui donne des informations sur le processus CreateInfo n\u0026rsquo;est rempli que si le processus est créé : en cas de terminaison, ce pointeur vaut simplement NULL.\nAu début de cette routine, il faut créer l\u0026rsquo;événement de type KdaMonEventProcess :\nKDAMON_EVENT Event = { 0 }; Event.Type = KdaMonEventProcess; KeQuerySystemTimePrecise(\u0026amp;Event.Timestamp); // ProcessId est déjà donné dans la signature de la fonction ! Event.Data.Process.ProcessId = ProcessId; Il faut ensuite distinguer deux cas : la création et la terminaison d\u0026rsquo;un processus.\nCréation de processus # Lors de la création du processus, il est possible de remplir toute la structure de l\u0026rsquo;événement (KDAMON_EVENT Event). Il suffit de récupérer les informations disponibles à partir des structures passées en arguments :\nif (CreateInfo) { // --- Process creation case --- Event.Data.Process.ParentProcessId = CreateInfo-\u0026gt;ParentProcessId; Event.Data.Process.IsCreate = TRUE; PCUNICODE_STRING ImageFileName = CreateInfo-\u0026gt;ImageFileName; if (ImageFileName \u0026amp;\u0026amp; ImageFileName-\u0026gt;Buffer != NULL) { SIZE_T MaxCopyLength = sizeof(Event.Data.Process.ImageFileName) - sizeof(WCHAR); SIZE_T ImageFileNameLength = (ImageFileName-\u0026gt;Length \u0026lt; MaxCopyLength) ? ImageFileName-\u0026gt;Length : MaxCopyLength; RtlCopyMemory(Event.Data.Process.ImageFileName, ImageFileName-\u0026gt;Buffer, ImageFileNameLength); Event.Data.Process.ImageFileName[ImageFileNameLength / sizeof(WCHAR)] = L\u0026#39;\\0\u0026#39;; } } Terminaison de processus # Lors de la terminaison du processus, on ne peut récupérer que le ProcessId et donc mettre IsCreate à FALSE. Dans le code, on a :\nelse { // --- Process termination case --- Event.Data.Process.ParentProcessId = NULL; Event.Data.Process.IsCreate = FALSE; Event.Data.Process.ImageFileName[0] = L\u0026#39;\\0\u0026#39;; } Dans les deux cas, une fois la structure Event remplie, on l\u0026rsquo;ajoute à la file avec KdaMonEventQueuePush(\u0026amp;Event);.\nCode complet de KdaMonProcessNotifyRoutine # static VOID KdaMonProcessNotifyRoutine(_Inout_ PEPROCESS Process, _In_ HANDLE ProcessId, _Inout_opt_ PPS_CREATE_NOTIFY_INFO CreateInfo) { UNREFERENCED_PARAMETER(Process); KDAMON_EVENT Event = { 0 }; Event.Type = KdaMonEventProcess; KeQuerySystemTimePrecise(\u0026amp;Event.Timestamp); Event.Data.Process.ProcessId = ProcessId; if (CreateInfo) { // --- Process creation case --- Event.Data.Process.ParentProcessId = CreateInfo-\u0026gt;ParentProcessId; Event.Data.Process.IsCreate = TRUE; PCUNICODE_STRING ImageFileName = CreateInfo-\u0026gt;ImageFileName; if (ImageFileName \u0026amp;\u0026amp; ImageFileName-\u0026gt;Buffer != NULL) { SIZE_T MaxCopyLength = sizeof(Event.Data.Process.ImageFileName) - sizeof(WCHAR); SIZE_T ImageFileNameLength = (ImageFileName-\u0026gt;Length \u0026lt; MaxCopyLength) ? ImageFileName-\u0026gt;Length : MaxCopyLength; RtlCopyMemory(Event.Data.Process.ImageFileName, ImageFileName-\u0026gt;Buffer, ImageFileNameLength); Event.Data.Process.ImageFileName[ImageFileNameLength / sizeof(WCHAR)] = L\u0026#39;\\0\u0026#39;; } } else { // --- Process termination case --- Event.Data.Process.ParentProcessId = NULL; Event.Data.Process.IsCreate = FALSE; Event.Data.Process.ImageFileName[0] = L\u0026#39;\\0\u0026#39;; } KdaMonEventQueuePush(\u0026amp;Event); } Enregistrer et désenregistrer le callback # Comme expliqué dans Qu\u0026rsquo;est-ce qu\u0026rsquo;un callback ?, on utilise PsSetCreateProcessNotifyRoutineEx aussi bien pour enregistrer le callback que pour le désenregistrer (le paramètre Remove inverse simplement le sens de l\u0026rsquo;appel) :\nNTSTATUS KdaMonProcessCallbackRegister(VOID) { NTSTATUS status = PsSetCreateProcessNotifyRoutineEx(KdaMonProcessNotifyRoutine, FALSE); if (!NT_SUCCESS(status)) { KdPrint((DRIVER_TAG \u0026#34;[ERROR] PsSetCreateProcessNotifyRoutineEx failed: 0x%08X\\n\u0026#34;, status)); } return status; } VOID KdaMonProcessCallbackUnregister(VOID) { NTSTATUS status = PsSetCreateProcessNotifyRoutineEx(KdaMonProcessNotifyRoutine, TRUE); if (!NT_SUCCESS(status)) { KdPrint((DRIVER_TAG \u0026#34;[ERROR] PsSetCreateProcessNotifyRoutineEx failed: 0x%08X\\n\u0026#34;, status)); } } Sérialiser les événements processus en JSONL # Comme expliqué dans l\u0026rsquo;article 04, chaque événement a son propre type de sérialisation en JSONL, avec sa fonction dédiée. Voici celle pour les processus :\nstatic NTSTATUS KdaMonLogWriterWriteProcessEvent(_In_ const KDAMON_EVENT* Event, _Out_writes_z_(BufferSize) PSTR EventBuffer, _In_ SIZE_T BufferSize) { CHAR EscapedImage[520]; CHAR PpidField[16]; if (!KdaMonJsonEscapeW(Event-\u0026gt;Data.Process.ImageFileName, EscapedImage, sizeof(EscapedImage))) { KdPrint((DRIVER_TAG \u0026#34; [WARNING]: Image path truncated during JSON escape (event %lu)\\n\u0026#34;, Event-\u0026gt;Id)); } if (Event-\u0026gt;Data.Process.IsCreate \u0026amp;\u0026amp; Event-\u0026gt;Data.Process.ParentProcessId != NULL) { RtlStringCbPrintfA(PpidField, sizeof(PpidField), \u0026#34;%lu\u0026#34;, (ULONG)(ULONG_PTR)Event-\u0026gt;Data.Process.ParentProcessId); } else { RtlStringCbCopyA(PpidField, sizeof(PpidField), \u0026#34;null\u0026#34;); } return RtlStringCbPrintfA( EventBuffer, BufferSize, \u0026#34;{\\\u0026#34;id\\\u0026#34;:%lu,\\\u0026#34;type\\\u0026#34;:\\\u0026#34;%s\\\u0026#34;,\\\u0026#34;timestamp\\\u0026#34;:%lld,\u0026#34; \u0026#34;\\\u0026#34;pid\\\u0026#34;:%lu,\\\u0026#34;ppid\\\u0026#34;:%s,\\\u0026#34;is_create\\\u0026#34;:%s,\\\u0026#34;image\\\u0026#34;:\\\u0026#34;%s\\\u0026#34;}\\n\u0026#34;, Event-\u0026gt;Id, KdaMonEventTypeToString(Event-\u0026gt;Type), Event-\u0026gt;Timestamp.QuadPart, (ULONG)(ULONG_PTR)Event-\u0026gt;Data.Process.ProcessId, PpidField, Event-\u0026gt;Data.Process.IsCreate ? \u0026#34;true\u0026#34; : \u0026#34;false\u0026#34;, EscapedImage ); } Voici le déroulement de la fonction :\nOn échappe les caractères spéciaux avec la fonction KdaMonJsonEscapeW Cette fonction ne sera pas détaillée ici pour des raisons de simplicité. Néanmoins, elle peut être consultée dans le code du fichier log_writer.c.\nOn écrit dans le tableau PpidField (Parent Process Id Field) le ParentProcessId s\u0026rsquo;il existe, soit null dans le cas inverse. On remplit le buffer EventBuffer avec les informations récupérées par l\u0026rsquo;événement. La ligne d\u0026rsquo;événement d\u0026rsquo;un processus aura la forme suivante :\n{\u0026#34;id\u0026#34;:42,\u0026#34;type\u0026#34;:\u0026#34;process\u0026#34;,\u0026#34;timestamp\u0026#34;:134025123456789012,\u0026#34;pid\u0026#34;:1234,\u0026#34;ppid\u0026#34;:856,\u0026#34;is_create\u0026#34;:true,\u0026#34;image\u0026#34;:\u0026#34;C:\\\\Windows\\\\System32\\\\notepad.exe\u0026#34;} Intégration dans driver_entry.c # Tout d\u0026rsquo;abord, il faut enregistrer le callback dans DriverEntry et son désenregistrement dans DriverUnload :\nvoid DriverUnload(_In_ PDRIVER_OBJECT DriverObject) { UNREFERENCED_PARAMETER(DriverObject); KdaMonProcessCallbackUnregister(); // Désenregistrement du callback KdaMonLogWriterStop(); KdaMonEventQueueDestroy(); KdaMonDeleteDevice(g_DeviceObject); KdPrint((DRIVER_TAG \u0026#34; [SUCCESS]: Driver Unload called\\n\u0026#34;)); } NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) { UNREFERENCED_PARAMETER(RegistryPath); DriverObject-\u0026gt;DriverUnload = DriverUnload; DriverObject-\u0026gt;MajorFunction[IRP_MJ_CREATE] = KdaMonCreateClose; DriverObject-\u0026gt;MajorFunction[IRP_MJ_CLOSE] = KdaMonCreateClose; DriverObject-\u0026gt;MajorFunction[IRP_MJ_DEVICE_CONTROL] = KdaMonDeviceControl; NTSTATUS status = KdaMonCreateDevice(DriverObject, \u0026amp;g_DeviceObject); if (!NT_SUCCESS(status)) { return status; } // --- Initialize the event queue --- if (!KdaMonEventQueueInitialize()) { KdPrint((DRIVER_TAG \u0026#34; [ERROR]: EventQueueInitialize failed\\n\u0026#34;)); return STATUS_UNSUCCESSFUL; } // --- Start the log writer thread --- if (!KdaMonLogWriterStart(DriverObject)) { KdPrint((DRIVER_TAG \u0026#34; [ERROR]: KdaMonLogWriterStart failed\\n\u0026#34;)); return STATUS_UNSUCCESSFUL; } // --- Register process creation callback --- if (!NT_SUCCESS(KdaMonProcessCallbackRegister())) { KdPrint((DRIVER_TAG \u0026#34; [ERROR]: KdaMonProcessCallbackRegister failed\\n\u0026#34;)); return STATUS_UNSUCCESSFUL; } KdPrint((DRIVER_TAG \u0026#34; [SUCCESS]: Initialized successfully\\n\u0026#34;)); return STATUS_SUCCESS; } Pour cette version, le test de validation va être de créer un processus et de voir si un événement est bien créé et écrit dans le fichier de log avec le bon type. On va donc ouvrir Notepad juste après le lancement de notre driver puis l\u0026rsquo;arrêter une fois Notepad ouvert. Pour cela, j\u0026rsquo;ai réalisé le script suivant (test_v05.ps1) :\n# test_v05.ps1 $Driver = \u0026#34;KDAMonitor\u0026#34; $DriverPath = \u0026#34;$env:USERPROFILE\\Desktop\\$Driver.sys\u0026#34; sc.exe stop $Driver sc.exe delete $Driver sc.exe create $Driver type= kernel binPath= $DriverPath sc.exe start $Driver Start-Process notepad.exe -Wait sc.exe stop $Driver sc.exe delete $Driver Voici une démonstration de ce test en exécution :\nConclusion # Finalement, le premier capteur est implémenté. Cette version a aussi permis de confirmer que les modules précédents (la file d\u0026rsquo;événement et la journalisation jsonl) fonctionnent comme prévu.\nLors de cette release (v0.5), j\u0026rsquo;ai aussi rajouté au README.md un schéma de l\u0026rsquo;architecture actuelle du projet :\nLes prochaines versions (jusqu\u0026rsquo;à v0.10) seront consacrées à l\u0026rsquo;implémentation de nouveaux callbacks et callouts, je passerai donc moins de temps à expliquer la sérialisation des événements en jsonl.\nMerci d\u0026rsquo;avoir lu jusqu\u0026rsquo;au bout et à bientôt pour le prochain et sixième article de cette série : Deuxième capteur : suivi du chargement des images et des DLL.\n","date":"7 septembre 2026","externalUrl":null,"permalink":"/posts/05-process-sensor/","section":"Blog","summary":"Implémentation du premier capteur de KDAMonitor : surveillance de la création et de la terminaison des processus.","title":"05 - Premier capteur : surveillance de la création et de la terminaison des processus","type":"posts"},{"content":"Bienvenue dans le quatrième article de la série sur le développement de KDAMonitor !\nDans 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\u0026rsquo;implémentation de la journalisation via des fichiers .jsonl. Cette journalisation permettra de donner un sens à la file implémentée précédemment.\nVoici les fichiers concernés par cet article, et la section qui les explique :\nFichier Rôle Section kdamon_config.h Nouvelles constantes centralisées (chemins de log) Pourquoi un thread dédié à l\u0026rsquo;écriture ? log_writer.h Interface publique du log writer (Start/Stop) Le thread dédié à l\u0026rsquo;écriture (log_writer.c) log_writer.c Ouverture/fermeture du fichier, sérialisation JSONL, thread, contrôleurs start/stop Le thread dédié à l\u0026rsquo;é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.\nPourquoi un thread dédié à l\u0026rsquo;écriture ? # Il y a deux raisons à faire un thread dédié à l\u0026rsquo;écriture et ne pas compter sur les callbacks :\nCela permet de répartir clairement ce que chaque objet créé (device, queue, \u0026hellip;) 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\u0026rsquo;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\u0026rsquo;il vont rajouter les événements à la file ils vont s\u0026rsquo;exécuter à DISPATCH_LEVEL. Cependant, à DISPATCH_LEVEL le scheduler ne peut intervenir, c\u0026rsquo;est-à-dire qu\u0026rsquo;aucune I/O bloquante n\u0026rsquo;est autorisée (pas de lecture/écriture disque, pas d\u0026rsquo;attente sur un objet qui peut dormir).\nPour 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.\nNouvelles constantes centralisées (kdamon_config.h) # C\u0026rsquo;est le bon moment pour introduire les nouvelles constantes :\nKDAMON_DIR: Le chemin du dossier attribué au driver (tout le projet) L\u0026quot;\\\\??\\\\C:\\\\KDAMonitor\\\\\u0026quot;. KDAMON_LOG_DIR: Le chemin du dossier attribué au journaux du driver L\u0026quot;\\\\??\\\\C:\\\\KDAMonitor\\\\logs\\\\\u0026quot;. KDAMON_LOG_FILE_PREFIX et KDAMON_LOG_FILE_EXTENSION: Respectivement, le préfixe et suffixe donnés au fichier de log. Pourquoi cette notation \\??\\C:\\ ?\nCe lien symbolique appartient à l\u0026rsquo;espace de noms objets du noyau (Object Manager namespace) ; il permet de faire le pont vers le disque C:, une notion propre à l\u0026rsquo;espace Win32 que cet espace de noms objets ne connaît pas nativement.\n\\??\\ est aussi appelé \\DosDevices\\.\nDans cette version, kdamon_config.h reçoit aussi une correction de compilation à cette occasion : les identifiants STATUS_* (utilisés entre autres par ZwCreateFile) n\u0026rsquo;étaient pas déclarés. Il faut inclure \u0026lt;ntstatus.h\u0026gt; avant \u0026lt;ntddk.h\u0026gt;, en définissant WIN32_NO_STATUS entre les deux pour éviter les conflits de macros, voici le fichier final :\n#pragma once #include \u0026lt;ntstatus.h\u0026gt; #define WIN32_NO_STATUS #include \u0026lt;ntddk.h\u0026gt; #undef WIN32_NO_STATUS #define DRIVER_TAG \u0026#34;[KDAMonitor]\u0026#34; #define KDAMON_DEVICE_NAME L\u0026#34;\\\\Device\\\\KDAMonitor\u0026#34; #define KDAMON_SYMLINK_NAME L\u0026#34;\\\\DosDevices\\\\KDAMonitor\u0026#34; #define KDAMON_DIR L\u0026#34;\\\\??\\\\C:\\\\KDAMonitor\\\\\u0026#34; #define KDAMON_LOG_DIR L\u0026#34;\\\\??\\\\C:\\\\KDAMonitor\\\\logs\\\\\u0026#34; #define KDAMON_LOG_FILE_PREFIX L\u0026#34;kdamon_\u0026#34; #define KDAMON_LOG_FILE_EXTENSION L\u0026#34;.jsonl\u0026#34; É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 WakeEvent dans la structure de la file (event_queue.c), de type SynchronizationEvent. Dans KdaMonEventQueuePush, juste avant de relâcher le spinlock : KeSetEvent(\u0026amp;g_EventQueue.WakeEvent, IO_NO_INCREMENT, FALSE) — donc signalé à chaque push réussi, sous spinlock, à DISPATCH_LEVEL. Point à expliciter : KeSetEvent est justement conçu pour pouvoir être appelé jusqu\u0026rsquo;à DISPATCH_LEVEL, contrairement à des primitives d\u0026rsquo;attente côté appelant — cohérent avec le choix du spinlock de l\u0026rsquo;article 03. Nouveau getter KdaMonEventQueueGetWakeEvent exposé dans event_queue.h, utilisé par log_writer.c pour récupérer le pointeur vers cet événement sans exposer toute la structure de la file. Distinction avec l\u0026rsquo;événement d\u0026rsquo;arrêt # Bien préciser qu\u0026rsquo;il y a deux événements distincts : WakeEvent (porté par la file, signalé à chaque nouvel événement) et g_StopEvent (local à log_writer.c, signalé une seule fois à l\u0026rsquo;arrêt). Le thread attend les deux en même temps via KeWaitForMultipleObjects, ce qui lui permet de réagir immédiatement à l\u0026rsquo;arrêt sans attendre un hypothétique prochain événement. Le thread dédié à l\u0026rsquo;é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 :\nKdaMonLogWriterOpenFile s\u0026rsquo;occupe de créer (ou vérifier l\u0026rsquo;existence de) C:\\KDAMonitor\\, puis C:\\KDAMonitor\\logs\\, puis construire un nom de fichier horodaté (kdamon_YYYYMMDD_HHMMSS.jsonl) et l\u0026rsquo;ouvrir en écriture. KdaMonLogWriterCloseFile ferme le handle du fichier de log s\u0026rsquo;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.\nSans callbacks implémentés, cette fonction va écrire un événement basique, sans informations pertinentes. L\u0026rsquo;événement contiendra : l\u0026rsquo;ID, le type d\u0026rsquo;événement et le timestamp.\nDe plus, voici l\u0026rsquo;état global maintenu par ce module :\ng_ThreadObject : le pointeur vers l\u0026rsquo;objet thread noyau, conservé pour pouvoir l\u0026rsquo;attendre à l\u0026rsquo;arrêt (voir plus bas). g_StopEvent : l\u0026rsquo;événement signalé pour demander l\u0026rsquo;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\u0026rsquo;é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.\nCréation du thread : IoCreateSystemThread # Commençons par la fonction qui va nous permettre de lancer le thread dédié au journaliseur, KdaMonLogWriterStart.\nElle commence par initialiser g_StopEvent en tant que NotificationEvent, qui va nous permettre de réveiller le thread en attente :\nKeInitializeEvent(\u0026amp;g_StopEvent, NotificationEvent, FALSE); NotificationEvent signifie qu\u0026rsquo;une fois signalé, l\u0026rsquo;événement reste signalé indéfiniment. Une fois l\u0026rsquo;arrêt demandé, il ne doit pas \u0026ldquo;s\u0026rsquo;auto-consommer\u0026rdquo;, il doit rester signalé de façon permanente. Le dernier paramètre, FALSE, fixe l\u0026rsquo;état initial à non-signalé.\nEnsuite, la fonction vérifie que le fichier de log existe puis l\u0026rsquo;ouvre (KdaMonLogWriterOpenFile). Juste après, on crée le thread système avec IoCreateSystemThread :\nstatus = IoCreateSystemThread( DriverObject, // Objet driver auquel associer le thread \u0026amp;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\u0026#39;entrée du thread NULL // StartContext ); Le premier paramètre, DriverObject, est ce qui distingue cette fonction de PsCreateSystemThread. En le passant ici, l\u0026rsquo;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\u0026rsquo;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\u0026rsquo;offre pas nativement.\nLes paramètres suivants sont laissés à NULL :\nObjectAttributes : pas de nom d\u0026rsquo;objet à associer ProcessHandle : le thread est créé dans le contexte du processus System par défaut ClientId : on n\u0026rsquo;a pas besoin de récupérer son PID/TID puisqu\u0026rsquo;on va travailler directement avec un pointeur d\u0026rsquo;objet (voir plus bas). KdaMonLogWriterThread est la routine d\u0026rsquo;entrée du thread (voir Boucle principale du thread (KdaMonLogWriterThread)). StartContext reste NULL : elle n\u0026rsquo;a besoin de rien recevoir en paramètre, elle récupère elle-même le WakeEvent de la file.\nEn cas d\u0026rsquo;échec à cette étape, la fonction appelle KdaMonLogWriterCloseFile.\nVoici le code final de cette fonction :\nBOOLEAN KdaMonLogWriterStart(_In_ PDRIVER_OBJECT DriverObject) { NTSTATUS status; HANDLE threadHandle; KeInitializeEvent(\u0026amp;g_StopEvent, NotificationEvent, FALSE); status = KdaMonLogWriterOpenFile(); if (!NT_SUCCESS(status)) { KdPrint((DRIVER_TAG \u0026#34; [ERROR]: KdaMonLogWriterStart: failed to open log file (0x%08X)\\n\u0026#34;, status)); return FALSE; } status = IoCreateSystemThread( DriverObject, \u0026amp;threadHandle, THREAD_ALL_ACCESS, NULL, NULL, NULL, KdaMonLogWriterThread, NULL ); if (!NT_SUCCESS(status)) { KdPrint((DRIVER_TAG \u0026#34; [ERROR]: IoCreateSystemThread failed (0x%08X)\\n\u0026#34;, status)); KdaMonLogWriterCloseFile(); return FALSE; } status = ObReferenceObjectByHandle( threadHandle, THREAD_ALL_ACCESS, NULL, KernelMode, \u0026amp;g_ThreadObject, NULL ); ZwClose(threadHandle); if (!NT_SUCCESS(status)) { KdPrint((DRIVER_TAG \u0026#34; [ERROR]: ObReferenceObjectByHandle failed (0x%08X)\\n\u0026#34;, status)); return FALSE; } KdPrint((DRIVER_TAG \u0026#34; [SUCCESS]: Log writer started\\n\u0026#34;)); return TRUE; } Boucle principale du thread (KdaMonLogWriterThread) # Une fois lancé par IoCreateSystemThread, le thread exécute KdaMonLogWriterThread en boucle jusqu\u0026rsquo;à ce qu\u0026rsquo;on lui demande de s\u0026rsquo;arrêter :\nstatic VOID KdaMonLogWriterThread(_In_ PVOID StartContext) { UNREFERENCED_PARAMETER(StartContext); PRKEVENT WakeEvent = KdaMonEventQueueGetWakeEvent(); PVOID WaitObjects[WAIT_OBJECT_COUNT]; NTSTATUS WaitStatus; KDAMON_EVENT Event; WaitObjects[0] = \u0026amp;g_StopEvent; WaitObjects[1] = WakeEvent; KdPrint((DRIVER_TAG \u0026#34; [SUCCESS]: Log writer thread started\\n\u0026#34;)); for (;;) { WaitStatus = KeWaitForMultipleObjects( WAIT_OBJECT_COUNT, WaitObjects, WaitAny, Executive, KernelMode, FALSE, NULL, NULL ); if (WaitStatus == STATUS_WAIT_0) { break; } while (KdaMonEventQueuePop(\u0026amp;Event)) { KdaMonLogWriterWriteEvent(\u0026amp;Event); } } KdPrint((DRIVER_TAG \u0026#34; [SUCCESS]: Log writer thread exiting\\n\u0026#34;)); PsTerminateSystemThread(STATUS_SUCCESS); } Le thread récupère d\u0026rsquo;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.\nLa 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\u0026rsquo;un des deux objets est signalé.\nLa valeur de retour est utilisée pour déterminer le comportement que doit adopter le code :\nSi la valeur de retour est STATUS_WAIT_0, alors cela correspond à l\u0026rsquo;index 0 de WaitObjects et donc l\u0026rsquo;objet signalé est g_StopEvent. Si c\u0026rsquo;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(\u0026amp;Event)) qui retire et écrit, via KdaMonLogWriterWriteEvent, les événements un par un, jusqu\u0026rsquo;à 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.\nCycle de vie et arrêt propre # L\u0026rsquo;arrêt du thread se fait via KdaMonLogWriterStop, appelée depuis DriverUnload, avant KdaMonEventQueueDestroy :\nVOID KdaMonLogWriterStop(VOID) { if (g_ThreadObject == NULL) { return; } KeSetEvent(\u0026amp;g_StopEvent, IO_NO_INCREMENT, FALSE); KeWaitForSingleObject(g_ThreadObject, Executive, KernelMode, FALSE, NULL); ObDereferenceObject(g_ThreadObject); g_ThreadObject = NULL; KdaMonLogWriterCloseFile(); KdPrint((DRIVER_TAG \u0026#34; [SUCCESS]: Log writer stopped\\n\u0026#34;)); } KeSetEvent(\u0026amp;g_StopEvent, ...) signale l\u0026rsquo;arrêt, le thread en attente dans KeWaitForMultipleObjects se réveille avec STATUS_WAIT_0 et sort de sa boucle.\nEn utilisant KeWaitForSingleObject(g_ThreadObject, ...), un objet thread passe à l\u0026rsquo;état signalé exactement quand le thread se termine réellement (au moment du PsTerminateSystemThread). Cet appel bloque donc jusqu\u0026rsquo;à ce que le thread ait fini de s\u0026rsquo;exécuter, pas seulement jusqu\u0026rsquo;à ce qu\u0026rsquo;on le lui ait demandé. Sans cette attente, DriverUnload pourrait continuer, et le driver être déchargé, pendant que le thread tourne encore.\nUne 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.\nJournalisation au format JSONL # Pourquoi JSONL ? # Le format JSONL (JSON Lines) consiste à écrire un objet JSON valide par ligne, plutôt qu\u0026rsquo;un unique tableau JSON englobant l\u0026rsquo;ensemble des événements. J\u0026rsquo;ai choisi ce format pour deux raisons :\nchaque événement peut être écrit indépendamment en simple append, sans avoir à réécrire ou refermer une structure englobante la correspondance stricte \u0026ldquo;une ligne = un événement\u0026rdquo; rend le fichier trivial à parser ensuite. Un autre point qui a justifié ce choix que j\u0026rsquo;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.\nSérialisation d\u0026rsquo;un événement (KdaMonLogWriterWriteEvent) # Dans cette partie, le format donné sera la sérialisation de base commune à tous les types d\u0026rsquo;événements. La construction de la ligne JSON est faite avec RtlStringCbPrintfA :\n{\u0026#34;id\u0026#34;:...,\u0026#34;type\u0026#34;:\u0026#34;...\u0026#34;,\u0026#34;timestamp\u0026#34;:...}\\n Dans le code cela donne :\nNTSTATUS status = RtlStringCbPrintfA( EventBuffer, sizeof(EventBuffer), \u0026#34;{\\\u0026#34;id\\\u0026#34;:%lu,\\\u0026#34;type\\\u0026#34;:\\\u0026#34;%s\\\u0026#34;,\\\u0026#34;timestamp\\\u0026#34;:%lld}\\n\u0026#34;, Event-\u0026gt;Id, KdaMonEventTypeToString(Event-\u0026gt;Type), Event-\u0026gt;Timestamp.QuadPart ); KdaMonEventTypeToString : petite fonction de mapping de l\u0026rsquo;enum KDAMON_EVENT_TYPE vers une chaîne lisible (\u0026quot;Process\u0026quot;, \u0026quot;Network\u0026quot;, etc.).\nRtlStringCbPrintfA est utilisée à la place d\u0026rsquo;un sprintf classique. C\u0026rsquo;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à.\nUne fois la ligne construite, sa longueur exacte est récupérée avec RtlStringCbLengthA :\nstatus = RtlStringCbLengthA(EventBuffer, sizeof(EventBuffer), \u0026amp;Length); Cette longueur (sans le \\0 final) est nécessaire pour indiquer à ZwWriteFile combien d\u0026rsquo;octets écrire précisément :\nstatus = ZwWriteFile( g_LogFileHandle, NULL, // Event NULL, // ApcRoutine NULL, // ApcContext \u0026amp;IoStatusBlock, EventBuffer, (ULONG)Length, NULL, // ByteOffset NULL // Key ); ZwWriteFile est l\u0026rsquo;é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\u0026rsquo;appel est synchrone, grâce au flag FILE_SYNCHRONOUS_IO_NONALERT posé lors de l\u0026rsquo;ouverture du fichier. IoStatusBlock reçoit en sortie le nombre d\u0026rsquo;octets réellement écrits ainsi que le statut de l\u0026rsquo;opération.\nLe code de cette fonction ne sera pas donné en entier car ce n\u0026rsquo;est qu\u0026rsquo;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.\nIntégration dans driver_entry.c # Tout d\u0026rsquo;abord, il faut rajouter le lancement du log writer (KdaMonLogWriterStart) dans DriverEntry et son arrêt (KdaMonLogWriterStop) dans DriverUnload :\nvoid DriverUnload(_In_ PDRIVER_OBJECT DriverObject) { UNREFERENCED_PARAMETER(DriverObject); KdaMonLogWriterStop(); KdaMonEventQueueDestroy(); KdaMonDeleteDevice(g_DeviceObject); KdPrint((DRIVER_TAG \u0026#34; [SUCCESS]: Driver Unload called\\n\u0026#34;)); } NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) { ... if (!KdaMonEventQueueInitialize()) { KdPrint((DRIVER_TAG \u0026#34; [ERROR]: EventQueueInitialize failed\\n\u0026#34;)); return STATUS_UNSUCCESSFUL; } if (!KdaMonLogWriterStart(DriverObject)) { KdPrint((DRIVER_TAG \u0026#34; [ERROR]: KdaMonLogWriterStart failed\\n\u0026#34;)); return STATUS_UNSUCCESSFUL; } KdPrint((DRIVER_TAG \u0026#34; [SUCCESS]: Initialized successfully\\n\u0026#34;)); ... 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\u0026rsquo;ordre d\u0026rsquo;arrivée et écrits dans un fichier .jsonl. Ci-dessous le code final de DriverEntry :\nNTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) { UNREFERENCED_PARAMETER(RegistryPath); DriverObject-\u0026gt;DriverUnload = DriverUnload; DriverObject-\u0026gt;MajorFunction[IRP_MJ_CREATE] = KdaMonCreateClose; DriverObject-\u0026gt;MajorFunction[IRP_MJ_CLOSE] = KdaMonCreateClose; DriverObject-\u0026gt;MajorFunction[IRP_MJ_DEVICE_CONTROL] = KdaMonDeviceControl; NTSTATUS status = KdaMonCreateDevice(DriverObject, \u0026amp;g_DeviceObject); if (!NT_SUCCESS(status)) { return status; } if (!KdaMonEventQueueInitialize()) { KdPrint((DRIVER_TAG \u0026#34; [ERROR]: EventQueueInitialize failed\\n\u0026#34;)); return STATUS_UNSUCCESSFUL; } if (!KdaMonLogWriterStart(DriverObject)) { KdPrint((DRIVER_TAG \u0026#34; [ERROR]: KdaMonLogWriterStart failed\\n\u0026#34;)); return STATUS_UNSUCCESSFUL; } KdPrint((DRIVER_TAG \u0026#34; [SUCCESS]: Initialized successfully\\n\u0026#34;)); // --- BEGIN TEST QUEUE --- KDAMON_EVENT testEvent1 = { 0 }; testEvent1.Type = KdaMonEventProcess; KeQuerySystemTimePrecise(\u0026amp;testEvent1.Timestamp); KdaMonEventQueuePush(\u0026amp;testEvent1); KDAMON_EVENT testEvent2 = { 0 }; testEvent2.Type = KdaMonEventNetwork; KeQuerySystemTimePrecise(\u0026amp;testEvent2.Timestamp); KdaMonEventQueuePush(\u0026amp;testEvent2); // --- END TEST QUEUE --- return STATUS_SUCCESS; } Contrairement au test de l\u0026rsquo;article 03, on ne dépile plus manuellement : c\u0026rsquo;est le thread de log qui va consommer ces deux événements de lui-même, dès qu\u0026rsquo;il sera réveillé par le WakeEvent.\nVoici une démonstration de ce test en exécution :\nLe 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).\nLe bugcheck relevé est IRQL_NOT_LESS_OR_EQUAL (0xA) :\nIRQL_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 memory Arg1 confirme que l\u0026rsquo;adresse mémoire référencée était NULL, et Arg2 confirme un IRQL de 2, soit DISPATCH_LEVEL.\nL\u0026rsquo;instruction fautive elle-même se trouve dans nt!KeSetEvent :\nIP_IN_PAGED_CODE: nt!KeSetEvent+1af fffff807`cdc7274f 4d8b2424 mov r12,qword ptr [r12] Et la pile d\u0026rsquo;appel confirme le point d\u0026rsquo;entrée dans le driver :\nSTACK_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`00000000 KDAMonitor+0x162c correspond à l\u0026rsquo;appel à KeSetEvent fait dans KdaMonEventQueuePush (event_queue.c), donc juste après l\u0026rsquo;ajout d\u0026rsquo;un nouvel événement à la file.\nDiagnostic # Le code fautif se trouvait dans KdaMonEventQueueInitialize :\nBOOLEAN KdaMonEventQueueInitialize(VOID) { // Initialisé AVANT le zéro-out KeInitializeEvent(\u0026amp;g_EventQueue.WakeEvent, SynchronizationEvent, FALSE); KeInitializeSpinLock(\u0026amp;g_EventQueue.Lock); RtlZeroMemory(\u0026amp;g_EventQueue, sizeof(g_EventQueue)); // \u0026lt;- 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\u0026rsquo;objet event était corrompu avant même d\u0026rsquo;avoir servi.\nLe crash se produit au premier Push. C\u0026rsquo;est KeSetEvent qui tente de parcourir cette liste d\u0026rsquo;attente interne, déréférence le pointeur NULL laissé par le zéro-out, et provoque le bugcheck.\nRésolution # Le correctif consiste simplement à inverser l\u0026rsquo;ordre des opérations : RtlZeroMemory d\u0026rsquo;abord, initialiser les objets noyau ensuite.\nBOOLEAN KdaMonEventQueueInitialize(VOID) { RtlZeroMemory(\u0026amp;g_EventQueue, sizeof(g_EventQueue)); KeInitializeEvent(\u0026amp;g_EventQueue.WakeEvent, SynchronizationEvent, FALSE); KeInitializeSpinLock(\u0026amp;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.\nLa prochaine version, v0.5, viendra enfin remplir la file pour de vrai avec le premier capteur : la création et la destruction de processus.\nMerci d\u0026rsquo;avoir lu jusqu\u0026rsquo;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.\n","date":"1 septembre 2026","externalUrl":null,"permalink":"/posts/04-log-writer/","section":"Blog","summary":"Écriture des logs sur disque et premier crash noyau du driver KDAMonitor.","title":"04 - Écriture des événements sur disque : journalisation JSONL, événements de réveil et premier crash","type":"posts"},{"content":"","date":"23 août 2026","externalUrl":"https://github.com/HalfTimeOfLife/aarch64-baremetal-kernel","permalink":"/projects/aartch64baremetal/","section":"Projets","summary":"Noyau bare-metal minimaliste pour AArch64, écrit de zéro et exécuté sur la machine virt de QEMU.","title":"AArch64 Bare-Metal Kernel","type":"projects"},{"content":"","date":"23 août 2026","externalUrl":null,"permalink":"/tags/armv8-a/","section":"Tags","summary":"","title":"ARMv8-A","type":"tags"},{"content":"","date":"23 août 2026","externalUrl":null,"permalink":"/tags/qemu/","section":"Tags","summary":"","title":"QEMU","type":"tags"},{"content":"Bienvenue dans le troisième article de la série sur le développement de KDAMonitor !\nDans cet article, je vais couvrir la version v0.3 de ce projet. Dans cette version, je me suis contenté d\u0026rsquo;implémenter la structure de données (en l\u0026rsquo;occurrence une file) qui va permettre de stocker, transmettre et supprimer les « événements » (voir Définition d\u0026rsquo;un événement dans le contexte de KDAMonitor) récupérés par les capteurs.\nVoici les fichiers concernés par cet article, et la section qui les explique :\nFichier Rôle Section event_types.h Types d\u0026rsquo;événements et structure KDAMON_EVENT Définition d\u0026rsquo;un événement dans le contexte de KDAMonitor event_queue.h Interface de la file d\u0026rsquo;événements et déclarations des fonctions Implémentation de la file event_queue.c Implémentation de la file d\u0026rsquo;événements et synchronisation Implémentation de la file driver_entry.c Point d\u0026rsquo;entrée du pilote, initialisation de la file d\u0026rsquo;événements et test Exemple : test de la file d\u0026rsquo;événements Le projet peut être retrouvé dans ce dépôt : KDAMonitor.\nDéfinition d\u0026rsquo;un événement dans le contexte de KDAMonitor # Avant d\u0026rsquo;expliquer en détail la structure de données que j\u0026rsquo;ai utilisée, voyons ce que je considère comme un événement. Voici comment la structure est définie dans le code :\ntypedef struct _KDAMON_EVENT { KDAMON_EVENT_TYPE Type; LARGE_INTEGER Timestamp; ULONG Id; // future members for event data //union //{ // //} Data; } KDAMON_EVENT, * PKDAMON_EVENT; Commençons par les trois premiers champs de la structure KDAMON_EVENT :\nType : Ce champ contiendra une valeur correspondant à un type désigné dans l\u0026rsquo;enum KDAMON_EVENT_TYPE. Timestamp : L\u0026rsquo;horodatage de la réception par le callback/callout associé et par la même occasion de la création de l\u0026rsquo;événement. Id : Un identifiant unique à la session de capture attribué à l\u0026rsquo;événement. Dans cette version (v0.3), les événements ne contiennent que le Type, Timestamp et Id. De plus, étant donné qu\u0026rsquo;aucun callback/callout n\u0026rsquo;est implémenté dans cette version, l\u0026rsquo;horodatage est fait manuellement.\nLe champ Type va permettre de distinguer les événements entre eux, voici l\u0026rsquo;enum qui lui est attribué :\ntypedef enum _KDAMON_EVENT_TYPE { KdaMonEventImageLoad, // TODO: implemented in v0.6 KdaMonEventNetwork, // TODO: implemented in v0.8 KdaMonEventProcess, // TODO: implemented in v0.5 KdaMonEventRegistry, // TODO: implemented in v0.9 KdaMonEventThread // TODO: implemented in v0.10 } KDAMON_EVENT_TYPE; Le dernier champ, qui est une union nommée Data, va contenir la structure de l\u0026rsquo;événement selon le type, rappelons que les événements supportés seront les suivants :\nCréation/Destruction de processus Chargement d\u0026rsquo;image Connexion réseau Création/Suppression/Modification de registre Création/Destruction de thread Chacun de ces types se verra attribuer une structure correspondante avec les infos souhaitées. Les événements auront une base commune mais un bon nombre de détails divergent. Par exemple, pour un événement de chargement d\u0026rsquo;une dll (qui sera un KdaMonEventImageLoad), le chemin de la dll sera récupéré. De la même façon, pour un événement de création de processus, c\u0026rsquo;est le chemin de l\u0026rsquo;exécutable qui sera conservé. Par contre, une adresse IP de destination ne concerne que KdaMonEventNetwork tout comme un chemin de clef de registre.\nLe contenu spécifique de ces événements (c\u0026rsquo;est-à-dire les champs des structures correspondantes) sera détaillé dans les articles expliquant chaque capteur associé.\nStructure de données utilisée # Qu\u0026rsquo;est-ce qu\u0026rsquo;une file ? # Une file est une structure de données \u0026hellip; qui ressemble à une file :-), plus précisément c\u0026rsquo;est ce qu\u0026rsquo;on appelle une structure de données FIFO = First In First Out (Premier Rentré, Premier Sorti), comme dans une file d\u0026rsquo;attente, le premier arrivé est celui qui passe en premier.\nEn programmation, une file contient, en pratique, une référence vers le dernier élément ajouté (la queue) et le premier (la tête) dans la file. Ajouter un élément à cette structure signifie que l\u0026rsquo;on place cet élément à la queue de la file.\nIl existe d\u0026rsquo;autres types de structures de données :\nUne pile, qui est une structure LIFO = Last In First Out, à l\u0026rsquo;image d\u0026rsquo;une pile d\u0026rsquo;assiettes, la dernière mise sur la pile est aussi la première sortie. Une liste chaînée (ou une liste) est une structure où il est possible de rajouter des éléments au début, à la fin et au milieu de cette dernière. On a donc la structure suivante représentant la file :\ntypedef struct _KDAMON_EVENT_QUEUE { KDAMON_EVENT Buffer[KDAMON_EVENT_QUEUE_SIZE]; ULONG Head; ULONG Tail; ULONG Count; ULONG DroppedEvents; ULONG NextId; KSPIN_LOCK Lock; } KDAMON_EVENT_QUEUE; static KDAMON_EVENT_QUEUE g_EventQueue; Voici une explication de tous les champs de cette structure :\nBuffer : Tableau des événements (KDAMON_EVENT) actuellement dans la file Head : La tête de la file (élément le plus ancien de la file) Tail : La queue de la file (dernier élément ajouté à la file) Count : Nombre d\u0026rsquo;éléments dans la file DroppedEvents : Nombre d\u0026rsquo;éléments que la file n\u0026rsquo;a pas gardés NextId : ID à attribuer au prochain événement Lock : Détaillé dans la partie Synchronisation Pourquoi utiliser une file ? # Prenons l\u0026rsquo;exemple suivant :\nOn choisit de prendre une pile comme structure de données pour ce projet. Un premier événement arrive, on le place en haut de la pile. Un deuxième événement arrive, on le place en haut de la pile, au-dessus du premier événement. Et ainsi de suite, finalement on arrive à l\u0026rsquo;événement 1000. Il y a deux scénarios : scénario 1 : on a dépilé (enlevé l\u0026rsquo;élément en haut de la pile) à chaque fois qu\u0026rsquo;un événement arrivait -\u0026gt; opération coûteuse, et finalement, quelle différence avec l\u0026rsquo;absence totale de structure de données ? scénario 2 : on n\u0026rsquo;a rien dépilé, dans ce cas, le premier élément que l\u0026rsquo;on va sortir sera en fait le dernier récupéré par les callbacks, ainsi on doit reconstruire via les timestamps la chronologie. On en déduit que la pile n\u0026rsquo;est pas un bon choix. D\u0026rsquo;autant plus que plusieurs callbacks vont alimenter la structure, il nous faut donc une structure qui soit faite pour un ordre chronologique.\nLa file remplit parfaitement ce rôle. On définit une taille maximale à notre file, et pour chaque événement, on le pousse dans la file (push) puis on le retire plus tard depuis la tête de la file (pop), dans l\u0026rsquo;ordre où il a été ajouté. Un désavantage de cette implémentation est le fait que la file a une taille fixe, ce qui veut dire que si trop d\u0026rsquo;événements arrivent alors que la file est déjà pleine, les nouveaux événements sont rejetés plutôt qu\u0026rsquo;ajoutés.\nMais que se passe-t-il si deux capteurs rajoutent dans la file en même temps un événement ? Afin de résoudre cela, nous allons faire de la synchronisation.\nSynchronisation # Concrètement, le problème est le suivant :\nLe callback réseau et le callback de création de processus rajoutent tous les deux en même temps un événement. Sans protection, les deux pourraient lire la même valeur de NextId avant que l\u0026rsquo;un ou l\u0026rsquo;autre ne l\u0026rsquo;incrémente, et donc attribuer le même ID à deux événements différents.\nAutre problème : que se passe-t-il si un producteur (un callback) est en train de rajouter un événement dans la file (donc en train de modifier Tail et Count) pendant qu\u0026rsquo;un consommateur en retire un en même temps, en lisant ces mêmes champs ? Le consommateur pourrait alors lire un Count ou un Tail dans un état intermédiaire, incohérent, ce qui peut corrompre l\u0026rsquo;ordre de la file ou faire lire un événement qui n\u0026rsquo;a pas encore été complètement écrit.\nAfin de résoudre ce problème, on a besoin d\u0026rsquo;exclure mutuellement les champs de la structure de notre file. En effet, lorsqu\u0026rsquo;un composant de notre driver modifie le buffer, les index Head/Tail/Count ou le compteur NextId, personne d\u0026rsquo;autre ne doit être capable de les modifier en même temps.\nOn va donc utiliser une primitive de synchronisation.\nPrimitives de synchronisation : le spinlock # La primitive de synchronisation que j\u0026rsquo;ai choisie est le spinlock. Pour deux raisons :\nJe n\u0026rsquo;avais jamais implémenté cette primitive. Elle correspondait à une contrainte technique du projet. Mais concrètement, qu\u0026rsquo;est-ce qu\u0026rsquo;un spinlock ?\nPour résumer, un spinlock, c\u0026rsquo;est un peu comme une cabine d\u0026rsquo;essayage : si quelqu\u0026rsquo;un est déjà à l\u0026rsquo;intérieur, la porte est verrouillée. Si une nouvelle personne arrive, elle n\u0026rsquo;a pas d\u0026rsquo;autre choix que d\u0026rsquo;attendre juste devant, en vérifiant sans arrêt si la porte s\u0026rsquo;est déverrouillée. Elle ne va pas s\u0026rsquo;asseoir ailleurs et attendre qu\u0026rsquo;on la prévienne que la cabine est libre.\nC\u0026rsquo;est exactement ce que fait un spinlock : un thread qui ne peut pas l\u0026rsquo;acquérir reste actif à « vérifier » en boucle (busy-wait) au lieu de se mettre en sommeil, contrairement à un mutex où le thread en attente serait plutôt notifié une fois la ressource libérée. Cependant, le thread qui patiente consomme du CPU pendant toute la durée de l\u0026rsquo;attente. Un spinlock n\u0026rsquo;est donc adapté qu\u0026rsquo;à des sections critiques très courtes.\nVoici un exemple d\u0026rsquo;utilisation avec les fonctions KeAcquireSpinLock et KeReleaseSpinLock :\nKIRQL OldIrql; KeAcquireSpinLock(\u0026amp;g_EventQueue.Lock, \u0026amp;OldIrql); // section critique : accès exclusif à g_EventQueue qui représente notre file KeReleaseSpinLock(\u0026amp;g_EventQueue.Lock, OldIrql); KeAcquireSpinLock élève l\u0026rsquo;IRQL courant à DISPATCH_LEVEL et sauvegarde l\u0026rsquo;ancien IRQL dans OldIrql, afin que KeReleaseSpinLock puisse le restaurer une fois la section critique terminée.\nMaintenant, pourquoi avoir choisi un spinlock plutôt qu\u0026rsquo;un mutex pour protéger g_EventQueue ? La réponse est l\u0026rsquo;IRQL (Interrupt Request Level), qui représente le niveau de priorité d\u0026rsquo;interruption auquel le processeur exécute du code à un instant donné.\nUn mutex ne peut être acquis qu\u0026rsquo;à PASSIVE_LEVEL, le niveau le plus bas. En effet, lorsqu\u0026rsquo;un thread ne parvient pas à acquérir un mutex, il est mis en sommeil par le scheduler en attendant que la ressource se libère. Or, cette mise en sommeil n\u0026rsquo;est possible que si le scheduler lui-même peut intervenir, ce qui n\u0026rsquo;est plus le cas dès qu\u0026rsquo;on dépasse PASSIVE_LEVEL.\nUn spinlock n\u0026rsquo;a pas cette limitation. En effet, comme expliqué plus haut, un thread qui attend un spinlock boucle activement plutôt que de se mettre en sommeil : il peut donc être acquis à n\u0026rsquo;importe quel IRQL, jusqu\u0026rsquo;à DISPATCH_LEVEL inclus.\nLes futurs capteurs du driver (création de processus, chargement d\u0026rsquo;image, accès registre, réseau via WFP) seront chacun implémentés via un callback ou callout noyau, et ces callbacks ne s\u0026rsquo;exécutent pas tous au même IRQL. Si g_EventQueue avait été protégée par un mutex, un callback exécuté à DISPATCH_LEVEL aurait provoqué un bugcheck en tentant de l\u0026rsquo;acquérir.\nMaintenant que les bases théoriques sont posées, passons à l\u0026rsquo;implémentation !\nImplémentation de la file # Nous avons déjà introduit, plus haut, la structure KDAMON_EVENT_QUEUE que nous allons utiliser pour la file (voir Qu\u0026rsquo;est-ce qu\u0026rsquo;une file ?). Passons aux méthodes qui vont nous permettre d\u0026rsquo;interagir avec cette dernière.\nToutes les fonctions (et la structure) présentées sont dans le fichier event_queue.c.\nInitialisation (et destruction de la file) # Afin de créer la file, nous allons créer une fonction KdaMonEventQueueInitialize qui n\u0026rsquo;aura que deux responsabilités :\nmettre à zéro la structure KDAMON_EVENT_QUEUE via l\u0026rsquo;objet global : static KDAMON_EVENT_QUEUE g_EventQueue; initialiser le spinlock de la structure avec KeInitializeSpinLock : KeInitializeSpinLock(\u0026amp;g_EventQueue.Lock); Cette fonction retourne TRUE.\nLa fonction KdaMonEventQueueDestroy existe mais ne fait rien pour l\u0026rsquo;instant (elle est vide), pour une raison simple : l\u0026rsquo;entièreté de la structure est statique, il n\u0026rsquo;y a donc rien à libérer manuellement. Voici le code des fonctions :\nBOOLEAN KdaMonEventQueueInitialize(VOID) { RtlZeroMemory(\u0026amp;g_EventQueue, sizeof(g_EventQueue)); KeInitializeSpinLock(\u0026amp;g_EventQueue.Lock); return TRUE; } VOID KdaMonEventQueueDestroy(VOID) { } Ajout et retrait d\u0026rsquo;événements # Afin d\u0026rsquo;interagir avec la file, il y a 3 fonctions :\nEventQueueNextIndex : Calcule l\u0026rsquo;index suivant dans le buffer circulaire, en revenant à 0 une fois la fin du buffer atteinte (KDAMON_EVENT_QUEUE_SIZE). Utilisée aussi bien par Push que par Pop pour faire progresser Tail et Head. KdaMonEventQueuePush : Ajoute un élément à la file. KdaMonEventQueuePop : Retire un élément de la file. Concrètement, EventQueueNextIndex est assez explicite :\nstatic ULONG EventQueueNextIndex(_In_ ULONG Index) { Index++; if (Index == KDAMON_EVENT_QUEUE_SIZE) { Index = 0; } return Index; } Les codes de KdaMonEventQueuePush et KdaMonEventQueuePop sont plus compliqués. Voici celui de KdaMonEventQueuePush :\nBOOLEAN KdaMonEventQueuePush(_In_ KDAMON_EVENT* Event) { KIRQL OldIrql; if (Event == NULL) { return FALSE; } KeAcquireSpinLock(\u0026amp;g_EventQueue.Lock, \u0026amp;OldIrql); if (g_EventQueue.Count == KDAMON_EVENT_QUEUE_SIZE) { g_EventQueue.DroppedEvents++; KeReleaseSpinLock(\u0026amp;g_EventQueue.Lock, OldIrql); return FALSE; } Event-\u0026gt;Id = g_EventQueue.NextId++; g_EventQueue.Buffer[g_EventQueue.Tail] = *Event; g_EventQueue.Tail = EventQueueNextIndex(g_EventQueue.Tail); g_EventQueue.Count++; KeReleaseSpinLock(\u0026amp;g_EventQueue.Lock, OldIrql); return TRUE; } La fonction commence par une vérification : si Event est NULL, elle retourne immédiatement FALSE sans toucher au lock.\nLe spinlock est ensuite acquis, et toute la logique qui suit se déroule dans la section critique qu\u0026rsquo;on a présentée plus haut (voir Primitives de synchronisation : le spinlock).\nIl y a deux cas à gérer :\nPremier cas : la file est pleine (Count == KDAMON_EVENT_QUEUE_SIZE). Dans ce cas, l\u0026rsquo;événement n\u0026rsquo;est pas ajouté. On incrémente DroppedEvents et on retourne FALSE. L\u0026rsquo;appelant sait ainsi que l\u0026rsquo;événement n\u0026rsquo;a pas été pris en compte, sans jamais risquer de mettre en attente un callback noyau. Deuxième cas : la file n\u0026rsquo;est pas pleine, l\u0026rsquo;événement peut être ajouté. La première chose faite est l\u0026rsquo;attribution de l\u0026rsquo;Id : Event-\u0026gt;Id = g_EventQueue.NextId++. Enfin, l\u0026rsquo;événement est copié dans le buffer à la position Tail, l\u0026rsquo;index Tail est avancé via EventQueueNextIndex, et Count est incrémenté pour refléter le nouvel état de la file. Le lock est relâché, et la fonction retourne TRUE. Voici celui de KdaMonEventQueuePop :\nBOOLEAN KdaMonEventQueuePop(_Out_ KDAMON_EVENT* Event) { KIRQL OldIrql; if (Event == NULL) { return FALSE; } KeAcquireSpinLock(\u0026amp;g_EventQueue.Lock, \u0026amp;OldIrql); if (g_EventQueue.Count == 0) { KeReleaseSpinLock(\u0026amp;g_EventQueue.Lock, OldIrql); return FALSE; } *Event = g_EventQueue.Buffer[g_EventQueue.Head]; g_EventQueue.Head = EventQueueNextIndex(g_EventQueue.Head); g_EventQueue.Count--; KeReleaseSpinLock(\u0026amp;g_EventQueue.Lock, OldIrql); return TRUE; } Cette fonction est globalement un miroir de KdaMonEventQueuePush. Le début est strictement identique. Mais les cas à gérer sont différents :\nPremier cas : la file est vide (Count == 0), dans ce cas on relâche le spinlock et on retourne FALSE. Il n\u0026rsquo;y a rien à retirer de la file. Deuxième cas : la file a au moins un événement. L\u0026rsquo;événement situé à g_EventQueue.Buffer[g_EventQueue.Head] est copié dans *Event, le paramètre de sortie fourni par l\u0026rsquo;appelant. On calcule ensuite l\u0026rsquo;index suivant dans le buffer à l\u0026rsquo;aide de EventQueueNextIndex et on le met dans g_EventQueue.Head. Enfin, on décrémente le nombre total d\u0026rsquo;éléments dans la file (g_EventQueue.Count--). Taille actuelle de la file # La dernière fonction implémentée est KdaMonEventQueueCount. Cette fonction va nous permettre de vérifier de manière sécurisée (avec le spinlock) le nombre d\u0026rsquo;éléments actuellement dans la file :\nULONG KdaMonEventQueueCount(VOID) { KIRQL OldIrql; ULONG EventCount; KeAcquireSpinLock(\u0026amp;g_EventQueue.Lock, \u0026amp;OldIrql); EventCount = g_EventQueue.Count; KeReleaseSpinLock(\u0026amp;g_EventQueue.Lock, OldIrql); return EventCount; } Exemple : test de la file d\u0026rsquo;événements # Pour valider que la file fonctionne comme prévu, j\u0026rsquo;ai ajouté un test directement dans DriverEntry, entre les balises // --- BEGIN TEST QUEUE --- et // --- END TEST QUEUE --- :\n#include \u0026#34;driver.h\u0026#34; #include \u0026#34;device.h\u0026#34; #include \u0026#34;ioctl.h\u0026#34; #include \u0026#34;kdamon_config.h\u0026#34; #include \u0026#34;event_queue.h\u0026#34; PDEVICE_OBJECT g_DeviceObject = NULL; void DriverUnload(_In_ PDRIVER_OBJECT DriverObject) { UNREFERENCED_PARAMETER(DriverObject); KdaMonEventQueueDestroy(); KdaMonDeleteDevice(g_DeviceObject); KdPrint((DRIVER_TAG \u0026#34; [SUCCESS]: Driver Unload called\\n\u0026#34;)); } NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) { UNREFERENCED_PARAMETER(RegistryPath); DriverObject-\u0026gt;DriverUnload = DriverUnload; DriverObject-\u0026gt;MajorFunction[IRP_MJ_CREATE] = KdaMonCreateClose; DriverObject-\u0026gt;MajorFunction[IRP_MJ_CLOSE] = KdaMonCreateClose; DriverObject-\u0026gt;MajorFunction[IRP_MJ_DEVICE_CONTROL] = KdaMonDeviceControl; NTSTATUS status = KdaMonCreateDevice(DriverObject, \u0026amp;g_DeviceObject); if (!NT_SUCCESS(status)) { return status; } if (!KdaMonEventQueueInitialize()) { KdPrint((DRIVER_TAG \u0026#34; [ERROR]: EventQueueInitialize failed\\n\u0026#34;)); return STATUS_UNSUCCESSFUL; } KdPrint((DRIVER_TAG \u0026#34; [SUCCESS]: Initialized successfully\\n\u0026#34;)); // --- BEGIN TEST QUEUE --- KDAMON_EVENT testEvent1 = { 0 }; testEvent1.Type = KdaMonEventProcess; KeQuerySystemTimePrecise(\u0026amp;testEvent1.Timestamp); KdaMonEventQueuePush(\u0026amp;testEvent1); KDAMON_EVENT testEvent2 = { 0 }; testEvent2.Type = KdaMonEventNetwork; KeQuerySystemTimePrecise(\u0026amp;testEvent2.Timestamp); KdaMonEventQueuePush(\u0026amp;testEvent2); KdPrint((DRIVER_TAG \u0026#34; [TEST]: Queue count after 2 pushes = %lu\\n\u0026#34;, KdaMonEventQueueCount())); KDAMON_EVENT popped; while (KdaMonEventQueuePop(\u0026amp;popped)) { KdPrint((DRIVER_TAG \u0026#34; [TEST]: Popped event Id=%lu Type=%d\\n\u0026#34;, popped.Id, popped.Type)); } KdPrint((DRIVER_TAG \u0026#34; [TEST]: Queue count after pops = %lu\\n\u0026#34;, KdaMonEventQueueCount())); // --- END TEST QUEUE --- return STATUS_SUCCESS; } Le déroulé est simple :\nDeux événements sont poussés dans la file (un KdaMonEventProcess et un KdaMonEventNetwork) KdaMonEventQueueCount est appelée pour vérifier que la file en contient bien 2. Une boucle retire tous les événements un par un jusqu\u0026rsquo;à ce que KdaMonEventQueuePop retourne FALSE (file vide), en affichant à chaque fois l\u0026rsquo;Id et le Type de l\u0026rsquo;événement récupéré. On s\u0026rsquo;attend à récupérer d\u0026rsquo;abord l\u0026rsquo;événement Process (Id = 0), puis l\u0026rsquo;événement Network (Id = 1) — dans l\u0026rsquo;ordre exact où ils ont été ajoutés, confirmant le comportement FIFO de la file. Enfin, KdaMonEventQueueCount est appelée une dernière fois pour vérifier que la file est bien revenue à 0.\nVoici une démonstration de ce test en exécution :\nConclusion # KDAMonitor dispose maintenant d\u0026rsquo;une structure d\u0026rsquo;événement générique (KDAMON_EVENT) et d\u0026rsquo;une file circulaire capable de la stocker, protégée par un spinlock compatible avec n\u0026rsquo;importe quel IRQL jusqu\u0026rsquo;à DISPATCH_LEVEL.\nLe compteur DroppedEvents, qui suit le nombre d\u0026rsquo;événements rejetés faute de place dans la file, est bien incrémenté mais n\u0026rsquo;est encore ni exposé ni consulté nulle part.\nLa prochaine version (v0.4) viendra donner une utilité concrète à cette file : un thread dédié viendra la vider automatiquement vers un fichier de log. Une fois cette étape posée, les versions suivantes (v0.5 et au-delà) pourront enfin commencer à remplir la file avec les callbacks/callouts noyau (création de processus, chargement d\u0026rsquo;image, registre, réseau et thread).\nMerci d\u0026rsquo;avoir lu jusqu\u0026rsquo;au bout et à bientôt pour le prochain et quatrième article de cette série : Écriture des événements sur disque : journalisation JSONL, événements de réveil et premier crash.\n","date":"21 août 2026","externalUrl":null,"permalink":"/posts/03-event-queue/","section":"Blog","summary":"Construction de la file d’événements du driver KDAMonitor.","title":"03 - La file d'événements : structure et synchronisation dans le noyau","type":"posts"},{"content":"Bienvenue dans le deuxième article de la série sur le développement de KDAMonitor !\nDans cet article, je vais couvrir les versions v0.1 et v0.2 de ce projet. Pour rappel :\nv0.1 : Base du driver (DriverEntry) et un court exemple v0.2 : Ajout d\u0026rsquo;un device et communication IOCTL avec un exemple de client Voici les fichiers concernés par cet article, et la section qui les explique :\nFichier Rôle Section driver_entry.c Point d\u0026rsquo;entrée du driver, enregistrement des routines Qu\u0026rsquo;est-ce qu\u0026rsquo;un driver ? device.c Création du device et du lien symbolique Comment communiquer avec le driver ? ioctl.c Dispatch des IRP et traitement de l\u0026rsquo;IOCTL echo IOCTL kdamon_shared.h Code IOCTL et structures partagées driver/client Anatomie d\u0026rsquo;un code IOCTL kdamon_config.h Constantes centralisées (nom du device, du lien symbolique, tag de log) - client/src/client.c Client usermode de test, valide l\u0026rsquo;échange echo Exemple : le client de test Le projet peut être retrouvé dans ce dépôt : KDAMonitor.\nQu\u0026rsquo;est-ce qu\u0026rsquo;un driver ? # Un driver (ou pilote en bon français) est un programme qui permet au système d\u0026rsquo;exploitation de communiquer avec les composants d\u0026rsquo;une machine. Sans pilote, le système d\u0026rsquo;exploitation ne saurait pas comment communiquer avec la carte graphique, la carte réseau, le clavier, la souris, etc. Sous Windows, ces programmes possèdent l\u0026rsquo;extension .sys.\nNéanmoins, il existe des drivers qui ne servent pas qu\u0026rsquo;à la communication entre les composants et le système. En effet, on distingue plusieurs types de driver :\nles drivers matériels décrits ci-dessus les drivers logiciels qui sont des drivers qui ne dépendent pas d\u0026rsquo;un composant particulier Par exemple, KDAMonitor est un driver logiciel, il ne dépend pas d\u0026rsquo;un composant physique de la machine sur laquelle il est installé.\nOn peut alors se demander quel est l\u0026rsquo;intérêt d\u0026rsquo;un driver par rapport à un exécutable classique (.exe). L\u0026rsquo;avantage principal d\u0026rsquo;un driver est qu\u0026rsquo;il s\u0026rsquo;exécute en kernel space, ce qui lui permet d\u0026rsquo;avoir des privilèges bien plus élevés, de pouvoir accéder directement à la mémoire physique, au matériel, et de n\u0026rsquo;être protégé par (presque) aucune des barrières de sécurité qui isolent normalement les processus usermode entre eux. Voici un diagramme illustrant la communication entre les composants user-mode et kernel-mode :\nSource : Microsoft - User Mode and Kernel Mode\nConcrètement, pour KDAMonitor, ce niveau de privilège va nous permettre d\u0026rsquo;observer des événements système (création de processus, connexions réseau, etc.) qu\u0026rsquo;une application standard (en usermode) ne peut pas observer.\nCependant, bien qu\u0026rsquo;apportant beaucoup d\u0026rsquo;avantages, un driver vient avec quelques désagréments, notamment lorsqu\u0026rsquo;un crash se produit. Dans un exécutable classique, la plupart du temps, si le programme rencontre une erreur ou crash, le système continue sa vie. À l\u0026rsquo;inverse, une erreur dans un driver (crash, mauvais accès mémoire) peut faire planter tout le système (Blue Screen Of Death (BSOD)).\nDans un article prochain, j\u0026rsquo;expliquerai le premier problème que j\u0026rsquo;ai rencontré qui a fait crasher la VM de test :-)\nDe plus, contrairement à un exécutable où il suffit de cliquer sur le fichier pour le lancer, un driver nécessite plus d\u0026rsquo;étapes. En effet, un driver est chargé dynamiquement dans l\u0026rsquo;espace mémoire du noyau par le Windows I/O Manager, via le Service Control Manager (SCM).\nMaintenant que la notion de driver est plus claire, je vais commencer par montrer comment créer un driver, pour cela, il faut d\u0026rsquo;abord comprendre la structure du programme.\nDriverEntry et DriverUnload # DriverEntry est le point d\u0026rsquo;entrée d\u0026rsquo;un driver, c\u0026rsquo;est l\u0026rsquo;équivalent d\u0026rsquo;un main() mais avec certaines différences. La signature standard de DriverEntry est :\nNTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath); Voici une explication des arguments de la fonction :\nDriverObject : la structure représentant le driver dans le système RegistryPath : le chemin registre associé au driver Les annotations _In_ font partie du Source (Code) Annotation Language (SAL). Elles sont transparentes pour le compilateur, mais fournissent des métadonnées utiles pour les lecteurs humains et les outils d\u0026rsquo;analyse statique. Pour plus d\u0026rsquo;informations sur le SAL, voici la documentation Microsoft le concernant : Understanding SAL.\nDriverEntry DOIT retourner un NTSTATUS, cela peut prendre beaucoup de valeurs différentes, mais la plus importante est STATUS_SUCCESS. Si DriverEntry ne retourne pas STATUS_SUCCESS alors le chargement du driver échoue.\nVoir 2.3.1 NTSTATUS Values pour la liste des valeurs possibles de NTSTATUS.\nEt si on veut retirer proprement le driver ? Pour cela, on utilise la routine DriverUnload du DriverObject :\nDriverObject-\u0026gt;DriverUnload = ...; Cette routine est facultative, mais fortement recommandée afin que le driver soit proprement déchargé.\nExemple : afficher la version de Windows # Dans le livre de Pavel Yosifovich, Windows Kernel Programming, un exercice est proposé. En se basant sur le squelette suivant de DriverEntry, il faut faire en sorte que le driver affiche via KdPrint la version du système Windows (major, minor et build number) en utilisant la fonction RtlGetVersion :\n#include \u0026#34;driver.h\u0026#34; // DRIVER_TAG is defined in driver.h : #define DRIVER_TAG \u0026#34;[KDAMonitor]\u0026#34; void DriverUnload(_In_ PDRIVER_OBJECT DriverObject) { UNREFERENCED_PARAMETER(DriverObject); KdPrint((DRIVER_TAG \u0026#34; [SUCCESS]: Driver Unload called\\n\u0026#34;)); } NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) { UNREFERENCED_PARAMETER(RegistryPath); DriverObject-\u0026gt;DriverUnload = DriverUnload; KdPrint((DRIVER_TAG \u0026#34; [SUCCESS]: Initialized successfully\\n\u0026#34;)); return STATUS_SUCCESS; } Pour répondre à cet exercice, il faut utiliser la fonction RtlGetVersion, qui remplit une structure RTL_OSVERSIONINFOW contenant les informations de version recherchées. Un point important à ne pas oublier : le champ dwOSVersionInfoSize de cette structure doit être renseigné avant l\u0026rsquo;appel à RtlGetVersion, sans quoi la fonction échoue.\nVoici le DriverEntry complété avec cette logique, juste avant le return STATUS_SUCCESS :\nNTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) { UNREFERENCED_PARAMETER(RegistryPath); DriverObject-\u0026gt;DriverUnload = DriverUnload; KdPrint((DRIVER_TAG \u0026#34; [SUCCESS]: Initialized successfully\\n\u0026#34;)); RTL_OSVERSIONINFOW lpVersionInformation = { 0 }; lpVersionInformation.dwOSVersionInfoSize = sizeof(lpVersionInformation); NTSTATUS status = RtlGetVersion(\u0026amp;lpVersionInformation); if (NT_SUCCESS(status)) { KdPrint((DRIVER_TAG \u0026#34; [SUCCESS]: Windows %lu.%lu Build %lu\\n\u0026#34;, lpVersionInformation.dwMajorVersion, lpVersionInformation.dwMinorVersion, lpVersionInformation.dwBuildNumber )); } else { KdPrint((DRIVER_TAG \u0026#34; [ERROR]: Windows version not found\\n\u0026#34;)); } return STATUS_SUCCESS; } Pour plus d\u0026rsquo;info sur la fonction RtlGetVersion, voir la documentation microsoft : RtlGetVersion function (wdm.h).\nOn notera l\u0026rsquo;usage de la macro NT_SUCCESS, qui vérifie si un NTSTATUS représente un succès, cette macro sera utilisée tout au long de ce projet.\nEnfin, un dernier détail important : les messages envoyés via KdPrint ne s\u0026rsquo;affichent pas dans une console classique. Ils ne sont visibles qu\u0026rsquo;à travers un débuggeur kernel comme WinDbg, ou un outil comme DebugView (Sysinternals). Par défaut, KdPrint ne fonctionne d\u0026rsquo;ailleurs qu\u0026rsquo;en build debug. Pour le reste du projet, je vais utiliser DebugView.\nComment communiquer avec le driver ? # De par la séparation user/kernel qui existe dans les systèmes, le driver reste injoignable depuis l\u0026rsquo;espace utilisateur.\nPour l\u0026rsquo;instant, ce problème ne concerne pas notre driver, mais lorsqu\u0026rsquo;un client sera ajouté au projet il deviendra nécessaire de permettre au driver de communiquer avec ce client. Pour faire cela, nous avons besoin de créer un device.\nUn device est l\u0026rsquo;objet que le driver va exposer au reste du système à travers lequel les messages (entre driver et client) vont transiter.\nCes messages échangés entre le client et le driver prennent la forme d\u0026rsquo;I/O Request Packet (IRP), la structure de données standard que Windows utilise pour transmettre toute requête d\u0026rsquo;entrée/sortie à un driver. Chaque action (ouvrir le device, envoyer une commande, le fermer) génère un IRP différent, que le driver doit savoir traiter.\nDans le code, un device est une instance de la structure DEVICE_OBJECT et nous allons voir comment le créer.\nCréer un device avec IoCreateDevice # Pour créer un device, nous avons besoin de la fonction IoCreateDevice. Voici ses paramètres les plus importants :\nDriverObject : le driver auquel le device sera \u0026ldquo;attaché\u0026rdquo; DeviceName : le nom kernel du device (pour KDAMonitor, L\u0026quot;\\\\Device\\\\KDAMonitor\u0026quot;) DeviceType : le type de device, FILE_DEVICE_UNKNOWN dans notre cas, puisque KDAMonitor n\u0026rsquo;est lié à aucun matériel spécifique Exclusive : si TRUE, un seul client peut ouvrir un handle à la fois ; FALSE autorise plusieurs connexions simultanées DeviceObject : reçoit en sortie le DEVICE_OBJECT nouvellement créé Comme la plupart des fonctions kernel, elle retourne un NTSTATUS à vérifier.\nIoCreateDevice laisse le flag DO_DEVICE_INITIALIZING actif sur le device créé, ce qui empêche tout client de l\u0026rsquo;ouvrir. Il faut le retirer explicitement une fois l\u0026rsquo;initialisation terminée :\n(*DeviceObject)-\u0026gt;Flags \u0026amp;= ~DO_DEVICE_INITIALIZING; Le device doit enfin être détruit avec IoDeleteDevice quand il n\u0026rsquo;est plus utilisé, c\u0026rsquo;est le rôle de KdaMonDeleteDevice, appelé depuis DriverUnload.\nRendre le device accessible : IoCreateSymbolicLink # Même une fois créé, le device reste identifié uniquement par son nom kernel (\\Device\\KDAMonitor). Pour permettre à un client d\u0026rsquo;ouvrir ce device avec un simple CreateFileW, il faut faire le pont entre l\u0026rsquo;espace de noms kernel et l\u0026rsquo;espace de noms usermode. C\u0026rsquo;est le rôle de IoCreateSymbolicLink.\nCette fonction prend en paramètres :\nSymbolicLinkName : le nom accessible depuis l\u0026rsquo;espace utilisateur (par exemple \\DosDevices\\KDAMonitor, qu\u0026rsquo;un client ouvrira sous la forme \\\\.\\KDAMonitor) DeviceName : le nom kernel du device visé, celui donné précédemment à IoCreateDevice Elle retourne, comme d\u0026rsquo;habitude, un NTSTATUS à vérifier.\nMicrosoft précise que cette fonction n\u0026rsquo;est en principe pas recommandée pour les drivers WDM : un vrai driver WDM devrait exposer son device via IoRegisterDeviceInterface.\nSans ce lien symbolique, le device existerait bien en mémoire, mais resterait complètement injoignable depuis n\u0026rsquo;importe quel programme usermode.\nBuffered I/O vs Direct I/O # Cette section touche un peu à la structure IRP, présentée en détail dans la partie suivante. N\u0026rsquo;hésitez pas à la sauter pour lire la partie suivante.\nQuand un client envoie ou reçoit des données via le device, il faut bien que ces données transitent quelque part entre l\u0026rsquo;espace utilisateur et l\u0026rsquo;espace kernel. Windows propose plusieurs méthodes pour ça, et notre driver utilise le flag DO_BUFFERED_IO.\nAvec le Buffered I/O, le gestionnaire d\u0026rsquo;I/O (I/O Manager) alloue un buffer intermédiaire en mémoire kernel, copie les données du client vers ce buffer (ou l\u0026rsquo;inverse), puis rend ce buffer accessible au driver via Irp-\u0026gt;AssociatedIrp.SystemBuffer (un champ de la structure IRP que je détaillerai dans la partie suivante). Le driver n\u0026rsquo;accède donc jamais directement à la mémoire du client.\nL\u0026rsquo;alternative est le Direct I/O (DO_DIRECT_IO), qui utilise des Memory Descriptor Lists (MDL) pour laisser le driver accéder directement aux pages physiques du buffer client, sans copie intermédiaire. C\u0026rsquo;est plus rapide pour de gros volumes de données (car ça évite la copie), mais plus complexe à mettre en œuvre (un exemple est donné dans le livre de Pavel Yosifovich, Windows Kernel Programming, Chapitre 7).\nPour KDAMonitor, les échanges restent petits (un echo pour l\u0026rsquo;instant, des événements JSON plus tard), donc Buffered I/O est largement suffisant et bien plus simple à implémenter.\nLe tout assemblé : device.c # Voici à quoi ressemble KdaMonCreateDevice et KdaMonDeleteDevice une fois toutes ces briques réunies :\n#include \u0026#34;device.h\u0026#34; #include \u0026#34;kdamon_config.h\u0026#34; // KDAMON_DEVICE_NAME est défini dans le header kdamon_config.h : L\u0026#34;\\\\Device\\\\KDAMonitor\u0026#34; // KDAMON_SYMLINK_NAME est défini dans le header kdamon_config.h : L\u0026#34;\\\\DosDevices\\\\KDAMonitor\u0026#34; NTSTATUS KdaMonCreateDevice(_In_ PDRIVER_OBJECT DriverObject, _Outptr_ PDEVICE_OBJECT* DeviceObject) { UNICODE_STRING devName = RTL_CONSTANT_STRING(KDAMON_DEVICE_NAME); UNICODE_STRING symLink = RTL_CONSTANT_STRING(KDAMON_SYMLINK_NAME); NTSTATUS status = IoCreateDevice( DriverObject, 0, \u0026amp;devName, FILE_DEVICE_UNKNOWN, 0, FALSE, DeviceObject ); if (!NT_SUCCESS(status)) { KdPrint((DRIVER_TAG \u0026#34; [ERROR]: IoCreateDevice failed (0x%08X)\\n\u0026#34;, status)); return status; } (*DeviceObject)-\u0026gt;Flags |= DO_BUFFERED_IO; status = IoCreateSymbolicLink(\u0026amp;symLink, \u0026amp;devName); if (!NT_SUCCESS(status)) { KdPrint((DRIVER_TAG \u0026#34; [ERROR]: IoCreateSymbolicLink failed (0x%08X)\\n\u0026#34;, status)); IoDeleteDevice(*DeviceObject); *DeviceObject = NULL; return status; } (*DeviceObject)-\u0026gt;Flags \u0026amp;= ~DO_DEVICE_INITIALIZING; KdPrint((DRIVER_TAG \u0026#34; [SUCCESS]: Device object and symbolic link created\\n\u0026#34;)); return STATUS_SUCCESS; } void KdaMonDeleteDevice(_In_opt_ PDEVICE_OBJECT DeviceObject) { UNICODE_STRING symLink = RTL_CONSTANT_STRING(KDAMON_SYMLINK_NAME); IoDeleteSymbolicLink(\u0026amp;symLink); if (DeviceObject != NULL) { IoDeleteDevice(DeviceObject); } KdPrint((DRIVER_TAG \u0026#34; [SUCCESS]: Device object and symbolic link deleted\\n\u0026#34;)); } IOCTL # Un device ouvert ne suffit pas à lui seul : il faut encore un moyen pour qu\u0026rsquo;un client envoie une commande au driver, et que le driver y réponde. C\u0026rsquo;est le rôle des IOCTL (I/O Control), une requête générique qu\u0026rsquo;un programme usermode envoie à un driver via l\u0026rsquo;appel Win32 DeviceIoControl, en dehors des simples opérations de lecture/écriture classiques (ReadFile/WriteFile). C\u0026rsquo;est le mécanisme qui permet de définir des \u0026ldquo;commandes\u0026rdquo; propres à chaque driver. Dans notre cas, cela sera un simple echo pour commencer.\nQu\u0026rsquo;est-ce qu\u0026rsquo;un IRP ? # Chaque fois qu\u0026rsquo;un client interagit avec le device (l\u0026rsquo;ouvrir, envoyer une commande, le fermer), Windows encapsule cette requête dans une structure appelée I/O Request Packet (IRP).\nPour plus d\u0026rsquo;informations sur cette structure, voici la documentation officielle : IRP structure (wdm.h).\nC\u0026rsquo;est le gestionnaire d\u0026rsquo;I/O (I/O Manager) qui crée l\u0026rsquo;IRP et l\u0026rsquo;envoie au driver via la fonction IoCallDriver. Une fois la requête traitée, le driver signale sa complétion via IoCompleteRequest.\nUn IRP est toujours accompagné d\u0026rsquo;au moins une structure I/O Stack Location (IO_STACK_LOCATION), qui contient les paramètres propres à la requête (le code IOCTL demandé, la taille du buffer, etc.). Pour y accéder, le driver utilise la macro IoGetCurrentIrpStackLocation.\nC\u0026rsquo;est précisément dans l\u0026rsquo;IRP que réside le champ SystemBuffer mentionné dans la partie précédente : quand le code IOCTL utilise METHOD_BUFFERED, c\u0026rsquo;est via Irp-\u0026gt;AssociatedIrp.SystemBuffer que le driver accède aux données envoyées par le client.\nLe dispatch des IRP (CREATE, CLOSE, DEVICE_CONTROL) # Chaque IRP contient un code de fonction majeur (IRP_MJ_XXX), qui indique au driver quelle opération accomplir. Pour chaque code que le driver souhaite gérer, il doit enregistrer une routine de dispatch correspondante, autrement dit, une fonction appelée automatiquement par le système dès qu\u0026rsquo;un IRP portant ce code arrive. Toutes les routines de dispatch partagent la même signature :\nNTSTATUS DriverDispatch(PDEVICE_OBJECT DeviceObject, PIRP Irp); Cet enregistrement se fait dans DriverEntry, via le tableau DriverObject-\u0026gt;MajorFunction[...]. Dans le cas de KDAMonitor, il faut rajouter cela dans DriverEntry :\nDriverObject-\u0026gt;MajorFunction[IRP_MJ_CREATE] = KdaMonCreateClose; DriverObject-\u0026gt;MajorFunction[IRP_MJ_CLOSE] = KdaMonCreateClose; DriverObject-\u0026gt;MajorFunction[IRP_MJ_DEVICE_CONTROL] = KdaMonDeviceControl; IRP_MJ_CREATE correspond à un appel CreateFile côté client. La plupart des drivers se contentent de compléter l\u0026rsquo;IRP avec un statut de succès, ce qui est le cas de KDAMonitor à ce stade IRP_MJ_CLOSE est l\u0026rsquo;opposé, déclenché par CloseHandle IRP_MJ_DEVICE_CONTROL est le véritable point d\u0026rsquo;entrée de la communication : c\u0026rsquo;est via ce code que transitent toutes les requêtes DeviceIoControl du client, avec le code IOCTL demandé stocké dans l\u0026rsquo;IO_STACK_LOCATION de l\u0026rsquo;IRP Il y a d\u0026rsquo;autres routines de dispatch. Voici la documentation officielle : DRIVER_DISPATCH callback function (wdm.h).\nUne fois qu\u0026rsquo;une routine de dispatch décide de traiter un IRP, elle doit impérativement le compléter via IoCompleteRequest.\nAnatomie d\u0026rsquo;un code IOCTL # Un code IOCTL est simplement une valeur numérique qui identifie une commande précise auprès du driver. Le code en question n\u0026rsquo;est donc pas aléatoire, il doit être construit via la macro CTL_CODE, qui encode plusieurs informations dans un seul entier 32 bits :\n#define CTL_CODE(DeviceType, Function, Method, Access) \\ (((DeviceType) \u0026lt;\u0026lt; 16) | ((Access) \u0026lt;\u0026lt; 14) | ((Function) \u0026lt;\u0026lt; 2) | (Method)) DeviceType : le type de device visé. Les valeurs 0–32767 sont réservées à Microsoft, 32768 (0x8000) et au-delà sont libres pour les développeurs tiers, c\u0026rsquo;est justement la valeur utilisée par KDAMON_DEVICE_TYPE Function : le code interne de l\u0026rsquo;opération demandée. Les valeurs 0–2047 sont réservées à Microsoft, 2048 (0x800) et au-delà sont libres, encore une fois la valeur de départ choisie pour IOCTL_KDAMON_ECHO Method : la méthode de transfert des buffers, METHOD_BUFFERED, déjà vu dans la partie précédente, ou les variantes Direct I/O (METHOD_IN_DIRECT, METHOD_OUT_DIRECT), ou encore METHOD_NEITHER où le driver reçoit directement des pointeurs bruts et doit les valider lui-même Access : le niveau d\u0026rsquo;accès requis pour envoyer cet IOCTL, FILE_ANY_ACCESS dans notre cas, qui n\u0026rsquo;impose aucune restriction particulière Voici comment ces éléments se combinent dans kdamon_shared.h pour définir notre premier IOCTL, un simple echo :\n#define KDAMON_DEVICE_TYPE 0x8000 #define IOCTL_KDAMON_ECHO CTL_CODE(KDAMON_DEVICE_TYPE, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS) Ce fichier définit aussi les structures de requête et réponse associées à cet IOCTL :\ntypedef struct _KDAMON_ECHO_REQUEST { ULONG Value; } KDAMON_ECHO_REQUEST, * PKDAMON_ECHO_REQUEST; typedef struct _KDAMON_ECHO_REPLY { ULONG Value; } KDAMON_ECHO_REPLY, * PKDAMON_ECHO_REPLY; Ce fichier kdamon_shared.h est destiné à être partagé entre le driver et le client afin de garantir que les deux côtés s\u0026rsquo;accordent sur le même code IOCTL et les mêmes structures de données.\nLe tout assemblé : ioctl.c # Voici comment tous ces éléments (dispatch IRP, IRP_MJ_CREATE/CLOSE/DEVICE_CONTROL, I/O Stack Location, SystemBuffer, code IOCTL) s\u0026rsquo;assemblent concrètement dans ioctl.c :\n#include \u0026#34;ioctl.h\u0026#34; #include \u0026#34;kdamon_shared.h\u0026#34; #include \u0026#34;kdamon_config.h\u0026#34; NTSTATUS KdaMonCreateClose(_In_ PDEVICE_OBJECT DeviceObject, _In_ PIRP Irp) { UNREFERENCED_PARAMETER(DeviceObject); Irp-\u0026gt;IoStatus.Status = STATUS_SUCCESS; Irp-\u0026gt;IoStatus.Information = 0; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_SUCCESS; } NTSTATUS KdaMonDeviceControl(_In_ PDEVICE_OBJECT DeviceObject, _In_ PIRP Irp) { UNREFERENCED_PARAMETER(DeviceObject); PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp); NTSTATUS status = STATUS_SUCCESS; ULONG_PTR information = 0; switch (stack-\u0026gt;Parameters.DeviceIoControl.IoControlCode) { case IOCTL_KDAMON_ECHO: { ULONG inLen = stack-\u0026gt;Parameters.DeviceIoControl.InputBufferLength; ULONG outLen = stack-\u0026gt;Parameters.DeviceIoControl.OutputBufferLength; if (inLen \u0026lt; sizeof(KDAMON_ECHO_REQUEST) || outLen \u0026lt; sizeof(KDAMON_ECHO_REPLY)) { status = STATUS_BUFFER_TOO_SMALL; break; } PKDAMON_ECHO_REQUEST request = (PKDAMON_ECHO_REQUEST)Irp-\u0026gt;AssociatedIrp.SystemBuffer; KDAMON_ECHO_REPLY reply; reply.Value = request-\u0026gt;Value; RtlCopyMemory(Irp-\u0026gt;AssociatedIrp.SystemBuffer, \u0026amp;reply, sizeof(reply)); information = sizeof(reply); break; } default: status = STATUS_INVALID_DEVICE_REQUEST; KdPrint((DRIVER_TAG \u0026#34; [ERROR]: Unknown IOCTL 0x%08X\\n\u0026#34;, stack-\u0026gt;Parameters.DeviceIoControl.IoControlCode)); break; } Irp-\u0026gt;IoStatus.Status = status; Irp-\u0026gt;IoStatus.Information = information; IoCompleteRequest(Irp, IO_NO_INCREMENT); return status; } Quelques points à noter sur ce code :\nKdaMonCreateClose gère à la fois IRP_MJ_CREATE et IRP_MJ_CLOSE KdaMonDeviceControl récupère d\u0026rsquo;abord l\u0026rsquo;IO_STACK_LOCATION courante via IoGetCurrentIrpStackLocation, pour accéder au code IOCTL demandé (stack-\u0026gt;Parameters.DeviceIoControl.IoControlCode) Avant de traiter la requête, on vérifie que les buffers d\u0026rsquo;entrée et de sortie sont assez grands On accède aux données envoyées par le client via Irp-\u0026gt;AssociatedIrp.SystemBuffer Comme Buffered I/O utilise le même buffer pour l\u0026rsquo;entrée et la sortie, on écrit directement la réponse par-dessus la requête reçue avec RtlCopyMemory Si le code IOCTL reçu ne correspond à rien de connu, on retourne STATUS_INVALID_DEVICE_REQUEST Dans tous les cas, la requête est complétée avec IoCompleteRequest, comme vu dans la partie sur le dispatch Exemple : le client de test # Ce client est volontairement minimal. Son seul but est de valider que la chaîne complète fonctionne : ouverture du device, envoi d\u0026rsquo;une requête IOCTL, réception de la réponse. Ce n\u0026rsquo;est pas le client final du projet, qui sera développé en détail dans l\u0026rsquo;article 12 (v0.12).\n#include \u0026lt;windows.h\u0026gt; #include \u0026lt;stdio.h\u0026gt; #include \u0026#34;..\\include\\client.h\u0026#34; #include \u0026#34;..\\..\\driver\\include\\kdamon_shared.h\u0026#34; // CLIENT_TAG is defined in client.h as : \u0026#34;[KDAMonitor-Client]\u0026#34; int Error(const char* message) { printf(CLIENT_TAG \u0026#34; [ERROR]: %s (error=%lu)\\n\u0026#34;, message, GetLastError()); return 1; } int main(int argc, const char* argv[]) { HANDLE hDevice = CreateFileW( L\u0026#34;\\\\\\\\.\\\\KDAMonitor\u0026#34;, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL ); if (hDevice == INVALID_HANDLE_VALUE) { return Error(\u0026#34;Failed to open device\u0026#34;); } printf(CLIENT_TAG \u0026#34; [SUCCESS]: Device opened successfully\\n\u0026#34;); KDAMON_ECHO_REQUEST request; request.Value = 42; KDAMON_ECHO_REPLY reply; DWORD bytesReturned = 0; BOOL success = DeviceIoControl( hDevice, IOCTL_KDAMON_ECHO, \u0026amp;request, sizeof(request), \u0026amp;reply, sizeof(reply), \u0026amp;bytesReturned, NULL ); if (!success) { CloseHandle(hDevice); return Error(\u0026#34;DeviceIoControl failed\u0026#34;); } printf(CLIENT_TAG \u0026#34; [INFO]: Sent %lu, received %lu (bytes returned: %lu)\\n\u0026#34;, request.Value, reply.Value, bytesReturned); if (reply.Value == request.Value) { printf(CLIENT_TAG \u0026#34; [SUCCESS]: Echo matches\\n\u0026#34;); } else { printf(CLIENT_TAG \u0026#34; [ERROR]: Echo mismatch\\n\u0026#34;); } CloseHandle(hDevice); return 0; } Le déroulement est simple :\nOuverture du device via CreateFileW sur \\\\.\\KDAMonitor, c\u0026rsquo;est ici que le lien symbolique créé dans device.c est utilisé et que l\u0026rsquo;IRP IRP_MJ_CREATE est déclenché côté driver Préparation de la requête : une structure KDAMON_ECHO_REQUEST avec une valeur arbitraire (42) Envoi via DeviceIoControl, avec le code IOCTL_KDAMON_ECHO, ce qui déclenche IRP_MJ_DEVICE_CONTROL côté driver, et fait entrer en jeu tout le mécanisme de dispatch vu précédemment Vérification : si reply.Value correspond bien à la valeur envoyée, l\u0026rsquo;aller-retour usermode -\u0026gt; kernel -\u0026gt; usermode a fonctionné de bout en bout Fermeture du handle via CloseHandle, qui déclenche IRP_MJ_CLOSE Ce petit programme suffit à valider toute la mécanique construite dans cet article : device, lien symbolique, dispatch IRP, et traitement d\u0026rsquo;un IOCTL. Ci-dessous, un gif démontrant la fonctionnalité du projet :\nPoint d\u0026rsquo;attention : le driver est compilé en configuration Debug (pour que les KdPrint fonctionnent), mais le client, lui, est compilé en Release. En Debug, le client compile correctement mais ne se lance pas (certaines DLL sont introuvables au démarrage). Le compiler en Release contourne le problème.\nConclusion # Avec ces deux premières versions, KDAMonitor dispose maintenant du strict nécessaire pour exister en tant que driver : un point d\u0026rsquo;entrée (DriverEntry), un device accessible depuis l\u0026rsquo;espace utilisateur, et un premier échange IOCTL fonctionnel.\nCet article est plus long que prévu, il sera peut-être raccourci ultérieurement.\nLes prochains articles seront moins longs :-)\nMerci d\u0026rsquo;avoir lu jusqu\u0026rsquo;au bout et à bientôt pour le prochain et troisième article de cette série : La file d\u0026rsquo;événements : structure et synchronisation dans le noyau.\n","date":"11 août 2026","externalUrl":null,"permalink":"/posts/02-driver-foundation/","section":"Blog","summary":"Construction du socle du driver KDAMonitor : device object et première communication IOCTL avec un client de test.","title":"02 - Fondations du driver : DriverEntry, device et communication IOCTL","type":"posts"},{"content":"Bienvenue dans cette première série de mon blog ! Elle va servir à la fois d\u0026rsquo;expérimentation pour mes prochaines séries d\u0026rsquo;articles et de journal de développement de KDAMonitor.\nLe projet peut être retrouvé dans ce dépôt : KDAMonitor.\nEn quoi consiste KDAMonitor ? # KDAMonitor est l\u0026rsquo;abréviation de Kernel Driver Activity Monitor; le but de ce projet est de faire ce que fait globalement Sysmon de Microsoft. Ce projet sera constitué de deux composants :\nLe driver qui va récupérer des événements, les logger dans un fichier .jsonl et les envoyer au client. Voici la liste des événements gérés par le driver : création/destruction de processus chargement d\u0026rsquo;image connexion réseau modification/création/suppression de registres création/suppression de threads Le client qui sera une interface (console dans un premier temps) pour l\u0026rsquo;utilisateur afin qu\u0026rsquo;il puisse constater en temps réel les événements Le choix de ces événements est en partie arbitraire, mais il correspond aussi aux bases de ce qu\u0026rsquo;on regarde en analyse malware : quel processus a été lancé, quelles DLL ont été chargées, avec qui le processus communique sur le réseau, etc.\nPourquoi ce projet ? # Il y a deux raisons principales pour lesquelles j\u0026rsquo;ai décidé de faire ce projet et pas un autre :\nApprendre à développer un driver pour le noyau Windows Développer un outil utile en analyse malware que je peux réutiliser de mon côté (bien que moins bien que Sysmon) Ce projet est issu d\u0026rsquo;une simple envie d\u0026rsquo;apprendre et de découvrir quelque chose. Je m\u0026rsquo;étais dit que pour surveiller une activité système, quoi de mieux qu\u0026rsquo;un driver noyau qui a la capacité de tout regarder ?\nÀ noter que, lors de l\u0026rsquo;écriture de cet article, je suis déjà à la version 0.7 du projet et que donc j\u0026rsquo;ai déjà un peu de recul sur celui-ci.\nSans plus tarder, nous allons passer à l\u0026rsquo;idée de l\u0026rsquo;architecture que je me suis faite de ce projet au début.\nArchitecture prévue # À l\u0026rsquo;issue de ce projet (en v1.0), l\u0026rsquo;architecture de ce dernier ressemblera à ceci :\nEn résumé :\nUn événement se produit (processus, image, connexion réseau, etc.). Un capteur (callbacks ou callouts) va capturer cet événement. Le capteur concerné va aussi ajouter cet événement à la file. L\u0026rsquo;événement est retiré de la file et distribué à deux destinations : le journaliseur (log writer), qui l\u0026rsquo;écrit dans le fichier .jsonl le client, pour affichage en temps réel Technologies utilisées # Langage C IDE / Build Visual Studio 2026 Modèle de driver WDM Environnement de test VM Windows 11 (VirtualBox), test signing désactivé Les choix de technologie seront expliqués tout au long de la série.\nPrérequis # Afin de correctement suivre cette série, je conseille au lecteur d\u0026rsquo;avoir une connaissance basique du C et du fonctionnement interne du noyau Windows. J\u0026rsquo;apprends en même temps que vous :-), donc j\u0026rsquo;essaierai de rendre les articles les plus clairs possible.\nPlan détaillé de développement # Ci-dessous, un tableau représentant un plan détaillé de chaque release du projet :\nVersion Fichier(s) concerné(s) Fonctionnalité v0.1 driver_entry.c Squelette du driver (chargement/déchargement) v0.2 device.c, ioctl.c Device + IOCTL v0.3 event_queue.c File d’événements noyau v0.4 log_writer.c Journalisation v0.5 process_callback.c Surveillance de la création/fermeture des processus v0.6 image_callback.c Surveillance du chargement des images/DLL v0.7 wfp_session.c Mise en place de la session WFP v0.8 wfp_callout.c Surveillance des connexions réseau v0.9 registry_callback.c Surveillance de l’activité du registre v0.10 thread_callback.c Surveillance de la création/fermeture des threads v0.11 - Nettoyage de la structure du projet v0.12 client/ Client en mode utilisateur v1.0 - Stabilisation + publication Programme des articles à venir # Vous pouvez ignorer cette partie si vous souhaitez découvrir les articles au fur et à mesure que je les écris. Il est aussi possible que ce programme évolue au fil du développement.\nCet article sert d\u0026rsquo;introduction à la série; je vais maintenant détailler brièvement le contenu des prochains articles. Voici dans l\u0026rsquo;ordre les articles qui vont sortir ainsi qu\u0026rsquo;un court descriptif de ce qu\u0026rsquo;ils contiendront :\nArticle Version(s) Titre Contenu principal 02 v0.1 – v0.2 Fondations du driver : DriverEntry, device et communication IOCTL Création du squelette du driver, DriverEntry/DriverUnload, DEVICE_OBJECT, lien symbolique, IOCTL et premier client usermode de test. 03 v0.3 La file d\u0026rsquo;événements : structure et synchronisation dans le noyau Conception de la structure d\u0026rsquo;événement générique, ring buffer, spinlock, file d\u0026rsquo;attente, FIFO, identifiants uniques et premier test interne. 04 v0.4 Écriture des événements sur disque : journalisation JSONL, événements de réveil et premier crash Implémentation du thread de journalisation, création des fichiers JSONL, utilisation des KEVENT pour supprimer le polling, premier crash (IRQL) et sa résolution. 05 v0.5 Premier capteur : surveillance de la création et de la terminaison des processus Utilisation de PsSetCreateProcessNotifyRoutineEx, intégration dans le pipeline d\u0026rsquo;événements, sérialisation JSON et premiers événements réels. 06 v0.6 Deuxième capteur : suivi du chargement des images et des DLL Ajout de PsSetLoadImageNotifyRoutine, récupération des informations sur les DLL/EXE chargés, intégration au système existant. 07 v0.7 Préparation de la surveillance réseau : mise en place de la session WFP Présentation de la Windows Filtering Platform, ouverture de la session WFP, création du fournisseur/sous-couche, second crash rencontré et corrections apportées. 08 v0.8 Surveillance des connexions réseau avec la Windows Filtering Platform Développement du callout WFP, interception des connexions réseau sortantes, collecte des PID, adresses IP, ports et protocoles. 09 v0.9 Surveillance de l\u0026rsquo;activité du registre Mise en œuvre des callbacks registre (CmRegisterCallbackEx), surveillance des créations, modifications et suppressions de clés/valeurs. 10 v0.10 Surveillance de la création et de la terminaison des threads Ajout du callback thread (PsSetCreateThreadNotifyRoutine), collecte des événements de création et de terminaison des threads. 11 v0.11 Refactorisation de KDAMonitor : organisation du code pour la scalabilité Réorganisation de l\u0026rsquo;arborescence du projet, séparation des composants, amélioration de la maintenabilité et préparation à l\u0026rsquo;évolution du projet. 12 v0.12 Création d\u0026rsquo;un client usermode pour la surveillance des événements en temps réel Développement d\u0026rsquo;un client console communiquant avec le driver via les IOCTL afin d\u0026rsquo;afficher les événements en temps réel. 13 v1.0 KDAMonitor v1.0 : stabilisation, validation et retour d\u0026rsquo;expérience Validation sur des échantillons réels en VM, performances, limites du projet, documentation finale, bilan du développement et perspectives d\u0026rsquo;évolution. Merci d\u0026rsquo;avance à toutes celles et ceux qui suivront cette série. Si vous avez des questions, des retours ou simplement envie d\u0026rsquo;échanger sur le projet, n\u0026rsquo;hésitez pas à me contacter par mail ou sur LinkedIn.\nÀ bientôt pour le prochain article où l\u0026rsquo;on commencera vraiment à développer le driver !\n","date":"7 août 2026","externalUrl":null,"permalink":"/posts/01-introduction-kdamonitor/","section":"Blog","summary":"Introduction au projet KDAMonitor et ses objectifs.","title":"01 - Introduction à KDAMonitor : développer un driver Windows Kernel","type":"posts"},{"content":"","date":"24 juillet 2026","externalUrl":null,"permalink":"/tags/driver/","section":"Tags","summary":"","title":"Driver","type":"tags"},{"content":"","date":"24 juillet 2026","externalUrl":"https://github.com/HalfTimeOfLife/KDAMonitor","permalink":"/projects/kdamonitor/","section":"Projets","summary":"Driver noyau Windows pour l’analyse de malwares, journalisant en temps réel l’activité processus, chargement d’images, réseau et registre.","title":"KDAMonitor","type":"projects"},{"content":"","date":"16 juillet 2026","externalUrl":null,"permalink":"/tags/misp/","section":"Tags","summary":"","title":"MISP","type":"tags"},{"content":"","date":"16 juillet 2026","externalUrl":"https://github.com/HalfTimeOfLife/mispSK","permalink":"/projects/mispsk/","section":"Projets","summary":"Collection de scripts Python automatisant des opérations sur des instances MISP via PyMISP.","title":"mispSK","type":"projects"},{"content":"","date":"16 juillet 2026","externalUrl":null,"permalink":"/tags/threat-intelligence/","section":"Tags","summary":"","title":"Threat Intelligence","type":"tags"},{"content":"","date":"25 juin 2026","externalUrl":"https://github.com/HalfTimeOfLife/attackmap","permalink":"/projects/attackmap/","section":"Projets","summary":"Outil CLI générant des heatmaps MITRE ATT\u0026CK depuis des couches JSON ATT\u0026CK Navigator.","title":"attackmap","type":"projects"},{"content":"","date":"25 juin 2026","externalUrl":null,"permalink":"/tags/cti/","section":"Tags","summary":"","title":"CTI","type":"tags"},{"content":"","date":"25 juin 2026","externalUrl":null,"permalink":"/tags/mitre-attck/","section":"Tags","summary":"","title":"MITRE ATT\u0026CK","type":"tags"},{"content":"","date":"25 juin 2026","externalUrl":null,"permalink":"/tags/visualization/","section":"Tags","summary":"","title":"Visualization","type":"tags"},{"content":"","date":"19 juin 2026","externalUrl":"https://github.com/HalfTimeOfLife/scowl","permalink":"/projects/scowl/","section":"Projets","summary":"Bot Discord de triage et d’analyse statique de fichiers.","title":"scOWL","type":"projects"},{"content":"","date":"19 juin 2026","externalUrl":null,"permalink":"/tags/static-analysis/","section":"Tags","summary":"","title":"Static Analysis","type":"tags"},{"content":"","date":"18 février 2026","externalUrl":"https://github.com/HalfTimeOfLife/GhidraMAT","permalink":"/projects/ghidramat/","section":"Projets","summary":"Framework de scripts Ghidra pour la détection statique automatisée de comportements malveillants : anti-debug, anti-VM, packing, indicateurs C2, injection de processus, persistance et contournement des défenses.","title":"GhidraMAT","type":"projects"},{"content":"","date":"2 janvier 2026","externalUrl":null,"permalink":"/tags/automation/","section":"Tags","summary":"","title":"Automation","type":"tags"},{"content":"","date":"2 janvier 2026","externalUrl":null,"permalink":"/tags/dynamic-analysis/","section":"Tags","summary":"","title":"Dynamic Analysis","type":"tags"},{"content":"","date":"2 janvier 2026","externalUrl":"https://github.com/HalfTimeOfLife/malauto-windbg","permalink":"/projects/malauto/","section":"Projets","summary":"Extension WinDbg pour l’analyse automatisée de malwares Windows. Prépare des breakpoints, dumpe les régions mémoire au format PE et génère des rapports structurés.","title":"Malauto","type":"projects"},{"content":"","date":"2 janvier 2026","externalUrl":null,"permalink":"/tags/windbg/","section":"Tags","summary":"","title":"WinDbg","type":"tags"},{"content":"","date":"29 août 2025","externalUrl":null,"permalink":"/tags/anti-vm/","section":"Tags","summary":"","title":"Anti-VM","type":"tags"},{"content":"","date":"29 août 2025","externalUrl":null,"permalink":"/tags/article/","section":"Tags","summary":"","title":"Article","type":"tags"},{"content":"","date":"29 août 2025","externalUrl":null,"permalink":"/tags/eshard/","section":"Tags","summary":"","title":"EShard","type":"tags"},{"content":"Mémoires, articles et ressources publiques.\n","date":"29 août 2025","externalUrl":null,"permalink":"/publications/","section":"Publications","summary":"","title":"Publications","type":"publications"},{"content":"","date":"29 août 2025","externalUrl":"https://eshard.com/posts/windows-anti-vm-detection-bypass","permalink":"/publications/anti-emulation-article/","section":"Publications","summary":"Article expliquant les principales techniques de détection d’émulation utilisées par les malwares Windows, ainsi que des stratégies de contournement.","title":"Techniques anti-émulation sur Windows","type":"publications"},{"content":"","date":"22 août 2025","externalUrl":null,"permalink":"/tags/m%C3%A9moire/","section":"Tags","summary":"","title":"Mémoire","type":"tags"},{"content":"","date":"22 août 2025","externalUrl":"https://halftimeoflife.github.io/assets/publications/master-thesis-anti-emulation.pdf","permalink":"/publications/master-thesis-anti-vm/","section":"Publications","summary":"Recherche approfondie sur les techniques anti-émulation et leur contournement dans le contexte du Time Travel Debugging (TTD) en émulation système complète.","title":"Mémoire – Détection de protections Anti-VM utilisées par les malwares","type":"publications"},{"content":"","date":"22 août 2025","externalUrl":null,"permalink":"/tags/pdf/","section":"Tags","summary":"","title":"PDF","type":"tags"},{"content":"","date":"22 août 2025","externalUrl":null,"permalink":"/en/tags/thesis/","section":"Tags","summary":"","title":"Thesis","type":"tags"},{"content":"","date":"22 août 2025","externalUrl":null,"permalink":"/tags/ttd/","section":"Tags","summary":"","title":"TTD","type":"tags"},{"content":"","date":"18 juillet 2025","externalUrl":"https://github.com/HalfTimeOfLife/Analysis_wine_APT29_2025","permalink":"/projects/wine-apt29/","section":"Projets","summary":"Analyse du malware WINE associé au groupe APT29 en 2025, incluant une preuve de concept.","title":"Analyse WINE – APT29 (2025)","type":"projects"},{"content":"","date":"18 juillet 2025","externalUrl":null,"permalink":"/tags/apt29/","section":"Tags","summary":"","title":"APT29","type":"tags"},{"content":"","date":"6 juin 2025","externalUrl":null,"permalink":"/tags/dll-injection/","section":"Tags","summary":"","title":"DLL Injection","type":"tags"},{"content":"","date":"6 juin 2025","externalUrl":null,"permalink":"/tags/hooking/","section":"Tags","summary":"","title":"Hooking","type":"tags"},{"content":"","date":"6 juin 2025","externalUrl":"https://github.com/HalfTimeOfLife/Panoptiv","permalink":"/projects/panoptiv/","section":"Projets","summary":"DLL utilisant des techniques de hooking pour contourner des protections anti-VM.","title":"Panoptiv","type":"projects"},{"content":"","date":"28 mars 2025","externalUrl":null,"permalink":"/en/tags/educational/","section":"Tags","summary":"","title":"Educational","type":"tags"},{"content":"","date":"28 mars 2025","externalUrl":"https://www.youtube.com/playlist?list=PLM4pMV6TkMl3bdBKI0eIP6roX72TX72HT","permalink":"/publications/anti-vm-youtube/","section":"Publications","summary":"Série de vidéos pédagogiques présentant les mécanismes de détection de virtualisation et leurs implications dans l’analyse de malwares.","title":"Introduction aux protections anti-VM","type":"publications"},{"content":"","date":"28 mars 2025","externalUrl":null,"permalink":"/tags/vulgarisation/","section":"Tags","summary":"","title":"Vulgarisation","type":"tags"},{"content":"","date":"28 mars 2025","externalUrl":null,"permalink":"/tags/youtube/","section":"Tags","summary":"","title":"YouTube","type":"tags"},{"content":"","date":"15 janvier 2025","externalUrl":null,"permalink":"/tags/linux/","section":"Tags","summary":"","title":"Linux","type":"tags"},{"content":"","date":"15 janvier 2025","externalUrl":"https://halftimeoflife.github.io/assets/publications/linux-kernel-rootkit-2025.pdf","permalink":"/publications/linux-rootkit/","section":"Publications","summary":"Étude de l’état des rootkits Linux en 2025 à travers l’exemple du rootkit KoviD. Analyse des techniques de furtivité, persistance et contournement du noyau.","title":"Linux Kernel Rootkit en 2025","type":"publications"},{"content":"","date":"15 janvier 2025","externalUrl":null,"permalink":"/tags/rootkit/","section":"Tags","summary":"","title":"Rootkit","type":"tags"},{"content":"","date":"15 mai 2024","externalUrl":null,"permalink":"/tags/cryptographie/","section":"Tags","summary":"","title":"Cryptographie","type":"tags"},{"content":"","date":"15 mai 2024","externalUrl":null,"permalink":"/en/tags/cryptography/","section":"Tags","summary":"","title":"Cryptography","type":"tags"},{"content":"","date":"15 mai 2024","externalUrl":null,"permalink":"/tags/pir/","section":"Tags","summary":"","title":"PIR","type":"tags"},{"content":"","date":"15 mai 2024","externalUrl":"https://halftimeoflife.github.io/assets/publications/pir-paillier.pdf","permalink":"/publications/pir-paillier/","section":"Publications","summary":"Implémentation d’un protocole PIR (Private Information Retrieval) en C, basé sur le chiffrement homomorphe de Paillier pour l’interrogation anonyme de bases de données.","title":"Protocole d'interrogation anonyme de base de données","type":"publications"},{"content":" Diplômé d\u0026rsquo;un master en cryptologie et sécurité informatique, spécialisé en rétro-ingénierie et analyse de malwares. Télécharger mon CV Compétences techniques # Langages\nPython · C · x86/x64 Assembly · LaTeX Analyse statique\nGhidra · Binary Ninja Analyse dynamique\nGDB · WinDbg · Reven TTD Virtualisation\nQEMU · VMware · VirtualBox Pentest\nNmap · Burp Suite · Wireshark · Metasploit CTI\nYARA · MITRE ATT\u0026CK Systèmes\nLinux · Windows Expérience # Stage en reverse engineering — eShard Mars – Août 2025 Pessac, France Implémentation, détection et contournement de techniques anti-émulation pour Windows. Travail sur le Time Travel Debugging (TTD) Reven en émulation système complète. Implémentation de nombreuses techniques anti-VM (CPUID, RDTSC, Windows API, ...) en assembleur x86 et C Conception d'un pipeline de génération automatisée de binaires intégrant des techniques anti-VM, utilisé pour valider les détections Reverse engineering de binaires et d'échantillons malveillants pour identifier des marqueurs anti-émulation (Ghidra, Binary Ninja) Automatisation de la détection de comportements suspects via des scripts TTD (Python) Contournement des protections anti-VM par patch d'APIs Windows (WinDbg) et modification de configurations QEMU Création de contenu de vulgarisation : playlist YouTube et article technique publié sur le blog d'eShard Documentation des travaux sous Jupyter Notebook Formation # Master Cryptologie \u0026amp; Sécurité Informatique 2023 – 2025 Université de Bordeaux mastercsi.labri.fr SemestreMatières S7Arithmétique · Programmation · Calcul Formel · Algèbre linéaire · Systèmes d'exploitation S8Cryptologie · Sécurité des logiciels · Théorie de l'information · Théorie de la Complexité · Programmation des architectures parallèles S9Cryptanalyse · Post Quantum Cryptography · Cartes à puce · Sécurité des réseaux · Sécurité des systèmes S10Stage en entreprise Licence Mathématiques \u0026amp; Informatique 2020 – 2023 Université de Bordeaux Certifications # BTL1 – Blue Team Level 1 Juillet 2026 Security Blue Team Score : 85% - Silver Challenge Coin Phishing Analysis · SIEM · Digital Forensics · Incident Response · SOC Vérifier le certificat Télécharger le certificat CTF \u0026amp; Compétitions # Année Compétition Classement 2025 TUCTF 208e 2024 EKOPARTY CTF 18e 2024 HKCERT CTF 94e 2024 1337UP Live CTF 189e Contact # Email\nec.charbonnier@gmail.com GitHub\nHalfTimeOfLife LinkedIn\nelouan-charbonnier RootMe\nAzralt ","externalUrl":null,"permalink":"/about/","section":"Accueil","summary":"","title":"À propos","type":"page"},{"content":"","externalUrl":null,"permalink":"/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"},{"content":"","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"}]