
Fuzzer guiado por cobertura de arestas para bibliotecas PHP que detecta bugs por meio de crashes, timeouts e avisos. Suporta gerenciamento de corpus, minimização de crashes e relatórios de cobertura de código.
Esta biblioteca implementa um fuzzer para PHP, que pode ser usado para encontrar bugs em bibliotecas (particularmente bibliotecas de análise sintática) alimentando-as com entradas "aleatórias". O feedback da instrumentação de cobertura de arestas é usado para guiar a escolha de entradas "aleatórias", de modo que novos caminhos de código sejam visitados.
Phar (recomendado): Você pode baixar um pacote phar desta biblioteca a partir da página de releases. O uso do phar é recomendado, pois evita conflitos de dependência com bibliotecas que usam PHP-Parser.
Composer: composer global require nikic/php-fuzzer
Primeiro, é necessária uma definição da função alvo. Aqui está um exemplo de alvo para encontrar bugs em microsoft/tolerant-php-parser:
<?php // target.php
/** @var PhpFuzzer\Config $config */
require 'path/to/tolerant-php-parser/vendor/autoload.php';
// Obrigatório: O alvo aceita uma única string de entrada e a executa através da biblioteca
// testada. O alvo pode lançar Exceções normais (que são ignoradas),
// mas exceções do tipo Error são consideradas como um bug encontrado.
$parser = new Microsoft\PhpParser\Parser();
$config->setTarget(function(string $input) use($parser) {
$parser->parseSourceFile($input);
});
// Opcional: Muitos alvos não exibem bugs em entradas grandes que não possam ser
// produzidas também com entradas pequenas. Limitar o comprimento pode melhorar o desempenho.
$config->setMaxLen(1024);
// Opcional: Um dicionário pode ser usado para fornecer fragmentos úteis ao fuzzer,
// como palavras-chave da linguagem. Isso é particularmente importante se estas
// não puderem ser facilmente descobertas pelo fuzzer, porque são tratadas
// por uma função de extensão PHP não instrumentada, como token_get_all().
$config->addDictionary('example/php.dict');
O fuzzer é executado contra um corpus de entradas iniciais "interessantes", que podem, por exemplo, ser semeadas com base em testes unitários existentes. Se nenhum corpus for especificado, um diretório de corpus temporário será criado.
# Executar sem corpus inicial
php-fuzzer fuzz target.php
# Executar com corpus inicial (uma entrada por arquivo)
php-fuzzer fuzz target.php corpus/
Se o fuzzing for interrompido, ele pode ser retomado posteriormente especificando o mesmo diretório de corpus.
Uma vez que uma falha for encontrada, ela é escrita em um arquivo crash-HASH.txt. Ela é fornecida na
forma em que foi originalmente encontrada, que pode ser desnecessariamente complexa e conter fragmentos
não relevantes para a falha. Portanto, é provável que você queira reduzir a entrada causadora da falha primeiro:
php-fuzzer minimize-crash target.php crash-HASH.txt
Isso produzirá uma sequência de arquivos minimized-HASH.txt cada vez menores. Se você quiser
verificar rapidamente o trace de exceção produzido para uma entrada de falha, pode usar o comando run-single:
php-fuzzer run-single target.php minimized-HASH.txt
Finalmente, é possível gerar um relatório de cobertura de código HTML, que mostra quais blocos de código no alvo são atingidos ao executar entradas de um determinado corpus:
php-fuzzer report-coverage target.php corpus/ coverage_dir/
Opções de configuração adicionais podem ser exibidas com php-fuzzer --help.
Enquanto o fuzzer está em execução, ele relata seu status continuamente usando uma única linha de saída. Esta linha compreende estas partes na seguinte ordem:
NEW ou REDUCED: A ação que acionou esta linha de status. NEW indica que uma nova entrada foi adicionada ao corpus enquanto REDUCED indica que uma entrada existente do corpus foi substituída por uma entrada mais curta.run: N: O número total de iterações de fuzzing (execuções do alvo) realizadas desde o início do fuzzer(N/s): A velocidade de execução atual, medida em execuções por segundoft: N: O número total de características únicas descobertas até agora(N/s): O número médio de novas características descobertas por segundo desde o início do fuzzercorp: N: O número de entradas interessantes atualmente armazenadas no corpus(%s): O tamanho total de todas as entradas no corpuslen: %d/%d: O primeiro número é o comprimento (em bytes) da entrada atual que acionou a ação, o segundo número é o comprimento máximo atual permitido para entradat: O tempo total decorrido desde o início do fuzzer, em segundosmem: O uso atual de memória do processo PHPPor padrão, o fuzzer detecta três tipos de bugs:
Error lançadas pelo alvo de fuzzing. Enquanto exceções Exception são consideradas um resultado normal para
entrada malformada, exceções Error não capturadas sempre indicam um erro de programação. Elas são mais comumente produzidas pelo
próprio PHP, por exemplo, ao chamar um método em null.Error.pcntl_alarm() e um manipulador de sinal assíncrono que lança um Error no
timeout.Notavelmente, nenhum destes verifica se a saída do alvo está correta, eles apenas determinam que o alvo não se comporta de forma gravemente incorreta. Uma maneira de verificar a correção da saída é comparar duas implementações diferentes que deveriam produzir resultados idênticos:
$fuzzer->setTarget(function(string $input) use($parser1, $parser2) {
$result1 = $parser1->parse($input);
$result2 = $parser2->parse($input);
if ($result1 != $result2) {
throw new Error('Results do not match!');
}
});
Muitos dos detalhes técnicos deste fuzzer são baseados no libFuzzer do projeto LLVM. O seguinte descreve alguns dos detalhes de implementação.
Para funcionar eficientemente, o fuzzing requer feedback sobre os caminhos de código que foram executados durante o teste de uma entrada de fuzzing específica. Esse feedback de cobertura é coletado "instrumentando" o alvo de fuzzing. A biblioteca include-interceptor é usada para transformar o código de todos os arquivos incluídos em tempo real. A biblioteca PHP-Parser é usada para analisar o código e encontrar todos os lugares onde o código de instrumentação adicional precisa ser inserido.
Dentro de cada bloco básico, o seguinte código é inserido, onde BLOCK_INDEX é um inteiro único por bloco:
$___key = (\PhpFuzzer\FuzzingContext::$prevBlock << 28) | BLOCK_INDEX;
\PhpFuzzer\FuzzingContext::$edges[$___key] = (\PhpFuzzer\FuzzingContext::$edges[$___key] ?? 0) + 1;
\PhpFuzzer\FuzzingContext::$prevBlock = BLOCK_INDEX;
Isto assume que o índice do bloco tem no máximo 28 bits e conta o número de pares (prev_block, cur_block)
que são observados durante a execução. O código gerado é, infelizmente, bastante caro, devido à necessidade de lidar com
contagens de arestas não inicializadas e ao uso de propriedades estáticas. No futuro, seria possível criar uma extensão
PHP que pudesse coletar o feedback de cobertura de forma muito mais eficiente.
Em alguns casos, blocos básicos fazem parte de expressões, caso em que não podemos facilmente inserir código adicional. Nestes casos, em vez disso, inserimos uma chamada a um método que contém o código acima: