PROXECTO · KERNEL / DRIVER
Comunicación KM ↔ UM mediante shared memory
Proxecto persoal de programación de sistemas: un driver en modo kernel (Ring 0) en C++ que se comunica cun proceso en espazo de usuario mediante un buffer compartido, sen recorrer ás interfaces clásicas como IOCTL. O driver pode ler e escribir a memoria de calquera proceso, obter a dirección base de módulos e resolver direccións de forma dinámica.
Tecnoloxías
C++Windows Kernel (WDK)Ring 0 / Ring 3Shared MemoryMmCopyVirtualMemoryDPC / Timer / WorkItem
Arquitectura · 01
Visión xeral da arquitectura
O sistema consta de dous compoñentes: un driver en modo kernel (KM) e unha aplicación en modo usuario (UM). Ambos comparten un buffer de memoria sen usar DeviceIoControl. No canto diso, o proceso UM aloja un buffer con VirtualAlloc e publica o seu enderezo xunto a un valor 'magic' calculado en runtime. O driver escanea a memoria do proceso UM buscando ese magic, obtén o punteiro ao buffer e a partir de aí ambos comunícanse lendo e escribindo campos nesa estrutura compartida.
- Sen IOCTL: a comunicación baséase enteiramente nun buffer compartido en memoria do proceso UM.
- Magic dinámico: o valor de rendezvous calcúlase a partir do PID, de modo que non hai constantes recoñecibles no binario.
- Timer + DPC + WorkItem: o driver sondea o buffer de forma periódica usando un KTIMER e executa as peticións nun work item do pool de fíos do kernel.
Arquitectura · 02
DriverEntry — punto de entrada
O punto de entrada do driver é mínimo: só chama a SharedMemInit(), que crea un fío do sistema para non bloquear a carga do 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 baseado en PID
O valor de rendezvous ('magic') calcúlase en runtime a partir do PID do proceso UM. Tanto o driver como a aplicación UM usan a mesma fórmula, así que obteñen o mesmo resultado sen necesidade dunha constante hardcodeada no binario. Se alguén volca o binario e busca constantes coñecidas, non atopa 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 — atopando o buffer
O driver átase ao espazo de enderezos do proceso UM (KeStackAttachProcess) e percorre as súas rexións de memoria comprometidas con permisos de escritura. Busca o magic páxina a páxina; cando o atopa, le os 8 bytes seguintes, que conteñen o punteiro ao 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 e protocolo de peticións
A estrutura compartida ten tres campos atómicos de control (request_ready, response_ready, km_alive) e un bloque de datos (REQUEST_DATA). O fluxo é sinxelo: UM enche o bloque de datos e pon request_ready = 1. O driver detéctao, procesa a petición, escribe a resposta e pon response_ready = 1. UM agarda response_ready para ler o 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 e WorkItem — o bucle do driver
O driver non usa un bucle while(true). No canto diso, crea un KTIMER que dispara un DPC cada ~100 µs. O DPC encola un work item que comproba se UM escribiu unha petición. Se a hai, procésaa; se non, sae. Así o driver non bloquea ningún fío do kernel de forma permanente e mantén a latencia baixa.
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
Procesamento de peticións
Cando o work item detecta request_ready == 1, copia REQUEST_DATA desde a memoria do proceso UM usando MmCopyVirtualMemory, procesa a petición e escribe o resultado de volta. Soporta tres operacións: obter a base dun módulo (req_base), ler memoria (read) e 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 — o lado de usuario
A aplicación de usuario aloja o buffer con VirtualAlloc, calcula o magic co seu propio PID e agarda a que o driver sinalize km_alive = 1. A partir de aí pode enviar peticións enchendo REQUEST_DATA e poñendo request_ready = 1, agardando a resposta 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
}