Memory Corruption
Definición
Memory corruption es la clase de bugs de software donde un programa lee o escribe memoria fuera de sus límites previstos, rompiendo la integridad del estado del programa. La corrupción puede ser silenciosa (el programa continúa, quizá produciendo resultados incorrectos) o fatal (segmentation fault). Crucialmente para seguridad, un atacante que controla el input corruptor a veces puede convertir el bug en una primitive que controla el comportamiento del programa: desde filtrar información hasta ejecutar código arbitrario. Memory corruption es la clase fundacional de vulnerabilidad de la rama binary-exploitation; todo lo demás en esta rama es un tipo específico de corrupción, una mitigación contra la cadena de corrupción a explotación, o una herramienta usada para encontrar o explotar esos bugs.
Por qué importa
Las vulnerabilidades de memory-safety produjeron más de la mitad de todos los CVEs críticos en software Microsoft durante dos décadas (Microsoft Security Response Center publicó esta estadística repetidamente desde 2019) y siguen siendo la clase de bug explotable dominante en codebases C, C++ y unsafe-Rust. Cada cadena mayor de explotación contra un sistema operativo, navegador o kernel moderno — targets Pwn2Own, jailbreaks iOS/Android, cada CVE-2024 / CVE-2025 crítico en Chrome / Windows / Linux kernel — reduce en alguna capa a una memory-corruption primitive.
La clase importa como nota de enseñanza porque expone tres hechos senior transferibles sobre systems security:
- Memory safety es una propiedad del lenguaje, no del programador. Programadores C/C++ diligentes escribiendo código cuidadoso igual producen bugs de memory-corruption porque los lenguajes no proveen enforcement. Por eso existen Rust, Swift, guías modernas de C++ y features de hardware como ARM MTE: para mover la garantía desde disciplina humana hacia enforcement mecánico.
- Un bug no es un exploit. La mayoría de bugs de memory-corruption en el mundo real no se convierten en exploits porque mitigaciones modernas (exploit-mitigations) interrumpen la cadena. El trabajo del atacante es cada vez más encadenar múltiples bugs — info-leak + control-flow primitive + memory write — en vez de weaponizar un solo bug.
- La detección vive tanto en compile-time como en runtime. Sanitizers (AddressSanitizer, MemorySanitizer, UBSan), fuzzers (libFuzzer, AFL++, syzkaller), features de hardware (Intel CET, ARM MTE, ARM PAC) y analizadores estáticos encuentran subconjuntos distintos de memory corruption. La práctica senior los combina en vez de elegir uno.
El posicionamiento de la rama es: esta nota es el concepto raíz; stack-buffer-overflow es el ejemplo trabajado canónico; futuras notas cubren tipos específicos de corrupción (use-after-free, heap overflow, integer overflow) y el paisaje de mitigaciones. El par defensor es exploit-mitigations.
Cómo funciona
Memory corruption se reduce a 6 clases canónicas de bugs que tanto un operador de exploit como un defensor necesitan tener presentes:
1. Stack buffer overflow. Un buffer local en el stack recibe más datos de los que puede contener, sobrescribiendo datos adyacentes del stack: variables locales, saved frame pointers, saved return address. El bug introductorio canónico. Ver stack-buffer-overflow para el deep-dive completo.
2. Heap buffer overflow. Un buffer alocado en heap recibe más datos que su allocation. Chunks de heap adyacentes (allocator metadata, objetos vecinos) quedan corruptos. La explotación suele apuntar a allocator metadata (técnicas "House of *" contra glibc/ptmalloc) u objetos adyacentes cuyos function pointers pueden hijackearse.
3. Use-after-free (UAF). Un programa libera una allocation y luego accede a ella mediante un puntero viejo (un "dangling pointer"). Si el allocator reutilizó esa memoria para otro objeto — muchas veces controlable por el atacante — el programa lee o escribe por un puntero que ahora refiere a datos moldeados por el atacante. Los browser exploits viven acá.
4. Double-free. Un programa libera la misma allocation dos veces. El estado interno del allocator se corrompe (free-list links, size metadata). Muchos patrones "House of *" de allocator-exploitation nacen de double-frees.
5. Out-of-bounds read. Un programa lee memoria fuera del límite previsto de un objeto. Menos explotable directamente para code execution, pero es la info-leak primitive canónica: filtrar stack canaries, PIE base addresses, punteros libc necesarios para derrotar ASLR. Heartbleed (CVE-2014-0160) es el ejemplo de libro.
6. Integer overflow / integer conversion bug. Aritmética sobre tipos enteros produce un valor fuera del rango del tipo, haciendo wrap-around. El valor wrapped se usa luego como size o index, llevando a allocations demasiado chicas, reads demasiado grandes o confusión signed/unsigned. Suele ser una causa de buffer overflows más que una exploit primitive directa.
La cadena de explotación desde cualquiera de estos seis bugs hasta code execution funcional suele ser:
bug primitive (write / read)
→ info leak (derrotar ASLR/PIE/canary)
→ arbitrary read (leer estado del programa, estado del allocator)
→ control-flow hijack (sobrescribir function pointer, vtable, return address, GOT entry)
→ arbitrary execute (saltar a código controlado por atacante o ROP chain)
Una función C representativa con un stack buffer overflow de libro:
void greet(char *name) {
char buf[64];
strcpy(buf, name); // bug: no bounds check
printf("Hello, %s\n", buf);
}
strcpy escribe hasta ver \0. Un name mayor a 63 bytes (más el null terminator) desborda buf. Sin stack canary, sin PIE y sin DEP, esta es la forma del exploit de 1996 de Aleph One "Smashing the Stack for Fun and Profit"; con mitigaciones modernas, requiere primitives adicionales. Ver stack-buffer-overflow.
El bug no es "el programa crasheó"; es la noción del programa sobre qué bytes pertenecen a qué variable / objeto / región fue violada, y un atacante que controla el input que cruza el límite a veces puede aprovechar la violación para controlar la ejecución del programa.
Técnicas / patrones
- Ubicá la clase de bug antes del exploit. La mayoría de principiantes intenta "escribir un exploit"; investigadores senior primero identifican la clase de bug (¿UAF? ¿heap overflow? ¿integer overflow que produce undersized alloc?). La clase determina qué primitives están disponibles y qué mitigaciones importan.
- Encontrá la primitive antes del payload. Una "primitive" es una capacidad controlada: arbitrary read de N bytes en una dirección elegida por el atacante, arbitrary write de un puntero en una dirección elegida, control one-shot de
rip. La explotación senior es la disciplina de construir primitives, no de escribir shellcode. La explotación moderna Linux/Windows rara vez involucra shellcode: son ROP chains, JIT spraying, data-only attacks. - Usá sanitizers durante desarrollo y fuzzing. Compilá con
-fsanitize=address,undefinedy corré la suite o fuzz harness. ASAN captura stack overflow, heap overflow, UAF y double-free en el momento del bug, no en el punto eventual de crash horas después. - Coverage-guided fuzzing encuentra memory corruption mejor que humanos. libFuzzer / AFL++ / honggfuzz instrumentan el target binary, generan mutaciones de input y priorizan inputs que exploran caminos nuevos. Encuentran bugs en código escrito por expertos todos los días.
- Entendé el allocator. Heap exploitation depende del allocator: glibc ptmalloc (Linux), tcmalloc (Chrome), mimalloc (Microsoft), jemalloc (FreeBSD, mobile), Windows segment heap, Linux kernel SLUB allocator. Cada uno tiene su propio metadata layout, free-list structure y técnicas de explotación.
- Leé el disassembly. El source code muestra intención; el disassembly muestra comportamiento. Optimizaciones del compilador (inlining, dead-code elimination, stack reuse) pueden introducir u ocultar bugs de memory-corruption invisibles a nivel source.
- OPSEC del lado defensor: toda memory corruption exploit-quality produce telemetría detectable. Crash dumps con direcciones no estándar, alertas EDR sobre transferencias inusuales de control-flow, eventos GS-cookie failure. El exploit puede tener éxito; el indicador del intento rara vez es cero.
Variantes y bypasses
Memory corruption se divide en 4 categorías operacionales modernas, cada una con su propio paisaje de mitigaciones y toolkit de explotación.
1. Corrupción stack-based
Stack buffer overflow, stack-based UAF, falta de bounds checks sobre arrays locales. La categoría más simple de enseñar y explotar. Mitigaciones: stack canaries (GS cookies / SSP), Shadow Stack (Intel CET, ARM PAC), -fstack-protector-strong. Los sistemas operativos modernos dificultan mucho la explotación puramente stack-based; suele requerir encadenar con un info leak.
2. Corrupción heap-based
Heap buffer overflow, UAF, double-free, type confusion. La categoría dominante en explotación moderna porque objetos heap-allocated (vtables, function pointers, callback structs) abundan como targets. Mitigaciones: hardened allocators (PartitionAlloc en Chrome, MiraclePtr, GuardedMalloc), heap canaries, defensas anti heap-spray, memory tagging ARM MTE. La mayoría de CVEs de navegador aterriza acá.
3. Corrupción kernel-space
Mismas clases de bug (stack/heap overflow, UAF, double-free) pero en código kernel. Las primitives de explotación difieren: kernel exploitation busca contexto privilegiado (root/SYSTEM/EL1) más que code execution user-space. Mitigaciones: SMEP, SMAP, KPTI, KASLR, control-flow integrity (Intel CET kernel mode) y hardening específico de OS. Cada vez más difícil, pero todavía produce CVEs críticos anualmente.
4. Corrupción JIT / VM
JavaScript engines, WebAssembly runtimes, eBPF verifier y otros sistemas de compilación dinámica generan código executable en runtime. Bugs en el compilador JIT o en su inferencia de tipo/rango producen memory corruption con primitives privilegiadas (regiones writeable + executable, type confusion a nivel IR). Browser pwns y exploits kernel-eBPF aterrizan acá. Mitigaciones: JIT hardening, guards de type-checking, sandbox isolation.
Impacto
Ordenado por severidad real típica:
- Remote code execution (RCE). Memory corruption en un servicio network-facing o contenido cargado por navegador reachable desde internet. El outcome de mayor impacto. Cadenas browser RCE (Pwn2Own) son la categoría prestigio.
- Local privilege escalation (LPE). Kernel memory corruption que entrega code execution en contexto kernel. La cadena estándar después de RCE: browser RCE escapa el renderer sandbox vía kernel LPE.
- Sandbox escape. Memory corruption que permite salir de un sandbox (browser renderer, container, hypervisor, mobile app sandbox). Siempre es un eslabón de cadena, rara vez terminal.
- Information disclosure. Out-of-bounds reads que filtran secrets: private keys (Heartbleed), session cookies, address-space layouts. No siempre code execution directo, pero frecuentemente el primer paso.
- Denial of service. Memory corruption que crashea el programa confiablemente. El outcome menos impactante, pero relevante para servicios críticos.
- Data corruption / violación silenciosa de integridad. El outcome más insidioso: la memoria corrupta se usa como válida, produciendo comportamiento incorrecto pero plausible. Cálculos financieros corridos por un incremento de wrap-around; lógica safety-critical salteando un check. La detección es más difícil porque no hay crash.
Detección y defensa
Ordenado por efectividad (cadena desde prevención hasta detección):
1. Usá un lenguaje memory-safe cuando sea posible. Rust, Swift, Go, Java moderno, C#, TypeScript. Código nuevo en servicios críticos debería default-ear a lenguajes memory-safe. Esta es la solución estructural que saltea toda la clase de bug. Microsoft Azure y maintainers del Linux kernel se comprometieron públicamente a Rust para código nuevo kernel-adjacent desde 2023+.
2. CI con sanitizers para C/C++/unsafe-Rust. -fsanitize=address,undefined,thread corre en CI en cada PR. AddressSanitizer solo captura stack overflow, heap overflow, UAF y double-free en el momento de la violación, con stack trace completo. El control técnico de mayor leverage para código C/C++ inevitable.
3. Coverage-guided fuzzing en CI. Cobertura libFuzzer / AFL++ / OSS-Fuzz en cada PR para código de input parsing. Encuentra memory corruption que tests escritos a mano pierden. Estándar industrial para navegadores, componentes OS e implementaciones de protocolos.
4. Mitigaciones modernas a nivel compilador. -D_FORTIFY_SOURCE=3, -fstack-protector-strong, -fPIE -pie, -Wl,-z,relro -Wl,-z,now, Intel CET (-fcf-protection=full), ARM PAC. No arreglan los bugs pero suben el costo de explotación. Ver exploit-mitigations para el paisaje completo.
5. Hardware memory tagging. ARM MTE (Memory Tagging Extension) etiqueta allocations con un "color" de 4 bits almacenado en el puntero; la CPU trapea ante mismatch de color en access time. Captura UAF, heap overflow y out-of-bounds access a velocidad de hardware. Disponible en hardware ARMv8.5+. Plataformas Apple shippean allocators MTE-enabled en 2024+; dispositivos Android Pixel shippean MTE en developer preview.
6. Detección runtime vía EDR / kernel telemetry. La cadena de explotación — info leak seguida por ROP gadgets seguida por transferencias inusuales de control-flow — produce anomalías runtime detectables. EDR modernos instrumentan LoadLibrary, VirtualProtect y cambios de process-memory; telemetría de seguridad kernel (Sysmon, ETW, Linux kernel audit, tools basadas en eBPF) captura muchas cadenas reales de exploit.
Qué no funciona como defensa primaria
- "Tenemos code review." Manual code review encuentra algunos bugs de memory-corruption; sanitizers y fuzzers encuentran más, más rápido y con reproducers. Code review sigue siendo valioso para issues de diseño; no debería ser la defensa primaria para issues bug-class.
- "Usamos C++ smart pointers."
std::unique_ptrystd::shared_ptrmitigan algunos casos UAF y double-free, pero no todos: raw pointer access, lifetime management manual por performance e integración con APIs C dejan huecos. Smart pointers son buena práctica; no son respuesta estructural. - "Nuestro código es maduro y bien testeado." Codebases C/C++ maduras producen CVEs de memory-corruption constantemente. La madurez no resuelve un issue de propiedad del lenguaje.
- "Bloqueamos explotación, así que el bug no importa." Las mitigaciones compran tiempo; no arreglan el bug. Cadenas multi-stage derrotan mitigaciones individuales rutinariamente. Las mitigaciones son necesarias pero no suficientes.
Labs prácticos
Corré solo contra labs propios o engagements autorizados. Memory corruption en targets productivos sin autorización es una falta seria.
// Lab 1 — Minimal stack buffer overflow target. Compile without mitigations.
// Save as vuln.c
#include <stdio.h>
#include <string.h>
void greet(char *name) {
char buf[64];
strcpy(buf, name);
printf("Hello, %s\n", buf);
}
int main(int argc, char **argv) {
if (argc < 2) return 1;
greet(argv[1]);
return 0;
}
# Lab 1 (cont.) — Compile with mitigations disabled and observe the crash.
gcc -m32 -fno-stack-protector -no-pie -z execstack -o vuln vuln.c
./vuln $(python3 -c 'print("A"*200)')
# Expected: segmentation fault. Inspect with dmesg | tail or gdb.
# Lab 2 — Re-compile with sanitizer; observe the precise catch.
gcc -fsanitize=address -g -o vuln_asan vuln.c
./vuln_asan $(python3 -c 'print("A"*200)')
# Expected: AddressSanitizer prints "stack-buffer-overflow" with exact stack trace.
# This is what every C/C++ CI should do on every test run.
# Lab 3 — Re-compile with mitigations enabled; observe canary detection.
gcc -fstack-protector-strong -O2 -o vuln_canary vuln.c
./vuln_canary $(python3 -c 'print("A"*200)')
# Expected: "*** stack smashing detected ***: terminated".
# The canary changes the failure mode from silent corruption to controlled abort.
# Lab 4 — Build a UAF target and observe with ASAN.
cat > uaf.c <<'EOF'
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
int main() {
char *p = malloc(64);
strcpy(p, "before free");
free(p);
printf("UAF read: %s\n", p); // bug: read after free
return 0;
}
EOF
gcc -fsanitize=address -g -o uaf_asan uaf.c
./uaf_asan
# Expected: ASAN reports "heap-use-after-free" with the alloc/free/use stacks.
# Lab 5 — Fuzz a tiny C parser with libFuzzer.
cat > fuzz_target.c <<'EOF'
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
int parse(const uint8_t *data, size_t size) {
char buf[32];
if (size > 0) memcpy(buf, data, size); // intentional bug
return buf[0];
}
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
return parse(data, size);
}
EOF
clang -fsanitize=fuzzer,address -g -o fuzz fuzz_target.c
./fuzz -runs=10000
# Expected: libFuzzer finds the memcpy overflow within seconds, prints the
# reproducer input, and writes it to a crash-* file.
# Lab 6 — Inspect compiler-emitted mitigation primitives.
gcc -O2 -fstack-protector-strong -fPIE -pie -fcf-protection=full -c vuln.c -o vuln.o
objdump -d vuln.o | grep -E "endbr|__stack_chk_fail" | head
# Expected: see __stack_chk_fail references (canary check) and endbr32/endbr64
# instructions (Intel CET indirect-branch tracking). These are the primitives
# that exploit mitigations are built from.
Ejemplos prácticos
- Heartbleed (CVE-2014-0160, OpenSSL). Out-of-bounds read clásico en la extensión TLS heartbeat. El servidor lee una longitud especificada por atacante de memoria adyacente a un payload chico provisto por el cliente, devolviendo hasta 64 KB de memoria del proceso por request. Filtró private keys, session cookies, contraseñas. El fix fue un bounds check; el costo de la ventana sin fix fueron miles de millones en rotaciones de certs a escala industrial.
- Use-after-free en Chrome V8 (target regular de Pwn2Own). Las cadenas modernas de browser exploitation suelen empezar con type-confusion o UAF del JIT engine en el rendering engine. Entrega RCE de renderer-process -> escapa el sandbox vía bug Mojo IPC -> escala vía kernel UAF -> finalmente aterriza en contexto kernel. Tres o cuatro memory-corruption primitives encadenadas.
- iMessage zero-click (CVE-2023-41064 / familia FORCEDENTRY). Código de image-parsing de Apple procesaba contenido WebP / PDF provisto por atacante; integer overflow en el parser produjo una allocation demasiado chica, seguida por heap overflow. RCE completo en el dispositivo sin interacción de usuario. La cadena dependía de múltiples memory-corruption primitives apiladas entre stages del parser.
- CVE-2021-3156 (Sudo "Baron Samedit") heap buffer overflow. Off-by-one en parsing de command-line de Sudo. Pese a stack canaries y ASLR, el bug entrega heap corruption suficiente para obtener root localmente en prácticamente toda distribución Linux que shippeaba Sudo en ese momento. Demuestra que código maduro de décadas (Sudo) no es inmune.
- ASAN captura un UAF en CI antes de deploy. Un run rutinario de fuzzing sobre una nueva librería image-parser encuentra un UAF en el cleanup path sobre input malformado. El stack trace de ASAN apunta exactamente al dangling-pointer access; ingenieros arreglan en 20 minutos. Este es el outcome modal real de detección de bugs en codebases bien instrumentadas: la mayoría de memory corruption se captura pre-deploy, no en producción.
Notas relacionadas
- stack-buffer-overflow — el ejemplo trabajado canónico de clase 1 (corrupción stack-based); companion deep-dive de esta nota.
- exploit-mitigations — el par defensor estructural; qué hacen realmente ASLR, DEP, CET, MTE, canaries y PAC, y cuánto cuestan a un atacante.
- Tríada CIA — memory corruption suele ser una violación de integridad (estado del programa modificado sin autorización) que habilita violaciones posteriores; el framing importa para clasificación de incidentes.
- Threat Modeling Quickstart — memory corruption es la categoría STRIDE "Tampering" canónica a nivel systems-software.
- Dualidad atacante-defensor — exploit research y mitigation engineering son el mismo campo visto desde sillas opuestas; la dualidad es especialmente limpia acá.
- AEAD and nonce misuse — memory corruption que filtra nonces AEAD (o los reutiliza vía cross-allocation aliasing) destruye seguridad criptográfica; las dos notas se cruzan.
- EDR / process correlation — exploits modernos de memory-corruption dejan señales visibles para EDR (
VirtualProtectinesperado, patrones de ROP gadget, transferencias inusuales de control-flow). - Windows Privilege Escalation — kernel memory corruption es una de las primitives listadas de privesc; cadenas LPE suelen terminar en bugs kernel memory-corruption.
Futuras notas atómicas sugeridas
- heap-buffer-overflow-and-allocator-exploitation — deep dive clase 2: ptmalloc / tcmalloc / mimalloc / segment heap.
- use-after-free-and-dangling-pointers — deep dive clase 3: la primitive dominante de browser exploit.
- double-free-and-allocator-corruption — deep dive clase 4: linaje House-of-*.
- out-of-bounds-read-and-info-leaks — deep dive clase 5: patrón Heartbleed, info-leak primitives.
- integer-overflow-and-type-confusion — deep dive clase 6: causa canónica de allocation-size bugs.
- rop-and-ret2libc — la técnica primaria para code execution pese a DEP.
- aslr-pie-and-info-leak-chains — derrotar address-space layout randomization.
- stack-canaries-and-shadow-stacks — deep dive Intel CET / ARM PAC / SSP.
- arm-mte-and-memory-tagging — el enfoque de hardware para memory safety.
- control-flow-integrity-cfi — la capa de protección indirect-branch.
- fuzzing-with-libfuzzer-and-afl — patrones modernos coverage-guided fuzzing.
- sanitizers-asan-msan-ubsan — el toolkit runtime-detection para desarrollo y CI.
- detect-memory-corruption-exploitation — par playbook del lado defensor.
Referencias
- Fundamental: Aleph One — Smashing the Stack for Fun and Profit (Phrack 49, 1996; the foundational text) — http://phrack.org/issues/49/14.html
- Investigación / 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/
- Investigación / Deep Dive: Dennis Andriesse — Practical Binary Analysis (No Starch Press, 2018) — the modern reference on binary analysis and exploitation toolchain
- Docs oficiales de herramienta: AddressSanitizer — https://clang.llvm.org/docs/AddressSanitizer.html
- Investigación / Deep Dive: ARM — Memory Tagging Extension (MTE) — https://developer.arm.com/documentation/108035/latest/