
Abusando del mecanismo de callback del kernel win32k.sys para la ejecución arbitraria de código
Este repositorio se proporciona estrictamente con fines educativos y de investigación defensiva.
Demuestra los mecanismos internos de despacho de callbacks de Windows y conceptos de flujo de control relacionados con KernelCallbackTable en un contexto de prueba de concepto.
El autor no asume ninguna responsabilidad por el mal uso.
Esta técnica de inyección abusa de la ruta de despacho de callbacks de kernel a usuario utilizada por el subsistema gráfico de Windows (win32k.sys) para obtener ejecución de código dentro de un proceso remoto. Al localizar el KernelCallbackTablea través delBloque de Entorno de Proceso (PEB), un operador puede enumerar las entradas de callback e identificar rutinas legítimas de modo usuario invocadas durante transiciones del kernel relacionadas con la GUI.
En lugar de realizar la tradicional inyección de KernelCallbackTable, donde una entrada de callback se sobrescribe directamente con la dirección del shellcode, esta variante intercepta el destino de callback legítimo referenciado por la tabla y redirige la ejecución al shellcode controlado por el atacante al momento de la invocación. Debido a que la ejecución se secuestra a través de una ruta de callback existente y esperada, la técnica puede proporcionar una alternativa más sigilosa que primitivas más convencionales como la creación de hilos remotos o la inyección basada en APC.
El subsistema gráfico de Windows delega partes del procesamiento relacionado con la GUI al modo usuario a través de un mecanismo de callback iniciado desde el modo kernel. Cuando win32k.sys necesita ejecutar lógica dentro del contexto de un proceso GUI, invoca KeUserModeCallback para realizar una transición controlada del modo kernel al modo usuario preservando el límite de aislamiento entre ambos contextos de ejecución.
Esta transición establece la ruta de ejecución legítima a través de la cual el kernel despacha callbacks del subsistema gráfico hacia el modo usuario—la misma ruta de la que luego abusa la técnica presentada.
Una vez completada la transición, la ejecución entra en KiUserCallbackDispatcher, una rutina de ntdll.dll responsable de recibir el índice de callback suministrado por el kernel y despachar la ejecución al manejador de callback correspondiente en modo usuario. Esta rutina sirve como punto de entrada obligatorio para todos los callbacks iniciados a través de KeUserModeCallback.
Debido a que toda la resolución de callbacks converge en este despachador, KiUserCallbackDispatcher actúa como el pivote central entre las solicitudes de callback del kernel y su eventual ejecución en modo usuario.
Para resolver el destino de un callback solicitado, KiUserCallbackDispatcher consulta la KernelCallbackTable almacenada en el PEB del proceso objetivo. Cada entrada de la tabla contiene un puntero a una rutina de callback en modo usuario asociada con una operación específica del subsistema gráfico, típicamente implementada en user32.dll.
Las técnicas tradicionales de inyección de KernelCallbackTable sobrescriben directamente una o más de estas entradas para redirigir la ejecución. Si bien son efectivas, modificar la tabla en sí introduce anomalías estructurales que pueden detectarse trivialmente mediante la validación de integridad del PEB o del contenido de la tabla de callbacks. La técnica presentada evita esto preservando la estructura de la tabla y, en su lugar, desviando el destino de callback referenciado por la entrada.
Entre las entradas disponibles de KernelCallbackTable, __fnCOPYDATA proporciona una primitiva de activación particularmente conveniente porque puede invocarse externamente mediante el envío de un mensaje WM_COPYDATA a través de SendMessage(). Esto permite activar el callback de forma determinista sin requerir un estado inusual del proceso ni una interacción compleja.
Al aprovechar un destino de callback naturalmente accesible y de uso frecuente, la técnica obtiene una primitiva de ejecución fiable mientras permanece completamente dentro de la cadena esperada de despacho de callbacks antes de la redirección.

La técnica comienza localizando el proceso objetivo y leyendo su PEB para recuperar la dirección de la KernelCallbackTable, desde la cual se resuelve el puntero de callback para __fnCOPYDATA. A continuación, se asigna memoria ejecutable dentro del proceso remoto y se escribe el shellcode controlado por el atacante en la región asignada. Antes de la modificación, los bytes originales de la rutina de callback legítima se conservan para permitir su restauración posterior.
Posteriormente, se instala un hook inline al inicio de la rutina resuelta __fnCOPYDATA, reemplazando su prólogo con un salto absoluto al shellcode inyectado. Para desencadenar la ejecución, se envía un mensaje WM_COPYDATA a la ventana objetivo, lo que hace que el subsistema gráfico de Windows despache __fnCOPYDATA a través de la cadena estándar de callbacks de kernel a usuario. Una vez completada la ejecución, los bytes originales del callback se restauran para preservar la estabilidad del proceso y reducir los artefactos de modificación residuales.
La KernelCallbackTable se encuentra en el desplazamiento 0x58 dentro del PEB:

La siguiente lógica se utiliza para obtenerla:
PROCESS_BASIC_INFORMATION pbi;
PEB peb;
KERNELCALLBACKTABLE kct;
if (NtQueryInformationProcess(hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), NULL) != STATUS_SUCCESS)
{
NtClose(hProcess);
return 1;
}
/* Read remote PEB */
if (NtReadVirtualMemory(hProcess, pbi.PebBaseAddress, &peb, sizeof(peb), NULL) != STATUS_SUCCESS)
{
NtClose(hProcess);
return 1;
}
/* Ensure KernelCallbackTable is present */
if (!peb.KernelCallbackTable) {
NtClose(hProcess);
return 1;
}
/* Read KernelCallbackTable contents */
if (NtReadVirtualMemory(hProcess, peb.KernelCallbackTable, &kct, sizeof(kct), NULL) != STATUS_SUCCESS)
{
NtClose(hProcess);
return 1;
}
El código comienza llamando a NtQueryInformationProcess con la clase de información ProcessBasicInformation para rellenar una estructura PROCESS_BASIC_INFORMATION, que expone la dirección del PEB del proceso remoto a través del campo PebBaseAddress.
A continuación, se utiliza NtReadVirtualMemory para leer el PEB remoto en una estructura local PEB, lo que permite extraer el puntero a KernelCallbackTable almacenado dentro del entorno del proceso. Después de validar que la tabla de callbacks está presente, una segunda llamada a NtReadVirtualMemory copia la estructura remota KERNELCALLBACKTABLE en la memoria local, lo que permite la resolución directa de destinos de callback como __fnCOPYDATA para el posterior desvío.