← Volver ao portfolio

PROXECTO · KERNEL / DRIVER

Driver de kernel en Windows

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
}