Originalmente desenvolvido por Michal Zalewski [email protected].
Consulte QuickStartGuide.txt se não tiver tempo para ler este arquivo.
Fuzzing é uma das estratégias mais poderosas e comprovadas para identificar problemas de segurança em software do mundo real; ele é responsável pela grande maioria dos bugs de execução remota de código e escalonamento de privilégios encontrados até o momento em softwares críticos para a segurança.
Infelizmente, a difusão também é relativamente superficial; mutações cegas e aleatórias tornam muito improvável o alcance de determinados caminhos de código no código testado, deixando algumas vulnerabilidades firmemente fora do alcance desta técnica.
Houve inúmeras tentativas de resolver este problema. Uma das primeiras abordagens - iniciada por 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, em seguida, difundi-los 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 bloco fornecem apenas uma compreensão muito simplista do estado do programa e são menos úteis para orientar o esforço de difusão a longo prazo.
Outras pesquisas mais sofisticadas concentraram-se 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 de problemas de confiabilidade e desempenho em usos práticos - e atualmente não oferecem uma alternativa viável às técnicas de difusão "burras".
American Fuzzy Lop é um fuzzer de força bruta acoplado a um algoritmo genético guiado por instrumentação extremamente simples, mas sólido. Ele usa uma forma modificada de cobertura de borda para captar facilmente alterações sutis em escala local no fluxo de controle do programa.
Simplificando um pouco, o algoritmo geral pode ser resumido como:
Carregue os casos de teste iniciais fornecidos pelo usuário na fila,
Pegue o próximo arquivo de entrada da fila,
Tente cortar o caso de teste para o menor tamanho que não altere o comportamento medido do programa,
Altere repetidamente o arquivo usando uma variedade equilibrada e bem pesquisada de estratégias tradicionais de difusão,
Se alguma das mutações geradas resultar em uma nova transição de estado registrada pela instrumentação, adicione a saída mutada como uma nova entrada na fila.
Vá para 2.
Os casos de teste descobertos também são selecionados periodicamente para eliminar aqueles que se tornaram obsoletos por descobertas mais recentes e de maior cobertura; e passar por várias outras etapas de minimização de esforço orientadas por instrumentação.
Como resultado secundário do processo de difusão, a ferramenta cria um corpus pequeno e independente de casos de teste interessantes. Eles são extremamente úteis para semear outros regimes de testes que exigem muita mão de obra ou recursos - por exemplo, para testes de estresse de navegadores, aplicativos de escritório, suítes gráficas ou ferramentas de código fechado.
O fuzzer é exaustivamente testado para oferecer desempenho pronto para uso muito superior ao de fuzzing cego ou ferramentas somente de cobertura.
Quando o código-fonte está disponível, a instrumentação pode ser injetada por uma ferramenta complementar que funciona como um substituto imediato para gcc ou clang em qualquer processo de construção 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 fuzzed tão rápido ou até mais rápido do que possível com ferramentas tradicionais.
A maneira correta de recompilar o programa alvo pode variar dependendo das especificidades do processo de construção, mas uma abordagem quase universal seria:
$ CC=/path/to/afl/afl-gcc ./configure
$ make clean all
Para programas C++, você também deseja definir CXX=/path/to/afl/afl-g++
.
Os wrappers clang (afl-clang e afl-clang++) podem ser usados da mesma maneira; os usuários do clang também podem optar por aproveitar um modo de instrumentação de alto desempenho, conforme descrito em llvm_mode/README.llvm.
Ao testar bibliotecas, você precisa encontrar ou escrever um programa simples que leia dados de 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 certificar-se de que o arquivo .so correto seja carregado em tempo de execução (geralmente configurando LD_LIBRARY_PATH
). A opção mais simples é uma construção estática, geralmente possível através de:
$ CC=/path/to/afl/afl-gcc ./configure --disable-shared
Definir AFL_HARDEN=1
ao chamar 'make' fará com que o wrapper CC habilite automaticamente opções de proteção de código que facilitam a detecção de bugs simples de memória. Libdislocator, uma biblioteca auxiliar incluída no AFL (consulte libdislocator/README.dislocator) também pode ajudar a descobrir problemas de corrupção de heap.
PS. Os usuários do ASAN são aconselhados a revisar o arquivo notes_for_asan.txt para obter advertências importantes.
Quando o código-fonte NÃO está disponível, o fuzzer oferece suporte experimental para instrumentação rápida e dinâmica de binários de caixa preta. Isso é conseguido com uma versão do QEMU rodando no modo menos conhecido de "emulação de espaço do usuário".
QEMU é um projeto separado do AFL, mas você pode construir o recurso de maneira conveniente fazendo:
$ cd qemu_mode
$ ./build_qemu_support.sh
Para obter instruções e advertências adicionais, consulte qemu_mode/README.qemu.
O modo é aproximadamente 2 a 5x mais lento que a instrumentação em tempo de compilação, é menos propício à paralelização e pode ter algumas outras peculiaridades.
Para funcionar corretamente, o fuzzer requer um ou mais arquivos iniciais que contenham um bom exemplo dos dados de entrada normalmente esperados pela aplicação alvo. Existem duas regras básicas:
Mantenha os arquivos pequenos. Menos de 1 kB é o ideal, embora não seja estritamente necessário. Para uma discussão sobre por que o tamanho é importante, consulte perf_tips.txt.
Use vários casos de teste somente se eles forem funcionalmente diferentes uns dos outros. Não faz sentido usar cinquenta fotos de férias diferentes para confundir uma biblioteca de imagens.
Você pode encontrar muitos bons exemplos de arquivos iniciais no subdiretório testcases/ que vem com esta ferramenta.
PS. Se um grande corpus de dados estiver disponível para triagem, você poderá usar o utilitário afl-cmin para identificar um subconjunto de arquivos funcionalmente distintos que exercem diferentes caminhos de código no binário de destino.
O próprio processo de difusão é realizado 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.
Para binários de destino que aceitam entrada diretamente do stdin, a sintaxe usual é:
$ ./afl-fuzz -i testcase_dir -o findings_dir /path/to/program [...params...]
Para programas que recebem entrada de um arquivo, use '@@' para marcar o local na linha de comando do destino onde o nome do arquivo de entrada deve ser colocado. O fuzzer substituirá isso para você:
$ ./afl-fuzz -i testcase_dir -o findings_dir /path/to/program @@
Você também pode usar a opção -f para que os dados modificados sejam gravados em um arquivo específico. Isso é útil se o programa espera uma extensão de arquivo específica ou algo assim.
Binários não instrumentados podem ser difundidos no modo QEMU (adicione -Q na linha de comando) ou no modo tradicional blind-fuzzer (especifique -n).
Você pode usar -t e -m para substituir o tempo limite padrão e o limite de memória para o processo executado; raros exemplos de alvos que podem precisar dessas configurações incluem compiladores e decodificadores de vídeo.
Dicas para otimizar o desempenho da difusão são discutidas em perf_tips.txt.
Observe que o afl-fuzz começa executando uma série de etapas determinísticas de difusão, que podem levar vários dias, mas tendem a produzir casos de teste perfeitos. Se você deseja resultados rápidos e sujos imediatamente - semelhantes ao zzuf e outros fuzzers tradicionais - adicione a opção -d à linha de comando.
Consulte o arquivo status_screen.txt para obter informações sobre como interpretar as estatísticas exibidas e monitorar o funcionamento do processo. Certifique-se de consultar este arquivo, especialmente se algum elemento da UI estiver destacado em vermelho.
O processo de difusão continuará até você pressionar Ctrl-C. No mínimo, você deseja permitir que o fuzzer conclua um ciclo de fila, o que pode levar de algumas horas a uma semana ou mais.
Existem três subdiretórios criados no diretório de saída e atualizados em tempo real:
fila/ - casos de teste para cada caminho de execução distinto, além de todos os arquivos iniciais fornecidos pelo usuário. Este é o corpus sintetizado mencionado na seção 2. Antes de usar este corpus para qualquer outro propósito, você pode reduzi-lo para um tamanho menor usando a ferramenta afl-cmin. A ferramenta encontrará um subconjunto menor de arquivos que oferecem cobertura de borda equivalente.
crashes/ - casos de teste únicos que fazem com que o programa testado receba um sinal fatal (por exemplo, SIGSEGV, SIGILL, SIGABRT). As entradas são agrupadas pelo sinal recebido.
trava/ - casos de teste exclusivos que fazem com que o programa testado expire. O limite de tempo padrão antes que algo seja classificado como travado é maior que 1 segundo e o valor do parâmetro -t. O valor pode ser ajustado definindo AFL_HANG_TMOUT, mas isso raramente é necessário.
Falhas e travamentos são considerados "únicos" se os caminhos de execução associados envolverem quaisquer transições de estado não vistas em falhas registradas anteriormente. Se um único bug puder ser alcançado de várias maneiras, haverá alguma inflação na contagem no início do processo, mas isso deverá diminuir rapidamente.
Os nomes de arquivos para travamentos e travamentos são correlacionados com entradas de fila pai e sem falhas. Isso deve ajudar na depuração.
Quando você não consegue reproduzir uma falha encontrada pelo afl-fuzz, a causa mais provável é que você não está definindo o mesmo limite de memória usado pela ferramenta. Tentar:
$ LIMIT_MB=50
$ ( ulimit -Sv $[LIMIT_MB << 10] ; /path/to/tested_binary ... )
Altere LIMIT_MB para corresponder ao parâmetro -m passado para afl-fuzz. No OpenBSD, altere também -Sv para -Sd.
Qualquer diretório de saída existente também pode ser usado para retomar trabalhos abortados; tentar:
$ ./afl-fuzz -i- -o existing_output_dir [...etc...]
Se você tiver o gnuplot instalado, também poderá gerar alguns gráficos bonitos para qualquer tarefa de difusão ativa usando o afl-plot. Para obter um exemplo de como isso se parece, consulte http://lcamtuf.coredump.cx/afl/plot/.
Cada instância do afl-fuzz ocupa aproximadamente um núcleo. Isso significa que em sistemas multinúcleo, a paralelização é necessária para utilizar totalmente o hardware. Para dicas sobre como fuzzing um alvo comum em múltiplos núcleos ou múltiplas máquinas em rede, consulte paralelo_fuzzing.txt.
O modo de difusão paralela também oferece uma maneira simples de interfacear o AFL com outros fuzzers, com mecanismos de execução simbólica ou concólica, e assim por diante; novamente, consulte a última seção de parallel_fuzzing.txt para dicas.
Por padrão, o mecanismo de mutação afl-fuzz é otimizado para formatos de dados compactos - por exemplo, imagens, multimídia, dados compactados, sintaxe de expressão regular ou scripts de shell. É um pouco menos adequado para linguagens com palavreado particularmente detalhado e redundante - incluindo HTML, SQL ou JavaScript.
Para evitar o incômodo de construir ferramentas com reconhecimento de sintaxe, o afl-fuzz fornece uma maneira de propagar o processo de difusão com um dicionário opcional de palavras-chave de linguagem, cabeçalhos mágicos ou outros tokens especiais associados ao tipo de dados alvo - e usar isso para reconstruir a gramática subjacente em movimento:
http://lcamtuf.blogspot.com/2015/01/afl-fuzz-making-up-grammar-with.html
Para usar esse recurso, primeiro você precisa criar um dicionário em um dos dois formatos discutidos em dicionários/README.dictionaries; e aponte o fuzzer para ele por meio da opção -x na linha de comando.
(Vários dicionários comuns também já são fornecidos nesse subdiretório.)
Não há como fornecer descrições mais estruturadas da sintaxe subjacente, mas o fuzzer provavelmente descobrirá parte disso com base apenas no feedback da instrumentação. Isso realmente funciona na prática, digamos:
http://lcamtuf.blogspot.com/2015/04/finding-bugs-in-sqlite-easy-way.html
PS. Mesmo quando nenhum dicionário explícito é fornecido, o afl-fuzz tentará extrair tokens de sintaxe existentes no corpus de entrada, observando a instrumentação bem de perto durante inversões determinísticas de bytes. Isso funciona para alguns tipos de analisadores e gramáticas, mas não é tão bom quanto o modo -x.
Se for realmente difícil encontrar um dicionário, outra opção é deixar o AFL rodar por um tempo e depois usar a biblioteca de captura de token que vem como um utilitário complementar do AFL. Para isso, consulte libtokencap/README.tokencap.
O agrupamento de falhas baseado em cobertura geralmente produz um pequeno conjunto de dados que pode ser rapidamente triado manualmente ou com um script GDB ou Valgrind muito simples. Cada falha também é rastreável até seu caso de teste pai sem falha na fila, facilitando o diagnóstico de falhas.
Dito isto, é importante reconhecer que algumas falhas difusas podem ser difíceis de avaliar rapidamente quanto à explorabilidade sem muito trabalho de depuração e análise de código. Para ajudar nesta tarefa, o afl-fuzz suporta um modo exclusivo de "exploração de falhas" habilitado com o sinalizador -C.
Nesse modo, o fuzzer pega um ou mais casos de teste com falha como entrada e usa suas estratégias de difusão orientadas por feedback para enumerar muito rapidamente todos os caminhos de código que podem ser alcançados no programa, mantendo-o no estado de falha.
Mutações que não resultam em falha são rejeitadas; assim como quaisquer alterações que não afetem o caminho de execução.
A saída é um pequeno corpus de arquivos que pode ser examinado muito rapidamente para ver que grau de controle o invasor tem sobre o endereço com falha ou se é possível passar por uma leitura inicial fora dos limites - e ver o que está por baixo .
Ah, mais uma coisa: para minimizar o caso de teste, experimente o afl-tmin. A ferramenta pode ser operada de uma forma muito simples:
$ ./afl-tmin -i test_case -o minimized_result -- /path/to/program [...]
A ferramenta funciona tanto com casos de teste com falha quanto sem falha. No modo crash, ele aceitará alegremente binários instrumentados e não instrumentados. No modo sem travamento, o minimizador depende da instrumentação AFL padrão para tornar o arquivo mais simples sem alterar o caminho de execução.
O minimizador aceita a sintaxe -m, -t, -f e @@ de maneira compatível com afl-fuzz.
Outra adição recente ao AFL é a ferramenta afl-analyze. Ele pega um arquivo de entrada, tenta inverter bytes sequencialmente e observa o comportamento do programa testado. Em seguida, ele codifica a entrada por cores com base em quais seções parecem ser críticas e quais não são; embora não seja à prova de balas, muitas vezes pode oferecer insights rápidos sobre formatos de arquivo complexos. Mais informações sobre sua operação podem ser encontradas no final do arquivo Technical_details.txt.
Fuzzing é uma técnica maravilhosa e subutilizada para descobrir também erros de design e implementação que não travam. Alguns bugs interessantes foram encontrados modificando os programas alvo para chamar abort() quando, digamos:
Duas bibliotecas bignum produzem saídas diferentes quando recebem a mesma entrada gerada pelo fuzzer,
Uma biblioteca de imagens produz resultados diferentes quando solicitado a decodificar a mesma imagem de entrada várias vezes seguidas,
Uma biblioteca de serialização/desserialização falha em produzir saídas estáveis ao serializar e desserializar iterativamente dados fornecidos pelo fuzzer,
Uma biblioteca de compactação produz uma saída inconsistente com o arquivo de entrada quando solicitada a compactar e descompactar um blob específico.
A implementação dessas verificações de sanidade ou similares geralmente leva muito pouco tempo; se você é o mantenedor de um pacote específico, você pode tornar este código condicional com #ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
(um sinalizador também compartilhado com libfuzzer) ou #ifdef __AFL_COMPILER
(este é apenas para AFL).
Lembre-se de que, assim como muitas outras tarefas de uso intensivo de computação, a difusão pode sobrecarregar o hardware e o sistema operacional. Em particular:
Sua CPU ficará quente e precisará de resfriamento adequado. Na maioria dos casos, se o resfriamento for insuficiente ou parar de funcionar corretamente, as velocidades da CPU serão automaticamente aceleradas. Dito isto, especialmente quando se utiliza hardware menos adequado (laptops, smartphones, etc.), não é totalmente impossível que algo exploda.
Os programas direcionados podem acabar capturando gigabytes de memória de forma irregular ou preenchendo espaço em disco com arquivos inúteis. AFL tenta impor limites básicos de memória, mas não consegue evitar todo e qualquer acidente possível. O resultado final é que você não deve se preocupar com sistemas onde a perspectiva de perda de dados não é um risco aceitável.
A difusão envolve bilhões de leituras e gravações no sistema de arquivos. Em sistemas modernos, isso geralmente será armazenado em cache, resultando em E/S "física" bastante modesta - mas há muitos fatores que podem alterar essa equação. É sua responsabilidade monitorar possíveis problemas; com E/S muito pesada, a vida útil de muitos HDDs e SSDs pode ser reduzida.
Uma boa maneira de monitorar a E/S de disco no Linux é o comando ‘iostat’:
$ iostat -d 3 -x -k [...optional disk ID...]
Aqui estão algumas das advertências mais importantes para AFL:
O AFL detecta falhas verificando se o primeiro processo gerado morreu devido a um sinal (SIGSEGV, SIGABRT, etc). Os programas que instalam manipuladores personalizados para esses sinais podem precisar ter o código relevante comentado. Na mesma linha, falhas no processamento filho geradas pelo alvo difuso podem escapar da detecção, a menos que você adicione manualmente algum código para capturá-lo.
Como acontece com qualquer outra ferramenta de força bruta, o fuzzer oferece cobertura limitada se criptografia, somas de verificação, assinaturas criptográficas ou compactação forem usadas para envolver totalmente o formato de dados real a ser testado.
Para contornar isso, você pode comentar as verificações relevantes (veja experimental/libpng_no_checksum/ para inspiração); se isso não for possível, você também pode escrever um pós-processador, conforme explicado em experimental/post_library/.
Existem algumas compensações infelizes entre ASAN e binários de 64 bits. Isso não se deve a nenhuma falha específica do afl-fuzz; consulte notes_for_asan.txt para dicas.
Não há suporte direto para serviços de rede difusos, daemons em segundo plano ou aplicativos interativos que exigem interação da interface do usuário para funcionar. Talvez seja necessário fazer alterações simples no código para fazê-lo se comportar de maneira mais tradicional. Preeny também pode oferecer uma opção relativamente simples - consulte: https://github.com/zardus/preeny
Algumas dicas úteis para modificar serviços baseados em rede também podem ser encontradas em: https://www.fastly.com/blog/how-to-fuzz-server-american-fuzzy-lop
AFL não produz dados de cobertura legíveis por humanos. Se você deseja monitorar a cobertura, use afl-cov de Michael Rash: https://github.com/mrash/afl-cov
Ocasionalmente, máquinas sencientes se levantam contra seus criadores. Se isso acontecer com você, consulte http://lcamtuf.coredump.cx/prep/.
Além disso, consulte INSTALL para dicas específicas da plataforma.
Muitas das melhorias no afl-fuzz não seriam possíveis sem feedback, relatórios de bugs ou patches 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
Obrigado!
Questões? Preocupações? Relatórios de bugs? Por favor, use o GitHub.
Existe também uma lista de discussão do projeto; para participar, envie um e-mail para [email protected]. Ou, se preferir navegar pelos arquivos primeiro, tente: https://groups.google.com/group/afl-users.