Panorama#

El track de explotación binaria —3.6, 3.7, 3.8, 3.15 y 3.20— trabajó siempre en user-land: el espacio donde corren las aplicaciones, con sus privilegios acotados y su aislamiento por proceso. Este capítulo cruza la última frontera hacia abajo, al kernel-land: el núcleo del sistema operativo, el anillo de máximo privilegio, el código que arbitra la memoria, los procesos y el acceso a todo el hardware. La tesis que lo ordena es doble. Primero, que las primitivas de corrupción son las mismas que ya se conocen —desbordamientos, use-after-free, corrupción del asignador—, pero el objetivo cambia: no hay un return pointer de aplicación que secuestrar hacia una shell, sino que el éxito consiste en reescribir silenciosamente las credenciales del propio proceso atacante hasta root (UID 0) o SYSTEM. Segundo, que el kernel es un objetivo que no puede permitirse fallar: un error dentro de una zona sensible no produce un segfault recuperable como en 3.6, sino un panic —el sistema entero se detiene—, así que la explotación del núcleo es, más que ninguna otra, un problema de fiabilidad y limpieza.

Por qué importa: el kernel es el destino natural de la escalada de privilegios cuando todo lo demás falla —el «último recurso» que 3.9 describió—, y es la superficie que las mitigaciones de user-land (ASLR, DEP, las de 3.6-3.20) empujaron hacia arriba al cerrar la aplicación. Cuando el proceso ya no da más de sí, queda el núcleo con el que ese proceso habla constantemente a través de system calls y de los drivers de dispositivo. Como en el resto del track, cada técnica ofensiva se describe para reconocerla y cerrarla; el material se apoya en el tratado de referencia de Perla y Oldani, y el cierre púrpura recoge por qué la explotación de kernel se detecta mal en tiempo de ejecución y qué la ataca de raíz.

flowchart TD
  A["Bug en el kernel o en un driver\n(deref / overflow / UAF / race)"] --> B["1· Recolección\n/proc/kallsyms · sidt · infoleak\n(derrotar KASLR)"]
  B --> C["2· Detonación\ngrooming de SLUB / ventana de carrera\nsecuestro del flujo"]
  C --> D["3· Ejecución del payload"]
  D --> E["Linux: commit_creds(prepare_kernel_cred(NULL))\n→ UID 0"]
  D --> F["Windows: robar el Token de SYSTEM\nen EPROCESS → SYSTEM"]
  E --> G["Recuperación\nrestaurar el estado, evitar el panic/deadlock"]
  F --> G
  G --> H["Shell privilegiada\nsin colapsar la máquina"]

El modelo: por qué el kernel es un objetivo distinto#

Tres rasgos del núcleo definen todo lo que sigue. El primero es el espacio de direcciones compartido. En las arquitecturas clásicas, las entradas de la tabla de páginas del kernel se replican en la tabla de páginas de cada proceso: existe un único espacio virtual que abarca la porción del núcleo más las asignaciones de user-land del proceso. Para el atacante esto es una ventaja estructural enorme —«you are free to dereference a pointer inside it»—: puede alojar su shellcode o sus estructuras falsas en la memoria de usuario, que controla por completo y donde fija permisos sin trabas, y la vulnerabilidad del kernel solo necesita redirigir el flujo hacia esa zona ya preparada. (Esta comodidad es exactamente la que las mitigaciones modernas —SMEP/SMAP, KPTI— vinieron a cerrar, como se ve más abajo.)

El segundo es el costo del fallo. «A panicked target is a lost target»: un intento fallido no da una segunda oportunidad, tira la máquina y genera ruido. Por eso la explotación de kernel descarta la fuerza bruta —central en el bypass de ASLR de user-land de 3.15— y prioriza la fiabilidad: se perfila el objetivo antes de disparar y se prepara el entorno para que el comportamiento del núcleo sea determinista. Ayuda que la pila del kernel de un proceso se asigna una sola vez: recorrer el mismo camino de código produce el mismo layout de pila cada vez.

El tercero es el contexto de ejecución. El código del kernel corre en process context (a cuenta de un proceso que hizo una syscall, donde se puede dormir y esperar recursos) o en interrupt context (a cuenta de una interrupción de hardware, sin proceso subyacente, donde bloquearse está prohibido). La distinción no es académica: determina qué puede hacer el payload y por qué ciertas técnicas de detonación —forzar a un hilo a dormir— solo funcionan en el contexto adecuado. Para mapear la memoria de un proceso y sus permisos se usan cat /proc/<pid>/maps en Linux, pmap, o vmmap de Sysinternals en Windows.

La taxonomía de bugs del kernel#

Las clases de vulnerabilidad son las de siempre, con la vuelta de tuerca de que aquí viven en el anillo privilegiado. La desreferencia de punteros defectuosos —nulos, no inicializados o corruptos— es especialmente potente por el espacio compartido: un puntero no inicializado apunta a dead memory (restos de la pila) o, peor, a una dirección que el atacante puede mapear desde user-land. Un caso real en FreeBSD 8.0 asignaba dp->i_gid al primer elemento de un cr_groups sin inicializar, escribiendo un valor controlado en memoria residual. El integer overflow no ejecuta código por sí mismo: su daño es truncar la variable que dimensiona una asignación, de modo que el kernel reserva menos memoria de la que después copia —el mismo germen del desbordamiento de heap de 3.20, ahora contra el asignador del núcleo—.

Las race conditions ocupan un lugar central porque el kernel es masivamente concurrente (SMP, multi-core). El patrón arquetípico es el double-fetch: una rutina lee un valor de user-land con get_user para dimensionar un búfer y lo vuelve a leer para copiar los datos; si el atacante cambia el tamaño entre las dos lecturas, desborda el contenedor del kernel. La ventana temporal, minúscula, se ensancha con una técnica elegante: situar el búfer justo en el límite de dos páginas y forzar el desalojo de la segunda al disco, de modo que cuando el kernel la toca, el manejador de fallos de página suspende el hilo y regala una ventana enorme (demand paging usado como arma). El use-after-free —un puntero a memoria ya liberada, casi siempre por un contador de referencias desincronizado— es hoy la clase dominante, igual que en el heap de user-land.

No todo bug del kernel es un desbordamiento clásico: una porción enorme son fallos de diseño en la frontera kernel↔user. El caso canónico es udevd en Linux: su socket AF_UNIX exigía validar las credenciales del emisor (SO_PASSCRED), pero la interfaz AF_NETLINK paralela omitía esa comprobación, y un usuario sin privilegios podía enviar mensajes maliciosos que el demonio, corriendo como root, honraba. La lección transversal del capítulo: la superficie del kernel es toda entrada que cruza desde un contexto menos privilegiado —syscalls, ioctl, netlink, sistemas de archivos—, y validar en esa frontera es la misma tesis de «no confiar en la entrada» que recorre el manual.

La escalera de tres fases#

La explotación fiable del núcleo se organiza en tres etapas encadenadas.

1 · Recolección de información. Antes de disparar, hay que mapear el objetivo para reducir la incertidumbre que causaría un panic. En Linux, /proc/kallsyms expone las direcciones de las funciones y tablas del kernel; instrucciones de hardware como sidt (que vuelca el registro IDTR) revelan la ubicación de la tabla de interrupciones. Y cuando el kernel está aleatorizado (KASLR), la pieza clave es un infoleak: una vulnerabilidad secundaria que fuga un puntero del núcleo hacia user-land, derrotando la aleatorización —el mismo patrón info-leak-primero de 3.15, elevado al kernel—.

2 · Detonación. Es la fase de preparar la memoria para que el bug haga lo que se quiere. Contra corrupciones del asignador, se hace grooming: agotar la caché de objetos del núcleo (SLAB/SLUB) instanciando muchos objetos inocuos —por ejemplo, abriendo archivos repetidamente— para luego intercalar el objeto víctima (el vulnerable) inmediatamente antes del objeto objetivo (uno que contenga un puntero a función, como una estructura file_operations), garantizando que el desbordamiento pise los datos correctos. Es el mismo heap feng shui de 3.20, aplicado al asignador del kernel. Contra race conditions, se usan temporizadores de alta precisión y el truco de la falta de página para ensanchar la ventana.

3 · Ejecución y recuperación. El flujo secuestrado ejecuta el payload, que modifica las credenciales del proceso actual para darle privilegio total (los detalles por SO, abajo). Y entonces viene la parte que distingue al kernel de todo lo anterior: la recuperación. El payload debe devolver el núcleo a un estado consistente —liberar los locks o semáforos que retenía, restaurar los registros, ejecutar un retorno seguro a user-land (iretq con un stack frame fabricado, swapgs)— porque una limpieza defectuosa produce un colapso irrecuperable. El objetivo no es solo ejecutar código privilegiado, sino dejar la máquina viva y operativa, sin alertar. Un NOP sled grande (hasta 64 KB) en la memoria de usuario ayuda a atrapar saltos imprecisos y sube la fiabilidad del impacto.

Linux: de task_struct a commit_creds#

En Linux, escalar privilegios es reescribir las credenciales del proceso, y cómo se hace depende de la versión del núcleo —una ilustración perfecta de que las mitigaciones no cierran la técnica, la hacen más adaptativa—. En kernels anteriores a 2.6.29, las credenciales vivían embebidas en la estructura task_struct del proceso. El payload la localizaba enmascarando el puntero de pila con THREAD_SIZE para hallar thread_info, y desde ahí escaneaba la memoria buscando la secuencia de valores del UID del atacante, para sobrescribirlos con 0 y poner las capacidades (cap_effective) en 0xFFFFFFFF.

Los kernels modernos (post-2.6.29) separaron las credenciales en una estructura cred independiente, lo que eliminó el patrón heurístico. La respuesta del atacante es más limpia y es hoy el estándar: obtener de /proc/kallsyms las direcciones de prepare_kernel_cred y commit_creds, e invocar commit_creds(prepare_kernel_cred(NULL)) —dos funciones legítimas del núcleo que, encadenadas, fabrican y asignan al proceso un juego de credenciales con privilegio total—. Esa llamada de una línea es la firma de incontables exploits de LPE (local privilege escalation) de Linux.

La corrupción de heap del kernel se apoya en el asignador SLUB, que agrupa objetos del mismo tamaño y —a diferencia de SLAB— casi no guarda metadatos, salvo el puntero de lista libre (freelist) dentro del propio chunk liberado (idéntico al fd del tcache de 3.20). El atacante fija su proceso a una sola CPU con sched_setaffinity() —para operar sobre la lista local de ese procesador—, lee /proc/slabinfo para medir cuántos objetos faltan para agotar la caché kmalloc-n, hace exactamente esas asignaciones, y al detonar el desbordamiento pisa el freelist del objeto libre contiguo, secuestrando las asignaciones futuras. El caso didáctico del tratado es CVE-2009-1046, un off-by-two en set_selection() del subsistema de consola: dos bytes de más que corrompen el metadato del vecino.

Windows: robo de token en EPROCESS#

En Windows la mecánica difiere por la opacidad de las estructuras internas. El contexto de seguridad de un proceso cuelga de la estructura EPROCESS, que contiene —entre otras cosas— su tabla de objetos y su token de acceso. Hay dos caminos de escalada. El parcheo de SID localiza la EPROCESS (vía rutinas internas como PsGetCurrentProcess()), accede al token con PsReferencePrimaryToken() y reescribe el SID del propietario por el de NT AUTHORITY\SYSTEM (S-1-5-18), quitando banderas restrictivas. El robo de token (token stealing), más común, evita tocar los SID: escanea la EPROCESS buscando el campo Token —implementado como un EX_FAST_REF, donde los tres bits menos significativos del puntero son un contador de referencias— y, tras enmascarar esos bits para aislar la dirección pura, la sustituye por un puntero al token de un proceso ya privilegiado (típicamente el PID 4, System). En una línea conceptual: el proceso atacante pasa a apuntar al token de SYSTEM.

El vector de entrada más habitual son los drivers de terceros a través de DeviceIoControl(): una llamada desde user-land con un código de control (IOCTL) mal validado detona la corrupción al procesar el paquete de E/S (IRP). Pero secuestrar el flujo es solo la mitad. La recuperación en Windows es excepcionalmente delicada: si el atacante termina su proceso con ZwTerminateProcess() mientras el handle del driver explotado sigue abierto, el núcleo, al barrer la tabla de objetos (ExSweepHandleTable), intenta liberar un recurso aún en uso y entra en deadlock. Por eso el payload, antes de salir, recorre la ObjectTable de la EPROCESS, localiza la HANDLE_TABLE_ENTRY del driver y escribe ceros en sus primeros bytes, engañando a la rutina de limpieza. Es el equivalente Windows de la restauración de estado de Linux: sin ella, la máquina cae.

El kernel moderno: qué cambió desde el tratado#

El tratado de Perla y Oldani es de 2011 y establece los fundamentos; la práctica de 2026 acumuló una capa de mitigaciones y una superficie de ataque nueva que conviene nombrar. Las defensas estructurales que el capítulo final anticipaba ya son estándar:

  • SMEP/SMAP (Intel; PXN/PAN en ARM) atacan directamente la ventaja del espacio compartido: SMEP impide que el kernel ejecute código en páginas de user-land (mata el clásico salto al shellcode en memoria de usuario), y SMAP impide que lo lea/escriba sin desactivar explícitamente la protección. La respuesta del atacante es la misma que en user-land: no ejecutar código propio sino ROP dentro del kernel (encadenar gadgets del propio núcleo), el puente directo con 3.7.
  • KASLR aleatoriza la base del kernel, así que —como en 3.15— hace falta un infoleak previo; y KPTI (Kernel Page-Table Isolation, la mitigación de Meltdown) separa las tablas de páginas de kernel y usuario, reduciendo lo que un infoleak puede alcanzar. En Windows, HVCI/VBS (seguridad basada en virtualización) y PatchGuard (KPP) elevan mucho el costo de parchear el núcleo.
  • La firma obligatoria de drivers empujó la técnica dominante de hoy: BYOVD (Bring Your Own Vulnerable Driver) — en vez de escribir un rootkit nuevo, el atacante carga un driver legítimo y firmado pero vulnerable y lo explota para ganar el kernel. Es el puente con los rootkits y su detección (5.12) (cargar código en el anillo 0), y una de las vías de instalación de bootkits.
  • La superficie Linux moderna se corrió a subsistemas nuevos: eBPF, io_uring y netfilter son fuente recurrente de LPE, y objetivos de escritura como modprobe_path (sobrescribir la ruta del cargador de módulos para ejecutar un binario propio como root) reemplazaron a los viejos punteros de función. Casos como Dirty Pipe (CVE-2022-0847) y DirtyCred mostraron que un único bug de gestión de páginas o de reuso de objetos cred sigue bastando para llegar a root en kernels endurecidos.

Bajo el capó: VBS, HVCI y por qué el kernel moderno empuja al BYOVD#

La sección anterior nombró HVCI/VBS y PatchGuard como hechos; vale abrir esa caja negra una vez, porque su mecanismo explica el cambio más grande de la ofensiva de kernel de la última década —por qué murió el clásico «cargar un rootkit / inyectar shellcode en anillo 0» y por qué el BYOVD y los ataques data-only ocuparon su lugar— y porque es la misma arquitectura de hipervisor que en 4.5 esconde los secretos de LSASS.

VBS y los VTL: un árbitro por debajo del kernel. La Virtualization-Based Security (VBS) usa las extensiones de virtualización del procesador para correr un hipervisor delgado debajo del sistema operativo, y parte la ejecución en Virtual Trust Levels: VTL0 —el «mundo normal», donde vive el kernel de Windows entero y todos sus drivers— y VTL1, el «mundo seguro». La inversión es la clave: el kernel de anillo 0, históricamente la autoridad máxima, pasa a ser invitado de una capa más privilegiada que no puede ver ni modificar. Los permisos de memoria de VTL0 ya no los decide la tabla de páginas del propio kernel sino un segundo nivel de traducción (SLATEPT en Intel, NPT en AMD—) que el hipervisor gobierna desde VTL1. Es exactamente la arquitectura que en 4.5 pone los secretos de LSASS en un proceso aislado en VTL1, fuera del alcance de un volcado de VTL0: una sola frontera, dos aplicaciones.

HVCI: integridad de código forzada desde abajo (W^X en el kernel). HVCI (Hypervisor-Enforced Code Integrity) usa esa capa SLAT para imponer un invariante que el kernel jamás podría garantizar contra sí mismo: ninguna página de kernel es escribible y ejecutable a la vez (W^X), y una página solo se vuelve ejecutable si su contenido se validó como código firmado. Como los permisos SLAT los posee el hipervisor en VTL1, ni siquiera código corriendo con privilegio de anillo 0 en VTL0 puede marcar una página como RWX, ni parchear una página de código existente (es de solo lectura), ni ejecutar un búfer que acaba de escribir. Ese es el mecanismo detrás del lacónico «HVCI eleva el costo»: no detecta la inyección, la vuelve imposible —no hay dónde correr código propio dentro del núcleo—.

Por eso el ataque se corrió a BYOVD y a los data-only. Con HVCI activo, las dos técnicas clásicas de anillo 0 mueren a la vez: no se puede cargar un driver sin firmar (Driver Signature Enforcement, DSE) y no se puede inyectar ni ejecutar código propio ni siquiera desde uno firmado (HVCI). Lo que sobrevive es lo que toca datos, no código. De ahí las dos formas dominantes de hoy: (1) BYOVD —cargar un driver legítimo, firmado pero vulnerable, y usar su fallo como primitiva de lectura/escritura arbitraria, no para correr shellcode—; y (2) los data-only attacks —corromper estructuras de datos del kernel (el mismo robo de token en EPROCESS o el commit_creds de las secciones anteriores, o las entradas de la tabla de páginas) para escalar sin ejecutar jamás código del atacante—. Esto afina el «la firma empujó al BYOVD» del capítulo: es el par DSE + HVCI el que lo fuerza, y es lo que explica por qué el payload moderno es una primitiva de escritura y no un salto a shellcode. Es también, exactamente, lo que el rodeo de PPL de 4.5 necesita —un driver firmado para una escritura en kernel—: los dos capítulos se encuentran aquí.

PatchGuard: detección por crash, no prevención. Junto a HVCI, PatchGuard (KPP, Kernel Patch Protection) verifica periódicamente el checksum de las estructuras que un rootkit clásico manipularía —la SSDT, la IDT, la GDT, ciertos MSR críticos (LSTAR), las secciones de código del kernel, la tabla de funciones invertida— y, ante cualquier discrepancia, fuerza un bugcheck 0x109 CRITICAL_STRUCTURE_CORRUPTION. Es una garantía de naturaleza distinta a la de HVCI: no previene, detecta por colapso controlado, y corre desde contextos diferidos y ofuscados precisamente para que un atacante no encuentre ni neutralice fácilmente al verificador. El corolario púrpura, que 5.12 desarrolla: un 0x109 no es un BSOD azaroso —es PatchGuard anunciando que algo modificó una estructura protegida—, y hay que tratarlo como un posible incidente de integridad de kernel, no descartarlo como una pantalla azul inestable.

La asimetría con Linux, y el techo de todo esto. Linux no trae un equivalente obligatorio de VBS/HVCI activado de fábrica: su endurecimiento (lockdown, módulos firmados, IMA, CONFIG_STRICT_KERNEL_RWX) es opcional y vive dentro del kernel que protege —de modo que una escritura a nivel de kernel a menudo puede desactivarlo—, y esa es parte de la razón por la que la LPE de kernel en Linux sigue siendo comparativamente más permisiva con el reuso de código. Y hasta VBS tiene su techo: mueve el ancla de confianza hacia el hipervisor, así que el objetivo máximo se mueve con ella —un fallo en el hipervisor o en la interfaz VTL0↔VTL1, o un compromiso de firmware por debajo de VBS (el bootkit que 5.12 rastrea), derriba el esquema entero—. La frontera es más fuerte, no infinita; la misma lógica out-of-band que cierra el capítulo aplica a la propia VBS.

Defensa y detección#

Como con el heap de user-land de 3.20, la incomodidad de fondo es que la explotación de kernel se detecta muy mal en tiempo de ejecución: la corrupción ocurre dentro del núcleo, sin crear un proceso ni tocar el disco, y para cuando aparece una señal observable el atacante ya es root. Por eso la defensa es sobre todo de plataforma y preventiva, con la telemetría como red de seguridad tardía:

  • Mitigaciones de hardware y kernel al día: SMEP/SMAP/PAN, KASLR + KPTI, y en Windows HVCI/VBS, PatchGuard y HVCI bloqueando drivers vulnerables (la Microsoft vulnerable driver blocklist contra BYOVD). Ninguna cierra la clase, pero cada una fuerza al atacante a encadenar más bugs (infoleak + ROP), lo que sube el costo y la probabilidad de fallar ruidosamente.
  • Reducir la superficie: restringir la lectura de /proc/kallsyms y /proc/slabinfo (kptr_restrict, kernel.perf_event_paranoid), deshabilitar la carga de módulos no esenciales, lockdown del kernel, y minimizar los drivers de terceros —cada DeviceIoControl de un driver dudoso es superficie—. En Windows, activar la lista de bloqueo de drivers vulnerables ataca BYOVD de raíz.
  • Endurecer la asignación: SLAB_FREELIST_HARDENED y SLAB_FREELIST_RANDOM en Linux ofuscan y aleatorizan el freelist (la contramedida directa al grooming de SLUB, hermana del safe-linking de 3.20); las poison values de listas enlazadas deben quedar fuera del rango mapeable por un usuario —una lección que el propio LIST_POISON1 (0x00100100) enseñó al ser mapeable en 32 bits y convertir una defensa en vector—.
  • Detección: la señal más honesta es el crash. Un pico de kernel panics o bugchecks (BSOD) sobre una máquina, o core dumps del kernel, correlaciona con un exploit que falló la fiabilidad; el análisis forense del volcado ata el evento a la técnica. Un bugcheck 0x109 CRITICAL_STRUCTURE_CORRUPTION es un caso especial: no es azar, es PatchGuard delatando una modificación de estructura protegida (ver «Bajo el capó»), y debe tratarse como incidente de integridad de kernel —el mismo criterio que 5.12—. En runtime, un EDR de kernel puede alertar de la carga de un driver firmado pero vulnerable conocido (BYOVD — la vía más detectable), de escrituras a modprobe_path, o de la anomalía posterior: un proceso cuyo UID/token no concuerda con el de su creador (la huella del robo de token/commit_creds), que se cruza con la detección de ejecución anómala del lado blue (correlación en el SIEM, Event ID 4688 / Sysmon 1, auditd). El aislamiento por virtualización (contenedores con seccomp/gVisor, VMs) reduce el blast radius —aunque, como recordó el cierre del tratado, mueve el objetivo máximo al hipervisor, cuyos fallos de emulación permiten un VM escape que compromete todas las máquinas invitadas a la vez—.

En síntesis, la explotación de kernel es el techo del track binario: reutiliza las primitivas de 3.6-3.20 pero contra el anillo privilegiado, con la fiabilidad como restricción dominante y la escalada a UID 0/SYSTEM como premio. Y confirma el patrón de todo el track —una mitigación no cierra la explotación, la mueve—: SMEP empujó al ROP de kernel, KASLR al infoleak, la firma de drivers al BYOVD. Para el defensor, el mensaje es que aquí la prevención de plataforma pesa más que la detección, porque cuando el kernel cae, el sensor que debía avisar corre sobre el sistema que el atacante ya controla —la misma lección out-of-band de 3.7—.

Referencias#

  • red-infra/kernel-exploitation/cap-01,02,03,04,06,09 — Enrico Perla & Massimiliano Oldani, A Guide to Kernel Exploitation: Attacking the Core (Syngress, 2011): el tratado de referencia. Modelo del kernel, taxonomía, la escalera de tres fases, Linux (commit_creds, SLUB) y Windows (EPROCESS, robo de token), evolución de ataque y defensa.
  • MITRE ATT&CK — T1068 Exploitation for Privilege Escalation y T1547.006 Kernel Modules and Extensions; CWE-416 (Use After Free), CWE-190 (Integer Overflow), CWE-362 (Race Condition), CWE-476 (NULL Pointer Dereference).
  • CVE-2022-0847 (Dirty Pipe) y la técnica DirtyCred — ejemplos modernos de LPE por un único bug en kernels endurecidos.
  • Loldrivers.io y la Microsoft Vulnerable Driver Blocklist — el catálogo de drivers vulnerables firmados que habilita (y permite bloquear) el BYOVD.
  • Microsoft — Virtualization-based Security (VBS) y Hypervisor-Enforced Code Integrity (HVCI): los VTL, la traducción SLAT y la integridad de código forzada desde el hipervisor; base compartida con Credential Guard de 4.5. Nativo para la capa post-2011 que el tratado de Perla & Oldani no cubre.