
Fuzzer de formato binário orientado por template que gera e analisa entradas de teste válidas em alta velocidade, com integração AFL++ para fuzzing guiado por cobertura.
FormatFuzzer é uma estrutura para geração e análise de alta eficiência e alta qualidade de entradas binárias.
Ela recebe um template binário que descreve o formato de uma entrada binária e gera um executável que produz e analisa o formato binário fornecido.
A partir de um template binário para GIF, por exemplo, FormatFuzzer produz um gerador de GIF - também conhecido como fuzzer de GIF.
Os geradores produzidos pelo FormatFuzzer são altamente eficientes, produzindo milhares de entradas de teste válidas por segundo - em forte contraste com fuzzers baseados em mutação, onde a grande maioria das entradas é inválida. As entradas geradas pelo FormatFuzzer são independentes do programa sob teste (ou na verdade, de qualquer programa), então você também pode usá-las em configurações de caixa-preta. No entanto, o FormatFuzzer também se integra com o AFL++ para produzir entradas válidas que também visam a cobertura máxima. Em nossos experimentos, essa abordagem "melhor dos dois mundos" supera todas as outras configurações; veja nosso artigo para detalhes.
Os templates binários usados pelo FormatFuzzer vêm do editor 010.
Existem mais de 170 templates binários, que podem ser usados diretamente para o FormatFuzzer ou adaptados para seu uso. Prontos para uso, o FormatFuzzer produz formatos como AVI, BMP, GIF, JPG, MIDI, MP3, MP4, PCAP, PNG, WAV e ZIP; e continuamos estendendo esta lista toda semana.
Contribuições são bem-vindas! Visite a página do projeto FormatFuzzer para registrar ideias e problemas, ou adicionar pull requests. Para detalhes sobre como o FormatFuzzer funciona e como ele se compara, leia nosso artigo para mais informações.
O FormatFuzzer está disponível na página do projeto FormatFuzzer. Você pode baixar e extrair a versão mais recente na página de releases.
Para o mais recente e melhor, você também pode clonar seu repositório git:
git clone https://github.com/uds-se/FormatFuzzer.git
Todas as ações subsequentes ocorrem na sua pasta principal:
cd FormatFuzzer
Para executar o FormatFuzzer, você precisa do seguinte:
getopt_long()) como clang ou gccpy010parser, six e intervaltreezlib (para funções de compressão)boost (para funções de checksum)Se você planeja editar os scripts de construção e configuração (arquivos .ac e .am), você também precisará de
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
Em todos os sistemas, usando pip:
pip install py010parser
pip install six
pip install intervaltree
Nota: todos os comandos de construção exigem que você esteja na mesma pasta que este arquivo README. A construção de um fuzzer fora desta pasta ainda não é suportada.
Existe um script build.sh que automatiza todas as etapas de construção.
Basta executar
./build.sh gif
para criar um fuzzer de GIF.
Isso funciona para todos os formatos de arquivo fornecidos em templates/; se houver um arquivo templates/FOO.bt, então ./build.sh FOO construirá um fuzzer.
Existe um Makefile (fonte em Makefile.am) que automatiza todas as etapas de construção.
(Requer GNU make.)
Primeiro faça
touch configure Makefile.in
depois
./configure
e então
make gif-fuzzer
para criar um fuzzer de GIF.
Isso funciona para todos os formatos de arquivo fornecidos em templates/; se houver um arquivo templates/FOO.bt, então make FOO-fuzzer construirá um fuzzer.
Se o método make acima não funcionar, ou se você quiser mais controle, pode ser necessário prosseguir manualmente.
Execute o compilador ffcompile para compilar o template binário em código C++. Ele recebe dois argumentos: o template binário .bt e um arquivo C++ .cpp a ser gerado.
./ffcompile templates/gif.bt gif.cpp
Use os comandos a seguir para criar um fuzzer gif-fuzzer.
Primeiro, compile o driver de linha de comando genérico:
g++ -c -I . -std=c++17 -g -O3 -Wall fuzzer.cpp
(-I . denota a localização do arquivo bt.h; -std=c++17 define o padrão C++.)
Em seguida, compile o analisador/gerador binário:
g++ -c -I . -std=c++17 -g -O3 -Wall gif.cpp
Finalmente, vincule o analisador/gerador binário com o driver de linha de comando para obter um executável. Se você usar bibliotecas extras (como -lz), certifique-se de especificá-las aqui também.
g++ -O3 gif.o fuzzer.o -o gif-fuzzer -lz
O FormatFuzzer pode ser executado como um analisador, gerador ou mutador autônomo de formatos específicos. Além disso, ele pode ser chamado por fuzzers de propósito geral como AFL++ para integrar essas capacidades específicas de formato no processo de fuzzing (veja a seção abaixo sobre integração com AFL++).
O fuzzer gerado recebe um comando como primeiro argumento, seguido por opções e argumentos para esse comando.
O comando mais importante é fuzz, para produzir saídas. Seus argumentos são arquivos a serem gerados no formato apropriado.
Execute o gerador como
./gif-fuzzer fuzz output.gif
para criar um arquivo binário aleatório output.gif, ou
./gif-fuzzer fuzz out1.gif out2.gif out3.gif
para criar três arquivos GIF out1.gif, out2.gif e out3.gif.
Observe que o template gif.bt que fornecemos foi aumentado com funções especiais para facilitar a geração de arquivos válidos. Se você usar arquivos de template .bt originais sem adaptações, pode receber avisos durante a geração e criar arquivos inválidos.
Você também pode executar o fuzzer como um analisador para arquivos binários, usando o comando parse. Isso é útil se você quiser testar a precisão do template binário, ou se quiser mutar uma entrada (veja 'Arquivos de Decisão', abaixo).
Para executar o analisador, use
./gif-fuzzer parse input.gif
Você verá mensagens de erro se input.gif não puder ser analisado com sucesso.
Ao analisar, você também pode armazenar todas as decisões de análise (ou seja, quais alternativas de análise foram tomadas) em um arquivo de decisão. Esta é uma sequência de bytes enumerando as decisões tomadas.
Cada byte representa uma única decisão de análise. Um valor de byte 0 significa que a primeira alternativa foi tomada, um valor de byte 1 significa que a segunda alternativa foi tomada, e assim por diante.
Você pode gerar esse arquivo de decisão ao analisar uma entrada:
./gif-fuzzer parse --decisions input.dec input.gif
Aqui, input.dec armazena as decisões tomadas para analisar input.gif.
Você também pode usar esse arquivo de decisão ao gerar entradas. O fuzzer então tomará exatamente as mesmas decisões encontradas durante a análise. O seguinte comando gera um novo arquivo GIF usando as decisões determinadas ao analisar input.gif:
./gif-fuzzer fuzz --decisions input.dec input2.gif
Se tudo funcionar bem, ambos os arquivos devem ser idênticos:
cmp input.gif input2.gif
Ao mutar um arquivo de decisão (por exemplo, substituindo bytes individuais), você pode criar entradas que são semelhantes ao arquivo original analisado. Isso é útil para interagir com estratégias de teste específicas e fuzzers como AFL, onde você pode usar gif-fuzzer e similares como tradutores de arquivos de decisão para arquivos binários e vice-versa: o AFL mutaria arquivos de decisão, e o programa sob teste seria executado nos arquivos binários traduzidos. Em contraste com a mutação direta de arquivos binários (como o AFL normalmente faria), isso teria a vantagem de sempre ter entradas válidas - e assim progredir muito mais rápido em direção à cobertura.
Além dos fuzzers específicos de formato, como gif-fuzzer, o FormatFuzzer também pode ser compilado em bibliotecas compartilhadas específicas de formato, como gif.so (para isso, basta executar ./build.sh gif ou make gif.so).
Essas bibliotecas compartilhadas podem ser carregadas por fuzzers de propósito geral, como AFL++.
Para executar o AFL++ com o FormatFuzzer, basta seguir as instruções em nossa versão modificada do AFL++. Suportamos diferentes estratégias de fuzzing, incluindo:
AFL+FFMut: executa AFL++ usando o FormatFuzzer para fornecer mutações inteligentes específicas de formato.
AFL+FFGen: usa o FormatFuzzer como um gerador específico de formato, enquanto o AFL++ muta suas sementes de decisão.
Para escrever seus próprios templates binários .bt (e assim criar um fuzzer/analisador de alta eficiência para este formato), leia a seção Introdução a Templates e Scripts do Manual do Editor 010.
Em muitos casos, um template do formato que você está procurando (ou um similar) pode já existir. Dê uma olhada na coleção de templates binários do editor 010 para ver se há algo que você possa usar ou basear seu formato.
Observe que os arquivos .bt fornecidos no repositório geralmente visam analisar arquivos. Eles podem ser usados para gerar arquivos também; mas frequentemente carecem de informações exatas sobre quais partes da entrada são necessárias.
Nesta seção, discutimos algumas das maneiras pelas quais você pode personalizar arquivos .bt para funcionar bem com o FormatFuzzer.
Por exemplo, para o formato GIF, o arquivo templates/gif-orig.bt mostra o template binário original, que foi projetado apenas para análise, enquanto o arquivo templates/gif.bt é uma versão modificada capaz de gerar GIFs válidos. Comparando os dois arquivos, vemos que um pequeno número de alterações foi necessário para conseguir isso.
Se você criou um gif-fuzzer, seja executando make gif-fuzzer ou usando a ferramenta ffcompile, você já obteve um arquivo C++ gif.cpp que contém uma implementação do gerador e analisador de GIF. Isso é útil para ver como as alterações que você faz no template binário são traduzidas em código executável. Mais detalhes sobre o código C++ são apresentados na próxima seção.
O template binário GIF faz uso de funções de lookahead ReadUByte() e ReadUShort() para olhar adiante nos valores dos próximos bytes no arquivo antes de realmente analisá-los em um campo de struct. No momento da geração, permitimos que essas funções recebam um argumento adicional especificando um conjunto de valores conhecidos bons para escolher para os bytes que estamos olhando adiante. Além disso, também permitimos especificar um conjunto global de valores conhecidos bons para usar sempre ao chamar uma função de lookahead específica, como ReadUByte(). Eles são armazenados no vetor ReadUByteInitValues.
Por padrão, nosso procedimento de tradução ffcompile tenta minerar valores interessantes que foram usados em comparações contra bytes de lookahead e usá-los como um conjunto global de valores conhecidos. Ao executar
./ffcompile templates/gif.bt gif.cpp
uma mensagem impressa mostra as funções de lookahead identificadas, bem como os valores interessantes minerados:
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"']
Para a geração de GIF, no entanto, é melhor especificar o conjunto de valores conhecidos bons para ReadUByte() individualmente em cada chamada à função. Então definimos um array vazio (tamanho 0)
const local UBYTE ReadUByteInitValues[0];
para sobrescrever o conjunto global de ReadUByteInitValues e para cada chamada a ReadUByte(), usamos um argumento adicional para especificar o conjunto de valores bons a usar para aquela localização específica.
A linguagem do template binário também é suficientemente poderosa para permitir que essa escolha seja feita com base em condições de tempo de execução. Por exemplo, no código a seguir mostramos como a escolha de valores apropriados para uma chamada ReadUByte() pode depender da versão atual do GIF que estamos gerando. Uma versão GIF 89a permite um valor extra possível para o byte (0x21).
if(GifHeader.Version == "89a")
local UBYTE values[] = { 0x3B, 0x2C, 0x21 };
else
local UBYTE values[] = { 0x3B, 0x2C };
while (ReadUByte(FTell(), values) != 0x3B) {
...
}
As edições restantes necessárias para o template binário GIF são semelhantes. Por exemplo, para cada campo de struct também pode especificar um conjunto de valores conhecidos bons. Por exemplo, isso especifica os valores corretos para o campo Version: 87a e 89a.
char Version[3] = { {"87a"}, {"89a"} };
Para fins de depuração, bem como para entender como fazer alterações apropriadas para melhorar seus geradores e analisadores, pode ser útil entender alguns funcionamentos internos do código C++ gerado. Idealmente, você deve ser capaz de editar os arquivos de template binário até que eles possam ser usados para gerar arquivos válidos com alta probabilidade, para que você não precise editar o código C++ gerado.
O código C++ cria uma classe para cada struct e union definidos no template binário, bem como para tipos nativos, como int.
No momento da construção, ao inicializar uma variável, podemos definir um conjunto de valores conhecidos bons que essa variável pode assumir. Por exemplo, a chamada do construtor
char_array_class cname(cname_element, { "IHDR", "tEXt", "PLTE", "cHRM", "sRGB", "iEXt", "zEXt", "tIME", "pHYs", "bKGD", "sBIT", "sPLT", "acTL", "fcTL", "fdAT", "IHDR", "IEND" });
especificaria 17 valores bons para usar na variável cname. Mas isso muitas vezes não é suficiente, pois a escolha de tipos de chunk apropriados depende do contexto.
Portanto, também permitimos especificar um conjunto de valores bons no momento da geração ao gerar um novo chunk.
Por exemplo, esta chamada poderia ser usada para gerar uma instância de chunk para o primeiro chunk, que deve ter o tipo IHDR.
GENERATE(chunk, ::g->chunk.generate({ "IHDR" }, false));
Ao gerar o segundo chunk, podemos usar esta longa lista de chunks possíveis que podem vir entre o chunk IHDR e o 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));
O gerador então escolherá uniformemente um dos valores conhecidos bons para usar na nova instância. Também permitimos a escolha de um valor malicioso que não está entre os valores conhecidos bons com pequena probabilidade de 1/128.
Este recurso pode ser ativado ou desativado a qualquer momento usando o método set_evil_bit.
Todas as escolhas aleatórias tomadas pelo gerador são feitas chamando o método rand_int().
long long rand_int(unsigned long long x, std::function<long long (unsigned char*)> parse);
Ao executar o programa como gerador, este método amostra um inteiro de 0 a x-1 lendo bytes do buffer aleatório.
Ao executar o programa como analisador, este método usa a função parse() para descobrir quais bytes aleatórios devem estar presentes no buffer aleatório para gerar o arquivo alvo, e então escreve esses bytes no buffer aleatório.
A função parse recebe como argumento o buffer na posição atual do arquivo e deve então retornar qual valor seria retornado pela chamada atual a rand_int() para gerar esta configuração exata do arquivo.
O FormatFuzzer foi projetado e escrito por Rafael Dutra <[email protected]>.
O conceito de um compilador de fuzzer foi introduzido por Rahul Gopinath <[email protected]> e Andreas Zeller <[email protected]>.
O FormatFuzzer tem direitos autorais © 2020, 2021 do CISPA Helmholtz Center for Information Security. As seguintes licenças se aplicam:
O código do FormatFuzzer (notavelmente, todo o código C++ e código relacionado à sua geração) está sujeito à GNU GENERAL PUBLIC LICENSE, conforme encontrado em COPYING.
Como exceção ao acima, o código C++ gerado pelo FormatFuzzer (ou seja, fuzzers e analisadores para formatos específicos) está em domínio público.
O código pfp original, no qual o FormatFuzzer se baseia, está sujeito a uma licença MIT, conforme encontrado em LICENSE-pfp.