Asuka Nakajima

Détection des keyloggers basés sur des raccourcis clavier à l'aide d'une structure de données du noyau non documentée

Dans cet article, nous examinons ce que sont les keyloggers basés sur des raccourcis clavier et comment les identifier. Plus précisément, nous expliquons comment ces keyloggers interceptent les frappes, puis nous présentons une technique de détection qui exploite une table de raccourcis clavier non documentée dans l'espace du noyau.

Detecting Hotkey-Based Keyloggers Using an Undocumented Kernel Data Structure

Détection des keyloggers basés sur des raccourcis clavier à l'aide d'une structure de données du noyau non documentée

Dans cet article, nous examinons ce que sont les keyloggers basés sur des raccourcis clavier et comment les identifier. Plus précisément, nous expliquons comment ces keyloggers interceptent les frappes, puis nous présentons une technique de détection qui exploite une table de raccourcis clavier non documentée dans l'espace du noyau.

Introduction

En mai 2024, Elastic Security Labs a publié un article présentant les nouvelles fonctionnalités intégrées dans Elastic Defend (à partir de la version 8.12) pour améliorer la détection des keyloggers exécutés sous Windows. Dans cet article, nous avons abordé quatre types de keyloggers couramment utilisés dans les cyberattaques : les keyloggers basés sur des sondages, les keyloggers basés sur des hooks, les keyloggers utilisant le modèle d'entrée brute et les keyloggers utilisant DirectInput, et nous avons expliqué notre méthodologie de détection. Nous avons notamment présenté une méthode de détection basée sur le comportement en utilisant le fournisseur Microsoft-Windows-Win32k dans l'''Event Tracing for Windows'' (ETW).

Peu après sa publication, nous avons eu l'honneur de voir notre article remarqué par Jonathan Bar Or, chercheur principal en sécurité chez Microsoft. Il a fourni de précieux commentaires en soulignant l'existence de keyloggers basés sur des raccourcis clavier et en partageant du code de démonstration de faisabilité (PoC) avec nous. En s'appuyant sur son code PoC Hotkeyz comme point de départ, cet article présente une méthode potentielle pour détecter les keyloggers basés sur des raccourcis clavier.

Aperçu des keyloggers basés sur des raccourcis clavier

Qu'est-ce qu'un raccourci clavier ?

Avant de nous pencher sur les keyloggers basés sur des raccourcis clavier, clarifions d'abord ce qu'est un raccourci clavier. Un raccourci clavier est un type de raccourci qui invoque directement une fonction spécifique sur un ordinateur en appuyant sur une seule touche ou une combinaison de touches. Par exemple, de nombreux utilisateurs de Windows appuient sur Alt + Tab pour passer d'une tâche à l'autre (ou, en d'autres termes, d'une fenêtre à l'autre). Dans ce cas, Alt + Tab sert de raccourci clavier qui active directement la fonction de basculement de tâche.

(Remarque : bien qu’il existe d’autres types de raccourcis clavier, cet article se concentre uniquement sur les hotkeys. De plus, toutes les informations contenues ici sont basées sur Windows 10 version 22H2 OS Build 19045.5371 sans sécurité basée sur la virtualisation. Veuillez noter que les structures de données internes et le comportement peuvent différer dans d'autres versions de Windows).

Utilisation abusive de la fonctionnalité d’enregistrement des raccourcis clavier personnalisés

En plus d'utiliser les raccourcis clavier préconfigurés dans Windows, comme indiqué dans l'exemple précédent, vous pouvez également enregistrer vos propres raccourcis clavier personnalisés. Il existe plusieurs méthodes pour y parvenir, mais une approche simple consiste à utiliser la fonction RegisterHotKey de l'API Windows, qui permet à un utilisateur d'enregistrer une touche spécifique en tant que raccourci clavier. Par exemple, l’extrait de code suivant montre comment utiliser l’API RegisterHotKey pour enregistrer la touche A (avec un code de touche virtuelle de 0x41) comme raccourci clavier global :

/*
BOOL RegisterHotKey(
[in, optional] HWND hWnd,
[in] int id,
[in] UINT fsModifiers,
[in] UINT vk
);
*/
RegisterHotKey(NULL, 1, 0, 0x41);

Après l'enregistrement d'un raccourci clavier, lorsque la touche enregistrée est pressée, un message WM_HOTKEY est envoyé à la file d'attente de messages de la fenêtre spécifiée comme premier argument de l'API RegisterHotKey (ou au thread qui a enregistré le raccourci clavier si NULL est utilisé). Le code ci-dessous illustre une boucle de messages qui utilise l'API GetMessage pour rechercher un message WM_HOTKEY dans la file d'attente des messages et qui, le cas échéant, extrait le code de la touche virtuelle (dans ce cas, 0x41) du message.

MSG msg = { 0 };
while (GetMessage(&msg, NULL, 0, 0)) {
if (msg.message == WM_HOTKEY) {
int vkCode = HIWORD(msg.lParam);
std::cout << "WM_HOTKEY received! Virtual-Key Code: 0x"
<< std::hex << vkCode << std::dec << std::endl;
}
}

En d'autres termes, imaginez que vous écrivez quelque chose dans une application de type bloc-notes. Si la touche A est enfoncée, le caractère ne sera pas traité comme une saisie de texte normale : il sera plutôt reconnu comme un raccourci clavier global.

Dans cet exemple, seule la touche A est enregistrée comme raccourci clavier. Cependant, vous pouvez enregistrer plusieurs touches (comme B, C ou D) comme raccourcis distincts simultanément. Cela signifie que toute touche (c'est-à-dire tout code de touche virtuelle) pouvant être enregistrée avec l'API RegisterHotKey peut potentiellement être détournée en tant que raccourci clavier global. Un keylogger basé sur les raccourcis clavier exploite cette capacité pour enregistrer les frappes saisies par l'utilisateur.

D'après nos tests, nous avons constaté que non seulement les touches alphanumériques et les touches de symbole de base, mais aussi ces touches, lorsqu'elles sont combinées avec le modificateur SHIFT, peuvent toutes être enregistrées comme raccourcis clavier à l'aide de l'API RegisterHotKey. Cela signifie qu'un keylogger peut monitorer efficacement chaque frappe nécessaire pour dérober des informations sensibles.

Capturer furtivement les frappes au clavier

Examinons le processus réel par lequel un keylogger basé sur des raccourcis clavier capture les frappes, en prenant l'exemple du keylogger basé sur les raccourcis clavier Hotkeyz.

Dans Hotkeyz, il enregistre d'abord chaque code de touche virtuelle alphanumérique, ainsi que quelques touches supplémentaires, telles que VK_SPACE et VK_RETURN, en tant que touches de raccourci individuelles à l'aide de l'API RegisterHotkey.

Ensuite, dans la boucle de messages du keylogger, l'API PeekMessageW est utilisée pour vérifier si des messages WM_HOTKEY provenant de ces raccourcis clavier enregistrés sont apparus dans la file d'attente des messages. Lorsqu'un message WM_HOTKEY est détecté, le code de la touche virtuelle qu'il contient est extrait et éventuellement sauvegardé dans un fichier texte. Vous trouverez ci-dessous un extrait du code de la boucle de messages, mettant en évidence les parties les plus importantes.

while (...)
{
// Get the message in a non-blocking manner and poll if necessary
if (!PeekMessageW(&tMsg, NULL, WM_HOTKEY, WM_HOTKEY, PM_REMOVE))
{
Sleep(POLL_TIME_MILLIS);
continue;
}
....
// Get the key from the message
cCurrVk = (BYTE)((((DWORD)tMsg.lParam) & 0xFFFF0000) >> 16);
// Send the key to the OS and re-register
(VOID)UnregisterHotKey(NULL, adwVkToIdMapping[cCurrVk]);
keybd_event(cCurrVk, 0, 0, (ULONG_PTR)NULL);
if (!RegisterHotKey(NULL, adwVkToIdMapping[cCurrVk], 0, cCurrVk))
{
adwVkToIdMapping[cCurrVk] = 0;
DEBUG_MSG(L"RegisterHotKey() failed for re-registration (cCurrVk=%lu, LastError=%lu).", cCurrVk, GetLastError());
goto lblCleanup;
}
// Write to the file
if (!WriteFile(hFile, &cCurrVk, sizeof(cCurrVk), &cbBytesWritten, NULL))
{
....

Détail important : pour éviter d'alerter l'utilisateur de la présence du keylogger, une fois que le code de la touche virtuelle est extrait du message, l'enregistrement du raccourci clavier de la touche est temporairement supprimé à l'aide de l'API UnregisterHotKey. Ensuite, l'appui sur la touche est simulé par keybd_event, de sorte que l'utilisateur a l'impression d'avoir appuyé sur la touche normalement. Une fois la pression sur la touche simulée, la touche est réenregistrée à l'aide de l'API RegisterHotKey pour attendre une nouvelle saisie. C'est le mécanisme de base d'un keylogger basé sur les raccourcis clavier.

Détection des keyloggers basés sur des raccourcis clavier

Maintenant que nous comprenons ce que sont les keyloggers basés sur des raccourcis clavier et comment ils fonctionnent, expliquons comment les détecter.

L'ETW ne monitore pas l'API RegisterHotKey

En suivant l'approche décrite dans un article précédent, nous avons d'abord cherché à savoir si l'Event Tracing for Windows (ETW) pouvait être utilisé pour détecter les keyloggers basés sur des raccourcis clavier. Nos recherches ont rapidement révélé que l'ETW ne monitore actuellement pas les API RegisterHotKey ou UnregisterHotKey. En plus d'examiner le fichier manifeste du fournisseur Microsoft-Windows-Win32k, nous avons procédé à une rétro-ingénierie des composants internes de l'API RegisterHotKey, en particulier de la fonction NtUserRegisterHotKey dans win32kfull.sys. Malheureusement, nous n'avons trouvé aucune preuve que ces API déclenchent des événements ETW lors de leur exécution.

L’image ci-dessous montre une comparaison entre le code décompilé de NtUserGetAsyncKeyState (qui est monitoré par l'ETW) et NtUserRegisterHotKey. Notez qu’au début de NtUserGetAsyncKeyState, il y a un appel à EtwTraceGetAsyncKeyState, une fonction associée au logging des événements ETW, tandis que NtUserRegisterHotKey ne contient pas d'appel de ce type.

Figure 1: Comparison of the Decompiled Code for NtUserGetAsyncKeyState and NtUserRegisterHotKey
Figure 1 : comparaison du code décompilé pour NtUserGetAsyncKeyState et NtUserRegisterHotKey
 
Bien que nous ayons également envisagé de faire appel à des fournisseurs ETW autres que Microsoft-Windows-Win32k pour monitorer indirectement les appels à l’API RegisterHotKey, nous avons constaté que la méthode de détection utilisant la ''table de raccourcis clavier'' (qui sera présentée ultérieurement et ne repose pas sur l'ETW) permet d’obtenir des résultats comparables, voire supérieurs, à ceux du suivi de l’API RegisterHotKey. Nous avons finalement choisi de mettre en œuvre cette méthode.

Détection à l'aide de la table de raccourcis clavier (gphkHashTable))

Après avoir découvert que l'ETW ne pouvait pas monitorer directement les appels à l'API RegisterHotKey, nous avons commencé à explorer des méthodes de détection qui ne reposent pas sur l'ETW. Au cours de notre enquête, nous nous sommes demandé si les informations relatives aux raccourcis clavier enregistrés n'étaient pas stockées quelque part, et si oui, si ces données pourraient être utilisées pour la détection. Sur la base de cette hypothèse, nous avons rapidement trouvé une table de hachage nommée gphkHashTable dans NtUserRegisterHotKey. Une recherche dans la documentation en ligne de Microsoft n'a révélé aucun détail sur gphkHashTable, ce qui suggère qu'il s'agit d'une structure de données du noyau non documentée.

Figure 2: The hotkey table (gphkHashTable), discovered within the RegisterHotKey function called inside NtUserRegisterHotKey
Figure 2 : la table de raccourcis clavier (gphkHashTable), découverte dans la fonction RegisterHotKey appelée dans NtUserRegisterHotKey

Grâce à la rétro-ingénierie, nous avons découvert que cette table de hachage stocke des objets contenant des informations sur les raccourcis clavier enregistrés. Chaque objet contient des détails tels que le code de touche virtuelle et les modificateurs spécifiés dans les arguments de l'API RegisterHotKey. Le côté droit de la Figure 3 montre une partie de la définition de la structure d'un objet de raccourci (nommé HOT_KEY), tandis que le côté gauche montre comment apparaissent les objets de raccourci enregistrés lorsqu'ils sont accessibles via WinDbg.

Figure 3: Hotkey Object Details. WinDbg view (left) and HOT_KEY structure details (right)
Figure 3 : détails de l'objet de raccourci clavier. Aperçu de WinDbg (à gauche) et détails de la structure HOT_KEY (à droite)

Nous avons également déterminé que ghpkHashTable est structuré comme le montre la Figure 4. Plus précisément, il utilise le résultat de l'opération modulo (avec 0x80) sur le code de touche virtuelle (spécifié par l'API RegisterHotkey) comme index dans la table de hachage. Les objets de raccourci clavier partageant le même index sont liés ensemble dans une liste, ce qui permet à la table de stocker et de gérer les informations des raccourcis clavier même lorsque les codes de touches virtuelles sont identiques mais que les modificateurs diffèrent.

Figure 4: Structure of gphkHashTable
Figure 4 : Structure de gphkHashTable

En d'autres termes, en analysant tous les objets HOT_KEY stockés dans ghpkHashTable, nous pouvons obtenir des informations sur chaque raccourci clavier enregistré. Si nous constatons que chaque touche principale (par exemple, chaque touche alphanumérique individuelle) est enregistrée comme un raccourci clavier distinct, cela indique fortement la présence d'un keylogger actif basé sur les raccourcis clavier.

Mise en œuvre de l'outil de détection

Passons maintenant à la mise en œuvre de l’outil de détection. Étant donné que gphkHashTable réside dans l'espace noyau, il est inaccessible pour une application en mode utilisateur. C'est pourquoi il était nécessaire de développer un pilote de périphérique pour la détection. Plus précisément, nous avons décidé de développer un pilote de périphérique qui obtient l'adresse de gphkHashTable et analyse tous les objets de raccourci clavier stockés dans la table de hachage. Si le nombre de touches alphanumériques enregistrées en tant que raccourcis clavier dépasse un seuil prédéfini, cela nous alertera de la présence potentielle d'un keylogger basé sur les raccourcis clavier.

Comment obtenir l'adresse de gphkHashTable

Lors du développement de l'outil de détection, l'un des premiers défis auxquels nous avons été confrontés était d'obtenir l'adresse de gphkHashTable. Après mûre réflexion, nous avons décidé d'extraire l'adresse directement à partir d'une instruction du pilote win32kfull.sys qui accède à gphkHashTable.

Grâce à la rétro-ingénierie, nous avons découvert que dans la fonction IsHotKey (au tout début) il y a une instruction lea (lea rbx, gphkHashTable) qui accède à gphkHashTable. Nous avons utilisé la séquence d'octets de l'opcode (0x48, 0x8d, 0x1d) de cette instruction comme signature pour localiser la ligne correspondante, puis nous avons calculé l'adresse de gphkHashTable en utilisant le décalage de 32 bits (4 octets) obtenu.

Figure 5: Inside the IsHotKey function
Figure 5 : à l'intérieur de la fonction IsHotKey

De plus, comme IsHotKey n’est pas une fonction exportée, nous devons également connaître son adresse avant de rechercher gphkHashTable. En poursuivant la rétro-ingénierie, nous avons découvert que la fonction exportée EditionIsHotKey appelle la fonction IsHotKey. Nous avons donc décidé de calculer l'adresse de IsHotKey dans la fonction EditionIsHotKey en utilisant la même méthode que celle décrite précédemment. (Pour référence, l'adresse de base de win32kfull.sys peut être trouvée en utilisant l'API PsLoadedModuleList.)

Accès à l'espace mémoire de win32kfull.sys

Une fois que nous avons finalisé notre approche pour obtenir l'adresse de gphkHashTable, nous avons commencé à écrire le code pour accéder à l'espace mémoire de win32kfull.sys afin de récupérer cette adresse. L'une des difficultés rencontrées à ce stade était que win32kfull.sys est un pilote de session. Avant de poursuivre, voici une brève explication simplifiée de ce qu'est une session.

Sous Windows, lorsqu’un utilisateur se connecte, une session distincte (avec des numéros de session commençant à 1) est attribuée à chaque utilisateur. En d'autres termes, le premier utilisateur à se connecter se voit attribuer la Session 1. Si un autre utilisateur se connecte pendant que cette session est active, cet utilisateur se voit attribuer la Session 2, et ainsi de suite. Chaque utilisateur dispose alors de son propre environnement de bureau dans la session qui lui est attribuée.

Les données du noyau qui doivent être gérées séparément pour chaque session (c'est-à-dire par utilisateur connecté) sont stockées dans une zone isolée de la mémoire du noyau appelée espace de session. Cela inclut les objets de l'interface graphique gérés par les pilotes win32k, tels que les fenêtres et les données d'entrée de la souris et du clavier, garantissant que l'écran et les données d'entrée restent correctement séparés entre les utilisateurs.

(Ceci est une explication simplifiée. Pour une discussion plus détaillée sur les sessions, veuillez consulter l'article de blog de James Forshaw.).)

Figure 6: Overview of Sessions. Session 0 is dedicated exclusively to service processes
Figure 6 : aperçu des sessions. La session 0 est consacrée exclusivement aux processus de service

Sur la base de ce qui précède, win32kfull.sys est reconnu comme étant un pilote de session. Cela signifie que, par exemple, les informations de raccourci clavier enregistrées dans la session du premier utilisateur connecté (Session 1) ne sont accessibles qu'à partir de cette même session. Alors, comment contourner cette limitation ? Dans de tels cas, il est établi que KeStackAttachProcess peut être utilisé.

KeStackAttachProcess permet au thread actuel de s'attacher temporairement à l'espace d'adressage d'un processus spécifié. Si nous pouvons nous joindre un processus GUI dans la session cible (plus précisément, un processus qui a chargé win32kfull.sys), nous pouvons alors accéder à win32kfull.sys et à ses données associées dans cette session. Pour notre mise en œuvre, en supposant qu'un seul utilisateur est connecté, nous avons décidé de localiser et de joindre winlogon.exe, le processus chargé de gérer les opérations de connexion des utilisateurs.

Énumération des raccourcis clavier enregistrés

Une fois que nous avons réussi à joindre le processus winlogon.exe et déterminé l’adresse de gphkHashTable, l’étape suivante consiste simplement à analyser gphkHashTable pour vérifier les raccourcis clavier enregistrés. Vous trouverez ci-dessous un extrait de ce code :

BOOL CheckRegisteredHotKeys(_In_ const PVOID& gphkHashTableAddr)
{
-[skip]-
// Cast the gphkHashTable address to an array of pointers.
PVOID* tableArray = static_cast<PVOID*>(gphkHashTableAddr);
// Iterate through the hash table entries.
for (USHORT j = 0; j < 0x80; j++)
{
PVOID item = tableArray[j];
PHOT_KEY hk = reinterpret_cast<PHOT_KEY>(item);
if (hk)
{
CheckHotkeyNode(hk);
}
}
-[skip]-
}
VOID CheckHotkeyNode(_In_ const PHOT_KEY& hk)
{
if (MmIsAddressValid(hk->pNext)) {
CheckHotkeyNode(hk->pNext);
}
// Check whether this is a single numeric hotkey.
if ((hk->vk >= 0x30) && (hk->vk <= 0x39) && (hk->modifiers1 == 0))
{
KdPrint(("[+] hk->id: %u hk->vk: %x\n", hk->id, hk->vk));
hotkeyCounter++;
}
// Check whether this is a single alphabet hotkey.
else if ((hk->vk >= 0x41) && (hk->vk <= 0x5A) && (hk->modifiers1 == 0))
{
KdPrint(("[+] hk->id: %u hk->vk: %x\n", hk->id, hk->vk));
hotkeyCounter++;
}
-[skip]-
}
....
if (CheckRegisteredHotKeys(gphkHashTableAddr) && hotkeyCounter >= 36)
{
detected = TRUE;
goto Cleanup;
}

Le code en lui-même est simple : il parcourt chaque index de la table de hachage, suit la liste chaînée pour accéder à chaque objet HOT_KEY et vérifie si les raccourcis clavier enregistrés correspondent à des touches alphanumériques sans aucun modificateur. Dans notre outil de détection, si chaque touche alphanumérique est enregistrée comme raccourci clavier, une alerte est déclenchée, indiquant la présence possible d'un keylogger basé sur les raccourcis clavier. Pour des raisons de simplicité, cette implémentation ne cible que les raccourcis clavier alphanumériques, bien qu'il soit facile d'étendre l'outil pour vérifier la présence de raccourcis clavier à l'aide de modificateurs tels que SHIFT.

Détection de Hotkeyz

L'outil de détection (Hotkey-based Keylogger Detector) a été publié ci-dessous. Des instructions d'utilisation détaillées sont également fournies. En outre, cette recherche a été présentée à NULLCON Goa 2025, et les diapositives de présentation sont disponibles.

https://github.com/AsuNa-jp/HotkeybasedKeyloggerDetector

Vous trouverez ci-dessous une vidéo de démonstration montrant comment le keylogger basé sur les raccourcis clavier détecte Hotkeyz.

DEMO_VIDEO.mp4

Remerciements

Nous tenons à exprimer notre sincère gratitude à Jonathan Bar Or pour avoir lu notre article précédent, partagé ses connaissances sur les keyloggers basés sur des raccourcis clavier, et publié généreusement l'outil PoC Hotkeyz.

Partager cet article