Ir al contenido

Fuzzing moderno en 2026: Snapshots, frameworks y fuzzing de lo que no pudiste antes

· 13 min read · default
cybersecurityfuzzingvulnerability-researchreverse-engineeringtestingsecurity

El fuzzing tiene un problema de reputación: muchos ingenieros todavía lo imaginan como tirar bytes aleatorios a un programa hasta que crashee. Esa descripción era aproximadamente correcta en 1990 y ha sido engañosa por una década. El fuzzing moderno es guiado por cobertura — el fuzzer instrumenta el objetivo, observa qué rutas de código cada entrada alcanza, y dirige la mutación hacia entradas que exploran territorio nuevo. Ese loop de feedback es la diferencia entre hacer poke aleatorio a un parser y caminar sistemáticamente su espacio de estado, y es por qué el fuzzing ahora encuentra miles de CVEs reales por año en software que ha sido revisado por expertos.

El paisaje 2026 ha madurado bien más allá de una herramienta única, aunque, y los desarrollos interesantes son sobre alcanzar objetivos que el fuzzing convencional no podía: kernels del sistema operativo, código enterrado detrás de setup costoso, y aplicaciones cuya estructura de entrada derrota mutación de bytes. Esta guía cubre ese paisaje — los clásicos guiados por cobertura, syzkaller para kernels, Snapchange para fuzzing de snapshot, y LibAFL para construir el fuzzer que tu objetivo realmente necesita — más cargo-fuzz y honggfuzz para trabajo cotidiano.

Por qué el feedback de cobertura cambió todo

El mecanismo vale la pena entender porque explica para qué está bien el fuzzing y dónde se atasca. Un fuzzer guiado por cobertura compila el objetivo con instrumentación que registra qué edges del grafo de flujo de control una ejecución tocó. Mantiene un corpus de entradas, y cuando una entrada mutada alcanza un edge que ninguna entrada previa alcanzó, esa entrada es juzgada interesante y añadida al corpus para ser mutada más. Sobre millones de iteraciones, el corpus acumula entradas que colectivamente ejercitan rutas profundas e inusuales — incluyendo rutas que ningún humano escribió una prueba para.

Esto produce comportamiento que se ve casi inteligente. Dado un corpus semillado con un PNG válido, un fuzzer descubrirá la estructura de chunk, luego tipos de chunk válidos, luego las ramas de parsing para cada tipo, construyendo progresivamente entradas que alcanzan más profundo en el decodificador. Nunca "entiende" PNG; solo mantiene lo que movió cobertura.

El corolario es dónde el fuzzing se atasca: verificaciones difíciles que no puede adivinar. Una comparación de constante mágica, una checksum o una firma criptográfica crean una pared — la mutación aleatoria esencialmente nunca producirá los 8 bytes correctos, así que todo detrás de esa verificación permanece inexplorado. Las respuestas prácticas son semillar el corpus con entradas válidas, suministrar un diccionario de valores mágicos, o parchear checksums en una compilación fuzz. Reconocer una meseta de cobertura como "golpeé una pared" en lugar de "no hay más bugs" es uno de los instintos más útiles en este trabajo.

Fuzzing de kernel: syzkaller

Los kernels del sistema operativo son un objetivo hostil para los fuzzers convencionales. La entrada no es un archivo sino una secuencia de syscalls con argumentos interdependientes — un file descriptor de open debe fluir a read, y la mayoría de secuencias de syscall aleatorias fallan inmediatamente con EINVAL. Los crashes derriban la máquina entera en lugar de un proceso, y la cobertura debe ser recolectada desde el espacio kernel.

syzkaller soluciona los tres. Describe syscalls en un lenguaje declarativo (syzlang) así puede generar secuencias plausibles con argumentos correctamente tipados e interdependientes; ejecuta objetivos dentro de VMs desechables así los crashes son sobrevivibles y automáticamente recolectados; y usa KCOV para feedback de cobertura del kernel más KASAN para atrapar errores de memoria que de otro modo serían corrupción silenciosa. El syzbot continuo de Google lo ejecuta contra Linux y ha reportado miles de bugs.

La lección generaliza más allá de kernels: syzkaller funciona porque alguien codificó conocimiento de la estructura de entrada en descripciones. Cuando los entradas de un objetivo tienen gramática, enseñar al fuzzer esa gramática vence la mutación de bytes al azar por un margen amplio. El costo correspondiente es real — extender syzlang para un subsistema bajo-testeado es trabajo genuino, y también es la contribución de mayor valor que la mayoría de personas puede hacer al fuzzing de kernel.

Fuzzing de snapshot: pasar el setup

La segunda frontera es objetivos donde el código interesante se sienta detrás de inicialización costosa. Considera fuzzing el parser de queries de una base de datos: cada iteración necesitaría iniciar el servidor, inicializar almacenamiento, autenticar y establecer una sesión antes de que una sola query fuera parseada. A tal vez diez iteraciones por segundo, el fuzzing guiado por cobertura es desesperado — la técnica necesita miles.

El fuzzing de snapshot invierte esto. Ejecutas el objetivo una vez exactamente al momento de interés, tomas un snapshot de memoria del estado de máquina completa, y luego restauras ese snapshot para cada iteración subsecuente. Todo el costo de setup se paga una vez. Snapchange (de AWS) implementa esto con KVM: capturas un snapshot con QEMU, escribes un pequeño harness Rust describiendo dónde inyectar entrada y cuándo una iteración termina, y reproduce desde ese estado a tasas muy altas.

Esto desbloquea categorías que previamente fueron impracticables: protocolos de red stateful fuzzeados mid-sesión, código detrás de autenticación, código de hypervisor y kernel, y cualquier aplicación con startup pesado. El tradeoff es esfuerzo — escribes un fuzzer Rust en lugar de ejecutar un comando, y debes entender el layout de memoria del objetivo bien suficientemente para inyectar entrada correctamente. Es una técnica especialista que paga exactamente cuando la alternativa es no hacer fuzzing del objetivo en absoluto.

Frameworks: construye el fuzzer que el objetivo necesita

El tercer desarrollo es filosófico. AFL++ y libFuzzer son excelentes en lo que fueron diseñados para y torpes cuando tu objetivo no se ajusta — un protocolo binario personalizado, una imagen de firmware emulada, una entrada que es un árbol en lugar de un buffer. Históricamente forzabas la herramienta, usualmente mal.

LibAFL, del equipo de AFL++, trata un fuzzer como partes componibles: observers que registran datos, feedbacks que juzgan interés, mutators que transforman entradas, schedulers, stages y executors. Ensamblas la combinación que tu objetivo necesita, definas tu propio tipo de entrada y mutators si la entrada es estructurada, elige un executor (in-process, forkserver, emulación QEMU, instrumentación Frida, snapshot), y obtienes scaling multi-core gratis.

El encuadre honesto es que este es un compromiso más grande que ejecutar una herramienta, así que la secuencia importa: comienza con cargo-fuzz para Rust o honggfuzz/AFL++ para objetivos nativos, y muévete a LibAFL solo cuando puedas articular específicamente por qué no se ajustan. "La entrada es una máquina de estado de protocolo y la mutación de bytes nunca produce un segundo mensaje válido" es tal razón; "quiero que sea más rápido" usualmente no lo es.

Fuzzing cotidiano que los equipos realmente sostienen

La mayoría de valor, para la mayoría de equipos, viene de fuzzing continuo poco glamoroso de parsers y manejadores de entrada no confiable. cargo-fuzz hace esto casi sin fricción para Rust: escribe una función tomando &[u8] (o, mejor, un valor tipado vía el crate arbitrary) y ejecuta un comando. honggfuzz es similarmente fácil para código nativo y añade cobertura basada en hardware vía Intel PT/BTS, que te permite hacer fuzzing de binarios que no puedes recompilar — valioso para dependencias de código cerrado.

Dos prácticas separan los equipos que obtienen valor de los que abandonan el fuzzing después de una semana. Primero, semilla el corpus con entradas válidas reales; un fuzzer comenzando desde un corpus vacío pasa tiempo enormous redescubriendo validez de formato básico que podrías haberle entregado. Segundo, ejecuta continuamente y trata hallazgos como tests: convierte cada crash minimizado en un test de regresión para que permanezca arreglado, y deja que el corpus persista entre ejecuciones así el progreso se acumula. El fuzzing no es una auditoría de una tarde; es un proceso de fondo que mantiene encontrando cosas mientras el código cambia.

Los sanitizers merecen mención porque multiplican efectividad. AddressSanitizer convierte corrupción de memoria silenciosa en un crash inmediato y diagnosticable, y UBSan atrapa undefined behavior que de otro modo podría parecer una misteriosa descompilación después. El fuzzing sin sanitizers encuentra solo los bugs que sucede al crashear por sí mismos — típicamente una fracción pequeña de lo que realmente está allí.

Triage: el trabajo que comienza cuando el crash llega

Encontrar un crash es el comienzo, y los equipos rutinariamente subestiman el esfuerzo entre "el fuzzer paró" y "un desarrollador puede arreglarlo." Cuatro pasos marcan la diferencia entre un reporte útil y uno ignorado.

Minimiza la entrada. Una entrada que crashea de un fuzzer típicamente está llena de bytes irrelevantes que sobrevivieron solo porque nada los eliminó. Cada fuzzer serio envía un minimizer (cargo fuzz tmin, afl-tmin, syz-repro de syzkaller), y ejecutarlo convierte un blob de 4KB en un puñado de bytes que aíslan el trigger actual. Esto importa enormemente para el desarrollador que tiene que entenderlo.

Deduplica. Un fuzzer que ejecuta toda la noche reportará el mismo bug docenas de veces a través de diferentes entradas. Agrupar por ubicación de crash y firma de stack convierte 200 crashes en seis bugs distintos. Sin este paso, el triage se ve imposiblemente costoso y la gente se rinde.

Evalúa explotabilidad, cuidadosamente. No cada crash es una vulnerabilidad. Una desreferencia de null pointer en un parser es usualmente una denegación de servicio; un heap buffer overflow con longitud controlada por atacante es potencialmente mucho peor. La salida de sanitizer ayuda enormemente aquí — ASan te dice el tipo de error de memoria, los tamaños, y ambos stacks de asignación y acceso. Resiste la tentación de etiquetar todo como crítico, porque un equipo que recibe severidades infladas deja de confiar en los reportes.

Convierte a un test de regresión. La entrada minimizada se convierte en un test unitario comprometido junto con el fix. Esto es lo que mantiene el bug arreglado y lo que hace que el fuzzing se componga a lo largo del tiempo en lugar de redescubrir los mismos issues después de un refactor.

Los equipos que obtienen valor sostenido del fuzzing son los que construyen este pipeline una vez, no los que encuentran los crashes más.

Eligiendo dónde empezar

La decisión sigue el objetivo. Para código Rust, usa cargo-fuzz, y usa arbitrary para fuzzing de APIs tipadas en lugar de solo parsers de bytes. Para binarios userspace nativos con source, honggfuzz o AFL++ con sanitizers. Para binarios sin source, feedback de hardware de honggfuzz o modo QEMU de AFL++. Para OS kernels, syzkaller, y considera extender syzlang para el subsistema que te importa. Para código detrás de setup costoso o sesiones stateful, Snapchange o una configuración de LibAFL capaz de snapshot. Y para entradas con estructura real — protocolos, ASTs, formatos de archivo con gramática — ya sea un mutator consciente de gramática o un fuzzer LibAFL personalizado, porque la mutación de bytes se meseteará temprano.

Cruzando todos estos, la misma disciplina aplica: semilla bien, ejecuta con sanitizers, ejecuta continuamente, minimiza crashes y convierte hallazgos en tests de regresión. La herramienta importa menos que si el loop mantiene ejecutándose.

La conclusión

El fuzzing en 2026 es una disciplina sistemática de descubrimiento de vulnerabilidad construida sobre feedback de cobertura, y su frontera está alcanzando objetivos que previamente estaban fuera de scope. syzkaller hace fuzzing de kernels mediante codificar estructura de syscall y ejecutar en VMs desechables; Snapchange usa snapshots de KVM para fuzzing de código enterrado detrás de setup costoso; LibAFL te permite construir un fuzzer matched a un objetivo inusual en lugar de forzar una herramienta monolítica; y cargo-fuzz y honggfuzz hacen el fuzzing continuo cotidiano barato suficientemente para realmente sostener. Comienza con la herramienta fácil para tu lenguaje, semilla el corpus con entradas reales, siempre habilita sanitizers, trata una meseta de cobertura como una pared a ingeniar pasada en lugar de un all-clear, y convierte cada crash en un test.

Referencias y recursos

Herramientas

Background y análisis

Cheatsheets relacionados de 1337skills