
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/master/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.