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 (MapViewOfSectionenvuelto por drivers tipon.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¶
- Contexto de la clase
- Por qué armar una IDB estática con varios módulos
- Attach al proceso user-mode con backend WinDbg
- Switch de contexto y
.reload /user - Loader vs Debugger en la lista de segmentos
- Marcar módulos como Loader y prepararlos
- Take Memory Snapshot — bakear los módulos en la IDB
- Caveats — esta IDB es para análisis estático
- APIs vulnerables —
MapViewOfSectionenvuelto por drivers MmMapIoSpacey familia — mismo patrón en otra API- Checklist de análisis estático de APIs de memoria física
- Cómo detectar en runtime si el driver está parchado
- Token stealing vía memoria física
- Pool spray vía thread name manipulation
- Mitigaciones que complican estas técnicas
- 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:
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()osystem("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:
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:
Cada segmento muestra su marca. En una sesión típica de kernel debugging:
- Los segmentos de HEVD (
.text,.idata,.rdata, ...) están comoLporque HEVD se abrió como input principal. - Los segmentos de
console_application7,ucrtbase,ntoskrnl, etc. están comoDporque 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:
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:
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(§ionHandle, 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:
| 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é
0x1000y no0: la página0tiene cosas como vectores de interrupción yKUSER_SHARED_DATA. Algunos drivers explícitamente filtran0pero no las otras páginas bajas. Probar0x1000evita 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
0x1000se 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 | Sí | 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 |
Sí | Limitado |