
Fuzzer de formatos binarios basado en plantillas que genera y analiza entradas de prueba válidas a alta velocidad, con integración con AFL++ para fuzzing guiado por cobertura.
FormatFuzzer es un marco de trabajo para la generación y análisis de entradas binarias de alta eficiencia y alta calidad.
Toma una plantilla binaria que describe el formato de una entrada binaria y genera un ejecutable que produce y analiza el formato binario dado.
A partir de una plantilla binaria para GIF, por ejemplo, FormatFuzzer produce un generador de GIF, también conocido como fuzzer de GIF.
Los generadores producidos por FormatFuzzer son altamente eficientes, produciendo miles de entradas de prueba válidas por segundo, en marcado contraste con los fuzzers basados en mutación, donde la gran mayoría de las entradas son inválidas. Las entradas generadas por FormatFuzzer son independientes del programa bajo prueba (o, en realidad, de cualquier programa), por lo que también se pueden usar en entornos de caja negra. Sin embargo, FormatFuzzer también se integra con AFL++ para producir entradas válidas que también busquen la máxima cobertura. En nuestros experimentos, este enfoque de "lo mejor de dos mundos" supera a todos los demás entornos; consulte nuestro artículo para obtener más detalles.
Las plantillas binarias utilizadas por FormatFuzzer provienen del editor 010.
Hay más de 170 plantillas binarias, que se pueden usar directamente para FormatFuzzer o adaptarse para su uso. De serie, FormatFuzzer produce formatos como AVI, BMP, GIF, JPG, MIDI, MP3, MP4, PCAP, PNG, WAV y ZIP; y seguimos ampliando esta lista cada semana.
¡Los colaboradores son bienvenidos! Visite la página del proyecto FormatFuzzer para plantear ideas y problemas, o agregar pull requests. Para obtener detalles sobre cómo funciona FormatFuzzer y cómo se compara, lea nuestro artículo para más información.
FormatFuzzer está disponible en la página del proyecto FormatFuzzer. Puede descargar y descomprimir la última versión desde la página de versiones.
Para lo más reciente y mejor, también puede clonar su repositorio git:
git clone https://github.com/uds-se/FormatFuzzer.git
Todas las acciones posteriores se realizan en su carpeta principal:
cd FormatFuzzer
Para ejecutar FormatFuzzer, necesita lo siguiente:
getopt_long()) como clang o gccpy010parser, six e intervaltreezlib (para funciones de compresión)boost (para funciones de suma de comprobación)Si planea editar los scripts de compilación y configuración (archivos .ac y .am), también necesitará
sudo apt install git g++ make automake python3-full zlib1g-dev libboost-dev
python3 -m venv ~/fuzz
source ~/fuzz/bin/activate
pip3 install py010parser six intervaltree
xcode-select --install
brew install python3 automake boost
pip3 install py010parser six intervaltree
En todos los sistemas, usando pip:
pip install py010parser
pip install six
pip install intervaltree
Nota: todos los comandos de compilación requieren que esté en la misma carpeta que este archivo README. La compilación de un fuzzer fuera de esta carpeta aún no es compatible.
Hay un script build.sh que automatiza todos los pasos de construcción.
Simplemente ejecute
./build.sh gif
para crear un fuzzer de GIF.
Esto funciona para todos los formatos de archivo proporcionados en templates/; si hay un archivo templates/FOO.bt, entonces ./build.sh FOO compilará un fuzzer.
Hay un Makefile (fuente en Makefile.am) que automatiza todos los pasos de construcción.
(Requiere GNU make).
Primero haga
touch configure Makefile.in
luego
./configure
y luego
make gif-fuzzer
para crear un fuzzer de GIF.
Esto funciona para todos los formatos de archivo proporcionados en templates/; si hay un archivo templates/FOO.bt, entonces make FOO-fuzzer compilará un fuzzer.
Si el método make anterior no funciona, o si desea más control, es posible que deba proceder manualmente.
Ejecute el compilador ffcompile para compilar la plantilla binaria en código C++. Toma dos argumentos: la plantilla binaria .bt y un archivo .cpp de C++ que se generará.
./ffcompile templates/gif.bt gif.cpp
Utilice los siguientes comandos para crear un fuzzer gif-fuzzer.
Primero, compile el controlador de línea de comandos genérico:
g++ -c -I . -std=c++17 -g -O3 -Wall fuzzer.cpp
(-I . denota la ubicación del archivo bt.h; -std=c++17 establece el estándar de C++).
Luego, compile el analizador/generador binario:
g++ -c -I . -std=c++17 -g -O3 -Wall gif.cpp
Finalmente, enlace el analizador/generador binario con el controlador de línea de comandos para obtener un ejecutable. Si utiliza bibliotecas adicionales (como -lz), asegúrese de especificarlas aquí también.
g++ -O3 gif.o fuzzer.o -o gif-fuzzer -lz
FormatFuzzer se puede ejecutar como un analizador, generador o mutador independiente de formatos específicos. Además, puede ser llamado por fuzzers de propósito general como AFL++ para integrar esas capacidades específicas de formato en el proceso de fuzzing (consulte la sección a continuación sobre la integración con AFL++).
El fuzzer generado toma un comando como primer argumento, seguido de opciones y argumentos para ese comando.
El comando más importante es fuzz, para producir salidas. Sus argumentos son archivos que se generarán en el formato apropiado.
Ejecute el generador como
./gif-fuzzer fuzz output.gif
para crear un archivo binario aleatorio output.gif, o
./gif-fuzzer fuzz out1.gif out2.gif out3.gif
para crear tres archivos GIF out1.gif, out2.gif y out3.gif.
Tenga en cuenta que la plantilla gif.bt que proporcionamos ha sido aumentada con funciones especiales para facilitar la generación de archivos válidos. Si utiliza archivos de plantilla .bt originales sin adaptaciones, puede recibir advertencias durante la generación y crear archivos inválidos.
También puede ejecutar el fuzzer como un analizador de archivos binarios, usando el comando parse. Esto es útil si desea probar la precisión de la plantilla binaria, o si desea mutar una entrada (consulte 'Archivos de Decisión', a continuación).
Para ejecutar el analizador, use
./gif-fuzzer parse input.gif
Verá mensajes de error si input.gif no se puede analizar correctamente.
Mientras analiza, también puede almacenar todas las decisiones de análisis (es decir, qué alternativas de análisis se tomaron) en un archivo de decisión. Esta es una secuencia de bytes que enumera las decisiones tomadas.
Cada byte representa una única decisión de análisis. Un valor de byte 0 significa que se tomó la primera alternativa, un valor de byte 1 significa que se tomó la segunda alternativa, y así sucesivamente.
Puede generar dicho archivo de decisión al analizar una entrada:
./gif-fuzzer parse --decisions input.dec input.gif
Aquí, input.dec almacena las decisiones tomadas para analizar input.gif.
También puede usar dicho archivo de decisión al generar entradas. El fuzzer tomará entonces las mismas decisiones exactas que se encontraron durante el análisis. El siguiente comando genera un nuevo archivo GIF usando las decisiones determinadas al analizar input.gif:
./gif-fuzzer fuzz --decisions input.dec input2.gif
Si todo funciona bien, ambos archivos deberían ser idénticos:
cmp input.gif input2.gif
Al mutar un archivo de decisión (por ejemplo, reemplazando bytes individuales), puede crear entradas que sean similares al archivo original analizado. Esto es útil para interactuar con estrategias de prueba específicas y fuzzers como AFL, donde puede usar gif-fuzzer y similares como traductores de archivos de decisión a archivos binarios y viceversa: AFL mutaría los archivos de decisión, y el programa bajo prueba se ejecutaría sobre los archivos binarios traducidos. En contraste con la mutación directa de archivos binarios (como AFL haría normalmente), esto tendría la ventaja de tener siempre entradas válidas, y por lo tanto progresar mucho más rápido hacia la cobertura.
Además de los fuzzers específicos de formato, como gif-fuzzer, FormatFuzzer también se puede compilar en bibliotecas compartidas específicas de formato, como gif.so (para eso, simplemente ejecute ./build.sh gif o make gif.so).
Esas bibliotecas compartidas pueden ser cargadas por fuzzers de propósito general, como AFL++.
Para ejecutar AFL++ con FormatFuzzer, simplemente siga las instrucciones en nuestra versión modificada de AFL++. Admitimos diferentes estrategias de fuzzing, incluyendo:
AFL+FFMut: ejecuta AFL++ usando FormatFuzzer para proporcionar mutaciones inteligentes específicas de formato.
AFL+FFGen: usa FormatFuzzer como un generador específico de formato, mientras que AFL++ muta sus semillas de decisión.
Para escribir sus propias plantillas binarias .bt (y así crear un fuzzer/analizador de alta eficiencia para ese formato), lea la sección Introducción a Plantillas y Scripts del Manual del Editor 010.
En muchos casos, ya puede existir una plantilla del formato que busca (o uno similar). Eche un vistazo a la colección de plantillas binarias del editor 010 para ver si hay algo que pueda usar o en lo que basar su formato.
Tenga en cuenta que los archivos .bt proporcionados en el repositorio generalmente están destinados a analizar archivos. También se pueden usar para generar archivos, pero a menudo carecen de información exacta sobre qué partes de la entrada son requeridas.
En esta sección, discutimos algunas de las formas en que puede personalizar los archivos .bt para que funcionen bien con FormatFuzzer.
Por ejemplo, para el formato GIF, el archivo templates/gif-orig.bt muestra la plantilla binaria original, que solo fue diseñada para análisis, mientras que el archivo templates/gif.bt es una versión modificada capaz de generar GIFs válidos. Comparando los dos archivos, vemos que se requirió un pequeño número de cambios para lograr esto.
Si ha creado un gif-fuzzer, ya sea ejecutando make gif-fuzzer o usando la herramienta ffcompile, ya ha obtenido un archivo C++ gif.cpp que contiene una implementación del generador y analizador GIF. Esto es útil para ver cómo los cambios que realice en la plantilla binaria se traducen en código ejecutable. Más detalles sobre el código C++ se presentan en la siguiente sección.
La plantilla binaria GIF utiliza funciones de preanálisis ReadUByte() y ReadUShort() para previsualizar los valores de los siguientes bytes en el archivo antes de analizarlos realmente en un campo de estructura. En tiempo de generación, permitimos que estas funciones reciban un argumento adicional que especifique un conjunto de valores conocidos buenos para elegir entre los bytes que preanalizamos. Además, también permitimos especificar un conjunto global de valores conocidos buenos para usar siempre al llamar a una función de preanálisis particular, como ReadUByte(). Estos se almacenan en el vector ReadUByteInitValues.
Por defecto, nuestro procedimiento de traducción ffcompile intenta extraer valores interesantes que se hayan utilizado en comparaciones contra bytes de preanálisis y usarlos como un conjunto global de valores conocidos. Al ejecutar
./ffcompile templates/gif.bt gif.cpp
un mensaje impreso muestra las funciones de preanálisis identificadas, así como los valores interesantes extraídos:
Finished creating cpp generator.
Lookahead functions found:
ReadUByte
ReadUShort
Mined interesting values:
GlobalColorTableFlag: ['1']
LocalColorTableFlag: ['1']
ReadUByte: ['0x3B', '0x2C']
ReadUShort: ['0xF921', '0xFE21', '0x0121', '0xFF21']
Signature: ['"GIF"']
Sin embargo, para la generación de GIF, es mejor especificar el conjunto de valores conocidos buenos para ReadUByte() individualmente en cada llamada a la función. Así que definimos un array vacío (tamaño 0)
const local UBYTE ReadUByteInitValues[0];
para sobrescribir el conjunto de ReadUByteInitValues global y para cada llamada a ReadUByte(), usamos un argumento adicional para especificar el conjunto de valores buenos a usar para esa ubicación particular.
El lenguaje de plantillas binarias también es lo suficientemente potente como para permitir que esta elección se realice en función de condiciones de tiempo de ejecución. Por ejemplo, en el siguiente código mostramos cómo la elección de valores apropiados para una llamada a ReadUByte() puede depender de la versión de GIF que estamos generando. Una versión de GIF 89a permite un valor extra posible para el byte (0x21).
if(GifHeader.Version == "89a")
local UBYTE values[] = { 0x3B, 0x2C, 0x21 };
else
local UBYTE values[] = { 0x3B, 0x2C };
while (ReadUByte(FTell(), values) != 0x3B) {
...
}
Las ediciones restantes requeridas para la plantilla binaria GIF son similares. Por ejemplo, para cada campo de estructura también se puede especificar un conjunto de valores conocidos buenos. Por ejemplo, esto especifica los valores correctos para el campo Version: 87a y 89a.
char Version[3] = { {"87a"}, {"89a"} };
Con fines de depuración, así como para comprender cómo hacer cambios apropiados para mejorar sus generadores y analizadores, puede ser útil entender algunos de los mecanismos internos del código C++ generado. Idealmente, debería poder editar los archivos de plantilla binaria hasta que puedan usarse para generar archivos válidos con alta probabilidad, de modo que no tenga que editar el código C++ generado.
El código C++ crea una clase para cada struct y union definido en la plantilla binaria, así como para tipos nativos, como int.
En tiempo de construcción, al inicializar una variable, podemos definir un conjunto de valores conocidos buenos que esta variable puede asumir. Por ejemplo, la llamada al constructor
char_array_class cname(cname_element, { "IHDR", "tEXt", "PLTE", "cHRM", "sRGB", "iEXt", "zEXt", "tIME", "pHYs", "bKGD", "sBIT", "sPLT", "acTL", "fcTL", "fdAT", "IHDR", "IEND" });
especificaría 17 valores buenos para usar en la variable cname. Pero esto a menudo no es suficiente, ya que la elección de tipos de chunk apropiados depende del contexto.
Así que también permitimos especificar un conjunto de valores buenos en tiempo de generación al generar un nuevo chunk.
Por ejemplo, esta llamada podría usarse para generar una instancia de chunk para el primer chunk, que debe tener tipo IHDR.
GENERATE(chunk, ::g->chunk.generate({ "IHDR" }, false));
Al generar el segundo chunk, podríamos usar esta larga lista de chunks posibles que pueden venir entre el chunk IHDR y el chunk PLTE:
GENERATE(chunk, ::g->chunk.generate({ "iCCP", "sRGB", "sBIT", "gAMA", "cHRM", "pHYs", "sPLT", "tIME", "zTXt", "tEXt", "iTXt", "eXIf", "oFFs", "pCAL", "sCAL", "acTL", "fcTL", "fdAT", "fRAc", "gIFg", "gIFt", "gIFx", "sTER" }, true));
El generador elegirá entonces uniformemente uno de los valores conocidos buenos para usar en la nueva instancia. También permitimos la elección de un valor malicioso que no sea uno de los valores conocidos buenos con una probabilidad pequeña de 1/128.
Esta característica se puede habilitar o deshabilitar en cualquier momento usando el método set_evil_bit.
Todas las elecciones aleatorias realizadas por el generador se hacen llamando al método rand_int().
long long rand_int(unsigned long long x, std::function<long long (unsigned char*)> parse);
Cuando se ejecuta el programa como generador, este método muestra un entero de 0 a x-1 leyendo bytes del búfer aleatorio.
Cuando se ejecuta el programa como analizador, este método utiliza la función parse() para descubrir qué bytes aleatorios deben estar presentes en el búfer aleatorio para generar el archivo objetivo, y luego escribe esos bytes en el búfer aleatorio.
La función parse recibe como argumento el búfer en la posición actual del archivo y debe devolver qué valor tendría que ser devuelto por la llamada actual a rand_int() para generar esta configuración exacta del archivo.
FormatFuzzer fue diseñado y escrito por Rafael Dutra <[email protected]>.
El concepto de un compilador de fuzzer fue introducido por Rahul Gopinath <[email protected]> y Andreas Zeller <[email protected]>.
FormatFuzzer es Copyright © 2020, 2021 por CISPA Helmholtz Center for Information Security. Se aplican las siguientes licencias:
El código de FormatFuzzer (notablemente, todo el código C++ y el código relacionado con su generación) está sujeto a la GNU GENERAL PUBLIC LICENSE, como se encuentra en COPYING.
Como excepción a lo anterior, el código C++ generado por FormatFuzzer (es decir, fuzzers y analizadores para formatos específicos) es de dominio público.
El código pfp original, en el que se basa FormatFuzzer, está sujeto a una licencia MIT, como se encuentra en LICENSE-pfp.