
american fuzzy lop - un fuzzer orientado a la seguridad
Desarrollado originalmente por Michal Zalewski [email protected].
Consulta QuickStartGuide.txt si no tienes tiempo de leer este archivo.
El fuzzing es una de las estrategias más potentes y probadas para identificar problemas de seguridad en software del mundo real; es responsable de la gran mayoría de los errores de ejecución remota de código y de escalada de privilegios encontrados hasta la fecha en software crítico para la seguridad.
Desafortunadamente, el fuzzing también es relativamente superficial; las mutaciones aleatorias a ciegas hacen muy poco probable alcanzar ciertas rutas de código en el software probado, dejando algunas vulnerabilidades firmemente fuera del alcance de esta técnica.
Ha habido numerosos intentos de resolver este problema. Uno de los primeros enfoques - pionero de Tavis Ormandy - es la destilación de corpus. El método se basa en señales de cobertura para seleccionar un subconjunto de semillas interesantes de un corpus masivo y de alta calidad de archivos candidatos, y luego fuzzearlos por medios tradicionales. El enfoque funciona excepcionalmente bien, pero requiere que dicho corpus esté disponible fácilmente. Además, las mediciones de cobertura de bloques proporcionan una comprensión muy simplista del estado del programa y son menos útiles para guiar el esfuerzo de fuzzing a largo plazo.
Otras investigaciones más sofisticadas se han centrado en técnicas como el análisis de flujo de programa ("ejecución concolica"), la ejecución simbólica o el análisis estático. Todos estos métodos son extremadamente prometedores en entornos experimentales, pero tienden a sufrir problemas de fiabilidad y rendimiento en usos prácticos - y actualmente no ofrecen una alternativa viable a las técnicas de fuzzing "tonto".
American Fuzzy Lop es un fuzzer de fuerza bruta combinado con un algoritmo genético guiado por instrumentación extremadamente simple pero sólido como una roca. Utiliza una forma modificada de cobertura de aristas para capturar sin esfuerzo cambios sutiles a escala local en el flujo de control del programa.
Simplificando un poco, el algoritmo general se puede resumir como:
Cargar los casos de prueba iniciales proporcionados por el usuario en la cola,
Tomar el siguiente archivo de entrada de la cola,
Intentar recortar el caso de prueba al tamaño más pequeño que no altere el comportamiento medido del programa,
Mutar repetidamente el archivo usando una variedad equilibrada y bien investigada de estrategias tradicionales de fuzzing,
Si alguna de las mutaciones generadas produce una nueva transición de estado registrada por la instrumentación, añadir la salida mutada como una nueva entrada en la cola.
Ir al paso 2.
Los casos de prueba descubiertos también se eliminan periódicamente para descartar aquellos que han sido obsoletos por hallazgos más nuevos y de mayor cobertura; y se someten a varios otros pasos de minimización de esfuerzo guiados por instrumentación.
Como resultado secundario del proceso de fuzzing, la herramienta crea un corpus pequeño y autónomo de casos de prueba interesantes. Estos son extremadamente útiles para sembrar otros regímenes de prueba que requieren mucho trabajo o recursos - por ejemplo, para pruebas de estrés de navegadores, aplicaciones de oficina, suites gráficas o herramientas de código cerrado.
El fuzzer está exhaustivamente probado para ofrecer un rendimiento inmediato muy superior al del fuzzing a ciegas o al de las herramientas basadas solo en cobertura.
Cuando el código fuente está disponible, la instrumentación se puede inyectar mediante una herramienta complementaria que funciona como un reemplazo directo de gcc o clang en cualquier proceso de compilación estándar para código de terceros.
La instrumentación tiene un impacto de rendimiento bastante modesto; junto con otras optimizaciones implementadas por afl-fuzz, la mayoría de los programas se pueden fuzzear tan rápido o incluso más rápido que con las herramientas tradicionales.
La forma correcta de recompilar el programa objetivo puede variar dependiendo de los detalles específicos del proceso de compilación, pero un enfoque casi universal sería:```shell $ CC=/path/to/afl/afl-gcc ./configure $ make clean all
Para programas C++, también querrás establecer `CXX=/path/to/afl/afl-g++`.
Los wrappers de clang (afl-clang y afl-clang++) pueden usarse de la misma manera;
los usuarios de clang también pueden optar por aprovechar un modo de instrumentación de mayor rendimiento,
como se describe en llvm_mode/README.llvm.
Al probar librerías, necesitas encontrar o escribir un programa simple que lea
datos de stdin o de un archivo y los pase a la librería probada. En tal
caso, es esencial enlazar este ejecutable contra una versión estática de la
librería instrumentada, o asegurarse de que el archivo .so correcto se cargue en
tiempo de ejecución (normalmente estableciendo `LD_LIBRARY_PATH`). La opción más simple es una
compilación estática, normalmente posible mediante:```shell
$ CC=/path/to/afl/afl-gcc ./configure --disable-shared
Al establecer AFL_HARDEN=1 al invocar 'make', el wrapper de CC habilitará
automáticamente opciones de endurecimiento de código que facilitan la detección
de errores de memoria simples. Libdislocator, una biblioteca auxiliar incluida con AFL (consulta
libdislocator/README.dislocator) también puede ayudar a descubrir problemas de corrupción del heap.
PD. Se recomienda a los usuarios de ASAN revisar el archivo notes_for_asan.txt, ya que contiene advertencias importantes.
Cuando el código fuente NO está disponible, el fuzzer ofrece soporte experimental para instrumentación rápida y sobre la marcha de binarios de caja negra. Esto se logra con una versión de QEMU que se ejecuta en el modo menos conocido de "emulación de espacio de usuario".
QEMU es un proyecto independiente de AFL, pero puedes compilar fácilmente esta función haciendo:```shell $ cd qemu_mode $ ./build_qemu_support.sh
For additional instructions and caveats, see qemu_mode/README.qemu.
The mode is approximately 2-5x slower than compile-time instrumentation, is
less conducive to parallelization, and may have some other quirks.
## 5) Elección de los casos de prueba iniciales
Para funcionar correctamente, el fuzzer requiere uno o más archivos iniciales
que contengan un buen ejemplo de los datos de entrada que normalmente espera
la aplicación objetivo. Hay dos reglas básicas:
- Mantenga los archivos pequeños. Menos de 1 kB es lo ideal, aunque no es
estrictamente necesario. Para un análisis de por qué el tamaño importa,
consulte [perf_tips.txt](https://github.com/google/afl/blob/HEAD/docs/perf_tips.txt).
- Utilice varios casos de prueba solo si son funcionalmente diferentes
entre sí. No tiene sentido usar cincuenta fotos de vacaciones diferentes
para fuzzear una biblioteca de imágenes.
Puede encontrar muchos buenos ejemplos de archivos iniciales en el subdirectorio
testcases/ que viene con esta herramienta.
PD. Si dispone de un gran corpus de datos para examinar, puede que desee
utilizar la utilidad afl-cmin para identificar un subconjunto de archivos
funcionalmente distintos que ejerciten diferentes rutas de código en el
binario objetivo.
## 6) Fuzzing de binarios
El proceso de fuzzing en sí lo lleva a cabo la utilidad afl-fuzz. Este programa
requiere un directorio de solo lectura con los casos de prueba iniciales, un
lugar separado para almacenar sus hallazgos, además de una ruta al binario a
probar.
Para binarios objetivo que aceptan entrada directamente desde stdin, la
sintaxis habitual es:```shell
$ ./afl-fuzz -i testcase_dir -o findings_dir /path/to/program [...params...]
Para programas que toman entrada de un archivo, use '@@' para marcar la ubicación en la línea de comandos del objetivo donde debe colocarse el nombre del archivo de entrada. El fuzzer lo sustituirá por usted:```shell $ ./afl-fuzz -i testcase_dir -o findings_dir /path/to/program @@
También puede usar la opción -f para que los datos mutados se escriban en un
archivo específico. Esto es útil si el programa espera una extensión de archivo concreta o similar.
Los binarios no instrumentados pueden someterse a fuzzing en el modo QEMU (añada -Q en la
línea de comandos) o en un modo tradicional de fuzzing a ciegas (especifique -n).
Puede usar -t y -m para sobrescribir el tiempo de espera y el límite de memoria predeterminados del
proceso ejecutado; ejemplos poco comunes de objetivos que pueden necesitar estos ajustes
incluyen compiladores y decodificadores de video.
Consejos para optimizar el rendimiento del fuzzing se discuten en [perf_tips.txt](https://github.com/google/afl/blob/HEAD/docs/perf_tips.txt).
Tenga en cuenta que afl-fuzz comienza realizando una serie de pasos de fuzzing
deterministas, que pueden llevar varios días, pero tienden a producir casos de prueba ordenados. Si
desea resultados rápidos y sucios de inmediato - similares a zzuf y otros fuzzers
tradicionales - añada la opción -d a la línea de comandos.
## 7) Interpretación de la salida
Consulte el archivo [status_screen.txt](https://github.com/google/afl/blob/HEAD/docs/status_screen.txt) para obtener información sobre
cómo interpretar las estadísticas mostradas y monitorear la salud del proceso.
Asegúrese de consultar este archivo especialmente si algún elemento de la interfaz se resalta en
rojo.
El proceso de fuzzing continuará hasta que presione Ctrl-C. Como mínimo, querrá
permitir que el fuzzer complete un ciclo de cola, lo que puede tomar desde un par
de horas hasta una semana aproximadamente.
Se crean tres subdirectorios dentro del directorio de salida y se actualizan
en tiempo real:
- queue/ - casos de prueba para cada ruta de ejecución distintiva, más todos los
archivos iniciales proporcionados por el usuario. Este es el corpus sintetizado
mencionado en la sección 2.
Antes de usar este corpus para cualquier otro propósito, puede reducirlo
a un tamaño más pequeño usando la herramienta afl-cmin. La herramienta encontrará
un subconjunto más pequeño de archivos que ofrezca una cobertura de aristas equivalente.
- crashes/ - casos de prueba únicos que hacen que el programa probado reciba una
señal fatal (p. ej., SIGSEGV, SIGILL, SIGABRT). Las entradas se
agrupan según la señal recibida.
- hangs/ - casos de prueba únicos que provocan que el programa probado se agote por tiempo de espera. El
límite de tiempo predeterminado antes de que algo se clasifique como cuelgue es
el mayor entre 1 segundo y el valor del parámetro -t.
El valor se puede ajustar finamente estableciendo AFL_HANG_TMOUT, pero esto
rara vez es necesario.
Los crashes y los cuelgues se consideran "únicos" si las rutas de ejecución asociadas
implican transiciones de estado no vistas en fallos registrados anteriormente. Si un
mismo bug puede alcanzarse de múltiples maneras, habrá cierta inflación en el recuento
al inicio del proceso, pero esto debería disminuir rápidamente.
Los nombres de archivo de los crashes y cuelgues se correlacionan con las entradas de cola
padres y sin fallos. Esto debería ayudar con la depuración.
Cuando no pueda reproducir un crash encontrado por afl-fuzz, la causa más probable es
que no esté estableciendo el mismo límite de memoria que usa la herramienta. Pruebe:```shell
$ LIMIT_MB=50
$ ( ulimit -Sv $[LIMIT_MB << 10]; /path/to/tested_binary ... )
Cambia LIMIT_MB para que coincida con el parámetro -m pasado a afl-fuzz. En OpenBSD, cambia también -Sv a -Sd.
Cualquier directorio de salida existente también se puede utilizar para reanudar trabajos abortados; prueba:```shell $ ./afl-fuzz -i- -o existing_output_dir [...etc...]
Si tienes gnuplot instalado, también puedes generar algunas gráficas bonitas para cualquier tarea de fuzzing activa usando afl-plot. Para ver un ejemplo de cómo se ve esto, consulta [http://lcamtuf.coredump.cx/afl/plot/](http://lcamtuf.coredump.cx/afl/plot/).
## 8) Fuzzing paralelizado
Cada instancia de afl-fuzz ocupa aproximadamente un núcleo. Esto significa que en sistemas multinúcleo, la paralelización es necesaria para utilizar completamente el hardware. Para consejos sobre cómo fuzzear un objetivo común en múltiples núcleos o múltiples máquinas en red, consulta [parallel_fuzzing.txt](https://github.com/google/afl/blob/HEAD/docs/parallel_fuzzing.txt).
El modo de fuzzing paralelo también ofrece una forma sencilla de interconectar AFL con otros fuzzers, motores de ejecución simbólica o concólica, etc.; de nuevo, consulta la última sección de [parallel_fuzzing.txt](https://github.com/google/afl/blob/HEAD/docs/parallel_fuzzing.txt) para obtener consejos.
## 9) Diccionarios del fuzzer
Por defecto, el motor de mutaciones de afl-fuzz está optimizado para formatos de datos compactos, por ejemplo, imágenes, multimedia, datos comprimidos, sintaxis de expresiones regulares o scripts de shell. Es algo menos adecuado para lenguajes con un vocabulario especialmente verboso y redundante, en particular HTML, SQL o JavaScript.
Para evitar la molestia de construir herramientas que comprendan la sintaxis, afl-fuzz ofrece una forma de alimentar el proceso de fuzzing con un diccionario opcional de palabras clave del lenguaje, cabeceras mágicas u otros tokens especiales asociados al tipo de datos objetivo, y usarlo para reconstruir la gramática subyacente sobre la marcha:
[http://lcamtuf.blogspot.com/2015/01/afl-fuzz-making-up-grammar-with.html](http://lcamtuf.blogspot.com/2015/01/afl-fuzz-making-up-grammar-with.html)
Para usar esta función, primero debes crear un diccionario en uno de los dos formatos descritos en dictionaries/README.dictionaries; y luego apuntar el fuzzer a él mediante la opción -x en la línea de comandos.
(También se incluyen varios diccionarios comunes en ese subdirectorio).
No hay forma de proporcionar descripciones más estructuradas de la sintaxis subyacente, pero el fuzzer probablemente deducirá parte de esto basándose únicamente en la retroalimentación de instrumentación. Esto funciona en la práctica, por ejemplo:
[http://lcamtuf.blogspot.com/2015/04/finding-bugs-in-sqlite-easy-way.html](http://lcamtuf.blogspot.com/2015/04/finding-bugs-in-sqlite-easy-way.html)
PD. Incluso cuando no se proporciona un diccionario explícito, afl-fuzz intentará extraer los tokens de sintaxis existentes en el corpus de entrada observando muy de cerca la instrumentación durante los volteos de bytes deterministas. Esto funciona para algunos tipos de parsers y gramáticas, pero no es ni de lejos tan bueno como el modo -x.
Si es realmente difícil conseguir un diccionario, otra opción es dejar que AFL se ejecute durante un tiempo y luego usar la biblioteca de captura de tokens que se incluye como utilidad complementaria de AFL. Para ello, consulta libtokencap/README.tokencap.
## 10) Triage de crashes
La agrupación de crashes basada en cobertura suele producir un conjunto de datos pequeño que puede ser clasificado rápidamente de forma manual o con un script muy sencillo de GDB o Valgrind. Cada crash también se puede rastrear hasta su caso de prueba padre que no provoca el crash en la cola, lo que facilita el diagnóstico de fallos.
Dicho esto, es importante reconocer que algunos crashes de fuzzing pueden ser difíciles de evaluar rápidamente para determinar su explotabilidad sin mucho trabajo de depuración y análisis de código. Para ayudar con esta tarea, afl-fuzz soporta un modo muy singular de "exploración de crashes" habilitado con la opción -C.
En este modo, el fuzzer toma uno o más casos de prueba que provocan el crash como entrada, y usa sus estrategias de fuzzing basadas en retroalimentación para enumerar muy rápidamente todas las rutas de código que se pueden alcanzar en el programa mientras se mantiene en el estado de crash.
Las mutaciones que no resultan en un crash son rechazadas; también lo son los cambios que no afectan la ruta de ejecución.
La salida es un pequeño corpus de archivos que se pueden examinar muy rápidamente para ver qué grado de control tiene el atacante sobre la dirección que falla, o si es posible superar una lectura inicial fuera de los límites y ver qué hay debajo.
Ah, una cosa más: para la minimización de casos de prueba, prueba afl-tmin. La herramienta se puede operar de forma muy sencilla:```shell
$ ./afl-tmin -i test_case -o minimized_result -- /path/to/program [...]
La herramienta funciona tanto con casos de prueba que provocan fallos como con los que no. En el modo de fallo, acepta sin problemas binarios instrumentados y no instrumentados. En el modo sin fallos, el minimizador se basa en la instrumentación estándar de AFL para simplificar el archivo sin alterar la ruta de ejecución.
El minimizador acepta la sintaxis -m, -t, -f y @@ de forma compatible con afl-fuzz.
Otra incorporación reciente a AFL es la herramienta afl-analyze. Toma un archivo de entrada, intenta invertir bytes secuencialmente y observa el comportamiento del programa probado. Luego codifica por colores la entrada según qué secciones parecen críticas y cuáles no; aunque no es infalible, a menudo puede ofrecer información rápida sobre formatos de archivo complejos. Más información sobre su funcionamiento se encuentra cerca del final de technical_details.txt.
El fuzzing es una técnica maravillosa e infrautilizada para descubrir también errores de diseño e implementación que no provocan fallos. Se han encontrado bastantes bugs interesantes al modificar los programas objetivo para que llamen a abort() cuando, por ejemplo:
Dos librerías bignum producen salidas diferentes cuando reciben la misma entrada generada por el fuzzer,
Una librería de imágenes produce salidas diferentes cuando se le pide decodificar la misma imagen de entrada varias veces seguidas,
Una librería de serialización / deserialización no produce salidas estables al serializar y deserializar iterativamente datos suministrados por el fuzzer,
Una librería de compresión produce una salida inconsistente con el archivo de entrada cuando se le pide comprimir y luego descomprimir un blob concreto.
Implementar estas comprobaciones de cordura u otras similares suele llevar muy poco tiempo;
si eres el mantenedor de un paquete concreto, puedes hacer que este código sea
condicional con #ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION (una bandera también
compartida con libfuzzer) o #ifdef __AFL_COMPILER (esta última es solo para AFL).
Ten en cuenta que, al igual que muchas otras tareas de gran carga computacional, el fuzzing puede ejercer presión sobre tu hardware y sobre el sistema operativo. En particular:
Tu CPU se calentará y necesitará una refrigeración adecuada. En la mayoría de los casos, si la refrigeración es insuficiente o deja de funcionar correctamente, las velocidades de la CPU se limitarán automáticamente. Dicho esto, especialmente cuando se hace fuzzing en hardware menos adecuado (portátiles, teléfonos inteligentes, etc.), no es del todo imposible que algo llegue a estallar.
Los programas objetivo pueden terminar consumiendo erráticamente gigabytes de memoria o llenando el espacio del disco con archivos basura. AFL intenta imponer límites básicos de memoria, pero no puede evitar todos y cada uno de los posibles contratiempos. La conclusión es que no deberías hacer fuzzing en sistemas donde la posibilidad de pérdida de datos no sea un riesgo aceptable.
El fuzzing implica miles de millones de lecturas y escrituras en el sistema de archivos. En los sistemas modernos, esto suele estar fuertemente cacheado, lo que resulta en una E/S "física" bastante moderada - pero hay muchos factores que pueden alterar esta ecuación. Es tu responsabilidad vigilar posibles problemas; con una E/S muy intensa, la vida útil de muchos HDD y SSD puede verse reducida.
Una buena forma de monitorizar la E/S del disco en Linux es el comando 'iostat':```shell $ iostat -d 3 -x -k [...optional disk ID...]
## 13) Limitaciones conocidas y áreas de mejora
Estas son algunas de las advertencias más importantes para AFL:
- AFL detecta fallos comprobando si el primer proceso generado muere debido a
una señal (SIGSEGV, SIGABRT, etc.). Los programas que instalan manejadores personalizados para
estas señales pueden necesitar que el código relevante se comente. Del mismo
modo, los fallos en procesos hijos generados por el objetivo fuzzizado pueden evadir
la detección a menos que agregues manualmente algo de código para capturarlos.
- Como con cualquier otra herramienta de fuerza bruta, el fuzzer ofrece una cobertura limitada si
el cifrado, las sumas de comprobación, las firmas criptográficas o la compresión se utilizan para
envolver por completo el formato de datos real que se va a probar.
Para solucionar esto, puedes comentar las comprobaciones relevantes (consulta
experimental/libpng_no_checksum/ para inspirarte); si esto no es posible,
también puedes escribir un postprocesador, como se explica en
experimental/post_library/.
- Hay algunas desventajas desafortunadas con ASAN y los binarios de 64 bits. Esto
no se debe a ningún fallo específico de afl-fuzz; consulta [notes_for_asan.txt](https://github.com/google/afl/blob/HEAD/docs/notes_for_asan.txt)
para obtener consejos.
- No hay soporte directo para fuzzear servicios de red, demonios en segundo plano
o aplicaciones interactivas que requieran interacción con la interfaz de usuario para funcionar. Puede que
necesites hacer cambios simples en el código para que se comporten de una manera más tradicional.
Preeny también puede ofrecer una opción relativamente simple; consulta:
https://github.com/zardus/preeny
También puedes encontrar algunos consejos útiles para modificar servicios basados en red en:
https://www.fastly.com/blog/how-to-fuzz-server-american-fuzzy-lop
- AFL no genera datos de cobertura legibles por humanos. Si quieres monitorear
la cobertura, usa afl-cov de Michael Rash: https://github.com/mrash/afl-cov
- En ocasiones, las máquinas conscientes se rebelan contra sus creadores. Si esto
te sucede, consulta http://lcamtuf.coredump.cx/prep/.
Más allá de esto, consulta INSTALL para obtener consejos específicos de plataforma.
## 14) Agradecimientos especiales
Muchas de las mejoras a afl-fuzz no serían posibles sin los comentarios,
informes de errores o parches de:```
Jann Horn Hanno Boeck
Felix Groebert Jakub Wilk
Richard W. M. Jones Alexander Cherepanov
Tom Ritter Hovik Manucharyan
Sebastian Roschke Eberhard Mattes
Padraig Brady Ben Laurie
@dronesec Luca Barbato
Tobias Ospelt Thomas Jarosch
Martin Carpenter Mudge Zatko
Joe Zbiciak Ryan Govostes
Michael Rash William Robinet
Jonathan Gray Filipe Cabecinhas
Nico Weber Jodie Cunningham
Andrew Griffiths Parker Thompson
Jonathan Neuschfer Tyler Nighswander
Ben Nagy Samir Aguiar
Aidan Thornton Aleksandar Nikolich
Sam Hakim Laszlo Szekeres
David A. Wheeler Turo Lamminen
Andreas Stieger Richard Godbee
Louis Dassy teor2345
Alex Moneger Dmitry Vyukov
Keegan McAllister Kostya Serebryany
Richo Healey Martijn Bogaard
rc0r Jonathan Foote
Christian Holler Dominique Pelle
Jacek Wielemborek Leo Barnes
Jeremy Barnes Jeff Trull
Guillaume Endignoux ilovezfs
Daniel Godas-Lopez Franjo Ivancic
Austin Seipp Daniel Komaromy
Daniel Binderman Jonathan Metzman
Vegard Nossum Jan Kneschke
Kurt Roeckx Marcel Bohme
Van-Thuan Pham Abhik Roychoudhury
Joshua J. Drake Toby Hutton
Rene Freingruber Sergey Davidoff
Sami Liedes Craig Young
Andrzej Jackowski Daniel Hodson
¡Gracias!
¿Preguntas? ¿Problemas? ¿Informes de errores? Por favor, usa GitHub.
También hay una lista de correo para el proyecto; para unirte, envía un correo a [email protected]. O, si prefieres consultar primero los archivos, prueba: https://groups.google.com/group/afl-users.