O dia em que me apaixonei por zumbido

Em 2007, escrevi algumas ferramentas de modificação para o simulador de espaço do Freelancer . Os recursos do jogo são armazenados no formato "INI binário" ou "BINI". O formato binário foi provavelmente escolhido por uma questão de desempenho: esses arquivos são mais rápidos para carregar e ler do que o texto arbitrário no formato INI.

A maioria dos conteúdos de jogos pode ser editada diretamente desses arquivos, alterando nomes, preços de produtos, estatísticas de espaçonaves ou até adicionando novos navios. Os arquivos binários são difíceis de modificar diretamente; portanto, a abordagem natural é convertê-los em texto INI, fazer alterações em um editor de texto, depois converter novamente no formato BINI e substituir os arquivos no diretório do jogo.

Não analisei o formato BINI e não sou o primeiro a aprender como editá-los. Mas não gostei das ferramentas existentes e tive minha própria visão de como elas deveriam funcionar. Eu prefiro uma interface no estilo Unix, embora o jogo em si seja executado no Windows.

Naquela época, eu me familiarizei com as ferramentas yacc (na verdade Bison ) e lex (na verdade, flex ), além do Autoconf, então usei-as exatamente. Foi interessante experimentar esses utilitários na prática, embora eu tenha imitado servilmente outros projetos de código aberto, sem entender por que tudo foi feito dessa maneira, de nenhuma outra maneira. Devido ao uso do yacc / lex e à criação de scripts de configuração, foi necessário um sistema completo semelhante ao Unix. Tudo isso é visível na versão original dos programas .

O projeto acabou sendo bastante bem-sucedido: eu mesmo usei essas ferramentas com sucesso e elas apareceram em diferentes coleções para modding do Freelancer.

Refatoração


Em meados de 2018, voltei a este projeto. Você já olhou para o seu código antigo com o pensamento: o que você achou? Meu formato INI acabou sendo muito mais rígido e rigoroso do que o necessário, os binários foram gravados de maneira dúbia e a montagem nem sequer funcionou normalmente.

Graças a dez anos de experiência extra, eu tinha certeza de que escreveria essas ferramentas muito melhor agora. E fiz isso em alguns dias, reescrevendo-os do zero. Este novo código está agora no thread principal do Github.

Eu gosto de tornar tudo o mais simples possível , então me livrei do autoconf em favor de um Makefile mais simples e mais portátil . Chega de yacc ou lex, mas o analisador é escrito à mão. Somente o portátil apropriado é usado C. O resultado é tão simples que eu monto o projeto com um pequeno comando do Visual Studio , para que o Makefile não seja realmente necessário. Se você substituir stdint.h por typedef , poderá criar e executar binitools no DOS .

A nova versão é mais rápida, mais compacta, mais limpa e mais fácil. É muito mais flexível em relação à entrada INI, portanto, é mais fácil de usar. Mas isso é realmente correto?

Fuzzing


Eu me interesso por muitos anos, especialmente afl (american fuzzy lop). Mas ele nunca o dominou, apesar de ter testado algumas das ferramentas que eu uso regularmente. Mas a confusão não encontrou nada de notável, pelo menos antes de eu desistir. Testei minha biblioteca JSON e, por algum motivo, também não encontrei nada. Está claro que meu analisador JSON não pode ser tão confiável, certo? Mas o zumbido não mostrou nada. (Como se viu, minha biblioteca JSON é bastante confiável, em grande parte graças aos esforços da comunidade!)

Mas agora eu tenho um analisador INI relativamente novo. Embora possa analisar e montar corretamente o conjunto original de arquivos BINI no jogo, sua funcionalidade não foi realmente testada. Certamente aqui a confusão vai encontrar algo. Além disso, você não precisa escrever uma única linha para executar este código. As ferramentas padrão funcionam com entrada padrão, o que é ideal.

Supondo que você tenha as ferramentas necessárias instaladas (make, gcc, afl), eis como o fuzzing de binitools é iniciado facilmente:

 $ make CC=afl-gcc $ mkdir in out $ echo '[x]' > in/empty $ afl-fuzz -i in -o out -- ./bini 

O utilitário bini aceita INI na entrada e emite BINI, portanto, é muito mais interessante verificá-lo do que o procedimento de unbini reverso. Como a unbini analisa dados binários relativamente simples, o fuzzer (provavelmente) não tem nada a procurar. No entanto, apenas no caso, eu verifiquei de qualquer maneira.



Neste exemplo, alterei o compilador padrão para o shell do GCC para afl ( CC=afl-gcc ). Aqui, afl chama GCC em segundo plano, mas adiciona seu próprio kit de ferramentas ao binário. Ao fazer afl-fuzz , o afl-fuzz usa esse kit de ferramentas para monitorar o caminho de execução de um programa. A documentação afl explica os detalhes técnicos.

Também criei os diretórios de entrada e saída colocando no diretório de entrada um exemplo de trabalho mínimo que fornece um ponto de partida para o afl. Quando é iniciado, ele muda a fila de dados de entrada e observa as alterações durante a execução do programa. O diretório de saída contém os resultados e, mais importante, o corpo dos dados de entrada que causam caminhos de execução exclusivos. Em outras palavras, muitas entradas são processadas na saída do fuzzer, verificando muitos cenários de borda diferentes.

O resultado mais interessante e assustador é uma falha completa do programa. Quando iniciei o fuzzer para binitools, o bini mostrou muitas dessas falhas. Em questão de minutos, o afl descobriu vários erros sutis e interessantes no meu programa, o que foi incrivelmente útil. Fazzer até encontrou um bug improvável de um ponteiro obsoleto , verificando a ordem diferente de várias alocações de memória. Esse bug em particular foi um ponto de virada que me fez perceber o valor da confusão.

Nem todos os erros encontrados levaram a falhas. Também estudei a saída e verifiquei quais entradas deram um resultado bem-sucedido e quais não deram, e observei como o programa lidava com vários casos extremos. Ela rejeitou algumas sugestões que eu pensei que ela iria processar. E vice-versa, ela processou alguns dados que eu considerava incorretos e interpretou alguns dados de maneira inesperada para mim. Portanto, mesmo após corrigir bugs com falhas de programa, eu ainda alterava as configurações do analisador para corrigir cada um desses casos desagradáveis.

Crie um conjunto de testes


Assim que corrigi todos os erros detectados pelo fuzzer e ajustei o analisador em todas as situações de borda, fiz um conjunto de testes a partir do pacote de dados do fuzzer - embora não diretamente.

Primeiro, eu executei o fuzzer em paralelo - esse processo é explicado na documentação afl -, então recebi muita entrada redundante. Por redundância, quero dizer que a entrada é diferente, mas tem o mesmo caminho de execução. Felizmente, o afl tem uma ferramenta para lidar com isso: afl-cmin , uma ferramenta para minimizar o shell. Elimina entradas desnecessárias.

Em segundo lugar, muitas dessas entradas foram mais longas do que o necessário para chamar seu caminho de execução exclusivo. afl-tmin , um minimizador de caso de teste que reduziu o caso de teste, ajudou afl-tmin .

Separei as entradas válidas e inválidas - e verifiquei no repositório. Dê uma olhada em todas essas entradas estúpidas inventadas pelo fuzzer com base em uma única entrada mínima:


De fato, aqui o analisador é congelado em um estado e um conjunto de testes garante que uma construção específica se comporte de uma maneira muito específica. Isso é especialmente útil para garantir que os assemblies feitos por outros compiladores em outras plataformas se comportem da mesma forma com relação à sua saída. Minha suíte de testes até detectou um erro na biblioteca dietlibc porque o binitools não passou nos testes depois de vincular a ela. Se você tivesse que fazer alterações não triviais no analisador, em essência, teria que abandonar o conjunto atual de testes e começar tudo de novo para que o afl gerasse um corpo totalmente novo para o novo analisador.

Obviamente, a difusão se estabeleceu como uma técnica poderosa. Ele encontrou uma série de erros que eu nunca poderia ter descoberto sozinho. Desde então, comecei a usá-lo com mais competência para testar outros programas - não apenas o meu - e encontrei muitos novos bugs. Agora, o fuzzer ocupou um lugar permanente entre as ferramentas do meu kit de desenvolvimento.

Source: https://habr.com/ru/post/pt438662/


All Articles