Asuka Nakajima

Detección de keyloggers basados en teclas de acceso rápido usando una estructura de datos del kernel no documentada

En este artículo, exploramos qué son los keyloggers basados en teclas de acceso rápido y cómo detectarlos. Específicamente, explicamos cómo estos keyloggers interceptan las pulsaciones de teclas y, luego, presentamos una técnica de detección que aprovecha una tabla de teclas de acceso rápido no documentada en el espacio del kernel.

Detecting Hotkey-Based Keyloggers Using an Undocumented Kernel Data Structure

Detección de keyloggers basados en teclas de acceso rápido usando una estructura de datos del kernel no documentada

En este artículo, exploramos qué son los keyloggers basados en teclas de acceso rápido y cómo detectarlos. Específicamente, explicamos cómo estos keyloggers interceptan las pulsaciones de teclas y, luego, presentamos una técnica de detección que aprovecha una tabla de teclas de acceso rápido no documentada en el espacio del kernel.

Introducción

En mayo de 2024, Elastic Security Labs publicó un artículo en el que se destacaron las nuevas características agregadas a Elastic Defend (a partir de la versión 8.12) para mejorar la detección de keyloggers que se ejecutan en Windows. En esa publicación, cubrimos cuatro tipos de keyloggers que se suelen usar en ciberataques: keyloggers basados en sondeo, keyloggers basados en hooking, keyloggers que utilizan el modelo de entrada sin procesar y keyloggers que utilizan DirectInput, y explicamos nuestra metodología de detección. En particular, introdujimos un método de detección basado en el comportamiento utilizando el proveedor Microsoft-Windows-Win32k dentro de Event Tracing for Windows (ETW).

Poco después de la publicación, nos sentimos honrados de que Jonathan Bar Or, investigador principal de seguridad en Microsoft, se fijara en nuestro artículo. Nos proporcionó comentarios invaluables al señalar la existencia de keyloggers basados en teclas de acceso rápido e incluso compartió con nosotros un código de prueba de concepto (PoC). Tomando como punto de partida su código PoC Hotkeyz, este artículo presenta un método potencial para detectar keyloggers basados en teclas de acceso rápido.

Visión general de los keyloggers basados en teclas de acceso rápido

¿Qué es una tecla de acceso rápido?

Antes de profundizar en los keyloggers basados en teclas de acceso rápido, aclaremos primero qué es una tecla de acceso rápido. Una tecla de acceso rápido es un tipo de atajo de teclado que invoca directamente una función específica en una computadora al presionar una sola tecla o una combinación de teclas. Por ejemplo, muchos usuarios de Windows presionan Alt + Tab para cambiar entre tareas (o, en otras palabras, ventanas). En este caso, Alt + Tab funciona como un atajo de teclado que activa directamente la función de cambio de tareas.

(Nota: Aunque existen otros tipos de atajos de teclado, este artículo se centra únicamente en las teclas de acceso rápido). Además, toda esta información se basa en Windows 10 versión 22H2, compilación del sistema operativo 19045.5371, sin seguridad basada en virtualización. Ten en cuenta que las estructuras de datos internas y el comportamiento pueden diferir en otras versiones de Windows.

Abuso de la funcionalidad de registro de teclas de acceso rápido personalizadas

Además de utilizar las teclas de acceso rápido preconfiguradas en Windows, como se muestra en el ejemplo anterior, también puedes registrar tus propias teclas de acceso rápido personalizadas. Existen varios métodos para hacerlo, pero un enfoque directo es utilizar la función RegisterHotKey de la API de Windows, que permite a un usuario registrar una tecla específica como tecla de acceso rápido. Por ejemplo, el siguiente fragmento de código muestra cómo usar la API RegisterHotKey para registrar la tecla A (con un código de tecla virtual de 0x41) como una tecla de acceso rápido global:

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

Después de registrar una tecla de acceso rápido, cuando se presiona la tecla registrada, se envía un mensaje WM_HOTKEY a la cola de mensajes de la ventana especificada como primer argumento de la API RegisterHotKey (o al hilo que registró la tecla de acceso rápido si se usa NULL). El código a continuación demuestra un bucle de mensajes que utiliza la API GetMessage para verificar un mensaje WM_HOTKEY en la cola de mensajes, y si se recibe uno, extrae el código de tecla virtual (en este caso, 0x41) del mensaje.

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 otras palabras, imagina que estás escribiendo algo en una aplicación de bloc de notas. Si presionas la tecla A, el carácter no se tratará como una entrada de texto normal, sino que se reconocerá como una tecla de acceso rápido global.

En este ejemplo, solo la tecla A está registrada como tecla de acceso rápido. Sin embargo, puedes registrar varias teclas (como B, C o D) como teclas de acceso rápido separadas al mismo tiempo. Esto significa que cualquier tecla (es decir, cualquier código de tecla virtual) que se pueda registrar con la API RegisterHotKey puede secuestrarse como una tecla de acceso rápido global. Un keylogger basado en teclas de acceso rápido abusa de esta capacidad para capturar las pulsaciones de teclas que realiza el usuario.

Basándonos en nuestras pruebas, encontramos que no solo las teclas alfanuméricas y de símbolos básicos, sino también esas teclas combinadas con el modificador SHIFT, pueden registrarse como teclas de acceso rápido usando la API RegisterHotKey. Esto significa que un keylogger puede monitorear eficazmente cada pulsación de tecla necesaria para sustraer información confidencial.

Captura de pulsaciones de teclas de forma sigilosa

Repasemos el proceso real de cómo un keylogger basado en teclas de acceso rápido captura las pulsaciones de teclas, usando el keylogger Hotkeyz como ejemplo.

En Hotkeyz, primero registra cada código alfanumérico de clave virtual y algunas teclas adicionales, como VK_SPACE y VK_RETURN, como teclas de acceso rápido individuales mediante la API RegisterHotKey.

Luego, dentro del bucle de mensajes del keylogger, se utiliza la API PeekMessageW para comprobar si algún mensaje WM_HOTKEY de estas teclas de acceso rápido registradas ha aparecido en la cola de mensajes. Cuando se detecta un mensaje WM_HOTKEY, se extrae el código de tecla virtual que contiene y, finalmente, se guarda en un archivo de texto. A continuación, se presenta un extracto del código del bucle de mensajes, resaltando las partes más 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))
{
....

Un detalle importante es este: para evitar alertar al usuario sobre la presencia del keylogger, una vez que se extrae el código de tecla virtual del mensaje, el registro de la tecla de acceso rápido se elimina temporalmente mediante la API UnregisterHotKey. Después de eso, se simula la pulsación de la tecla con keybd_event para que parezca que la tecla se presionó normalmente. Una vez que se simula la pulsación de la tecla, la tecla se vuelve a registrar usando la API RegisterHotKey para esperar más entradas. Este es el mecanismo central de cómo opera un keylogger basado en teclas de acceso rápido.

Detección de keyloggers basados en teclas de acceso rápido

Ahora que entendemos qué son los keyloggers basados en teclas de acceso rápido y cómo operan, expliquemos cómo detectarlos.

ETW no monitorea la API RegisterHotKey

Siguiendo el enfoque descrito en un artículo anterior, primero investigamos si Event Tracing for Windows (ETW) podría usarse para detectar keyloggers basados en teclas de acceso rápido. Nuestra investigación reveló rápidamente que ETW en este momento no monitorea las API RegisterHotKey o UnregisterHotKey. Además de revisar el archivo de manifiesto para el proveedor Microsoft-Windows-Win32k, realizamos ingeniería inversa de los elementos internos de la API RegisterHotKey, específicamente, la función NtUserRegisterHotKey en win32kfull.sys. Lamentablemente, no encontramos evidencia de que estas API desencadenen eventos ETW al ejecutarse.

La imagen a continuación muestra una comparación entre el código descompilado de NtUserGetAsyncKeyState (que es monitoreado por ETW) y NtUserRegisterHotKey. Observa que al inicio de NtUserGetAsyncKeyState, hay una llamada a EtwTraceGetAsyncKeyState, una función asociada con el registro de eventos de ETW, mientras que NtUserRegisterHotKey no contiene tal llamada.

Figure 1: Comparison of the Decompiled Code for NtUserGetAsyncKeyState and NtUserRegisterHotKey
Figura 1: Comparación del código descompilado para NtUserGetAsyncKeyState y NtUserRegisterHotKey

Aunque también consideramos usar proveedores de ETW distintos de Microsoft-Windows-Win32k para monitorear indirectamente las llamadas a la API RegisterHotKey, descubrimos que el método de detección que utiliza la “tabla de teclas de acceso rápido”, que se presentará a continuación y no se basa en ETW, logra resultados comparables o incluso mejores que el monitoreo de la API RegisterHotKey. Al final, decidimos implementar este método.

Detección usando la tabla de teclas de acceso rápido (gphkHashTable))

Después de descubrir que ETW no puede monitorear directamente las llamadas a la API RegisterHotKey, comenzamos a explorar métodos de detección que no dependen de ETW. Durante nuestra investigación, nos preguntamos: “¿No se almacena en algún lugar la información de las teclas de acceso rápido registradas? Y si es así, ¿se podrían usar esos datos para la detección?” Basándonos en esa hipótesis, encontramos rápidamente una tabla hash etiquetada como gphkHashTable dentro de NtUserRegisterHotKey. Al buscar en la documentación en línea de Microsoft, no se revelaron detalles sobre gphkHashTable, lo que sugiere que es una estructura de datos del kernel no documentada.

Figure 2: The hotkey table (gphkHashTable), discovered within the RegisterHotKey function called inside NtUserRegisterHotKey
Figura 2: Tabla de teclas de acceso rápido (gphkHashTable), encontrada al analizar la función RegisterHotKey invocada dentro de NtUserRegisterHotKey.

Mediante ingeniería inversa, descubrimos que esta tabla hash almacena objetos que contienen información sobre las teclas de acceso rápido registradas. Cada objeto contiene detalles como el código de tecla virtual y los modificadores especificados en los argumentos de la API RegisterHotKey. El lado derecho de la Figura 3 muestra parte de la definición de estructura para un objeto de tecla de acceso rápido (llamado HOT_KEY), mientras que el lado izquierdo muestra cómo aparecen los objetos de las teclas de acceso rápido registradas cuando se accede a través de WinDbg.

Figure 3: Hotkey Object Details. WinDbg view (left) and HOT_KEY structure details (right)
Figura 3: Detalles del objeto de tecla de acceso rápido. Vista de WinDbg (izquierda) y detalles de la estructura HOT_KEY (derecha)

Además, determinamos que ghpkHashTable está estructurado como se muestra en la Figura 4. Específicamente, utiliza el resultado de la operación de módulo (con 0x80) en el código de tecla virtual (especificado por la API RegisterHotKey) como índice en la tabla hash. Los objetos de teclas de acceso rápido que comparten el mismo índice están vinculados en una lista, lo que permite que la tabla almacene y gestione la información de las teclas de acceso rápido incluso cuando los códigos de tecla virtual son idénticos pero los modificadores son diferentes.

Figure 4: Structure of gphkHashTable
Figura 4: Estructura de gphkHashTable

En otras palabras, al escanear todos los objetos HOT_KEY almacenados en ghpkHashTable, podemos recuperar detalles sobre cada tecla de acceso rápido registrada. Si encontramos que cada tecla principal, por ejemplo, cada tecla alfanumérica individual, está registrada como una tecla de acceso rápido independiente, eso indica con claridad la presencia de un keylogger activo basado en teclas de acceso rápido.

Implementación de la herramienta de detección

Ahora, avancemos a implementar la herramienta de detección. Dado que gphkHashTable se aloja en el espacio del kernel, una aplicación en modo usuario no puede acceder a él. Por esta razón, fue necesario desarrollar un controlador de dispositivo para la detección. Más específicamente, decidimos desarrollar un controlador de dispositivo que obtenga la dirección de gphkHashTable y escanee todos los objetos de teclas de acceso rápido almacenados en la tabla hash. Si el número de teclas alfanuméricas registradas como teclas de acceso rápido supera un umbral predefinido, nos alertará sobre la posible presencia de un keylogger basado en teclas de acceso rápido.

Cómo obtener la dirección de gphkHashTable

Mientras desarrollábamos la herramienta de detección, uno de los primeros desafíos que enfrentamos fue cómo obtener la dirección de gphkHashTable. Después de cierta consideración, decidimos extraer la dirección directamente de una instrucción en el controlador win32kfull.sys que accede a gphkHashTable.

A través de ingeniería inversa, descubrimos que dentro de la función IsHotKey, justo al principio, hay una instrucción lea (lea rbx, gphkHashTable) que accede a gphkHashTable. Usamos la secuencia de bytes del opcode (0x48, 0x8d, 0x1d) de esa instrucción como una firma para localizar la línea correspondiente y, luego, calculamos la dirección de gphkHashTable usando el desplazamiento de 32 bits (4 bytes) obtenido.

Figure 5: Inside the IsHotKey function
Figura 5: Dentro de la función IsHotKey

Además, dado que IsHotKey no es una función exportada, también necesitamos conocer su dirección antes de buscar gphkHashTable. A través de ingeniería inversa más detallada, descubrimos que la función exportada EditionIsHotKey invoca a la función IsHotKey. Por lo tanto, decidimos calcular la dirección de IsHotKey dentro de la función EditionIsHotKey usando el mismo método descrito anteriormente. (Como referencia, la dirección base de win32kfull.sys se puede encontrar mediante la API PsLoadedModuleList.)

Accediendo al espacio de memoria de win32kfull.sys

Una vez que finalizamos nuestro enfoque para obtener la dirección de gphkHashTable, comenzamos a escribir código para acceder al espacio de memoria de win32kfull.sys y recuperar esa dirección. Un desafío que encontramos en esta etapa fue que win32kfull.sys es un controlador de sesión. Antes de continuar, aquí tienes una explicación breve y simplificada de lo que es una sesión.

En Windows, cuando un usuario inicia sesión, se le asigna una sesión independiente (con números de sesión que comienzan desde 1) a cada usuario. En pocas palabras, al primer usuario que inicie sesión se le asigna la Sesión 1. Si otro usuario inicia sesión mientras esa sesión está activa, se le asigna la Sesión 2, y así sucesivamente. Luego, cada usuario tiene su propio entorno de escritorio dentro de su sesión asignada.

Los datos del kernel que deben gestionarse por separado para cada sesión (es decir, por usuario registrado) se almacenan en un área aislada de la memoria del kernel llamada espacio de sesión. Esto incluye objetos de la interfaz gráfica de usuario (GUI) gestionados por los controladores win32k, como ventanas y datos de entrada de mouse/teclado, lo que garantiza que la pantalla y la entrada permanezcan adecuadamente separadas entre los usuarios.

(Esta es una explicación simplificada). Para obtener un análisis más detallado sobre las sesiones, consulta la publicación de blog de James Forshaw.)

Figure 6: Overview of Sessions. Session 0 is dedicated exclusively to service processes
Figura 6: Visión general de las sesiones. La sesión 0 está dedicada exclusivamente a los procesos de servicio.

En función de lo anterior, win32kfull.sys es conocido como un controlador de sesión. Esto significa que, por ejemplo, solo se puede acceder a la información de las teclas de acceso rápido registradas en la sesión del primer usuario que inició sesión (Sesión 1) desde esa misma sesión. Entonces, ¿cómo podemos sortear esta limitación? En tales casos, se sabe que se puede usar KeStackAttachProcess.

KeStackAttachProcess permite que el hilo actual se conecte temporalmente al espacio de direcciones de un proceso especificado. Si podemos conectarnos a un proceso GUI en la sesión de destino, más precisamente, un proceso que ha cargado win32kfull.sys, podemos acceder a win32kfull.sys y sus datos asociados dentro de esa sesión. Para nuestra implementación, asumiendo que solo un usuario ha iniciado sesión, decidimos localizar y conectarnos a winlogon.exe, el proceso encargado de manejar las operaciones de inicio de sesión del usuario.

Enumeración de las teclas de acceso rápido registradas

Una vez que nos hayamos conectado correctamente al proceso winlogon.exe y hayamos determinado la dirección de gphkHashTable, el siguiente paso es simplemente escanear gphkHashTable para verificar las teclas de acceso rápido registradas. A continuación, se muestra un extracto de ese código:

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;
}

El código en sí es sencillo: itera a través de cada índice de la tabla hash, siguiendo la lista enlazada para acceder a cada objeto HOT_KEY, y verifica si las teclas de acceso rápido registradas corresponden a teclas alfanuméricas sin ningún modificador. En nuestra herramienta de detección, si cada tecla alfanumérica se registra como una tecla de acceso rápido, se activa una alerta que indica la posible presencia de un keylogger basado en teclas de acceso rápido. Para simplificar, esta implementación solo se enfoca en teclas de acceso rápido alfanuméricas, aunque sería fácil extender la herramienta para verificar teclas de acceso rápido con modificadores como SHIFT.

Detección de Hotkeyz

La herramienta de detección (detector de keylogger basado en teclas de acceso rápido) se publica a continuación. También se proporcionan instrucciones detalladas de uso. Además, esta investigación se presentó en NULLCON Goa 2025, y las diapositivas de la presentación están disponibles.

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

A continuación, se muestra un video de demostración que muestra cómo el detector de keylogger basado en teclas de acceso rápido detecta Hotkeyz.

DEMO_VIDEO.mp4

Agradecimientos

Queremos expresar nuestro más sincero agradecimiento a Jonathan Bar Or por leer nuestro artículo anterior, compartir sus conocimientos sobre los keyloggers basados en teclas de acceso rápido y publicar generosamente la herramienta PoC Hotkeyz.