Erkennung von Hotkey-basierten Keyloggern unter Verwendung einer nicht dokumentierten Kernel-Datenstruktur
In diesem Artikel untersuchen wir, was Hotkey-basierte Keylogger sind und wie man sie erkennen kann. Insbesondere erklären wir, wie diese Keylogger Tastatureingaben abfangen, und präsentieren dann eine Erkennungstechnik, die eine undokumentierte Hotkey-Tabelle im Kernel-Space nutzt.
Einführung
Im Mai 2024 veröffentlichte Elastic Security Labs einen Artikel, der die neuen Features in Elastic Defend (ab Version 8.12) hervorhebt, um die Erkennung von Keyloggern auf Windows-Systemen zu verbessern. In diesem Beitrag haben wir vier Arten von Keyloggern behandelt, die häufig bei Cyberangriffen eingesetzt werden – polling-basierte Keylogger, hooking-basierte Keylogger, Keylogger, die das Raw Input Model verwenden, und Keylogger, die DirectInput verwenden – und unsere Erkennungsmethodik erläutert. Insbesondere haben wir eine verhaltensbasierte Erkennungsmethode eingeführt, die den Microsoft-Windows-Win32k-Anbieter innerhalb von Event Tracing for Windows (ETW) verwendet.
Kurz nach der Veröffentlichung waren wir geehrt, dass Jonathan Bar Or, Principal Security Researcher bei Microsoft, auf unseren Artikel aufmerksam wurde. Er gab uns unschätzbares Feedback, indem er auf die Existenz von Hotkey-basierten Keyloggern hinwies und uns sogar Proof-of-Concept (PoC)-Code zur Verfügung stellte. Unter Verwendung seines PoC-Codes Hotkeyz als Ausgangspunkt präsentiert dieser Artikel eine mögliche Methode zur Erkennung von Hotkey-basierten Keyloggern.
Überblick über Hotkey-basierte Keylogger
Was ist ein Hotkey?
Bevor wir uns mit Hotkey-basierten Keyloggern befassen, sollten wir zunächst klären, was ein Hotkey ist. Ein Hotkey ist eine Art von Tastaturkürzel, das eine bestimmte Funktion auf einem Computer direkt durch das Drücken einer einzelnen Taste oder einer Tastenkombination aufruft. Zum Beispiel drücken viele Windows-Nutzer Alt + Tab, um zwischen Aufgaben (oder, mit anderen Worten, Fenstern) zu wechseln. In diesem Fall dient die Tastenkombination Alt + Tab als Hotkey, der direkt die Funktion zum Wechseln zwischen Aufgaben auslöst.
(Hinweis: Obwohl es auch andere Arten von Tastenkombinationen gibt, konzentriert sich dieser Artikel ausschließlich auf Hotkeys.) Außerdem basieren alle hierin enthaltenen Informationen auf Windows 10 Version 22H2 OS Build 19045.5371 ohne virtualisierungsbasierte Sicherheit. Bitte beachten Sie, dass die internen Datenstrukturen und das Verhalten in anderen Versionen von Windows abweichen können.
Missbrauch der benutzerdefinierten Hotkey-Registrierungsfunktionalität
Zusätzlich zur Verwendung der vorkonfigurierten Hotkeys in Windows, wie im vorherigen Beispiel gezeigt, können Sie auch Ihre eigenen benutzerdefinierten Hotkeys registrieren. Es gibt verschiedene Methoden, dies zu tun, aber ein einfacher Ansatz ist die Verwendung der Windows API-Funktion RegisterHotKey, die es einem Nutzer ermöglicht, eine bestimmte Taste als Hotkey zu registrieren. Zum Beispiel zeigt der folgende Codeausschnitt, wie die RegisterHotKey-API verwendet wird, um die A-Taste (mit einem virtuellen Schlüsselcode von 0x41) als globalen Hotkey zu registrieren:
/*
BOOL RegisterHotKey(
[in, optional] HWND hWnd,
[in] int id,
[in] UINT fsModifiers,
[in] UINT vk
);
*/
RegisterHotKey(NULL, 1, 0, 0x41);
Nach der Registrierung eines Hotkeys wird beim Drücken der registrierten Taste eine WM_HOTKEY-Nachricht an die Nachrichtenwarteschlange des Fensters gesendet, das als erstes Argument der RegisterHotKey-API angegeben wurde (oder an den Thread, der den Hotkey registriert hat, wenn NULL verwendet wird). Der folgende Code demonstriert eine Nachrichtenschleife, die die GetMessage-API verwendet, um nach einer WM_HOTKEY-Nachricht in der Nachrichtenwarteschlange zu suchen. Wenn eine empfangen wird, extrahiert sie den virtuellen Tastencode (in diesem Fall 0x41) aus der Nachricht.
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;
}
}
Mit anderen Worten: Stellen Sie sich vor, Sie schreiben etwas in einer Notizblock-Anwendung. Wenn die Taste A gedrückt wird, wird das Zeichen nicht als normaler Texteingang behandelt, sondern als globale Tastenkombination erkannt.
In diesem Beispiel wird nur die Taste A als Hotkey registriert. Sie können jedoch mehrere Tasten (wie B, C oder D) gleichzeitig als separate Hotkeys registrieren. Das bedeutet, dass jeder Schlüssel (d. h. jeder virtuelle Schlüsselcode), der mit der RegisterHotKey-API registriert werden kann, potenziell als globaler Hotkey missbraucht werden kann. Ein Hotkey-basierter Keylogger missbraucht diese Fähigkeit, um die vom Nutzer eingegebenen Tastenanschläge aufzuzeichnen.
Basierend auf unseren Tests haben wir festgestellt, dass nicht nur alphanumerische und einfache Symboltasten, sondern auch diese Tasten in Kombination mit dem SHIFT-Modifikator über die RegisterHotKey-API als Hotkeys registriert werden können. Das bedeutet, dass ein Keylogger effektiv jeden Tastenanschlag überwachen kann, um vertrauliche Informationen zu stehlen.
Heimliches Erfassen von Tastatureingaben
Lassen Sie uns den eigentlichen Prozess durchgehen, wie ein Hotkey-basierter Keylogger Tastenanschläge erfasst, am Beispiel des Hotkeyz-Hotkey-basierten Keyloggers.
In Hotkeyz registriert es zuerst jeden alphanumerischen virtuellen Schlüsselcode – und einige zusätzliche Tasten, wie VK_SPACE und VK_RETURN – als einzelne Hotkeys mithilfe der RegisterHotKey-API.
Dann wird innerhalb der Nachrichtenschleife des Keyloggers die PeekMessageW-API verwendet, um zu überprüfen, ob WM_HOTKEY-Nachrichten von diesen registrierten Hotkeys in der Nachrichtenwarteschlange erschienen sind. Wenn eine WM_HOTKEY-Nachricht erkannt wird, wird der darin enthaltene virtuelle Tastencode extrahiert und schließlich in einer Textdatei gespeichert. Nachfolgend finden Sie einen Auszug aus dem Code der Nachrichtenschleife, der die wichtigsten Teile hervorhebt.
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))
{
....
Ein wichtiges Detail ist folgendes: Um zu vermeiden, dass der Nutzer auf die Anwesenheit des Keyloggers aufmerksam wird, wird die Hotkey-Registrierung des Schlüssels mit Hilfe der UnregisterHotKey-API vorübergehend aufgehoben, sobald der virtuelle Schlüsselcode aus der Nachricht extrahiert wurde. Danach wird der Tastendruck mit keybd_event simuliert, sodass es für den Nutzer so aussieht, als ob die Taste normal gedrückt wurde. Sobald der Tastendruck simuliert wurde, wird die Taste mit der RegisterHotKey-API erneut registriert, um auf weiteren Eingang zu warten. Dies ist der Kernmechanismus, wie ein Hotkey-basierter Keylogger funktioniert.
Hotkey-basierte Keylogger erkennen
Nachdem wir nun verstehen, was Hotkey-basierte Keylogger sind und wie sie funktionieren, lassen Sie uns erklären, wie man sie erkennt.
ETW überwacht die RegisterHotKey-API nicht.
Dem in einem früheren Artikel beschriebenen Ansatz folgend, untersuchten wir zunächst, ob Event Tracing for Windows (ETW) zur Erkennung von Hotkey-basierten Keyloggern verwendet werden könnte. Unsere Recherchen ergaben schnell, dass ETW derzeit die RegisterHotKey oder UnregisterHotKey-APIs nicht überwacht. Zusätzlich zur Überprüfung der Manifestdatei für den Microsoft-Windows-Win32k-Anbieter haben wir die Interna der RegisterHotKey-API zurückentwickelt, insbesondere die NtUserRegisterHotKey-Funktion in win32kfull.sys. Leider haben wir keine Beweise dafür gefunden, dass diese APIs bei der Ausführung ETW-Ereignisse auslösen.
Das Bild unten zeigt einen Vergleich zwischen dem dekompilierten Code für **NtUserGetAsyncKeyState** (der von ETW überwacht wird) und **NtUserRegisterHotKey**. Beachten Sie, dass am Anfang von **NtUserGetAsyncKeyState** ein Aufruf von **EtwTraceGetAsyncKeyState** erfolgt – eine Funktion, die mit dem Logging von ETW-Ereignissen verknüpft ist – während **NtUserRegisterHotKey** keinen solchen Aufruf enthält.
Obwohl wir auch in Betracht gezogen haben, andere ETW-Anbieter als Microsoft-Windows-Win32k zu verwenden, um indirekt Aufrufe der RegisterHotKey-API zu überwachen, haben wir festgestellt, dass die Erkennungsmethode mit der „Hotkey-Tabelle“ – die als nächstes vorgestellt wird und sich nicht auf ETW stützt – Ergebnisse erzielt, die vergleichbar oder sogar besser sind als die Überwachung der RegisterHotKey-API. Am Ende haben wir uns entschieden, diese Methode zu implementieren.
Erkennung mithilfe der Hotkey-Tabelle (gphkHashTable)
Nachdem wir festgestellt hatten, dass ETW die Aufrufe der RegisterHotKey-API nicht direkt überwachen kann, begannen wir mit der Untersuchung von Erkennungsmethoden, die nicht auf ETW angewiesen sind. Während unserer Untersuchung fragten wir uns: „Werden die Informationen zu registrierten Hotkeys nicht irgendwo gespeichert?“ Und wenn ja, könnten diese Daten zur Erkennung verwendet werden? Basierend auf dieser Hypothese fanden wir schnell eine Hashtabelle mit der Bezeichnung gphkHashTable innerhalb von NtUserRegisterHotKey. Die Suche in der Online-Dokumentation von Microsoft ergab keine Details zu gphkHashTable, was darauf hindeutet, dass es sich um eine undokumentierte Kernel-Datenstruktur handelt.
Durch Reverse Engineering haben wir herausgefunden, dass diese Hash-Tabelle Objekte speichert, die Informationen über registrierte Hotkeys enthalten. Jedes Objekt enthält Details wie den Virtual-Key-Code und Modifikatoren, die in den Argumenten der RegisterHotKey-API angegeben sind. Die rechte Seite der Abbildung 3 zeigt einen Teil der Strukturdefinition für ein Hotkey-Objekt (mit dem Namen HOT_KEY), während die linke Seite anzeigt, wie die registrierten Hotkey-Objekte erscheinen, wenn über WinDbg darauf zugegriffen wird.
Wir haben auch festgestellt, dass ghpkHashTable so strukturiert ist, wie in Abbildung 4 gezeigt. Insbesondere verwendet es das Ergebnis der Modulo-Operation (mit 0x80) auf den Virtual-Key-Code (angegeben durch die RegisterHotKey-API) als Index in die Hash-Tabelle. Hotkey-Objekte, die denselben Index teilen, werden in einer Liste verknüpft, wodurch die Tabelle Hotkey-Informationen speichern und verwalten kann, selbst wenn die virtuellen Tastencodes identisch sind, die Modifikatoren jedoch unterschiedlich.
Mit anderen Worten, indem Sie alle in ghpkHashTable gespeicherten HOT_KEY-Objekte scannen, können Sie Details zu jedem registrierten Hotkey abrufen. Wenn wir feststellen, dass jede Haupttaste – z. B. jede einzelne alphanumerische Taste – als separater Hotkey registriert ist, deutet dies stark auf die Anwesenheit eines aktiven Hotkey-basierten Keyloggers hin.
Implementierung des Erkennungstools
Lassen Sie uns nun mit der Implementierung des Erkennungstools fortfahren. Da gphkHashTable im Kernelbereich liegt, kann eine Anwendung im Nutzermodus nicht darauf zugreifen. Aus diesem Grund war es notwendig, einen Gerätetreiber zur Erkennung zu entwickeln. Genauer gesagt haben wir beschlossen, einen Gerätetreiber zu entwickeln, der die Adresse von gphkHashTable abruft und alle Hotkey-Objekte durchsucht, die in der Hashtabelle gespeichert sind. Wenn die Anzahl der als Hotkeys registrierten alphanumerischen Tasten einen vordefinierten Schwellenwert übersteigt, werden wir auf das mögliche Vorhandensein eines Hotkey-basierten Keyloggers aufmerksam gemacht.
So erhalten Sie die Adresse von gphkHashTable
Während der Entwicklung des Erkennungstools war eine der ersten Herausforderungen, wie wir die Adresse von gphkHashTable ermitteln können. Nach eingehender Überlegung haben wir beschlossen, die Adresse direkt aus einer Anweisung im win32kfull.sys-Treiber zu extrahieren, die auf gphkHashTable zugreift.
Durch Reverse Engineering haben wir entdeckt, dass es innerhalb der Funktion IsHotKey – gleich zu Beginn – eine lea-Anweisung (lea rbx, gphkHashTable) gibt, die auf gphkHashTable zugreift. Wir haben die Opcode-Bytesequenz (0x48, 0x8d, 0x1d) aus dieser Anweisung als Signatur verwendet, um die entsprechende Zeile zu lokalisieren, und dann die Adresse von gphkHashTable mit dem erhaltenen 32-Bit-Offset (4-Byte) berechnet.
Zusätzlich, da IsHotKey keine exportierte Funktion ist, müssen wir auch ihre Adresse kennen, bevor wir nach gphkHashTable suchen. Durch weiteres Reverse Engineering haben wir entdeckt, dass die exportierte Funktion EditionIsHotKey die Funktion IsHotKey aufruft. Daher haben wir beschlossen, die Adresse von IsHotKey innerhalb der Funktion EditionIsHotKey mit derselben zuvor beschriebenen Methode zu berechnen. (Zur Referenz kann die Basisadresse von win32kfull.sys mit der PsLoadedModuleList-API gefunden werden.)
Zugriff auf den Speicherbereich von win32kfull.sys
Nachdem wir unseren Ansatz zum Abrufen der Adresse von gphkHashTable fertiggestellt hatten, begannen wir, Code zu schreiben, um auf den Speicherbereich von win32kfull.sys zuzugreifen und diese Adresse abzurufen. Eine Herausforderung, der wir in dieser Phase begegneten, war, dass win32kfull.sys ein Session-Treiber ist. Bevor Sie fortfahren, hier eine kurze, vereinfachte Erklärung, was eine Session ist.
Wenn sich ein Nutzer unter Windows anmeldet, wird jedem Nutzer eine separate Sitzung (mit Sitzungsnummern, die bei 1 beginnen) zugewiesen. Einfach ausgedrückt: Der erste Nutzer, der sich anmeldet, erhält Sitzung 1. Wenn sich ein anderer Nutzer anmeldet, während diese Sitzung aktiv ist, wird diesem Nutzer Session 2 zugewiesen, und so weiter. Jeder Nutzer hat dann innerhalb der ihm zugewiesenen Sitzung seine eigene Desktop-Umgebung.
Daten, die für jede Sitzung separat verwaltet werden müssen (d. h. pro angemeldetem Nutzer), werden in einem isolierten Bereich des Kernel-Speichers gespeichert, der als Session Space bezeichnet wird. Dies umfasst GUI-Objekte, die von win32k-Treibern verwaltet werden, wie Fenster und Maus-/Tastatureingabedaten, um sicherzustellen, dass Bildschirm und Eingang ordnungsgemäß zwischen den Nutzern getrennt bleiben.
(Dies ist eine vereinfachte Erklärung). Für eine detailliertere Diskussion über Sitzungen lesen Sie bitte James Forshaws Blogbeitrag.
Basierend auf dem oben Gesagten ist win32kfull.sys als Sitzungstreiber bekannt. Das bedeutet, dass beispielsweise Hotkey-Informationen, die in der Sitzung des ersten eingeloggten Nutzers (Sitzung 1) registriert wurden, nur innerhalb dieser Sitzung zugänglich sind. Wie können wir also diese Einschränkung umgehen? In solchen Fällen ist bekannt, dass KeStackAttachProcess verwendet werden kann.
KeStackAttachProcess ermöglicht es dem aktuellen Thread, sich vorübergehend an den Adressraum eines angegebenen Prozesses anzuhängen. Falls wir in der Zielsitzung an einen GUI-Prozess anknüpfen können – genauer gesagt, an einen Prozess, der win32kfull.sys geladen hat –, dann können wir innerhalb dieser Sitzung auf win32kfull.sys und die zugehörigen Daten zugreifen. Für unsere Implementierung, bei der wir davon ausgehen, dass nur ein Nutzer angemeldet ist, haben wir uns entschlossen, winlogon.exe zu lokalisieren und daran anzuhängen, Der Prozess, der für die Bearbeitung der Nutzeranmeldevorgänge verantwortlich ist.
Aufzählung registrierter Hotkeys
Sobald wir erfolgreich an den Prozess winlogon.exe angehängt und die Adresse von gphkHashTable ermittelt haben, besteht der nächste Schritt darin, gphkHashTable zu scannen, um die registrierten Hotkeys zu überprüfen. Im Folgenden finden Sie einen Auszug aus dem 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;
}
Der Code selbst ist unkompliziert: Er iteriert durch jeden Index der Hash-Tabelle, folgt der verketteten Liste, um auf jedes HOT_KEY-Objekt zuzugreifen, und überprüft, ob die registrierten Hotkeys alphanumerischen Tasten ohne Modifikatoren entsprechen. In unserem Erkennungs-Tool, wenn jede alphanumerische Taste als Hotkey registriert wird, wird ein Alarm ausgelöst, der auf das mögliche Vorhandensein eines Hotkey-basierten Keyloggers hinweist. Der Einfachheit halber zielt diese Implementierung nur auf alphanumerische Tastenkombinationen ab, obwohl es einfach wäre, das Tool so zu erweitern, dass es auch Tastenkombinationen mit Modifikatoren wie SHIFT überprüft.
Hotkeyz-Erkennung
Das Erkennungstool (Hotkey-basierter Keylogger-Detektor) wurde unten veröffentlicht. Detaillierte Gebrauchsanweisungen werden ebenfalls bereitgestellt. Darüber hinaus wurde diese Studie auf der NULLCON Goa 2025 vorgestellt, und die Präsentationsfolien sind verfügbar.
https://github.com/AsuNa-jp/HotkeybasedKeyloggerDetector
Das folgende Demovideo zeigt, wie der Hotkey-basierte Keylogger-Detektor Hotkeyz erkennt.
Danksagungen
Wir möchten Jonathan Bar Or ganz herzlich dafür danken, dass er unseren früheren Artikel gelesen hat, seine Einblicke über Hotkey-basierte Keylogger mit uns geteilt hat und das PoC-Tool Hotkeyz großzügig veröffentlicht hat.
