AFL++
AFL++ es un fork del fuzzer AFL original que ofrece mejor rendimiento de fuzzing y funciones más avanzadas, manteniendo la estabilidad. Una ventaja importante sobre libFuzzer es que AFL++ tiene soporte estable para ejecutar campañas de fuzzing en múltiples núcleos, lo que lo hace ideal para esfuerzos de fuzzing a gran escala.
Cuándo Usarlo
| Fuzzer | Mejor Para | Complejidad |
|---|---|---|
| AFL++ | Fuzzing multi-núcleo, mutaciones diversas, proyectos maduros | Media |
| libFuzzer | Configuración rápida, un solo hilo, harnesses simples | Baja |
| LibAFL | Fuzzers personalizados, investigación, casos de uso avanzados | Alta |
Elige AFL++ cuando:
- Necesites fuzzing multi-núcleo para maximizar el rendimiento
- Tu proyecto se pueda compilar con Clang o GCC
- Quieras estrategias de mutación diversas y herramientas maduras
- libFuzzer haya llegado a una meseta y necesites más cobertura
- Estés fuzzing bases de código de producción que se beneficien de la ejecución en paralelo
Inicio Rápido
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
// Llama a tu código con los datos proporcionados por el fuzzer
check_buf((char*)data, size);
return 0;
}
Compilar y ejecutar:
# Primero, configura el script wrapper de AFL++ (ver Instalación)
./afl++ docker afl-clang-fast++ -DNO_MAIN=1 -O2 -fsanitize=fuzzer harness.cc main.cc -o fuzz
mkdir seeds && echo "aaaa" > seeds/minimal_seed
./afl++ docker afl-fuzz -i seeds -o out -- ./fuzz
Instalación
AFL++ tiene muchas dependencias, incluyendo LLVM, Python y Rust. Recomendamos usar una distribución actual de Debian o Ubuntu para fuzzing con AFL++.
| Método | Cuándo Usarlo | Compiladores Compatibles |
|---|---|---|
| Repositorios de Ubuntu/Debian | Ubuntu reciente, solo funciones básicas | Ubuntu 23.10: Clang 14 y GCC 13Debian 12: Clang 14 y GCC 12 |
| Docker (desde Docker Hub) | Versión específica de AFL++, soporte para Apple Silicon | A partir de 4.35c: Clang 19 y GCC 11 |
| Docker (desde código fuente) | Probar funciones no publicadas, aplicar parches | Configurable en el Dockerfile |
| Desde código fuente | Evitar Docker, necesitar parches específicos | Ajustable mediante la variable de entorno LLVM_CONFIG |
Ubuntu/Debian
Antes de instalar afl++, verifica la dependencia de la versión de clang del paquete con apt-cache show afl++ e instala la versión correspondiente de lld (por ejemplo, lld-17).
apt install afl++ lld-17
Docker (desde Docker Hub)
docker pull aflplusplus/aflplusplus:stable
Docker (desde código fuente)
git clone --depth 1 --branch stable https://github.com/AFLplusplus/AFLplusplus
cd AFLplusplus
docker build -t aflplusplus .
Desde código fuente
Consulta el Dockerfile para conocer los requisitos de versión de Ubuntu y las dependencias. Establece LLVM_CONFIG para especificar la versión de Clang (por ejemplo, llvm-config-18).
Configuración del Script Wrapper
Crea un script wrapper para ejecutar AFL++ en el anfitrión o en Docker:
cat <<'EOF' > ./afl++
#!/bin/sh
AFL_VERSION="${AFL_VERSION:-"stable"}"
case "$1" in
host)
shift
bash -c "$*"
;;
docker)
shift
/usr/bin/env docker run -i \
--privileged \
-v ./:/src \
--rm \
--name "afl_fuzzing_$$" \
"aflplusplus/aflplusplus:$AFL_VERSION" \
bash -c "cd /src && bash -c \"$*\""
;;
*)
echo "Uso: $0 {host|docker}"
exit 1
;;
esac
EOF
chmod +x ./afl++
Los ejemplos a continuación usan el modo docker, excepto los comandos de configuración del sistema que deben acceder al núcleo del anfitrión. Sustituye host para ejecutar cualquiera de ellos contra una instalación de AFL++ en la propia máquina. El wrapper une todo lo que sigue al argumento de modo en una sola cadena de shell, por lo que las comillas no se conservan: un argumento que contenga un espacio (-x "my dict.dict") llegará dividido por palabras. Renombra esos archivos sin espacios o edita el wrapper para esa ejecución.
La falta de -t es intencionada. docker run -ti aborta con the input device is not a TTY siempre que la entrada estándar no sea una terminal, lo que cubre trabajos de CI y cualquier cosa que maneje un agente o script. afl-fuzz nota que no hay terminal e imprime líneas de estado simples en lugar de la interfaz de pantalla completa. Un programa que insista en una terminal, como watch, debe ejecutarse en el lado del anfitrión del wrapper. $$ se expande al PID del wrapper, por lo que las instancias paralelas obtienen nombres de contenedor distintos en lugar de chocar en un único afl_fuzzing. docker ps trunca la columna COMMAND, por lo que todas las filas se ven iguales; usa docker ps --no-trunc para distinguir las instancias antes de detener una.
Advertencia de Seguridad: Los scripts afl-system-config y afl-persistent-config requieren privilegios de root y desactivan funciones de seguridad del SO. No realices fuzzing en sistemas de producción ni en tu entorno de desarrollo. Usa una máquina virtual dedicada.
Configuración del Sistema
Ejecuta después de cada reinicio para obtener hasta un 15% más de ejecuciones por segundo:
./afl++ host afl-system-config
afl-system-config ajusta el núcleo contra el que se ejecuta, así que ejecútalo en la máquina que aloja la campaña. ./afl++ docker afl-system-config alcanza la misma configuración a través del contenedor --privileged del wrapper, que es la única ruta cuando AFL++ está instalado solo mediante Docker.
Para máximo rendimiento, desactiva las mitigaciones de seguridad del núcleo (requiere el gestor de arranque grub, no compatible en Docker):
./afl++ host afl-persistent-config
update-grub
reboot
./afl++ host afl-system-config
Verifica con cat /proc/cmdline; la salida debe incluir mitigations=off.
Escribir un Harness
Estructura del Harness
AFL++ es compatible con harnesses al estilo libFuzzer:
#include <stdint.h>
#include <stddef.h>
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
// 1. Valida el tamaño de entrada si es necesario
if (size < MIN_SIZE || size > MAX_SIZE) return 0;
// 2. Llama a la función objetivo con los datos del fuzzer
target_function(data, size);
// 3. Devuelve 0 (distinto de cero reservado para uso futuro)
return 0;
}
Reglas del Harness
| Hacer | No Hacer |
|---|---|
| Restablecer el estado global entre ejecuciones | Depender del estado de ejecuciones anteriores |
| Manejar casos extremos con gracia | Salir en caso de entrada no válida |
| Mantener el harness determinista | Usar generadores de números aleatorios |
| Liberar la memoria asignada | Crear fugas de memoria |
| Validar tamaños de entrada | Procesar entrada sin límites |
Ver También: Para técnicas detalladas de escritura de harnesses, patrones para manejar entradas complejas y estrategias avanzadas, consulta el skill técnico fuzz-harness-writing.
Compilación
AFL++ ofrece múltiples modos de compilación con diferentes compensaciones.
Árbol de Decisión del Modo de Compilación
Elige tu modo de compilación:
- Modo LTO (
afl-clang-lto): Mejor rendimiento e instrumentación. Pruébalo primero. - Modo LLVM (
afl-clang-fast): Recurre a este si LTO falla al compilar. - Plugin GCC (
afl-gcc-fast): Para proyectos que requieran GCC.
Compilación Básica (modo LLVM)
./afl++ docker afl-clang-fast++ -DNO_MAIN=1 -O2 -fsanitize=fuzzer harness.cc main.cc -o fuzz
Compilación con GCC
./afl++ docker afl-g++-fast -DNO_MAIN=1 -O2 -fsanitize=fuzzer harness.cc main.cc -o fuzz
Importante: La versión de GCC debe coincidir con la versión utilizada para compilar el plugin GCC de AFL++.
Con Sanitizers
./afl++ docker AFL_USE_ASAN=1 afl-clang-fast++ -DNO_MAIN=1 -O2 -fsanitize=fuzzer harness.cc main.cc -o fuzz
Ver También: Para configuración detallada de sanitizers, problemas comunes y banderas avanzadas, consulta los skills técnicos address-sanitizer y undefined-behavior-sanitizer.
Banderas de Compilación
Ten en cuenta que -g no es necesario, lo agregan por defecto los compiladores de AFL++.
| Bandera | Propósito |
|---|---|
-DNO_MAIN=1 | Omitir la función main cuando se usa un harness libFuzzer |
-O2 | Nivel de optimización para producción (recomendado para fuzzing) |
-fsanitize=fuzzer | Habilitar el modo de compatibilidad con libFuzzer y agregar el runtime del fuzzer al enlazar el ejecutable |
-fsanitize=fuzzer-no-link | Instrumentar sin enlazar el runtime del fuzzer (para bibliotecas estáticas y archivos objeto) |
Gestión del Corpus
Creación del Corpus Inicial
AFL++ requiere al menos un archivo semilla no vacío:
mkdir seeds
echo "aaaa" > seeds/minimal_seed
Para proyectos reales, recopila entradas representativas:
- Descarga archivos de ejemplo para el formato que estás fuzzeando
- Extrae casos de prueba del conjunto de pruebas del proyecto
- Usa entradas válidas mínimas para tu formato de archivo
Minimización del Corpus
Después de una campaña, minimiza el corpus para conservar solo la cobertura única:
./afl++ docker afl-cmin -i out/default/queue -o minimized_corpus -- ./fuzz
Ver También: Para estrategias de creación de corpus, diccionarios y selección de semillas, consulta el skill técnico fuzzing-corpus.
Ejecutar Campañas
Ejecución Básica
./afl++ docker afl-fuzz -i seeds -o out -- ./fuzz
Configurar Variables de Entorno
./afl++ docker AFL_FAST_CAL=1 afl-fuzz -i seeds -o out -- ./fuzz
Interpretar la Salida
AFL++ reporta estas estadísticas de cualquier manera, pero cómo las lees depende del
modo. docker run -i del wrapper le da al contenedor ningún TTY, por lo que afl-fuzz
abandona la interfaz de pantalla completa y escribe líneas de estado simples en el registro; los campos
a continuación aparecen allí, y en state/<instancia>/fuzzer_stats. Para obtener la
interfaz interactiva, ejecuta el modo host en una terminal, o agrega -t al wrapper para una
ejecución que estés supervisando manualmente.
| Salida | Significado |
|---|---|
| execs/sec | Velocidad de ejecución: cuanto más alto, mejor |
| cycles done | Número de pases de cola completados |
| corpus count | Número de casos de prueba únicos en la cola |
| saved crashes | Número de fallos únicos encontrados |
| stability | % de bordes estables (debe estar cerca del 100%) |
Estructura del Directorio de Salida
out/default/
├── cmdline # ¿Cómo se invocó el SUT?
├── crashes/ # Entradas que hacen fallar el SUT
│ └── id:000000,sig:06,src:000002,time:286,execs:13105,op:havoc,rep:4
├── hangs/ # Entradas que cuelgan el SUT
├── queue/ # Casos de prueba que reproducen el estado final del fuzzer
│ ├── id:000000,time:0,execs:0,orig:minimal_seed
│ └── id:000001,src:000000,time:0,execs:8,op:havoc,rep:6,+cov
├── fuzzer_stats # Estadísticas de la campaña
└── plot_data # Datos para graficar
Analizar Resultados
Ver estadísticas de campaña en vivo:
./afl++ docker afl-whatsup out
Crear gráficos de cobertura. La imagen aflplusplus ya incluye gnuplot-nox; en modo host, instala gnuplot primero con apt install gnuplot.
./afl++ docker afl-plot out/default out_graph/
Re-ejecutar Casos de Prueba
Pasa uno de los nombres de archivo de out/default/crashes/:
./afl++ docker ./fuzz out/default/crashes/id:000000,sig:06,src:000002,time:286,execs:13105,op:havoc,rep:4
Opciones del Fuzzer
| Opción | Propósito |
|---|---|
-G 4000 | Longitud máxima de entrada de prueba (por defecto: 1048576 bytes) |
-t 1000 | Tiempo de espera en milisegundos para cada caso de prueba (por defecto: 1000 ms) |
-m 1000 | Límite de memoria en megabytes (por defecto: 0 = sin límite) |
-x ./dict.dict | Usar archivo de diccionario para guiar mutaciones |
Variables de Entorno que Importan
AFL++ tiene muchas variables de entorno, pero la mayoría son nicho. Estas son las que importan en la práctica.
Siempre Establecer Estas
# Toda campaña debería usar tmpfs: tu SSD lo agradecerá y es más rápido
AFL_TMPDIR=/dev/shm
AFL_TMPDIR es una ganancia de rendimiento gratuita sin desventajas; no configurarlo desgasta tu SSD y ralentiza el fuzzing.
Objetivos Lentos
# Acelera la calibración ~2.5x; úsalo cuando los objetivos sean lentos (p. ej., >10 ms/ejecución)
AFL_FAST_CAL=1
AFL_FAST_CAL reduce el tiempo de calibración con una pérdida de precisión insignificante. Recomendado específicamente para objetivos lentos donde la calibración de otro modo tomaría mucho tiempo.
Campañas Multi-Núcleo
# Solo en la instancia principal (-M); necesario para afl-cmin, no para el fuzzing en sí
AFL_FINAL_SYNC=1
# En todas las instancias: almacena en caché los casos de prueba en memoria (por defecto: 50 MB, buen rango: 50-250 MB)
AFL_TESTCACHE_SIZE=100
AFL_FINAL_SYNC le indica a la instancia principal que realice una importación final de todas las secundarias al detenerse. Esto no afecta el proceso de fuzzing en sí; solo importa cuando luego ejecutes afl-cmin para la minimización del corpus, asegurando que la cola principal tenga el corpus combinado completo. AFL_TESTCACHE_SIZE almacena en caché los casos de prueba en memoria para reducir E/S de disco; el valor predeterminado es 50 MB y valores entre 50-250 MB funcionan bien para la mayoría de las campañas.
Fuzzing CI/Automatizado
# Fallar rápido si el fuzzing no encuentra nada
AFL_EXIT_ON_TIME=3600 # 1 hora sin nuevas rutas = detener
# O ejecutar hasta "terminado" (todos los elementos de la cola procesados)
AFL_EXIT_WHEN_DONE=1
# Entornos sin interfaz gráfica
AFL_NO_UI=1
El fuzzing sin límite en CI desperdicia recursos. Establece límites de tiempo o usa condiciones de salida.
Variables a Evitar
| Variable | Por Qué Omitirla |
|---|---|
AFL_NO_ARITH | Puede perjudicar la cobertura en formatos binarios, pero puede ser útil para objetivos basados en texto |
AFL_SHUFFLE_QUEUE | Solo para configuraciones exóticas, generalmente dañino |
AFL_DISABLE_TRIM | El recorte es valioso, no lo desactives sin motivo |
Fuzzing Multi-Núcleo
AFL++ destaca en fuzzing multi-núcleo con dos ventajas principales:
- Más ejecuciones por segundo (escala linealmente con los núcleos físicos)
- Fuzzing asimétrico (p. ej., un trabajo con ASan, el resto sin sanitizers)
Iniciar una Campaña
Iniciar el fuzzer principal (en segundo plano):
./afl++ docker afl-fuzz -M primary -i seeds -o state -- ./fuzz 1>primary.log 2>primary.error </dev/null &
Iniciar fuzzers secundarios (tantos como núcleos tengas):
./afl++ docker afl-fuzz -S secondary01 -i seeds -o state -- ./fuzz 1>secondary01.log 2>secondary01.error </dev/null &
./afl++ docker afl-fuzz -S secondary02 -i seeds -o state -- ./fuzz 1>secondary02.log 2>secondary02.error </dev/null &
El </dev/null es obligatorio, no decorativo. docker run -i mantiene al cliente
leyendo su propia entrada estándar, y un proceso en segundo plano que lee la terminal recibe SIGTTOU y se detiene.
Ver También: Para estrategias avanzadas de fuzzing multi-núcleo, distribución de carga de trabajo y resolución de problemas, consulta el skill técnico parallel-fuzzing.
Detener una Campaña
killall afl-fuzz
O para detener una instancia específica:
docker kill afl_fuzzing_1234
Encontrar la Configuración Correcta
Tomar archivos de configuración del directorio de estado de una campaña que funcione facilita la automatización y la depuración. Copia el contenido de state/<instancia>/fuzzer_stats, state/<instancia>/cmdline, state/<instancia>/fuzz_bitmap y otros archivos que los scripts de automatización necesiten leer.
Clasificación de Fallos (Crashes)
AFL++ guarda los fallos en out/default/crashes/. Clasificar las causas raíz es un proceso manual que requiere reproducir cada fallo contra el ejecutable original (sin instrumentación de AFL++) y recopilar la traza.
# Reconstruir sin instrumentación de AFL++
./afl++ host clang++ -DNO_MAIN=1 -O2 -fsanitize=fuzzer harness.cc main.cc -o fuzz.repro
# Reproducir y recolectar traza
./fuzz.repro < out/default/crashes/id:000000,* 2>&1 | head -100
Puedes usar asan_symbolize para simbolizar trazas de AddressSanitizer:
./fuzz.repro < out/default/crashes/id:000000,* 2>&1 | asan_symbolize | c++filt
Más Allá de lo Básico
- Fuzzing en Modo Persistente: Para harnesses que admiten múltiples ejecuciones sin reiniciar el proceso, agrega
AFL_NO_FORKSRV=1y usa el modo persistente de AFL++. - Diccionarios: Usa
-x dict.txtpara guiar las mutaciones con tokens específicos del formato (consulta el skill técnico fuzzing-dictionaries). - Fuzzing Customizado: Para necesidades avanzadas, considera LibAFL (consulta el skill técnico libafl).
Ver También: El skill técnico fuzzing-lifecycle cubre el panorama completo: desde la selección del objetivo y la escritura del harness, hasta la ejecución de la campaña, la minimización del corpus y la clasificación o reducción de fallos.