Heap buffer overflow y explotación del allocator
Definición
Un heap buffer overflow escribe pasado el final de una asignación del heap, corrompiendo lo que esté adyacente — el contenido de un objeto vecino o la propia metadata de chunk del allocator. La explotación del allocator es el oficio de convertir esa corrupción (o una corrupción de free-list relacionada) en una primitiva de asignación controlada: hacer que malloc devuelva un puntero que el atacante eligió, lo que se vuelve una escritura arbitraria. Esta es la clase 2 de las seis clases de bug de corrupción de memoria, y a diferencia de un stack overflow no tiene un canary en su camino — las defensas del heap viven en el allocator, no en el frame.
Por qué importa
Los heap overflows son una fuente líder de ejecución remota de código en servidores, navegadores y parsers de media/archivos, y la puerta de entrada al linaje de explotación de allocators "House of \*" que define el pwning de userland en CTF y en el mundo real. Tres lecciones transferibles:
- El objetivo es el vecino, no el buffer. Un heap overflow solo es interesante por lo que está al lado del chunk desbordado — el puntero a función de un objeto adyacente, o la metadata de tamaño/free-list del allocator. La explotación es un problema de layout (heap grooming) antes que un problema de escritura.
- El allocator es una máquina programable. El ptmalloc de glibc, el segment heap de Windows y jemalloc mantienen cada uno free lists y metadata de chunk in-band. Corrompé esa metadata y el allocator se vuelve una weird machine: unas pocas llamadas
malloc/freedevuelven chunks solapados o arbitrarios. Dominar los internals de un allocator es la verdadera habilidad; el overflow es solo la entrada. - La versión lo es todo. glibc 2.31 vs 2.34 vs 2.39 difieren enormemente:
__malloc_hook/__free_hook(los targets de escritura clásicos) fueron removidos en 2.34; el "safe-linking" del tcache aterrizó en 2.32. Un exploit se escribe contra una versión específica del allocator, y su defensa también. Por eso "explotación de heap" es en realidad "explotación de este-allocator-esta-versión".
El par defensivo es Mitigaciones de exploits; la cura estructural son los contenedores con bounds-check y los lenguajes memory-safe.
Cómo funciona
Los allocators de heap tallan la memoria en chunks. En glibc cada chunk tiene un header in-band (prev_size, size con bits de flag bajos) seguido de los datos del usuario; cuando un chunk se libera, el allocator reusa el inicio de su área de usuario para guardar punteros de free-list (fd/bk). La cadena del exploit:
- Layout. Groomeá el heap para que un chunk desbordable quede inmediatamente antes de una víctima elegida — ya sea un objeto con un puntero a código/longitud, o un chunk libre cuya metadata querés.
- Overflow. Escribí pasado los datos de usuario del chunk fuente hacia el header del próximo chunk (
size/flags) o, si está libre, sus punterosfd/bk. - Corrupt. O sobrescribís el contenido del objeto víctima directamente, o forjás metadata del allocator (un
sizefalso, unnextde free-list envenenado). - Trigger. Llamá
malloc/freepara que el allocator consuma la metadata corrompida y enlace una dirección elegida por el atacante en un bin. - Allocate-to-arbitrary. Un
mallocposterior de ese tamaño devuelve la dirección elegida por el atacante. Escribir a esa "asignación" es una escritura arbitraria — apuntala a una entrada GOT, un hook (glibc ≤ 2.33), un handler de exit/FILE, o un puntero a función de la app → control de flujo.
La sobrescritura de objeto adyacente, el caso más simple, no necesita trucos de metadata:
struct obj { char name[32]; void (*action)(void); };
struct obj *a = malloc(sizeof *a); // chunk A
struct obj *b = malloc(sizeof *b); // chunk B, asignado justo después de A
strcpy(a->name, attacker_string); // sin bounds check: desborda A hacia B->action
b->action(); // llama a un puntero controlado por el atacante
El caso de metadata — el tcache poisoning de glibc — es el caballo de batalla moderno, bosquejado:
p = malloc(0x20); q = malloc(0x20)
free(q); free(p) # freelist de tcache[0x20]: p -> q
<overflow o UAF sobrescribe el fd de p (next del tcache) con &target> # consciente de safe-linking
malloc(0x20) -> p
malloc(0x20) -> target # el allocator devuelve una dirección elegida por el atacante
*target = value # primitiva de escritura arbitraria
El bug es el overflow; el exploit es el allocator obedeciendo fielmente la metadata corrompida. El objetivo casi nunca es el buffer desbordado — es el chunk o la entrada de free-list que está al lado.
Técnicas / patrones
- Fingerprinteá el allocator y la versión primero. glibc (ptmalloc), segment heap de Windows, jemalloc (Android/FreeBSD), tcmalloc/PartitionAlloc (Chrome), SLUB del kernel — cada uno tiene un layout de metadata y un set de técnicas distinto. En glibc, la versión 2.x exacta selecciona qué primitivas sobreviven.
- Groomeá antes de desbordar. Asigná/liberá para forzar que el objeto víctima (uno que carga un puntero a código o un campo de longitud) quede inmediatamente adyacente al buffer desbordable.
- Preferí sobrescribir un campo de longitud/tamaño. Convertir un overflow chico y acotado en una longitud corrompida convierte una escritura relativa en una lectura/escritura absoluta y más grande — una primitiva mucho más fuerte que el overflow original.
- Filtrá antes de escribir. La explotación del allocator casi siempre necesita primero un leak de base de heap y libc para derrotar ASLR; una primitiva de info-leak (lectura OOB) suele preceder a la escritura.
- Conocé los targets de escritura por versión. Entradas GOT,
__free_hook/__malloc_hook(glibc ≤ 2.33),__exit_funcs/destructores TLS, y File-Stream-Oriented-Programming (vtables de_IO_FILE) para glibc post-hook. - El tcache es la primera parada. En glibc moderno el tcache (caché por hilo) tiene los chequeos más débiles y el reuso más confiable — empezá ahí antes de fastbin/unsorted/largebin.
Variantes y bypasses
La explotación del allocator abarca 5 familias — una agnóstica al allocator, tres internas-a-glibc, dos de otros mundos de allocator.
1. Sobrescritura de objeto adyacente (agnóstica al allocator)
Desbordar hacia el contenido del próximo chunk — un puntero a función, puntero a objeto o campo de longitud. Sin forja de metadata; funciona en cualquier allocator. La primitiva más simple y a menudo más confiable.
2. tcache poisoning de glibc
Corromper un puntero next de free-list del tcache (vía overflow o UAF) para que el próximo malloc de ese tamaño devuelva una dirección elegida por el atacante. La primitiva dominante de glibc moderno. Safe-linking (glibc 2.32+) codifica con XOR el next con chunk_addr >> 12, así que una sobrescritura ingenua necesita primero un leak de dirección de heap.
3. Ataques de fastbin / unsorted / largebin de glibc ("House of \*")
El linaje clásico: forjar un fd de fastbin para devolver un chunk falso (House of Spirit), abusar del bk del unsorted-bin para escribir un puntero grande a un target (unsorted-bin attack), o corromper el bk_nextsize del largebin para una escritura controlada (largebin attack). Mitigado por partes con chequeos de tamaño/alineación e integridad de bins a través de las versiones de glibc.
4. Segment heap y LFH de Windows
Default en Windows 8.1+. El front-end Low-Fragmentation-Heap más el back-end segment-heap usan metadata _HEAP_ENTRY con encoding/cookies de header y randomización de buckets LFH. La explotación favorece la sobrescritura de objeto adyacente y el "bucket spray" de LFH por sobre la forja de metadata, ya que la metadata está codificada.
5. jemalloc (Android, FreeBSD)
Estructura region/run/chunk con metadata mayormente out-of-line. La explotación apunta a regions adyacentes dentro de un run y a la ocasional metadata in-line — central en la explotación de navegadores y media-codecs de Android.
Impacto
Ordenado por severidad típica:
- Ejecución remota de código. Heap overflow en un servicio de red, navegador o parser de media/archivos alcanzable por contenido del atacante — una clase líder de RCE.
- Escalada de privilegios local. Heap overflow en un binario setuid o en el allocator del kernel (SLUB) que rinde ejecución privilegiada.
- Sandbox escape. Corrupción de heap en un proceso broker/IPC para cruzar una frontera de aislamiento; un eslabón de cadena.
- Primitiva de lectura/escritura arbitraria. Allocate-to-arbitrary o un campo de longitud corrompido rinde acceso a memoria controlado — el motor del resto de la cadena.
- Divulgación de información. Sobre-leer un chunk adyacente (o una longitud corrompida) filtra punteros de heap/libc que derrotan ASLR.
Detección y defensa
Ordenado por efectividad:
- Lenguajes memory-safe y contenedores con bounds-check.
Los slices de Rust,std::vector::at,std::spancon bounds checks, ystd::string(vsstrcpy) remueven el overflow en la fuente. El arreglo estructural. - AddressSanitizer en CI.
ASan rodea las asignaciones de heap con redzones envenenadas, así que un overflow trapea en la escritura ofensora con stacks de alloc + acceso — no en un crash posterior del allocator. El control de mayor palanca para el C/C++ inevitable. - Allocators endurecidos.
Chequeos de integridad del tcache + safe-linking de glibc; PartitionAlloc de Chrome (particiones por tipo, guard pages, hardening de freelist); encoding de metadata del segment-heap de Windows; GWP-ASan y allocators de page-heap/guard-page que ponen cada asignación en su propia página para que un overflow falle de inmediato. Suben el costo sin arreglar el bug. - Fuzzing guiado por cobertura.
libFuzzer/AFL++/OSS-Fuzz sobre parsers encuentran heap overflows a diario — la forma modal en que se descubren los bugs de heap reales antes de salir. - Mitigaciones del compilador.
-D_FORTIFY_SOURCE=3atrapa un subconjunto de llamadasmem*/str*desbordables con tamaños conocidos; los cookies de heap detectan algún tampering de metadata en el momento del free. - Memory tagging por hardware.
ARM MTE asigna tags distintos a asignaciones adyacentes, así que un overflow que cruza al próximo granule trapea en el mismatch de tag. - Telemetría de runtime / EDR.
La cadena post-overflow (heap spray, sobrescritura de GOT/hook, control de flujo anómalo) deja señales visibles para el EDR.
Qué no funciona como defensa primaria
- Stack canaries. El heap no tiene canary en el camino de escritura; SSP es irrelevante para los heap overflows.
- ASLR solo. Derrotado por un info leak, que los exploits de heap rutinariamente obtienen primero. ASLR sube la vara; no cierra la clase.
- Remover un target de escritura (p. ej.,
__malloc_hooken glibc 2.34). Cierra ese target, no la primitiva; FSOP, exit handlers y GOT quedan. La escritura allocate-to-arbitrary simplemente reapunta. _FORTIFY_SOURCEcomo garantía. Solo cubre llamadas donde el compilador conoce el tamaño de destino; las copias de tamaño dinámico y los loops custom se escapan.
Labs prácticos
Corré solo contra entornos de lab propios o engagements autorizados.
Atrapar un heap overflow con AddressSanitizer
cat > heap.c <<'EOF'
#include <stdlib.h>
#include <string.h>
int main(void) {
char *a = malloc(32);
char *b = malloc(32); // víctima adyacente
strcpy(a, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA_overflow_into_b"); // > 32
return b[0];
}
EOF
gcc -fsanitize=address -g -o heap heap.c && ./heap
# Esperado: ASan "heap-buffer-overflow" con la ubicación de la escritura y ambas asignaciones.
Confirmar la versión del allocator de la que dependen tus primitivas
ldd --version | head -1
# glibc 2.31 (Ubuntu 20.04) vs 2.35 (22.04) vs 2.39 (24.04) cambian qué técnicas
# de heap funcionan y qué targets de escritura (__malloc_hook etc.) todavía existen.
Inspeccionar la metadata de chunk en vivo
# En gdb con pwndbg o gef instalado contra un programa chico de malloc/free:
# gef> heap chunks # listar headers de chunk (size, flags, fd/bk)
# gef> heap bins # bins tcache / fastbin / unsorted / small / large
# Mirar el puntero fd de un chunk liberado es cómo se entiende el tcache poisoning.
echo "Usá 'heap chunks' y 'heap bins' de pwndbg/gef sobre un target de juguete de malloc/free."
Practicar las técnicas de forma segura
git clone https://github.com/shellphish/how2heap
# how2heap es el playground canónico, versión-taggeado, de técnicas de heap de glibc:
# tcache_poisoning.c, fastbin_dup.c, unsorted_bin_attack.c, house_of_*.c —
# cada uno una demostración mínima y ejecutable contra tu glibc local. Solo lab propio.
Ejemplos prácticos
- CVE-2021-3156 — Heap overflow "Baron Samedit" de Sudo → root. Un off-by-one en el parsing de argumentos de Sudo corrompe el heap; pese a ASLR y canaries rinde root local en casi toda distro de Linux de la época. El código maduro no es inmune.
- CVE-2023-4863 — Heap buffer overflow de libwebp (0-day in-the-wild). Una escritura de heap fuera de límites en el decoding de imágenes WebP, alcanzable con solo renderizar una imagen, pegó en Chrome, apps de Electron e incontables embebedores de libwebp simultáneamente — una lección de radio-de-explosión sobre los bugs de heap de librerías compartidas.
- CVE-2020-0796 — "SMBGhost" (Windows SMBv3). Un integer overflow en la descompresión de SMBv3 produce una asignación de tamaño insuficiente y un heap overflow controlado en
srv2.sys, dando RCE/LPE wormable — un heap overflow de kernel de Windows de la era segment-heap. - tcache poisoning de glibc en la práctica. La cadena modal de CTF/userland real: leak de heap + libc, envenenar un
nextde tcache pasando safe-linking, asignar sobre un hook/GOT/FILE-vtable, conseguir un one-shot. Eltcache_poisoning.cde how2heap es el modelo mínimo. - ASan atrapa un heap overflow de parser en CI. Un fuzzer alimenta un campo de longitud malformado; el
memcpysobrepasa su destino; ASan localiza la escritura y el arreglo es un bounds check — el resultado modal de atrapado-pre-deploy.
Notas relacionadas
- Corrupción de memoria — el padre; el heap overflow es la clase 2 de las seis clases de bug.
- Use-After-Free y punteros colgantes — el primo temporal del heap; comparte la maquinaria de reclaim/grooming.
- Double-free y corrupción del allocator — el hermano de corrupción-de-free-list y la otra raíz del linaje "House of \*".
- Stack buffer overflow — el primo espacial en el stack, donde vive el canary.
- Mitigaciones de exploits — el par defensivo estructural (allocators endurecidos, MTE, CET, ASLR).
- ROP y ret2libc — donde una escritura allocate-to-arbitrary suele pivotar una vez que controla un puntero a código.
- EDR / Correlación de procesos — las señales de runtime que emite una cadena de heap-exploit.
- La dualidad atacante-defensor — la investigación de overflows y la ingeniería de hardening de allocators son el mismo problema desde sillas opuestas.
Notas atómicas futuras sugeridas
- Double-free y corrupción del allocator
- Tcache poisoning y safe-linking
- Técnicas "House of" de glibc
- Internals del segment heap de Windows
- ARM MTE y memory tagging
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-122 — Heap-based Buffer Overflow — https://cwe.mitre.org/data/definitions/122.html
- Research / Deep Dive: Microsoft Security Response Center — A proactive approach to more secure code (memory-safety statistics) — https://msrc.microsoft.com/blog/2019/07/a-proactive-approach-to-more-secure-code/
- Official Tool Docs: AddressSanitizer (heap-buffer-overflow detection) — https://clang.llvm.org/docs/AddressSanitizer.html
- Research / Tooling: shellphish — how2heap (canonical, version-tagged glibc heap-technique reference) — https://github.com/shellphish/how2heap
- Hardening: ARM — Memory Tagging Extension (MTE) — https://developer.arm.com/documentation/108035/latest/