Use-After-Free: La Vulnerabilidad Favorita de los Exploits Modernos
Análisis técnico de Use-After-Free (UAF): cómo funciona la gestión de heap, por qué los navegadores son el objetivo principal, CVEs reales explotados in the wild y las mitigaciones que intentan frenar la clase de vulnerabilidad que reemplazó al buffer overflow como reina de los exploits.
Memoria liberada, puntero vivo: la anatomía de un UAF
Use-After-Free (UAF) es, en esencia, un error de ciclo de vida. Un programa asigna memoria en el heap para un objeto, obtiene un puntero a esa memoria, libera el objeto (devolviendo la memoria al allocator), pero conserva el puntero. Cuando el programa usa ese puntero "colgante" (dangling pointer) para acceder a la memoria ya liberada, ocurre el Use-After-Free.
El problema no es que la memoria desaparezca. La memoria sigue ahí, en la misma dirección virtual. Lo que cambia es su contenido y su propósito. El allocator puede haberla reasignado a otro objeto completamente diferente. Y ahí es donde un bug se convierte en un exploit.
// Ejemplo simplificado de Use-After-Free
struct User {
char name[64];
int privilege_level;
};
struct User *admin = malloc(sizeof(struct User));
strcpy(admin->name, "root");
admin->privilege_level = 9;
free(admin); // Memoria liberada
// ... más tarde en el programa ...
// admin sigue apuntando a la misma dirección
printf("Level: %d\n", admin->privilege_level); // UAF: lee memoria liberada
En este ejemplo trivial, admin->privilege_level podría devolver 9 (si nadie ha reutilizado esa memoria), basura aleatoria, o el valor que otro objeto escribió en esa posición exacta. Es comportamiento indefinido. Y el comportamiento indefinido es donde viven los exploits.
Cómo funciona el heap: malloc, free, chunks y bins
Para entender por qué UAF es tan explotable, hay que entender cómo funciona el allocator de memoria dinámica. En sistemas Linux con glibc, el allocator estándar es ptmalloc2 (una variante de dlmalloc). En Windows, el equivalente es el NT Heap o el Low Fragmentation Heap (LFH).
Chunks: la unidad básica del heap
Cuando un programa llama a malloc(size), el allocator devuelve un puntero a un bloque de memoria llamado chunk. Cada chunk tiene una cabecera (header) con metadatos:
┌─────────────────────────────────────────┐
│ Chunk en uso (allocated) │
├─────────────────────────────────────────┤
│ prev_size (si chunk anterior libre) │ 8 bytes
├─────────────────────────────────────────┤
│ size | flags (A|M|P) │ 8 bytes
├─────────────────────────────────────────┤
│ ┌─────────────────────────────────┐ │
│ │ Datos del usuario │ │ ← puntero que devuelve malloc()
│ │ (el payload real) │ │
│ └─────────────────────────────────┘ │
├─────────────────────────────────────────┤
│ │
│ Chunk libre (freed) │
├─────────────────────────────────────────┤
│ prev_size │ 8 bytes
├─────────────────────────────────────────┤
│ size | flags │ 8 bytes
├─────────────────────────────────────────┤
│ fd (forward pointer → next free) │ 8 bytes
├─────────────────────────────────────────┤
│ bk (backward pointer → prev free) │ 8 bytes
├─────────────────────────────────────────┤
│ (espacio restante sin usar) │
└─────────────────────────────────────────┘
El detalle crítico: cuando un chunk se libera con free(), el allocator reutiliza el espacio de datos del usuario para almacenar punteros de gestión interna (fd y bk). Esos punteros enlazan el chunk libre con otros chunks libres en las free lists.
Bins: las listas de chunks libres
El allocator organiza los chunks libres en estructuras llamadas bins, clasificados por tamaño:
- Fast bins: chunks pequeños (hasta 160 bytes en x86-64). Lista enlazada simple (LIFO). Sin coalescing.
- Unsorted bin: chunks recién liberados de cualquier tamaño, esperando clasificación.
- Small bins: chunks de 32 a 1.024 bytes. Listas doblemente enlazadas.
- Large bins: chunks mayores de 1.024 bytes. Ordenados por tamaño.
tcache: el acelerador que facilita la explotación
Desde glibc 2.26 (2017), ptmalloc2 incluye tcache (thread-local caching bins). Cada hilo tiene 64 bins de tcache, uno por cada tamaño de chunk (de 24 a 1.032 bytes en x86-64). Cada bin es una lista enlazada simple (singly-linked) con un máximo de 7 entries.
tcache se diseñó para rendimiento: elimina la necesidad de locks entre hilos para las asignaciones más comunes. Pero su simplicidad es un regalo para los atacantes:
- Las primeras versiones de tcache no tenían verificaciones de integridad (sin double-free checks, sin validación de punteros).
- La lista usa un solo puntero
next(equivalente afd) que apunta al siguiente chunk libre. - Sobreescribir ese puntero
nextpermite redirigirmalloc()a cualquier dirección.
De bug a exploit: técnicas de explotación de UAF
Un UAF por sí solo es un bug. Para convertirlo en un exploit que ejecute código arbitrario, el atacante necesita controlar qué ocurre con la memoria liberada antes de que el puntero colgante la acceda.
Paso 1: Heap spraying (preparar el terreno)
El atacante asigna muchos objetos del mismo tamaño que el objeto liberado. El objetivo es que el allocator reutilice la memoria del objeto liberado para uno de estos objetos controlados por el atacante.
// En un contexto de navegador (JavaScript)
let freed_element = document.createElement('div');
document.body.appendChild(freed_element);
// Forzar la liberación del objeto interno del DOM
document.body.removeChild(freed_element);
// Heap spray: asignar muchos objetos del mismo tamaño
let spray = [];
for (let i = 0; i < 1000; i++) {
// ArrayBuffer con tamaño exacto del objeto DOM liberado
spray.push(new ArrayBuffer(64));
}
Paso 2: Type confusion via reutilización de memoria
Si el objeto original era un DOMElement con un puntero a función virtual (vtable), y el atacante lo reemplaza con un ArrayBuffer que controla byte a byte, puede sobrescribir ese puntero vtable con una dirección de su elección.
Memoria antes de free():
┌──────────────────────────┐
│ vtable_ptr → 0x7fff... │ ← puntero a tabla de funciones virtuales
│ property_a = 42 │
│ property_b = "hello" │
│ refcount = 3 │
└──────────────────────────┘
Memoria después de spray (reutilizada por ArrayBuffer):
┌──────────────────────────┐
│ 0x41414141 41414141 │ ← datos controlados por el atacante
│ 0x42424242 42424242 │ sobrescriben vtable_ptr
│ 0x43434343 43434343 │
│ 0x44444444 44444444 │
└──────────────────────────┘
Paso 3: Redirigir la ejecución
Cuando el programa usa el puntero colgante para llamar a un método virtual del objeto "liberado", sigue el vtable_ptr (ahora controlado por el atacante) y salta a la dirección que el atacante eligió.
tcache poisoning: la variante moderna
En sistemas con glibc y tcache, la explotación es más directa:
- Liberar un chunk que va a la tcache free list.
- Usar el UAF para sobrescribir el puntero
nextdel chunk liberado. - La siguiente llamada a
malloc()del mismo tamaño devuelve el chunk liberado. - La siguiente llamada devuelve la dirección que el atacante escribió en
next.
El resultado: malloc() devuelve un puntero a una dirección arbitraria (stack, GOT, hooks de malloc, cualquier estructura de datos crítica). Escribir en esa dirección es control total.
Por qué los navegadores son el objetivo principal
Los navegadores web son, con diferencia, la superficie de ataque más explotada por UAF. No es casualidad. Hay razones estructurales.
Complejidad del DOM
El Document Object Model (DOM) de una página web es un árbol de objetos C++ en el motor del navegador. Cada elemento HTML (<div>, <span>, <input>) es un objeto asignado en el heap. Cada estilo CSS computado, cada event listener, cada texto renderizado ocupa memoria dinámica.
Una página web típica puede tener miles de nodos DOM. Cada interacción del usuario (scroll, clic, hover, resize) puede provocar que se creen, muevan y destruyan decenas de objetos. El garbage collector de JavaScript y el cycle collector del motor del navegador trabajan constantemente liberando objetos que ya no son referenciados.
El problema del ciclo de vida cruzado
JavaScript y el DOM viven en mundos distintos pero conectados. Un objeto JavaScript puede mantener una referencia a un nodo DOM, y un nodo DOM (via event listeners) puede mantener una referencia a un objeto JavaScript. Cuando el DOM se modifica (por ejemplo, al eliminar un nodo), el motor debe coordinar la liberación entre el heap de C++ (donde vive el nodo DOM) y el heap de JavaScript (donde viven los wrappers JS).
Los errores en esa coordinación son la fuente más común de UAF en navegadores.
JavaScript como herramienta de explotación
El atacante controla el JavaScript que se ejecuta en la página. Eso le da capacidad para:
- Crear y destruir objetos DOM a voluntad (para provocar el UAF).
- Asignar objetos de tamaño preciso (heap spray con ArrayBuffers o TypedArrays).
- Leer el contenido de la memoria reutilizada (information leak para derrotar ASLR).
- Ejecutar el puntero colgante (llamando métodos sobre el objeto "liberado").
Es un laboratorio de explotación con acceso interactivo al heap de la víctima.
CVEs reales: UAF en producción
CVE-2021-21224: Type confusion en Chrome V8
Chrome antes de la versión 90.0.4430.85 contenía una vulnerabilidad de type confusion en el motor V8 durante la fase de Simplified Lowering del compilador TurboFan. El compilador JIT optimizaba incorrectamente la representación de tipos, permitiendo que el atacante construyera un objeto cuyo campo no coincidía con su field map.
El resultado: ejecución arbitraria de código dentro del sandbox del navegador. Fue añadida al catálogo KEV de CISA como vulnerabilidad explotada activamente in the wild. Afectó a todos los navegadores basados en Chromium: Chrome, Edge, Opera, Brave.
CVE-2021-30551: Type confusion en Chrome V8 (zero-day)
Otra vulnerabilidad en V8, esta vez en Chrome antes de 91.0.4472.101. El motor JavaScript trataba un objeto JS como un array JS, permitiendo lecturas y escrituras fuera de los límites del heap. Fue explotada como zero-day antes de que existiera parche.
La cadena de ataque era clásica: página maliciosa con JavaScript diseñado para activar la confusión de tipos durante la optimización JIT, leak de memoria para derrotar ASLR, y escritura arbitraria para redirigir la ejecución.
CVE-2022-22047: Windows CSRSS, usado por Knotweed/DSIRF
Esta vulnerabilidad merece atención especial porque conecta UAF con la industria del spyware comercial.
CVE-2022-22047 es un bug en el Client Server Run-Time Subsystem (CSRSS) de Windows. Específicamente, un envenenamiento de la caché de contextos de activación (Activation Context Cache Poisoning) que permite escalada de privilegios local.
El exploit fabricaba una llamada a CSRSS solicitando un contexto de activación para un ejecutable privilegiado, pero especificando un manifiesto malicioso con un atributo XML no documentado llamado loadFrom, que redirigía la carga de DLLs a una ubicación arbitraria en disco.
Microsoft Threat Intelligence Center (MSTIC) atribuyó su explotación a Knotweed, el nombre en código de DSIRF GmbH, una empresa austriaca de "offensive security" que desarrollaba spyware comercial. DSIRF usó esta vulnerabilidad junto con zero-days de Adobe para desplegar su malware Subzero contra objetivos en Europa y Centroamérica.
La cadena completa:
- Adobe Reader renderiza un PDF malicioso (zero-day de Adobe).
- Desde el proceso sandbox de Adobe, se escribe una DLL maliciosa en disco.
- Se explota CVE-2022-22047 para que un proceso de sistema cargue esa DLL.
- Escalada de SYSTEM. Subzero desplegado.
La DLL estaba firmada por "DSIRF GmbH". Sin disimulo.
UAF en el kernel: win32k y Linux
Windows win32k.sys
El subsistema Win32k del kernel de Windows gestiona ventanas, mensajes GUI y componentes de interfaz de usuario. Es históricamente una de las superficies de ataque más fértiles de Windows.
Win32k maneja objetos de kernel complejos (ventanas, menús, cursores, aceleradores de teclado) con ciclos de vida intrincados que dependen de mensajes entre modo usuario y modo kernel. Cuando un callback de modo usuario se ejecuta durante el procesamiento de un mensaje de kernel, puede modificar o destruir objetos que el kernel todavía referencia. Eso es un UAF de kernel.
Un atacante con acceso local puede:
- Disparar operaciones Win32k que asignan y liberan objetos de kernel.
- Manipular el sistema para reasignar la memoria liberada con datos controlados.
- Cuando el kernel sigue el puntero colgante, opera sobre la memoria del atacante.
El resultado: ejecución de código en modo kernel, escalada de privilegios a SYSTEM. CVE-2021-26900, CVE-2025-24983 y decenas de vulnerabilidades similares documentan este patrón en win32k.
Linux kernel UAF
En Linux, los UAF de kernel son frecuentes en subsistemas complejos: el network stack (netfilter, nf_tables), el io_uring subsystem, y los drivers de dispositivos. La explotación sigue un patrón similar: liberar un objeto de kernel, reasignar esa memoria con datos controlados (usando técnicas como msg_msg spraying o pipe_buffer), y disparar el acceso al puntero colgante para escalar privilegios.
Mitigaciones: la carrera armamentista
AddressSanitizer (ASAN)
ASAN es un instrumentador de compilación (disponible en GCC y Clang) que detecta accesos a memoria liberada en tiempo de ejecución. Cuando se libera memoria, ASAN la pone en "cuarentena" y marca las shadow bytes correspondientes como inaccesibles. Cualquier acceso posterior genera un error inmediato.
Limitaciones: overhead de 2x en rendimiento y 2x a 3x en memoria. Útil en desarrollo y testing, impracticable en producción.
MTE (Memory Tagging Extension) en ARM
ARM Memory Tagging Extension, introducida en ARMv8.5-A, es la mitigación hardware más prometedora contra UAF.
MTE asigna un tag de 4 bits a cada granularidad de 16 bytes de memoria física, y otro tag de 4 bits al puntero que referencia esa memoria. En cada acceso (load/store), el procesador compara ambos tags. Si no coinciden, genera una excepción.
Cuando free() libera un chunk, el tag de la memoria se cambia a un valor aleatorio diferente. El puntero colgante conserva el tag antiguo. Cualquier acceso posterior detecta el mismatch.
Limitaciones: 4 bits dan solo 16 tags posibles, por lo que hay una probabilidad de 1/15 de que el tag nuevo coincida por azar con el antiguo. No es determinista. Además, en 2025 se descubrió CVE-2025-0072 en drivers de GPU Mali de ARM, demostrando que MTE puede ser bypasseado en ciertos contextos.
El procesador AmpereOne (2024) fue el primero de datacenter en soportar MTE, con un impacto de rendimiento de un solo dígito.
Lenguajes memory-safe: Rust y Go
La mitigación más radical es eliminar la clase de vulnerabilidad por diseño. Rust lo consigue con su sistema de ownership:
// En Rust, este código NO COMPILA
let data = Box::new(42);
drop(data); // Liberación explícita
println!("{}", data); // Error de compilación: use of moved value
El compilador de Rust garantiza en tiempo de compilación que no existen referencias a memoria liberada. No hay overhead en runtime; el check es estático.
Google, Microsoft, Mozilla y el kernel de Linux están adoptando Rust para componentes críticos. Chromium incorpora Rust en componentes nuevos. El kernel de Linux acepta módulos en Rust desde la versión 6.1.
Go usa garbage collection, que también previene UAF (el GC no libera memoria mientras existan referencias vivas). El tradeoff es el overhead del garbage collector.
Otras mitigaciones
- MarkUs / PartitionAlloc: Chrome usa PartitionAlloc, un allocator propio que aísla tipos de objetos en particiones de memoria separadas, dificultando que un UAF en un tipo de objeto afecte a otro tipo.
- Control Flow Integrity (CFI): no previene el UAF, pero dificulta que el atacante redirija la ejecución a código arbitrario tras explotar uno.
- Pointer Authentication (PAC) en ARM: firma criptográficamente los punteros de retorno y vtable pointers, dificultando su falsificación.
Por qué UAF reemplazó al buffer overflow como vulnerabilidad dominante
En las décadas de 1990 y 2000, el buffer overflow de stack era el rey de los exploits. Pero tres mitigaciones lo desplazaron progresivamente:
- Stack canaries (SSP/ProPolice): detectan sobrescritura del stack antes de que la función retorne.
- NX/DEP (No-Execute / Data Execution Prevention): impiden ejecutar código en el stack.
- ASLR (Address Space Layout Randomization): aleatoriza las direcciones de memoria.
Estas defensas hicieron que los stack overflows pasaran de "triviales" a "difíciles". Pero no protegen el heap de la misma manera. El heap es un espacio de datos ejecutable indirectamente (a través de punteros a funciones y vtables), y las mitigaciones de integridad del heap (safe unlinking, tcache key checks) llegaron mucho más tarde y con menor cobertura.
Además, la evolución del software amplificó el problema. Los navegadores modernos, los motores de JavaScript JIT, y las aplicaciones con frameworks complejos crean y destruyen millones de objetos en el heap durante su ejecución normal. Cada ciclo de asignación y liberación es una oportunidad potencial para un UAF.
Según estadísticas de Google Project Zero y Microsoft Security Response Center, los UAF representan entre el 40% y el 70% de las vulnerabilidades de corrupción de memoria explotadas in the wild en navegadores y sistemas operativos desde 2020. No son un tipo más de bug. Son el tipo de bug de la era moderna de la explotación.
Conclusión: el problema es el modelo de memoria
Use-After-Free no es un error de programador descuidado. Es una consecuencia inevitable de los modelos de memoria manuales (C/C++) aplicados a sistemas de complejidad creciente. Mientras el software crítico (navegadores, kernels, drivers) siga escrito en lenguajes sin garantías de seguridad de memoria, los UAF seguirán siendo la vulnerabilidad dominante.
Las mitigaciones hardware (MTE, PAC) y de compilación (CFI) reducen la superficie explotable pero no eliminan el problema. La adopción de Rust en componentes críticos es la tendencia más prometedora, pero reescribir bases de código de millones de líneas es un proceso de décadas.
Para los analistas de seguridad, el mensaje es claro: dominar UAF no es opcional. Es la clase de vulnerabilidad que define la era actual de la explotación.
Recursos adicionales
- how2heap (shellphish): repositorio con técnicas de explotación de heap, incluidas UAF y tcache poisoning.
- Google Project Zero: 0-days In-the-Wild: base de datos de zero-days explotados, muchos de ellos UAF.
- Microsoft MSRC Blog: Untangling KNOTWEED: análisis completo de la cadena de explotación de DSIRF.
- ARM MTE Developer Guide: documentación oficial de Memory Tagging Extension.
Preguntas frecuentes
Artículos relacionados
Este contenido tiene fines exclusivamente educativos y de investigación en ciberseguridad defensiva. No se proporcionan binarios maliciosos ni payloads ejecutables. El uso indebido de esta información es responsabilidad exclusiva del usuario. Leer disclaimer completo.