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

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/free devuelven 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:

  1. 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.
  2. Overflow. Escribí pasado los datos de usuario del chunk fuente hacia el header del próximo chunk (size/flags) o, si está libre, sus punteros fd/bk.
  3. Corrupt. O sobrescribís el contenido del objeto víctima directamente, o forjás metadata del allocator (un size falso, un next de free-list envenenado).
  4. Trigger. Llamá malloc/free para que el allocator consuma la metadata corrompida y enlace una dirección elegida por el atacante en un bin.
  5. Allocate-to-arbitrary. Un malloc posterior 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:

  1. Lenguajes memory-safe y contenedores con bounds-check.
    Los slices de Rust, std::vector::at, std::span con bounds checks, y std::string (vs strcpy) remueven el overflow en la fuente. El arreglo estructural.
  2. 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.
  3. 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.
  4. 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.
  5. Mitigaciones del compilador.
    -D_FORTIFY_SOURCE=3 atrapa un subconjunto de llamadas mem*/str* desbordables con tamaños conocidos; los cookies de heap detectan algún tampering de metadata en el momento del free.
  6. 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.
  7. 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_hook en glibc 2.34). Cierra ese target, no la primitiva; FSOP, exit handlers y GOT quedan. La escritura allocate-to-arbitrary simplemente reapunta.
  • _FORTIFY_SOURCE como 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 next de tcache pasando safe-linking, asignar sobre un hook/GOT/FILE-vtable, conseguir un one-shot. El tcache_poisoning.c de how2heap es el modelo mínimo.
  • ASan atrapa un heap overflow de parser en CI. Un fuzzer alimenta un campo de longitud malformado; el memcpy sobrepasa 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/