Parte: 5 — Explotación de sistemas y binarios · Fuente: Andriesse, Practical Binary Analysis · docs AFL++/LLVM ⏱️ Duración estimada: 140 min · Nivel: Avanzado
Descubrir vulnerabilidades automáticamente mediante fuzzing: alimentar a un programa con miles de entradas mutadas guiadas por cobertura hasta provocar crashes. Aprenderás a compilar objetivos con instrumentación, a lanzar campañas con AFL++ y libFuzzer, a combinar con sanitizers (ASan/UBSan) y a triar y minimizar los crashes encontrados.
⚠️ Ética: fuzzea software propio, open source con permiso o dentro de programas autorizados. Reporta hallazgos de forma responsable.
Al finalizar, el alumno podrá:
afl-cc/-fsanitize=fuzzer.LLVMFuzzerTestOneInput).| # | Tema | Por qué importa |
|---|---|---|
| 1 | Fuzzing guiado por cobertura | Encuentra rutas profundas |
| 2 | Instrumentación (AFL/SanCov) | Retroalimenta al mutador |
| 3 | Corpus semilla | Buen punto de partida |
| 4 | Diccionarios | Tokens del formato objetivo |
| 5 | Sanitizers (ASan/UBSan) | Detectan bugs silenciosos |
| 6 | libFuzzer y harness | Fuzzing in-process rápido |
| 7 | Triage y dedup de crashes | Del ruido al bug real |
| 8 | Minimización (tmin/casr) | Caso mínimo reproducible |
El fuzzing es la técnica automatizada más productiva para descubrir vulnerabilidades: consiste en alimentar un programa con enormes cantidades de entradas generadas automáticamente —muchas malformadas o aleatorias— y observar cuáles provocan un crash, porque un crash suele indicar un fallo de memoria (buffer overflow, UAF, integer overflow) potencialmente explotable. Su enorme valor está en que encuentra casos que un humano nunca probaría: entradas absurdas, tamaños extremos, combinaciones inesperadas. El fuzzing ha descubierto miles de vulnerabilidades en software crítico —navegadores, librerías de imágenes, parsers, kernels— y es hoy una parte estándar del desarrollo seguro y de la investigación ofensiva.
El fuzzing "tonto" —enviar bytes aleatorios— encuentra pocos bugs, porque casi ninguna entrada aleatoria pasa de las primeras comprobaciones del programa. La revolución fue el fuzzing guiado por cobertura, del que AFL (American Fuzzy Lop) y su sucesor AFL++ son el estándar. La idea es brillante: se instrumenta el binario para que informe qué caminos de código ejecuta cada entrada, y el fuzzer usa esa señal como brújula —cuando una entrada llega a un camino nuevo del programa, se guarda como semilla valiosa y se muta para explorar aún más allá—. Así el fuzzer "aprende" a construir entradas que penetran cada vez más profundo, evolucionando desde datos triviales hasta entradas que atraviesan parsers complejos. La instrumentación se añade al compilar (AFL) o con sanitizadores de cobertura (SanCov), y es lo que separa el fuzzing moderno del bombardeo ciego.
Un fuzzing productivo no es solo lanzar AFL: depende de varios elementos. El corpus semilla —un conjunto de entradas válidas de ejemplo (imágenes reales si se fuzzea un decodificador de imágenes, PDFs reales si es un lector de PDF)— da al fuzzer un punto de partida realista desde el que mutar, en lugar de empezar de cero. Los diccionarios aportan los tokens del formato (palabras clave, magic bytes, etiquetas) para que el fuzzer los combine, esencial en formatos estructurados que requieren palabras concretas. Y —crítico— los sanitizers: AddressSanitizer (ASan) y UBSanitizer (UBSan) de las clases 127–128 convierten corrupciones silenciosas en crashes inmediatos y detallados. Sin ellos, un buffer overflow que escribe unos bytes de más quizá no crashea (solo corrompe memoria que no se usa aún), y el fuzzer no lo detecta; con ASan, ese mismo overflow crashea al instante con un informe preciso. Fuzzear siempre con sanitizers es la práctica que multiplica los hallazgos.
Hay dos estilos de fuzzing. AFL++ fuzzea el binario completo a través de su entrada estándar o
ficheros. libFuzzer es un fuzzing in-process y dirigido: el desarrollador escribe un harness
—una pequeña función que recibe los bytes del fuzzer y llama a la función concreta que se quiere
probar—, lo que permite fuzzear una librería componente a componente con enorme velocidad. Escribir
buenos harness es una habilidad en sí misma y la base del fuzzing continuo (OSS-Fuzz de Google fuzzea
así miles de proyectos). El fuzzing produce muchos crashes, y el trabajo no acaba ahí: el triage y la
deduplicación agrupan los miles de crashes por su causa raíz (muchos crashes distintos pueden ser
el mismo bug), y la minimización (afl-tmin, casr) reduce cada entrada que provoca un crash a su
forma mínima —los pocos bytes esenciales—, lo que facilita enormemente entender y reportar el bug.
La lección de la clase es que el fuzzing es la forma más escalable de encontrar los bugs de memoria de
toda esta parte: no reemplaza el análisis humano, pero genera el material —los crashes— que luego
se analiza y se convierte en exploits (clase 138) o en parches.
LLVMFuzzerTestOneInput. Clave: muy rápido para
librerías/funciones.afl-tmin,
ASan backtrace, casr para dedup.| Término | Definición concisa |
|---|---|
| Fuzzing | Alimentar el programa con muchas entradas para provocar crashes |
| Crash | Fallo que suele indicar una corrupción de memoria explotable |
| Guiado por cobertura | El fuzzer usa qué código se ejecuta como brújula |
| AFL / AFL++ | Fuzzer estándar guiado por cobertura |
| Instrumentación | Añadir al binario el reporte de caminos ejecutados |
| Camino nuevo | Entrada que alcanza código no visto; se guarda como semilla |
| Corpus semilla | Entradas válidas de ejemplo desde las que mutar |
| Diccionario | Tokens del formato que el fuzzer combina |
| Sanitizer (ASan/UBSan) | Convierte corrupciones silenciosas en crashes claros |
| libFuzzer | Fuzzing in-process, dirigido a una función |
| Harness | Función que conecta el fuzzer con el código objetivo |
| Triage / dedup | Agrupar crashes por su causa raíz |
| Minimización | Reducir una entrada de crash a su forma mínima |
| OSS-Fuzz | Servicio de fuzzing continuo de proyectos abiertos |
# AFL++
sudo apt install -y afl++ # o compilar desde github.com/AFLplusplus/AFLplusplus
# libFuzzer viene con clang
clang --version
Entorno propio.
parser.c (tiene un overflow deliberado):c
#include <stdio.h>
#include <string.h>
void parse(const char *s){ char b[16]; if(s[0]=='F' && s[1]=='U') strcpy(b, s); }
int main(int c, char**v){ char buf[256]; FILE*f=fopen(v[1],"rb"); int n=fread(buf,1,255,f); buf[n]=0; parse(buf); }
bash
afl-cc -fsanitize=address -o parser_afl parser.c
mkdir in && printf 'FUabc' > in/seed
afl-fuzz -i in -o out -- ./parser_afl @@
Observa el panel de AFL++: paths, crashes únicos, ejecuciones/seg. En minutos deberían aparecer
crashes en out/default/crashes/.
Reproduce y tría un crash con ASan para ver el stack-buffer-overflow:
bash
./parser_afl out/default/crashes/id:000000* # ASan imprime el backtrace
Minimiza el caso: afl-tmin -i <crash> -o crash_min -- ./parser_afl @@.
Escribe un harness libFuzzer para la función parse:
c
extern void parse(const char*);
int LLVMFuzzerTestOneInput(const unsigned char *d, unsigned long n){
char *s = malloc(n+1); memcpy(s,d,n); s[n]=0; parse(s); free(s); return 0; }
bash
clang -g -fsanitize=fuzzer,address harness.c parser.c -o fz && ./fz -runs=100000
"FU" y compara la velocidad de descubrimiento.afl-tmin y explica qué eliminó.Encuentra y reproduce un crash en un objetivo instrumentado, minimiza el caso y explica la causa raíz con el backtrace de ASan.
Criterio de aceptación: entregas un input mínimo que provoca el crash de forma determinista y el
reporte de ASan que identifica el tipo de bug (p. ej. stack-buffer-overflow).
| Síntoma / mensaje | Causa y cómo arreglar |
|---|---|
| 0 crashes tras horas | Corpus/diccionario pobres; el fuzzer no supera un check |
| "suboptimal, core dumps" | Ajusta core_pattern como indica AFL al arrancar |
| Crashes no reproducibles | Falta ASan o hay no-determinismo (tiempo/aleatorio) |
| Muy pocas ejec/seg | Objetivo lento; usa modo persistente/in-process |
| Todos los crashes iguales | Falta dedup; usa triage por backtrace/casr |
❓ ¿AFL++ o libFuzzer? AFL++ para binarios/CLI y black-box con QEMU; libFuzzer para funciones de librería in-process. A menudo se usan ambos.
❓ ¿Necesito el código fuente? No siempre: AFL++ tiene modo QEMU/frida para binarios sin fuente, aunque más lento.
❓ ¿Y si no hay crashes? Mejora semillas y diccionario, añade sanitizers y aumenta el tiempo; también considera assertions.
Clase 135 — Ofuscación y técnicas anti-reversing