conceptExplotación de Binarios~10 min de lecturaActualizado Jun 03, 2026#cybersecurity#binary-exploitation#memory-corruption#use-after-free#heap#vulnerability-research

Use-After-Free y punteros colgantes

Definición

Un use-after-free (UAF) es un bug de seguridad de memoria temporal: un programa accede a memoria a través de un puntero después de que la asignación a la que ese puntero referenciaba fue liberada. El puntero obsoleto es un puntero colgante (dangling pointer). El bug no es el free() ni el acceso por separado — es que una referencia sobrevivió a la vida del objeto al que apuntaba, y el reuso de esa memoria por parte del allocator le deja al atacante decidir a qué apunta ahora la referencia obsoleta. UAF es la clase 3 de las seis clases de bug de corrupción de memoria y la primitiva dominante en la explotación moderna de navegadores y kernels.

Por qué importa

El UAF superó al stack overflow como la clase de corrupción de memoria de grado-exploit. Es el tipo de bug líder en los advisories de seguridad de Chrome, el kernel de Windows y Safari, y la estrella recurrente de las cadenas de renderer en Pwn2Own. Tres lecciones transferibles:

  • El bug real es la vida útil, no los límites. Los overflows de stack/heap son espaciales (escribiste pasado un borde); el UAF es temporal (usaste algo después de su tiempo). Por eso los bounds checks, canaries y _FORTIFY_SOURCE no hacen nada contra él — la escritura está perfectamente dentro de los límites; solo aterriza en un objeto reusado. El arreglo vive en la propiedad/vida útil, que es exactamente lo que codifican el borrow checker de Rust y el RAII de C++.
  • El allocator es cómplice del exploit. El UAF solo se vuelve poderoso porque los allocators reusan la memoria liberada de forma determinista (el tcache/fastbin de glibc son LIFO, así que el próximo malloc del mismo tamaño te devuelve el chunk que recién liberaste). El atacante reclama el slot liberado con bytes de su forma — el heap grooming convierte un bug de vida útil en control preciso.
  • Los punteros a función y las vtables están por todos lados en el heap. El código moderno orientado a objetos y dirigido por callbacks guarda punteros a código (vtables de C++, callbacks de handlers, shapes de objetos JS) dentro de asignaciones del heap. Un UAF sobre tal objeto se convierte directamente en hijack de control de flujo. Por eso el UAF domina donde antes lo hacían los overflows: el heap está lleno de objetivos de alto valor.

El par defensivo es Mitigaciones de exploits; la cura estructural son los lenguajes con tipado de propiedad.

Cómo funciona

Un exploit de UAF es una secuencia de 3 tiempos — free, reclaim, use:

  1. Allocate. Se asigna un objeto y se guarda un puntero p — posiblemente aliaseado en varios lugares (una caché, un registro de callback, otro hilo).
  2. Free. El objeto se libera; el chunk vuelve a la free list del allocator (tcache/fastbin para tamaños chicos). Una copia del puntero puede ser nulificada — pero una copia aliaseada p no. p ahora está colgante.
  3. Reclaim. El atacante dispara una nueva asignación de la misma clase de tamaño. El allocator devuelve el chunk recién liberado, y el atacante lo llena con bytes controlados.
  4. Use. El programa desreferencia p, creyendo que todavía apunta al objeto original — pero los bytes ahora están controlados por el atacante. Si el objeto cargaba un puntero a función o una vtable, la próxima llamada virtual salta a donde el atacante eligió.

Un ejemplo representativo en C donde el objeto colgante tiene un callback:

struct conn {
    char buf[32];
    void (*handler)(struct conn *);   // puntero a código = el objetivo
};

struct conn *c = malloc(sizeof *c);
c->handler = default_handler;
free(c);                              // liberado — pero c se sigue usando abajo (el bug)

/* el atacante ahora dispara una asignación de sizeof(struct conn) y controla sus bytes */
char *reclaim = malloc(sizeof *c);
memcpy(reclaim, attacker_bytes, sizeof *c);   // sobrescribe el slot liberado, incl. handler

c->handler(c);                        // UAF: llama a un puntero controlado por el atacante

El bug no es "se liberó memoria". Es que a una referencia se le permitió sobrevivir a su objeto, y el reuso del allocator hizo que esa referencia apunte a datos con la forma del atacante.

Técnicas / patrones

Qué mira un investigador y cómo sondea:

  • Cazá el aliasing, no el free. El UAF vive donde un puntero se copia o retiene y un camino libera mientras otro todavía tiene la copia: caminos de error/cleanup, registros de callbacks, handles cacheados, invalidación de iteradores (liberar mientras se itera un contenedor) y referencias compartidas entre hilos.
  • Los bugs de reference-count son fábricas de UAF. Un over-decrement (o un increment faltante) libera un objeto que alguien más todavía referencia. El UAF por refcount es todo un subgénero, especialmente en kernels y motores de scripting.
  • Hacé coincidir la clase de tamaño para el reclaim. La asignación que reclama debe caer en el mismo bin del allocator que el objeto liberado. El tcache de glibc agrupa por tamaño exacto; un reclaim confiable depende de pedir el mismo tamaño.
  • Groomeá el heap (Feng Shui). Acomodá asignaciones y frees para que el slot objetivo se libere y luego sea reclamado por un objeto que controlás del todo — a menudo un buffer del mismo tamaño, un typed array de JS o un string.
  • Ganá la carrera cuando haga falta. En objetivos multihilo o de motor JS, el free y el reclaim corren una carrera; el atacante rocía asignaciones para sesgar el resultado.
  • Elegí objetivos con punteros a código o longitudes. Objetos que contienen vtables, punteros a función o campos de tamaño convierten un UAF en hijack de control de flujo o en una primitiva de lectura/escritura arbitraria.

Variantes y bypasses

Los bugs de puntero colgante se dividen en 4 variantes operativas, cada una con una superficie de arreglo distinta.

1. Use-after-free de heap (temporal, canónico)

Liberar un objeto del heap, retener un puntero colgante, reclamar el slot con datos controlados, usarlo. La primitiva dominante de navegador/kernel. Se atrapa en el momento del acceso con la cuarentena de memoria liberada de AddressSanitizer y con el tag mismatch de ARM MTE.

2. Use-after-return / use-after-scope (puntero de stack colgante)

Una función devuelve la dirección de una local (o una referencia a una variable ahora fuera de scope en C++); el llamante la desreferencia después de que el frame se reusó. Espacialmente dentro de límites, temporalmente inválido. Detectado por -fsanitize=address con detect_stack_use_after_return y por -Wreturn-local-addr.

3. Type confusion por reclaim

Liberar un objeto de tipo A y reclamar su slot con un objeto de tipo B (o viceversa); el programa luego interpreta los bytes de B como un A — p. ej., leer el campo de datos de B como el puntero de vtable de A. Este es el asesino de motores JavaScript (confusión de object-shape en V8/JSC) y convierte un UAF directamente en una primitiva de lectura/escritura poderosa. Muy relacionado con los bugs de confusión de enteros/tipos.

4. Double-free → corrupción del allocator

Liberar el mismo chunk dos veces corrompe los enlaces de la free-list del allocator, dejando al atacante eventualmente devolver chunks solapados o arbitrarios. Un primo especial, interno-al-allocator, del UAF — analizado a fondo en Double-free y corrupción del allocator. El "safe-linking" de glibc 2.32+ y los chequeos de double-free del tcache mellan las versiones ingenuas.

Impacto

Ordenado por severidad típica:

  • Ejecución remota de código. UAF en un renderer de navegador o servicio de red alcanzable por contenido del atacante — la primitiva de prestigio de Pwn2Own. El control de la vtable/callback de un objeto liberado rinde control de flujo.
  • Escalada de privilegios local. UAF de kernel (a menudo guiado por refcount) que rinde ejecución en contexto kernel — la escalada estándar tras un compromiso de renderer.
  • Sandbox escape. UAF en objetos IPC/broker para cruzar una frontera de sandbox; casi siempre un eslabón de cadena, rara vez terminal.
  • Primitiva de lectura/escritura arbitraria. El type-confusion-por-reclaim frecuentemente da una lectura/escritura relativa o absoluta controlada antes de cualquier ejecución de código — el motor del resto de la cadena.
  • Divulgación de información. Una lectura UAF filtra el contenido del objeto reusado (punteros de heap, derrotando ASLR) incluso cuando no hay control de escritura.

Detección y defensa

Ordenado por efectividad:

  1. Lenguajes memory-safe con tipado de propiedad.
    El borrow checker de Rust rechaza código donde una referencia sobrevive a su referente en tiempo de compilación, eliminando la clase de raíz; Swift/Go usan ARC/GC con el mismo fin. Este es el arreglo estructural y la razón por la que los nuevos componentes de navegador/kernel se escriben cada vez más en Rust.
  2. RAII y smart pointers en C++ (con los ojos abiertos).
    std::unique_ptr impone propiedad única; std::shared_ptr/weak_ptr expresan vida compartida para que el objeto viva mientras lo tenga algún holder. Remueven la mayoría del UAF de vida-manual — pero no los escapes de raw pointer, la interop con C-API ni el mal uso de .get(). Buena práctica, no garantía.
  3. AddressSanitizer en CI.
    ASan pone en cuarentena la memoria liberada y la envenena, así que un use-after-free trapea en el momento del acceso con stacks de alloc/free/use — no en un crash aguas abajo. El control de mayor palanca para el C/C++ inevitable. Habilitá detect_stack_use_after_return para la variante 2.
  4. Allocators endurecidos y hardening de punteros.
    El PartitionAlloc + MiraclePtr (BackupRefPtr) de Chrome mantiene un refcount que evita que el chunk liberado se reuse mientras un raw pointer todavía lo referencia, neutralizando la mayoría del UAF de renderer; GWP-ASan muestrea asignaciones para atrapar UAF en producción; el safe-linking y los chequeos de integridad del tcache de glibc mellan los ataques de free-list. Estos suben el costo de explotación sin arreglar el bug.
  5. Memory tagging por hardware.
    ARM MTE asigna un tag a cada asignación y re-taggea al liberar; un puntero colgante mantiene el tag viejo y trapea al acceder. Atrapa UAF (y overflow) a velocidad de hardware; en plataformas ARM recientes y dispositivos Pixel.
  6. Telemetría de runtime / EDR sobre la cadena.
    La cadena post-UAF (heap spray, transferencia de control de flujo inusual, cambios de región VirtualProtect/JIT) deja señales visibles para el EDR incluso cuando el bug en sí es silencioso.

Qué no funciona como defensa primaria

  • Nulificar el puntero tras el free (free(p); p = NULL;). Buena higiene, pero solo nulifica la única copia que ves — el UAF vive en la copia aliaseada que te olvidaste. No puede arreglar referencias entre hilos o cacheadas.
  • Bounds checks, canaries, _FORTIFY_SOURCE. Son defensas espaciales; la escritura del UAF está dentro de límites. No hacen nada por un bug temporal.
  • "Usamos smart pointers". El acceso por raw pointer, las vidas manuales por rendimiento y la interop con C dejan agujeros de UAF. Necesario, no suficiente.
  • Revisión manual de código. Los bugs de vida-aliaseada son exactamente lo que los humanos se pierden y los sanitizers/fuzzers atrapan con un reproductor.

Labs prácticos

Corré solo contra entornos de lab propios o engagements autorizados.

Atrapar una lectura UAF con AddressSanitizer

cat > uaf.c <<'EOF'
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
int main(void) {
    char *p = malloc(64);
    strcpy(p, "live object");
    free(p);
    printf("use-after-free: %s\n", p);   // bug: lectura a través de puntero colgante
    return 0;
}
EOF
gcc -fsanitize=address -g -o uaf uaf.c && ./uaf
# Esperado: ASan reporta "heap-use-after-free" con stacks de alloc + free + use.

Demostrar el reclaim determinista del tcache

cat > reclaim.c <<'EOF'
#include <stdlib.h>
#include <stdio.h>
int main(void) {
    void *a = malloc(64); free(a);
    void *b = malloc(64);            // misma clase de tamaño
    printf("a=%p b=%p same_slot=%d\n", a, b, a==b);   // tcache de glibc: b == a
    return 0;
}
EOF
gcc -O0 -o reclaim reclaim.c && ./reclaim
# Esperado: a == b. Este reuso LIFO es exactamente en lo que se apoya un exploit para
# poner datos del atacante donde solía vivir el objeto liberado.

Atrapar use-after-return

cat > uar.c <<'EOF'
#include <stdio.h>
int *leak(void){ int x = 0x41414141; return &x; }   // devuelve un ptr de stack colgante
int main(void){ int *p = leak(); printf("%x\n", *p); return 0; }
EOF
gcc -fsanitize=address -g -fno-omit-frame-pointer -o uar uar.c
ASAN_OPTIONS=detect_stack_use_after_return=1 ./uar
# Esperado: ASan reporta "stack-use-after-return".

Ver las primitivas de mitigación que emite el compilador

gcc -O2 -fsanitize=address -c uaf.c -o uaf.o
nm uaf.o | grep -i asan | head
# Esperado: hooks de instrumentación __asan_* — los chequeos por-acceso que convierten
# un UAF silencioso en un abort inmediato y localizado.

Ejemplos prácticos

  • CVE-2019-5786 — UAF de FileReader en Chrome (explotado in-the-wild). Un use-after-free en el FileReader de Blink se encadenó con un bug del kernel de Windows como un 0-day in-the-wild; el UAF del renderer dio la ejecución de código inicial. La primitiva de entrada arquetípica del navegador moderno.
  • CVE-2014-0322 — UAF de Internet Explorer 10 (in-the-wild). Un puntero colgante a un objeto DOM liberado, reclamado vía un heap spray controlado, rindió RCE — uno de los exploits canónicos de UAF-por-spray de IE.
  • CVE-2016-0728 — UAF de refcount del keyring del kernel de Linux (LPE). Un overflow de reference-count causó que un objeto todavía en uso fuera liberado, produciendo un UAF explotable para root local — el camino de manual refcount-bug → UAF → escalada de privilegios.
  • Type confusion por reclaim en motores JavaScript. Los exploits de V8/JavaScriptCore rutinariamente liberan un objeto y reclaman su slot como un "shape" de objeto distinto, luego leen los campos del tipo confundido como punteros — convirtiendo un UAF en una primitiva de lectura/escritura arbitraria que dirige el resto de la cadena.
  • ASan atrapa un UAF de cleanup-path en CI. Un fuzzer pega en un camino de error que libera un buffer todavía referenciado por un handler de reintento; ASan señala el acceso colgante exacto y el arreglo es una corrección de vida útil de una línea — el resultado modal de "atrapado pre-deploy".

Notas relacionadas

Notas atómicas futuras sugeridas

  • Type confusion y hijacking de vtable
  • Lectura fuera de límites e info leaks
  • ARM MTE y memory tagging
  • Heap grooming y Feng Shui
  • Detectar la explotación de corrupción de memoria

Las notas atómicas futuras se listan como [[wikilinks]] aunque el archivo destino todavía no exista, para que registren como forward-links en Obsidian.

Referencias

  • Foundational: MITRE CWE-416 — Use After Free — https://cwe.mitre.org/data/definitions/416.html
  • Research / Deep Dive: Microsoft Security Response Center — A proactive approach to more secure code (memory-safety vulnerability statistics) — https://msrc.microsoft.com/blog/2019/07/a-proactive-approach-to-more-secure-code/
  • Official Tool Docs: AddressSanitizer (heap-use-after-free / stack-use-after-return detection) — https://clang.llvm.org/docs/AddressSanitizer.html
  • Hardening: ARM — Memory Tagging Extension (MTE) — https://developer.arm.com/documentation/108035/latest/
  • Hardening / Industry: The Chromium Projects — Memory safety (PartitionAlloc, MiraclePtr) — https://www.chromium.org/Home/chromium-security/memory-safety/