Desarrollado originalmente por Michal Zalewski [email protected].
Consulte QuickStartGuide.txt si no tiene tiempo para leer este archivo.
Fuzzing es una de las estrategias más poderosas y probadas para identificar problemas de seguridad en el software del mundo real; es responsable de la gran mayoría de los errores de ejecución remota de código y escalada de privilegios encontrados hasta la fecha en software crítico para la seguridad.
Desafortunadamente, la confusión también es relativamente superficial; Las mutaciones ciegas y aleatorias hacen que sea muy poco probable que se alcancen ciertas rutas de código en el código probado, lo que deja algunas vulnerabilidades firmemente fuera del alcance de esta técnica.
Ha habido numerosos intentos de resolver este problema. Uno de los primeros enfoques, iniciado por 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 difuminarlos por medios tradicionales. El enfoque funciona excepcionalmente bien, pero requiere que dicho corpus esté fácilmente disponible. Además, las mediciones de cobertura de bloques proporcionan sólo 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 programas ("ejecución concólica"), 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 confiabilidad y rendimiento en usos prácticos y actualmente no ofrecen una alternativa viable a las técnicas de fuzzing "tontas".
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 borde para detectar sin esfuerzo cambios sutiles a escala local en el flujo de control del programa.
Simplificando un poco, el algoritmo general se puede resumir como:
Cargue los casos de prueba iniciales proporcionados por el usuario en la cola,
Tome el siguiente archivo de entrada de la cola,
Intente recortar el caso de prueba al tamaño más pequeño que no altere el comportamiento medido del programa,
Mute repetidamente el archivo utilizando una variedad equilibrada y bien investigada de estrategias tradicionales de fuzzing,
Si alguna de las mutaciones generadas resultó en una nueva transición de estado registrada por la instrumentación, agregue la salida mutada como una nueva entrada en la cola.
Ir a 2.
Los casos de prueba descubiertos también se seleccionan periódicamente para eliminar aquellos que han quedado obsoletos debido a hallazgos más nuevos y de mayor cobertura; y someterse a varios otros pasos de minimización del esfuerzo impulsados por la instrumentación.
Como resultado secundario del proceso de fuzzing, la herramienta crea un corpus pequeño e independiente de casos de prueba interesantes. Estos son extremadamente útiles para sembrar otros regímenes de prueba que requieren mucha mano de obra o recursos, por ejemplo, para probar navegadores, aplicaciones de oficina, suites gráficas o herramientas de código cerrado.
El fuzzer se prueba minuciosamente para ofrecer un rendimiento listo para usar muy superior al de las herramientas de fuzzing ciego o de solo cobertura.
Cuando el código fuente está disponible, la instrumentación se puede inyectar mediante una herramienta complementaria que funciona como 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 en el rendimiento bastante modesto; Junto con otras optimizaciones implementadas por afl-fuzz, la mayoría de los programas se pueden difuminar tan rápido o incluso más rápido que lo posible con las herramientas tradicionales.
La forma correcta de recompilar el programa de destino puede variar según las características específicas del proceso de compilación, pero un enfoque casi universal sería:
$ CC=/path/to/afl/afl-gcc ./configure
$ make clean all
Para programas C++, también querrás configurar CXX=/path/to/afl/afl-g++
.
Los envoltorios clang (afl-clang y afl-clang++) se pueden utilizar 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 bibliotecas, necesita encontrar o escribir un programa simple que lea datos de la entrada estándar o de un archivo y los pase a la biblioteca probada. En tal caso, es esencial vincular este ejecutable con una versión estática de la biblioteca instrumentada, o asegurarse de que se cargue el archivo .so correcto en tiempo de ejecución (generalmente configurando LD_LIBRARY_PATH
). La opción más sencilla es una compilación estática, normalmente posible mediante:
$ CC=/path/to/afl/afl-gcc ./configure --disable-shared
Configurar AFL_HARDEN=1
al llamar a 'make' hará que el contenedor CC habilite automáticamente opciones de refuerzo de código que facilitan la detección de errores simples de memoria. Libdislocator, una biblioteca auxiliar incluida con AFL (consulte libdislocator/README.dislocator) también puede ayudar a descubrir problemas de corrupción del montón.
PD. Se recomienda a los usuarios de ASAN que revisen el archivo notes_for_asan.txt para conocer 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 crear la función cómodamente haciendo:
$ cd qemu_mode
$ ./build_qemu_support.sh
Para obtener instrucciones y advertencias adicionales, consulte qemu_mode/README.qemu.
El modo es aproximadamente entre 2 y 5 veces más lento que la instrumentación en tiempo de compilación, es menos propicio para la paralelización y puede tener algunas otras peculiaridades.
Para funcionar correctamente, el fuzzer requiere uno o más archivos de inicio que contengan un buen ejemplo de los datos de entrada que normalmente espera la aplicación de destino. Hay dos reglas básicas:
Mantenga los archivos pequeños. Lo ideal es menos de 1 kB, aunque no es estrictamente necesario. Para obtener información sobre por qué el tamaño importa, consulte perf_tips.txt.
Utilice varios casos de prueba solo si son funcionalmente diferentes entre sí. No tiene sentido utilizar cincuenta fotografías de vacaciones diferentes para difuminar una biblioteca de imágenes.
Puede encontrar muchos buenos ejemplos de archivos iniciales en el subdirectorio testcases/ que viene con esta herramienta.
PD. Si hay un gran corpus de datos disponible para su análisis, es posible que desee utilizar la utilidad afl-cmin para identificar un subconjunto de archivos funcionalmente distintos que utilizan diferentes rutas de código en el binario de destino.
El proceso de fuzzing en sí lo lleva a cabo la utilidad afl-fuzz. Este programa requiere un directorio de solo lectura con casos de prueba iniciales, un lugar separado para almacenar sus hallazgos y una ruta al binario a probar.
Para los archivos binarios de destino que aceptan entradas directamente desde la entrada estándar, la sintaxis habitual es:
$ ./afl-fuzz -i testcase_dir -o findings_dir /path/to/program [...params...]
Para programas que toman información de un archivo, use '@@' para marcar la ubicación en la línea de comando del destino donde se debe colocar el nombre del archivo de entrada. El fuzzer sustituirá esto por ti:
$ ./afl-fuzz -i testcase_dir -o findings_dir /path/to/program @@
También puede utilizar la opción -f para escribir los datos mutados en un archivo específico. Esto es útil si el programa espera una extensión de archivo particular.
Los binarios no instrumentados se pueden difuminar en el modo QEMU (agregue -Q en la línea de comando) o en un modo tradicional de fuzzer ciego (especifique -n).
Puede utilizar -t y -m para anular el tiempo de espera predeterminado y el límite de memoria para el proceso ejecutado; Ejemplos raros de objetivos que pueden necesitar que se modifiquen estas configuraciones incluyen compiladores y decodificadores de video.
En perf_tips.txt se analizan consejos para optimizar el rendimiento de la fuzzing.
Tenga en cuenta que afl-fuzz comienza realizando una serie de pasos deterministas de fuzzing, que pueden tardar varios días, pero que tienden a producir casos de prueba interesantes. Si desea resultados rápidos y sucios de inmediato, similares a zzuf y otros fuzzers tradicionales, agregue la opción -d a la línea de comando.
Consulte el archivo status_screen.txt para obtener información sobre cómo interpretar las estadísticas mostradas y monitorear el estado del proceso. Asegúrese de consultar este archivo, especialmente si algún elemento de la interfaz de usuario está resaltado en rojo.
El proceso de fuzzing continuará hasta que presione Ctrl-C. Como mínimo, desea permitir que el fuzzer complete un ciclo de cola, lo que puede tardar desde un par de horas hasta una semana aproximadamente.
Hay tres subdirectorios creados dentro del directorio de salida y actualizados en tiempo real:
cola/: casos de prueba para cada ruta de ejecución distintiva, además de todos los archivos iniciales proporcionados por el usuario. Este es el corpus sintetizado mencionado en la sección 2. Antes de utilizar este corpus para cualquier otro propósito, puede reducirlo a un tamaño más pequeño utilizando la herramienta afl-cmin. La herramienta encontrará un subconjunto más pequeño de archivos que ofrezcan una cobertura de bordes equivalente.
crashes/ - casos de prueba únicos que hacen que el programa probado reciba una señal fatal (por ejemplo, SIGSEGV, SIGILL, SIGABRT). Las entradas se agrupan según la señal recibida.
se cuelga/: casos de prueba únicos que hacen que el programa probado expire. El límite de tiempo predeterminado antes de que algo se clasifique como colgado es 1 segundo y el valor del parámetro -t, el mayor. El valor se puede ajustar configurando AFL_HANG_TMOUT, pero esto rara vez es necesario.
Los bloqueos y bloqueos se consideran "únicos" si las rutas de ejecución asociadas implican transiciones de estado que no se ven en fallas registradas previamente. Si se puede alcanzar un solo error de varias maneras, habrá cierta inflación en el conteo al principio del proceso, pero esto debería disminuir rápidamente.
Los nombres de archivos para fallas y bloqueos se correlacionan con las entradas de la cola principal que no presentan errores. Esto debería ayudar con la depuración.
Cuando no puede reproducir un fallo encontrado por afl-fuzz, la causa más probable es que no esté configurando el mismo límite de memoria que utiliza la herramienta. Intentar:
$ LIMIT_MB=50
$ ( ulimit -Sv $[LIMIT_MB << 10] ; /path/to/tested_binary ... )
Cambie LIMIT_MB para que coincida con el parámetro -m pasado a afl-fuzz. En OpenBSD, cambie también -Sv a -Sd.
Cualquier directorio de salida existente también se puede utilizar para reanudar trabajos abortados; intentar:
$ ./afl-fuzz -i- -o existing_output_dir [...etc...]
Si tiene gnuplot instalado, también puede generar algunos gráficos bonitos para cualquier tarea de fuzzing activa usando afl-plot. Para ver un ejemplo de cómo se ve esto, consulte http://lcamtuf.coredump.cx/afl/plot/.
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 obtener sugerencias sobre cómo difuminar un objetivo común en múltiples núcleos o múltiples máquinas en red, consulte paralelo_fuzzing.txt.
El modo de fuzzing paralelo también ofrece una forma sencilla de interconectar AFL con otros fuzzers, con motores de ejecución simbólica o cónica, etc.; Nuevamente, consulte la última sección de paralelo_fuzzing.txt para obtener sugerencias.
De forma predeterminada, el motor de mutación 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 verborrea particularmente detallada y redundante, en particular HTML, SQL o JavaScript.
Para evitar la molestia de crear herramientas que tengan en cuenta la sintaxis, afl-fuzz proporciona una manera de iniciar el proceso de fuzzing con un diccionario opcional de palabras clave del idioma, encabezados mágicos u otros tokens especiales asociados con el tipo de datos objetivo, y usarlos para reconstruir. la gramática subyacente sobre la marcha:
http://lcamtuf.blogspot.com/2015/01/afl-fuzz-making-up-grammar-with.html
Para utilizar esta función, primero debe crear un diccionario en uno de los dos formatos descritos en diccionarios/README.diccionarios; y luego apunte el fuzzer a través de la opción -x en la línea de comando.
(En ese subdirectorio también se proporcionan varios diccionarios comunes).
No hay manera de proporcionar descripciones más estructuradas de la sintaxis subyacente, pero el fuzzer probablemente descubrirá algo de esto basándose únicamente en la retroalimentación de la instrumentación. Esto realmente funciona en la práctica, digamos:
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 tokens de sintaxis existentes en el corpus de entrada observando muy de cerca la instrumentación durante los cambios de bytes deterministas. Esto funciona para algunos tipos de analizadores y gramáticas, pero no es tan bueno como el modo -x.
Si es realmente difícil conseguir un diccionario, otra opción es dejar que AFL se ejecute por un tiempo y luego usar la biblioteca de captura de tokens que viene como una utilidad complementaria con AFL. Para eso, consulte libtokencap/README.tokencap.
La agrupación de fallas basada en cobertura generalmente produce un pequeño conjunto de datos que se puede clasificar rápidamente de forma manual o con un script GDB o Valgrind muy simple. Cada falla también se puede rastrear hasta su caso de prueba principal que no falla en la cola, lo que facilita el diagnóstico de fallas.
Dicho esto, es importante reconocer que algunas fallas 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 admite un modo exclusivo de "exploración de fallos" habilitado con el indicador -C.
En este modo, el fuzzer toma uno o más casos de prueba de fallas como entrada y utiliza sus estrategias de fuzzing basadas en retroalimentación para enumerar muy rápidamente todas las rutas de código a las que se puede acceder en el programa mientras lo mantiene en el estado de falla.
Se rechazan las mutaciones que no provocan un accidente; también lo son cualquier cambio que no afecte la ruta de ejecución.
El resultado es un pequeño corpus de archivos que se puede examinar muy rápidamente para ver qué grado de control tiene el atacante sobre la dirección errónea, o si es posible superar una lectura inicial fuera de límites y ver qué hay debajo. .
Ah, una cosa más: para minimizar los casos de prueba, prueba afl-tmin. La herramienta se puede utilizar de forma muy sencilla:
$ ./afl-tmin -i test_case -o minimized_result -- /path/to/program [...]
La herramienta funciona tanto con casos de prueba con fallas como sin fallas. En el modo de bloqueo, aceptará felizmente archivos binarios instrumentados y no instrumentados. En el modo sin bloqueos, el minimizador se basa en la instrumentación AFL estándar 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 ser críticas y cuáles no; Si bien no es a prueba de balas, a menudo puede ofrecer información rápida sobre formatos de archivos complejos. Puede encontrar más información sobre su funcionamiento cerca del final de Technical_details.txt.
Fuzzing también es una técnica maravillosa y subutilizada para descubrir errores de diseño e implementación que no fallan. Se han encontrado bastantes errores interesantes modificando los programas de destino para llamar a abort() cuando, por ejemplo:
Dos bibliotecas bignum producen resultados diferentes cuando se les proporciona la misma entrada generada por fuzzer,
Una biblioteca de imágenes produce diferentes resultados cuando se le pide que decodifique la misma imagen de entrada varias veces seguidas.
Una biblioteca de serialización/deserialización no logra producir resultados estables cuando serializa y deserializa iterativamente datos proporcionados por fuzzer,
Una biblioteca de compresión produce una salida incoherente con el archivo de entrada cuando se le solicita que comprima y luego descomprima un blob en particular.
La implementación de estos u otros controles de cordura similares suele llevar muy poco tiempo; Si es el mantenedor de un paquete en particular, puede hacer que este código sea condicional con #ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
(un indicador también compartido con libfuzzer) o #ifdef __AFL_COMPILER
(este es solo para AFL).
Tenga en cuenta que, al igual que muchas otras tareas computacionales intensivas, la fuzzing puede ejercer presión sobre su hardware y su sistema operativo. En particular:
Su 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 reducirán automáticamente. Dicho esto, especialmente cuando se utiliza hardware menos adecuado (portátiles, teléfonos inteligentes, etc.), no es del todo imposible que algo explote.
Los programas específicos pueden terminar acaparando gigabytes de memoria de forma errática o llenando espacio en el 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ía confundirse con sistemas donde la perspectiva de pérdida de datos no es un riesgo aceptable.
La fuzzing implica miles de millones de lecturas y escrituras en el sistema de archivos. En los sistemas modernos, esto suele estar muy almacenado en caché, lo que da como resultado una E/S "física" bastante modesta, pero hay muchos factores que pueden alterar esta ecuación. Es su responsabilidad monitorear posibles problemas; con E/S muy pesadas, la vida útil de muchos HDD y SSD puede verse reducida.
Una buena forma de monitorear la E/S del disco en Linux es el comando 'iostat':
$ iostat -d 3 -x -k [...optional disk ID...]
Estas son algunas de las advertencias más importantes para la AFL:
AFL detecta fallas verificando si el primer proceso generado muere debido a una señal (SIGSEGV, SIGABRT, etc.). Es posible que los programas que instalan controladores personalizados para estas señales necesiten comentar el código relevante. Del mismo modo, las fallas en el procesamiento secundario generadas por el objetivo difuso pueden evadir la detección a menos que agregue manualmente algún código para detectarlas.
Al igual que con cualquier otra herramienta de fuerza bruta, el fuzzer ofrece una cobertura limitada si se utiliza cifrado, sumas de verificación, firmas criptográficas o compresión para envolver completamente el formato de datos real que se va a probar.
Para solucionar este problema, puede comentar las comprobaciones relevantes (consulte experimental/libpng_no_checksum/ para inspirarse); Si esto no es posible, también puedes escribir un posprocesador, como se explica en experimental/post_library/.
Hay algunas compensaciones desafortunadas entre ASAN y los binarios de 64 bits. Esto no se debe a ningún fallo específico de afl-fuzz; consulte notes_for_asan.txt para obtener sugerencias.
No hay soporte directo para servicios de red difusos, demonios en segundo plano o aplicaciones interactivas que requieren interacción de la interfaz de usuario para funcionar. Es posible que deba realizar 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; consulte: https://github.com/zardus/preeny
También se pueden encontrar algunos consejos útiles para modificar los 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 desea monitorear la cobertura, use afl-cov de Michael Rash: https://github.com/mrash/afl-cov
De vez en cuando, las máquinas inteligentes se levantan contra sus creadores. Si esto le sucede, consulte http://lcamtuf.coredump.cx/prep/.
Más allá de esto, consulte INSTALAR para obtener consejos específicos de la plataforma.
Muchas de las mejoras de afl-fuzz no serían posibles sin 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? ¿Preocupaciones? ¿Informes de errores? Utilice GitHub.
También hay una lista de correo del proyecto; para unirse, envíe un correo electrónico a [email protected]. O, si prefiere buscar archivos primero, pruebe: https://groups.google.com/group/afl-users.