PROYECTO · KERNEL / DRIVER
Comunicación KM ↔ UM mediante shared memory
Proyecto personal de programación de sistemas: un driver en modo kernel (Ring 0) en C++ que se comunica con un proceso en espacio de usuario mediante un buffer compartido, sin recurrir a las interfaces clásicas como IOCTL. El driver puede leer y escribir la memoria de cualquier proceso, obtener la dirección base de módulos y resolver direcciones de forma dinámica.
Tecnologías
C++Windows Kernel (WDK)Ring 0 / Ring 3Shared MemoryMmCopyVirtualMemoryDPC / Timer / WorkItem
Arquitectura · 01
Visión general de la arquitectura
El sistema consta de dos componentes: un driver en modo kernel (KM) y una aplicación en modo usuario (UM). Ambos comparten un buffer de memoria sin usar DeviceIoControl. En su lugar, el proceso UM aloja un buffer con VirtualAlloc y publica su dirección junto a un valor 'magic' calculado en runtime. El driver escanea la memoria del proceso UM buscando ese magic, obtiene el puntero al buffer y a partir de ahí ambos se comunican leyendo y escribiendo campos en esa estructura compartida.
- Sin IOCTL: la comunicación se basa enteramente en un buffer compartido en memoria del proceso UM.
- Magic dinámico: el valor de rendezvous se calcula a partir del PID, de modo que no hay constantes reconocibles en el binario.
- Timer + DPC + WorkItem: el driver sondea el buffer de forma periódica usando un KTIMER y ejecuta las peticiones en un work item del pool de hilos del kernel.
Arquitectura · 02
DriverEntry — punto de entrada
El punto de entrada del driver es mínimo: sólo llama a SharedMemInit(), que crea un hilo del sistema para no bloquear la carga del driver.
DriverEntry.cpp
extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT driver_object,
PUNICODE_STRING reg_path)
{
UNREFERENCED_PARAMETER(driver_object);
UNREFERENCED_PARAMETER(reg_path);
NTSTATUS st = SharedMemInit();
if (!NT_SUCCESS(st))
return st;
return STATUS_SUCCESS;
}
Arquitectura · 03
Magic dinámico basado en PID
El valor de rendezvous ('magic') se calcula en runtime a partir del PID del proceso UM. Tanto el driver como la aplicación UM usan la misma fórmula, así que obtienen el mismo resultado sin necesidad de una constante hardcodeada en el binario. Si alguien vuelca el binario y busca constantes conocidas, no encuentra nada.
rendezvous.cpp
// Kernel-mode (y user-mode usa la misma fórmula)
ULONG64 ComputeMagic(ULONG pid)
{
return 0xC0FFEE0000000000ULL
^ ((ULONG64)pid * 0x9E3779B97F4A7C15ULL);
}
Arquitectura · 04
Rendezvous — encontrando el buffer
El driver se ata al espacio de direcciones del proceso UM (KeStackAttachProcess) y recorre sus regiones de memoria comprometidas con permisos de escritura. Busca el magic page a page; cuando lo encuentra, lee los 8 bytes siguientes, que contienen el puntero al buffer compartido publicado por UM.
rendezvous.cpp
// UM publica magic + ptr en una estructura global
volatile struct {
uint64_t magic; // calculado en runtime
SHARED_BUFFER* ptr; // buffer alocado con VirtualAlloc
} g_rdv = { 0, nullptr };
// KM escanea la memoria UM buscando el magic
ULONG_PTR find_magic_in_process(PEPROCESS process,
ULONG64 magic)
{
KAPC_STATE apc;
KeStackAttachProcess(process, &apc);
// Recorre regiones MEM_COMMIT con permisos RW
// Compara cada ULONG64 con el magic
// Si coincide → devuelve la dirección del campo ptr
KeUnstackDetachProcess(&apc);
return result;
}
Arquitectura · 05
Buffer compartido y protocolo de peticiones
La estructura compartida tiene tres campos atómicos de control (request_ready, response_ready, km_alive) y un bloque de datos (REQUEST_DATA). El flujo es sencillo: UM rellena el bloque de datos y pone request_ready = 1. El driver lo detecta, procesa la petición, escribe la respuesta y pone response_ready = 1. UM espera response_ready para leer el resultado.
shared_mem.h
typedef struct _SHARED_BUFFER {
volatile LONG request_ready; // UM → KM: "hay petición"
volatile LONG response_ready; // KM → UM: "respuesta lista"
volatile LONG km_alive; // KM → UM: "driver activo"
ULONG _pad;
REQUEST_DATA data; // bloque de datos
} SHARED_BUFFER;
typedef struct _REQUEST_DATA {
void* buffer_address;
UINT_PTR address;
ULONGLONG size;
ULONG pid;
BOOLEAN write;
BOOLEAN read;
BOOLEAN req_base;
UCHAR output[256];
char module_name[64];
ULONG64 base_address;
} REQUEST_DATA;
Arquitectura · 06
Timer, DPC y WorkItem — el bucle del driver
El driver no usa un bucle while(true). En su lugar, crea un KTIMER que dispara un DPC cada ~100 µs. El DPC encola un work item que comprueba si UM ha escrito una petición. Si la hay, la procesa; si no, sale. Así el driver no bloquea ningún hilo del kernel de forma permanente y mantiene la latencia baja.
shared_mem.cpp
static VOID TimerDpc(PKDPC, PVOID, PVOID, PVOID)
{
if (!InterlockedCompareExchange(&g_shutdown, 0, 0))
{
// Encolar work item (solo si no hay otro pendiente)
if (InterlockedCompareExchange(&g_workItemQueued, 1, 0) == 0)
ExQueueWorkItem(&g_workItem, DelayedWorkQueue);
// Re-armar el timer
LARGE_INTEGER due;
due.QuadPart = -1000LL; // ~100 µs
KeSetTimer(&g_timer, due, &g_timerDpc);
}
}
Arquitectura · 07
Procesamiento de peticiones
Cuando el work item detecta request_ready == 1, copia REQUEST_DATA desde la memoria del proceso UM usando MmCopyVirtualMemory, procesa la petición y escribe el resultado de vuelta. Soporta tres operaciones: obtener la base de un módulo (req_base), leer memoria (read) y escribir memoria (write).
process_request.cpp
void process_request(SHARED_BUFFER* shared)
{
REQUEST_DATA* req = &shared->data;
if (req->req_base == TRUE)
{
// PsLookupProcessByProcessId → KeStackAttachProcess
// → BBGetUserModule → devuelve base_address
}
else if (req->read == TRUE)
{
// read_kernel_memory(pid, address, output, size)
}
else if (req->write == TRUE)
{
// write_kernel_memory(pid, address, buffer, size)
}
}
Arquitectura · 08
Cliente UM — el lado de usuario
La aplicación de usuario aloja el buffer con VirtualAlloc, calcula el magic con su propio PID y espera a que el driver señalice km_alive = 1. A partir de ahí puede enviar peticiones rellenando REQUEST_DATA y poniendo request_ready = 1, esperando la respuesta en response_ready.
main.cpp (user-mode)
int main()
{
DWORD myPid = GetCurrentProcessId();
uint64_t magic = compute_magic(myPid);
SHARED_BUFFER* shared = (SHARED_BUFFER*)VirtualAlloc(
nullptr, sizeof(SHARED_BUFFER),
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
// Publicar magic + ptr para que el driver lo encuentre
g_rdv.magic = magic;
g_rdv.ptr = shared;
// Esperar a que el driver señalice km_alive = 1
while (!shared->km_alive) Sleep(10);
// Enviar peticiones: rellenar shared->data,
// poner request_ready = 1, esperar response_ready
}