Saltar a contenido

Clase 5 — Database estática multi-módulo en IDA, APIs vulnerables de memoria física y pool spray vía thread name

Resumen: Clase corta y miscelánea con tres bloques. Primero, cómo armar una IDB estática que incluya varios módulos a la vez (HEVD + el cliente user-mode + ntoskrnl + lo que haga falta) sin tener una base separada por cada uno: attach al user-mode con backend WinDbg, switch de contexto, .reload /user, marcar módulos como Loader y usar Take Memory Snapshot. Segundo, repaso de APIs de kernel que mapean memoria física (MapViewOfSection envuelto por drivers tipo n.sys, MmMapIoSpace, MmGetPhysicalAddress, MmMapLockedPages*) — qué chequeos tienen que estar presentes para considerar el driver parchado y por qué la falta de cualquiera abre token stealing por la vía física. Tercero, pool spray vía thread name manipulation como alternativa a pipes para controlar el inicio del chunk en UAFs.

← Volver al Módulo 5 · ← Clase 4


Tabla de Contenidos

  1. Contexto de la clase
  2. Por qué armar una IDB estática con varios módulos
  3. Attach al proceso user-mode con backend WinDbg
  4. Switch de contexto y .reload /user
  5. Loader vs Debugger en la lista de segmentos
  6. Marcar módulos como Loader y prepararlos
  7. Take Memory Snapshot — bakear los módulos en la IDB
  8. Caveats — esta IDB es para análisis estático
  9. APIs vulnerables — MapViewOfSection envuelto por drivers
  10. MmMapIoSpace y familia — mismo patrón en otra API
  11. Checklist de análisis estático de APIs de memoria física
  12. Cómo detectar en runtime si el driver está parchado
  13. Token stealing vía memoria física
  14. Pool spray vía thread name manipulation
  15. Mitigaciones que complican estas técnicas
  16. Cheat sheet de la clase

1. Contexto de la clase

Clase corta (~30 min) con tres temas sueltos que no forman una unidad pero todos quedan referenciados en demos futuras:

Bloque Para qué sirve
IDB estática multi-módulo Trabajar offline sobre HEVD + el cliente user-mode + ntoskrnl desde una sola base de IDA
APIs vulnerables de memoria física Identificar drivers explotables tipo n.sys durante el análisis estático
Thread name pool spray Alternativa a pipes cuando el UAF necesita controlar el primer qword del chunk

Los PDFs con las APIs vulnerables y la técnica de thread names quedan colgados en el Moodle del curso.


2. Por qué armar una IDB estática con varios módulos

Cuando se ataca un driver kernel típicamente hay tres binarios involucrados al mismo tiempo:

Cliente user-mode  ->  driver kernel (HEVD.sys)  ->  ntoskrnl.exe

Si se abre cada uno en su propia IDB se pierde la posibilidad de buscar símbolos, xrefs y estructuras desde la misma database. La idea de la clase es bakear todo en una sola IDB estática para hacer análisis cruzado entre user-mode y kernel sin tener que abrir tres ventanas.

Importante: la IDB resultante sirve solo para análisis estático. No se usa para debug live porque las direcciones de user-mode cambian en cada arranque del programa.


3. Attach al proceso user-mode con backend WinDbg

El punto de partida es una sesión de IDA con HEVD ya cargado (estático), conectada a la VM target con backend de WinDbg en modo kernel. Sobre eso se hace attach al proceso user-mode que carga HEVD y dispara el spray de pipes (el cliente de la Clase 4, por ejemplo console_application7.exe).

Pasos:

1. En la VM target: arrancar el cliente con la opción que pausa en el entry point
   (la mayoría de los debuggers lo hace por default).
2. En IDA (host): Debugger -> Attach to process -> elegir WinDbg como debugger.
3. Process options -> asegurarse de estar en modo kernel.
4. Attach to process -> seleccionar el proceso del cliente.

Tip: muchos exploits tienen un getchar() o system("pause") al principio justamente para que dé tiempo a hacer el attach antes de que se carguen los módulos relevantes. Si el programa ya pasó por el entry point, los módulos ya están mapeados en memoria, así que alcanza con dejarlo pausado donde haya parado.


4. Switch de contexto y .reload /user

Cuando se hace kernel debugging, el contexto activo no necesariamente coincide con el proceso del cliente. Para inspeccionar sus módulos hay que cambiar al contexto correcto.

0: kd> !dml_proc                          ; lista de procesos con links clickeables
0: kd> .process /i <EPROCESS_del_cliente> ; switch al contexto del cliente
0: kd> g                                  ; necesario para que el switch tome efecto

Una vez que rompe, el contexto ya es el del cliente. Falta refrescar los módulos de user porque cada arranque del proceso usa direcciones distintas:

0: kd> .reload /user

Por qué hace falta .reload /user

Tipo de módulo Política de recarga
Kernel (ntoskrnl, HEVD.sys, npfs.sys, ...) Misma posición en todos los procesos. Se recargan una vez y quedan.
User-mode (cliente, ucrtbase, ...) Posición específica por proceso. Hay que recargar al cambiar de contexto.

Si se hace lm antes del .reload /user aparecen los módulos de user del proceso anterior (típicamente System), no los del cliente. Después del reload, lm debería listar console_application7, ucrtbase, KERNEL32, etc.


5. Loader vs Debugger en la lista de segmentos

En IDA, cada módulo cargado puede estar marcado como Loader o Debugger:

Marca Significado Persiste estático
L (Loader) Módulo cargado por el loader de IDA al abrir el binario Sí, se guarda en la IDB
D (Debugger) Módulo que apareció solo porque el debugger lo encontró en runtime No, se pierde al cerrar

Para ver la lista:

View -> Open Subviews -> Segments

Cada segmento muestra su marca. En una sesión típica de kernel debugging:

  • Los segmentos de HEVD (.text, .idata, .rdata, ...) están como L porque HEVD se abrió como input principal.
  • Los segmentos de console_application7, ucrtbase, ntoskrnl, etc. están como D porque vinieron del attach al debugger.

Para que un módulo D pase a formar parte de la IDB estática hay que convertirlo a L antes de tomar el snapshot.


6. Marcar módulos como Loader y prepararlos

El flujo recomendado para cada módulo extra que se quiera incluir:

1. Module List (Debugger -> Debugger windows -> Module list):
   - Seleccionar el módulo.
   - Click derecho -> Load debug information / symbols (que cargue PDB si existe).
   - Click derecho -> Analyze module (que IDA termine de marcar funciones).

2. Segments view:
   - Encontrar los segmentos del módulo.
   - Edit segment -> cambiar la clase/flag a Loader (L) en lugar de Debugger (D).

Importante: dejar que el análisis termine antes de guardar. Si se hace el snapshot mientras IDA todavía está procesando funciones, los módulos quedan a medio analizar en la IDB resultante.

Módulos que típicamente conviene incluir para esta demo:

Módulo Por qué
HEVD.sys El driver vulnerable
console_application7 El cliente user-mode (Clase 4)
ucrtbase.dll / vcruntime Runtime de C usado por el cliente
ntoskrnl.exe Para resolver símbolos de syscalls y estructuras kernel
npfs.sys Si la demo usa pipes, conviene tener el reclaimer a mano

7. Take Memory Snapshot — bakear los módulos en la IDB

Una vez que los módulos relevantes están marcados como Loader y analizados, se ejecuta:

File -> Take Memory Snapshot

Aparece un diálogo con varias opciones. Las importantes:

Opción Qué hace
All segments Mete todos los segmentos vistos por el debugger (incluyendo D). Genera una IDB enorme.
Current segment Solo el segmento donde está el cursor. Poco útil.
Other segments Los marcados como L que estén presentes en memoria. Es la opción correcta para incluir los módulos elegidos.

Después del snapshot:

File -> Save (o Ctrl+W)

Crítico: si no se guarda, el snapshot se pierde al cerrar IDA. Conviene hacer una copia previa de la IDB original antes del snapshot, porque la IDB estática resultante deja de servir para debugging live.

Para verificar:

View -> Open Subviews -> Segments     ; los módulos extra ya aparecen como L
View -> Open Subviews -> Functions    ; las funciones de los módulos extra ya aparecen listadas

8. Caveats — esta IDB es para análisis estático

La IDB resultante no se debe reutilizar para debugging live porque:

Cuando arranca el cliente, las direcciones de user-mode son distintas a las
del snapshot. Si se hace attach con esta IDB, los módulos del cliente
quedan duplicados: el viejo (bakeado en la IDB) + el nuevo (vivo en memoria).

Estrategia recomendada:

IDB Uso
Una IDB "vacía" original de HEVD Debugging live, attaches, hooks
Una IDB "static-all" con todos los módulos Análisis estático, búsqueda de gadgets, xrefs cruzadas

Alternativa — cargar PDB en una dirección específica

Hay una variante: en vez de tomar snapshot, cargar el PDB manualmente y setearle como base la dirección que tiene el módulo en la sesión live. Si los offsets coinciden, las direcciones de la IDB matchean las de runtime y se puede usar para ambas cosas. Es más laborioso pero conserva la posibilidad de debugging.


9. APIs vulnerables — MapViewOfSection envuelto por drivers

El segundo bloque de la clase repasa drivers que exponen memoria física a user-mode vía wrappers inseguros. Ejemplo concreto: el driver n.sys (ASUS, parcheado hace tiempo) implementaba una función MapPhysicalMemoryToLinearSpace que terminaba llamando a MapViewOfSection con parámetros controlados por el usuario.

Patrón vulnerable

NTSTATUS MapPhysicalMemoryToLinearSpace(
    PHYSICAL_ADDRESS BusAddress,    /* viene de user-mode    */
    ULONG            Length,        /* viene de user-mode    */
    PVOID*           VirtualAddress /* devuelto a user-mode  */
)
{
    PHYSICAL_ADDRESS translated;
    HANDLE           sectionHandle;
    PVOID            mapped = NULL;
    SIZE_T           viewSize = Length;

    /* Traduce la bus address a una dirección física utilizable */
    HalTranslateBusAddress(/* ... */, BusAddress, /* ... */, &translated);

    /* Abre la \Device\PhysicalMemory como sección */
    ZwOpenSection(&sectionHandle, SECTION_ALL_ACCESS, /* ... */);

    /* Mapea la sección física en el espacio virtual del caller */
    ZwMapViewOfSection(
        sectionHandle,
        ZwCurrentProcess(),
        &mapped,
        0, 0,
        &translated,
        &viewSize,
        ViewShare,
        0,
        PAGE_READWRITE
    );

    *VirtualAddress = mapped;
    return STATUS_SUCCESS;
}

Si el IOCTL que llama a esta función no chequea nada, user-mode puede mapear cualquier página física en su propio proceso y leerla/escribirla como memoria virtual normal.

Qué buscar en strings al hacer reversing

"MapViewOfSection"
"ZwMapViewOfSection"
"NtMapViewOfSection"
"HalTranslateBusAddress"
"\\Device\\PhysicalMemory"

Aparecen en View -> Open Subviews -> Strings. Son señales fuertes de que el driver toca memoria física.


10. MmMapIoSpace y familia — mismo patrón en otra API

Otra API frecuente con el mismo problema es MmMapIoSpace (y su variante moderna MmMapIoSpaceEx). Esta sí es kernel-only, pero un IOCTL que la llame con bus address + size controlados desde user habilita exactamente la misma primitiva.

PVOID MmMapIoSpace(
    PHYSICAL_ADDRESS PhysicalAddress,  /* <- user-controlled */
    SIZE_T           NumberOfBytes,    /* <- user-controlled */
    MEMORY_CACHING_TYPE CacheType
);

El puntero devuelto es virtual-kernel, así que para llegar a explotación real hay que combinarlo con alguna primitiva de lectura/escritura kernel. Pero el bug es el mismo: cualquier dirección física mapeada → cualquier estructura kernel accesible.

Otras APIs con potencial parecido

API Riesgo si user controla los argumentos
MmMapIoSpace / MmMapIoSpaceEx Map físico → virtual-kernel
MmMapLockedPages / MmMapLockedPagesSpecifyCache Map de un MDL controlado
IoAllocateMdl + MmBuildMdlForNonPagedPool Construir MDL desde dirección arbitraria
MmGetPhysicalAddress Devuelve PA de una VA controlada (útil como primitiva inversa)
ZwMapViewOfSection sobre \Device\PhysicalMemory Lo mismo que n.sys

11. Checklist de análisis estático de APIs de memoria física

Cuando se ve una de estas APIs en un IOCTL, hay que verificar todos los chequeos siguientes. La ausencia de cualquiera basta para considerar el driver vulnerable.

Chequeo Cómo aparece en IDA Si falta
ExGetPreviousMode() == KernelMode Llamada a ExGetPreviousMode antes del map User-mode puede invocar la API
SeSinglePrivilegeCheck(SeDebugPrivilege, ...) Comparación contra un LUID conocido o llamada a Se*Privilege* Cualquier user no-admin puede pedir el map
Range validation low Comparación contra PAGE_SIZE o constante chica User puede mapear la página 0 (vectores, KUSER_SHARED_DATA, etc.)
Range validation high Comparación contra un límite máximo User puede pedir ranges enormes / out-of-range
Size cap Comparación contra Length o NumberOfBytes User puede pedir un mapeo gigantesco
Driver no blocklisted Driver firmado, no en HVCI/WDAC blocklist El driver puede cargarse en sistemas modernos

Patrón "seguro" típico

if (ExGetPreviousMode() != KernelMode) {
    if (!SeSinglePrivilegeCheck(SeDebugPrivilege, UserMode)) {
        return STATUS_ACCESS_DENIED;
    }
}

if (BusAddress.QuadPart < PAGE_SIZE) {
    return STATUS_INVALID_PARAMETER;
}

if (Length > MAX_ALLOWED_MAP_SIZE) {
    return STATUS_INVALID_PARAMETER;
}

/* Recién acá se llama a MapViewOfSection / MmMapIoSpace */

12. Cómo detectar en runtime si el driver está parchado

La forma más rápida de saber si una versión del driver está parchada es intentar mapear la página 0x1000 desde un cliente user-mode:

PA solicitada: 0x1000  (segunda página física)
Resultado Interpretación
El driver devuelve un puntero virtual No parchado — la validación de rango está ausente
El driver devuelve ACCESS_DENIED o falla Parchado o nunca fue vulnerable

Por qué 0x1000 y no 0: la página 0 tiene cosas como vectores de interrupción y KUSER_SHARED_DATA. Algunos drivers explícitamente filtran 0 pero no las otras páginas bajas. Probar 0x1000 evita el falso positivo de un driver que solo bloquea la página cero.


13. Token stealing vía memoria física

Cuando se logra mapear memoria física desde user-mode, el camino clásico a SYSTEM es:

1. Encontrar la PA del EPROCESS actual.
2. Recorrer ActiveProcessLinks (en física) hasta llegar al EPROCESS de PID 4 (System).
3. Leer EPROCESS.Token de System.
4. Escribir ese token en EPROCESS.Token del proceso actual.

El problema — VA → PA sin debugger

En la Clase 4 se usó !pte desde WinDbg para hacer la traducción. Un atacante no tiene !pte. Las opciones son:

Método Cómo funciona
Carving Empezar a leer desde 0x1000 y hacia arriba, buscar pool tags conocidos (Proc) hasta encontrar el EPROCESS
API helper Algunos drivers exponen también una primitiva tipo VirtualToPhysical — combinada con la de map cierra el círculo
Self-referencing PDE/PT entries En builds viejas con offsets conocidos, leer las tablas de páginas directamente

Importante: el carving "ciego" desde 0x1000 se complica en builds modernas porque algunos drivers de pool/memoria devuelven basura o cuelgan la máquina al leer ciertas regiones físicas. No es tan simple como dumpear linealmente.

Acceso "casi total"

Una vez que se controla un mapeo físico arbitrario:

  • Se puede leer/escribir cualquier estructura kernel (excepto las protegidas por el hypervisor).
  • No se puede tocar el hypervisor en sí (las páginas del hypervisor están protegidas por VBS).
  • Toda la zona del kernel "normal" (ntoskrnl, drivers, pool, heaps) queda accesible.

14. Pool spray vía thread name manipulation

Tercer bloque: una alternativa al pool spray con pipes (Clase 4). Sirve cuando el UAF necesita controlar el primer qword del chunk y el offset 0 de un pipe NpFr quedaba ocupado por metadata de NPFS.

Mecánica

1. CreateThread() varias veces -> guardar handles.
2. Por cada thread: NtSetInformationThread(handle, ThreadNameInformation, &name, sizeof(name)).
3. Internamente, Windows aloca un chunk kernel para guardar la UNICODE_STRING del nombre.
4. El contenido del chunk es la string que pasaste, controlable byte por byte (con la limitación Unicode).

APIs involucradas:

API Rol
NtSetInformationThread El syscall que recibe el ThreadNameInformation
ThreadNameInformation El info class que dispara la allocation del nombre
SetThreadDescription (Win32) Wrapper user-mode equivalente, más ergonómico

Ventaja sobre pipes

Pipe NpFr:                Thread name allocation:
  +0x00 metadata NPFS       +0x00 bytes controlados (string Unicode)
  +0x?? bytes del usuario   +0x?? mas bytes controlados

El chunk del thread name empieza directamente con bytes controlados, así que un UAF con Callback en offset 0x00 se puede explotar más limpio.

Desventajas

Limitación Detalle
Unicode La string es UTF-16. Los bytes que se controlan vienen intercalados con ceros, salvo que se construyan secuencias específicas.
Volumen No se pueden crear cientos de miles de threads sin volver la VM inmanejable. Mucho menos que con pipes.
Contigüidad En Win10 pre-21H2 las allocations salían más o menos contiguas y el método era muy reliable. En Win10/11 modernos hay randomización y se necesita más volumen para garantizar reclaim.

Esqueleto en C

#include <windows.h>

#define NUM_THREADS 2000

DWORD WINAPI ThreadStub(LPVOID arg) {
    /* El thread no hace nada, solo existe para que se le pueda setear el nombre */
    Sleep(INFINITE);
    return 0;
}

int spray_thread_names(const WCHAR* payload) {
    int created = 0;

    for (int i = 0; i < NUM_THREADS; i++) {
        HANDLE h = CreateThread(NULL, 0, ThreadStub, NULL, 0, NULL);
        if (!h) {
            break;
        }

        /* SetThreadDescription dispara la allocation interna del nombre */
        if (FAILED(SetThreadDescription(h, payload))) {
            CloseHandle(h);
            continue;
        }

        created++;
    }

    return created;
}

Comparativa con pipes (Clase 4)

Primitive Control desde offset 0 Volumen tolerable Mejor caso
Pipe NpFr No (metadata NPFS al inicio) Muy alto (decenas de miles) Pool grooming, pool overflow
Thread name Limitado UAF con callback/vtable al inicio del chunk

15. Mitigaciones que complican estas técnicas

Mención breve al final de la clase a las mitigaciones que pegan especialmente fuerte sobre estos vectores:

Mitigación Efecto
HVCI (Hypervisor-protected Code Integrity) Las primitivas de escritura kernel quedan muy restringidas. Pool spray sigue siendo posible pero el siguiente paso (escribir código ejecutable, hookear, etc.) se complica mucho.
VBS (Virtualization-Based Security) La región del hypervisor queda fuera del alcance incluso con mapeo físico arbitrario.
Kernel Shadow Stack / CET En procesadores que lo soportan, ROP en kernel queda bloqueado. Hardware todavía minoritario, pero es la dirección a futuro.
WDAC / Driver blocklist Drivers vulnerables conocidos (n.sys viejo, MSI Afterburner, etc.) no cargan en sistemas modernos sin downgrade.

16. Cheat sheet de la clase

IDB estática multi-módulo

Attach al user-mode (backend WinDbg, modo kernel):
  Debugger -> Attach -> WinDbg -> Process options -> kernel mode -> Attach

Cambiar contexto al proceso del cliente:
  !dml_proc
  .process /i <EPROCESS>
  g
  .reload /user

Convertir módulo Debugger -> Loader:
  View -> Open Subviews -> Segments
  (Edit segment) class -> L
  Debugger windows -> Module list -> Load symbols + Analyze

Bakear en la IDB:
  File -> Take Memory Snapshot -> Other segments
  File -> Save

Reglas de uso de la IDB resultante

Regla Motivo
Una IDB para estático, otra para live Las direcciones user-mode cambian en cada arranque
Hacer copia antes de tomar snapshot El snapshot baka direcciones y no se puede revertir limpio
Analizar antes de guardar Si IDA no terminó el análisis, las funciones quedan a medias

APIs de memoria física a buscar

MmMapIoSpace / MmMapIoSpaceEx
MmMapLockedPages / MmMapLockedPagesSpecifyCache
MmBuildMdlForNonPagedPool
MmGetPhysicalAddress
ZwMapViewOfSection sobre \Device\PhysicalMemory
HalTranslateBusAddress

Chequeos que tienen que estar presentes

ExGetPreviousMode() == KernelMode
SeSinglePrivilegeCheck(SeDebugPrivilege, ...)
BusAddress >= PAGE_SIZE
Length <= MAX_ALLOWED_SIZE
Driver no blocklisted

Detección rápida de driver parchado

Cliente user-mode -> mandar IOCTL con PA = 0x1000
  Si devuelve puntero virtual -> NO parchado
  Si falla con ACCESS_DENIED  -> parchado o no vulnerable

Pool spray comparativo

Técnica API Control de offset 0 Volumen
Pipe NpFr CreatePipe + WriteFile No Muy alto
Thread name SetThreadDescription / NtSetInformationThread + ThreadNameInformation Limitado

Recursos

Recurso URL
MmMapIoSpace docs https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-mmmapiospace
ZwMapViewOfSection docs https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-zwmapviewofsection
ExGetPreviousMode docs https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-exgetpreviousmode
SetThreadDescription docs https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-setthreaddescription
NtSetInformationThread (THREADINFOCLASS) https://learn.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntsetinformationthread
IDA Pro — Take memory snapshot https://hex-rays.com/products/ida/support/idadoc/1614.shtml
Microsoft driver blocklist https://learn.microsoft.com/en-us/windows/security/application-security/application-control/windows-defender-application-control/microsoft-recommended-driver-block-rules
LOLDrivers (BYOVD database) https://www.loldrivers.io/
HEVD repo https://github.com/hacksysteam/HackSysExtremeVulnerableDriver