
american fuzzy lop - um fuzzer voltado para segurança
Desenvolvido originalmente por Michal Zalewski [email protected].
Consulte QuickStartGuide.txt se você não tiver tempo de ler este arquivo.
Fuzzing é uma das estratégias mais poderosas e comprovadas para identificar problemas de segurança em software do mundo real; é responsável pela grande maioria dos bugs de execução remota de código e escalonamento de privilégios encontrados até hoje em software de segurança crítica.
Infelizmente, o fuzzing também é relativamente superficial; mutações cegas e aleatórias tornam muito improvável alcançar certos caminhos de código no software testado, deixando algumas vulnerabilidades totalmente fora do alcance dessa técnica.
Houve inúmeras tentativas de resolver esse problema. Uma das primeiras abordagens - cujo pioneiro foi Tavis Ormandy - é a destilação de corpus. O método depende de sinais de cobertura para selecionar um subconjunto de sementes interessantes de um corpus massivo e de alta qualidade de arquivos candidatos, e então submetê-los a fuzzing por meios tradicionais. A abordagem funciona excepcionalmente bem, mas exige que esse corpus esteja prontamente disponível. Além disso, as medições de cobertura de blocos fornecem apenas uma compreensão muito simplista do estado do programa e são menos úteis para guiar o esforço de fuzzing a longo prazo.
Outras pesquisas mais sofisticadas focaram em técnicas como análise de fluxo de programa ("execução concólica"), execução simbólica ou análise estática. Todos esses métodos são extremamente promissores em ambientes experimentais, mas tendem a sofrer com problemas de confiabilidade e desempenho em usos práticos - e atualmente não oferecem uma alternativa viável às técnicas de fuzzing "burro".
American Fuzzy Lop é um fuzzer de força bruta combinado com um algoritmo genético guiado por instrumentação extremamente simples, mas sólido como uma rocha. Ele usa uma forma modificada de cobertura de arestas para capturar sem esforço mudanças sutis em escala local no fluxo de controle do programa.
Simplificando um pouco, o algoritmo geral pode ser resumido como:
Carregar os casos de teste iniciais fornecidos pelo usuário na fila,
Pegar o próximo arquivo de entrada da fila,
Tentar reduzir o caso de teste ao menor tamanho que não altere o comportamento medido do programa,
Mutar repetidamente o arquivo usando uma variedade equilibrada e bem pesquisada de estratégias tradicionais de fuzzing,
Se alguma das mutações geradas resultar em uma nova transição de estado registrada pela instrumentação, adicionar a saída mutada como uma nova entrada na fila.
Ir para 2.
Os casos de teste descobertos também são periodicamente eliminados para remover aqueles que se tornaram obsoletos por descobertas mais novas e com maior cobertura; e passam por várias outras etapas de minimização de esforço orientadas por instrumentação.
Como resultado secundário do processo de fuzzing, a ferramenta cria um corpus pequeno e autocontido de casos de teste interessantes. Esses casos são extremamente úteis para semear outros regimes de teste que exigem muito trabalho ou recursos - por exemplo, para testes de estresse em navegadores, aplicativos de escritório, suítes gráficas ou ferramentas de código fechado.
O fuzzer é minuciosamente testado para entregar desempenho imediato muito superior ao fuzzing cego ou a ferramentas baseadas apenas em cobertura.
Quando o código-fonte está disponível, a instrumentação pode ser injetada por uma ferramenta complementar que funciona como substituto direto do gcc ou clang em qualquer processo de build padrão para código de terceiros.
A instrumentação tem um impacto de desempenho bastante modesto; em conjunto com outras otimizações implementadas pelo afl-fuzz, a maioria dos programas pode ser submetida a fuzzing tão rápido ou até mais rápido do que seria possível com ferramentas tradicionais.
A maneira correta de recompilar o programa alvo pode variar dependendo das especificidades do processo de build, mas uma abordagem quase universal seria:```shell $ CC=/path/to/afl/afl-gcc ./configure $ make clean all
Para programas em C++, você também vai querer definir `CXX=/path/to/afl/afl-g++`.
Os wrappers do clang (afl-clang e afl-clang++) podem ser usados da mesma forma;
usuários do clang também podem optar por usar um modo de instrumentação de maior desempenho,
conforme descrito em llvm_mode/README.llvm.
Ao testar bibliotecas, você precisa encontrar ou escrever um programa simples que leia
dados do stdin ou de um arquivo e os passe para a biblioteca testada. Nesse
caso, é essencial vincular esse executável a uma versão estática da
biblioteca instrumentada, ou garantir que o arquivo .so correto seja carregado em
tempo de execução (geralmente definindo `LD_LIBRARY_PATH`). A opção mais simples é uma compilação
estática, geralmente possível via:```shell
$ CC=/path/to/afl/afl-gcc ./configure --disable-shared
Definir AFL_HARDEN=1 ao chamar 'make' fará com que o wrapper CC
ative automaticamente opções de endurecimento de código que facilitam a detecção de
bugs simples de memória. Libdislocator, uma biblioteca auxiliar incluída com AFL (veja
libdislocator/README.dislocator) também pode ajudar a descobrir problemas de corrupção de heap.
PS. Usuários de ASAN são aconselhados a revisar o arquivo notes_for_asan.txt para importantes ressalvas.
Quando o código-fonte NÃO está disponível, o fuzzer oferece suporte experimental para instrumentação rápida e on-the-fly de binários black-box. Isso é realizado com uma versão do QEMU rodando no modo pouco conhecido de "emulação de espaço do usuário".
O QEMU é um projeto separado do AFL, mas você pode compilar o recurso de forma conveniente fazendo:```shell $ cd qemu_mode $ ./build_qemu_support.sh
Para instruções adicionais e ressalvas, consulte qemu_mode/README.qemu.
O modo é aproximadamente 2 a 5 vezes mais lento que a instrumentação em tempo de compilação, é
menos propício à paralelização e pode ter algumas outras peculiaridades.
## 5) Escolhendo casos de teste iniciais
Para operar corretamente, o fuzzer requer um ou mais arquivos iniciais que
contenham um bom exemplo dos dados de entrada normalmente esperados pelo
aplicativo alvo. Há duas regras básicas:
- Mantenha os arquivos pequenos. Abaixo de 1 kB é o ideal, embora não seja estritamente necessário.
Para uma discussão sobre por que o tamanho importa, consulte [perf_tips.txt](https://github.com/google/afl/blob/master/docs/perf_tips.txt).
- Use vários casos de teste somente se forem funcionalmente diferentes entre
si. Não adianta usar cinquenta fotos de férias diferentes para
fazer fuzzing em uma biblioteca de imagens.
Você pode encontrar muitos bons exemplos de arquivos iniciais no subdiretório testcases/
que acompanha esta ferramenta.
PS. Se um grande corpus de dados estiver disponível para triagem, você pode querer usar
o utilitário afl-cmin para identificar um subconjunto de arquivos funcionalmente distintos que
exercitem diferentes caminhos de código no binário alvo.
## 6) Fuzzing de binários
O processo de fuzzing em si é executado pelo utilitário afl-fuzz. Este programa
requer um diretório somente leitura com casos de teste iniciais, um local separado para
armazenar suas descobertas, além de um caminho para o binário a ser testado.