
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 segundosPor 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:
if ($foo && $bar) { ... }
// torna-se
if ($foo && \PhpFuzzer\FuzzingContext::traceBlock(BLOCK_INDEX, $bar)) { ... }
No futuro, seria benéfico também instrumentar comparações, de modo que possamos determinar automaticamente
entradas de dicionário a partir de comparações como $foo == "SOME_STRING".
As entradas de fuzzing são consideradas "interessantes" se contiverem novas características que não foram observadas com outras entradas que já fazem parte do corpus. Esta biblioteca usa contagens de arestas com granularidade grossa como características:
ft = (approx_hits << 56) | (prev_block << 28) | cur_block
A contagem aproximada de hits reduz a contagem real de hits a 8 categorias (com base no AFL):
0: 0 hits
1: 1 hit
2: 2 hits
3: 3 hits
4: 4-7 hits
5: 8-15 hits
6: 16-127 hits
7: >=128 hits
Assim, cada entrada está associada a um conjunto de inteiros que representam características. Adicionalmente, possui um conjunto de "características únicas", que são características não vistas em nenhuma outra entrada do corpus no momento em que a entrada foi testada.
Se uma entrada possui características únicas, então ela é adicionada ao corpus (NEW). Se uma entrada B foi criada a partir da mutação de uma entrada A, mas a entrada B é mais curta e possui todas as características únicas da entrada A, então A é substituída por B no corpus (REDUCE).
Em cada iteração, uma entrada aleatória do corpus atual é escolhida e, em seguida, mutada usando uma sequência de mutadores. Os seguintes mutadores (retirados do libFuzzer) estão atualmente implementados:
EraseBytes: Remove um número de bytes.InsertByte: Insere um novo byte aleatório.InsertRepeatedBytes: Insere um byte aleatório repetido várias vezes.ChangeByte: Substitui um byte por um byte aleatório.ChangeBit: Inverte um único bit.ShuffleBytes: Embaralha uma pequena substring.ChangeASCIIInt: Altera um inteiro ASCII incrementando/decrementando/dobrando/dividindo pela metade.ChangeBinInt: Altera um inteiro binário adicionando uma pequena quantidade aleatória.CopyPart: Copia parte da string para outra parte, seja sobrescrevendo ou inserindo.CrossOver: Cruza com outra entrada do corpus com múltiplas estratégias.AddWordFromManualDictionary: Insere ou sobrescreve com uma palavra do dicionário (se houver).A mutação está sujeita a uma restrição de comprimento máximo. Enquanto um comprimento máximo geral pode ser especificado pelo alvo
(setMaxLength()), o fuzzer também realiza controle automático de comprimento (--len-control-factor). O comprimento máximo
é inicialmente definido para um valor muito baixo e depois aumentado por log(maxlen) sempre que nenhuma ação (NEW ou REDUCE) tiver sido
realizada nas últimas len_control_factor * log(maxlen) execuções.
Quanto maior o fator de controle de comprimento, mais agressivamente o fuzzer explorará entradas curtas antes de permitir entradas mais longas. Isso reduz significativamente o tamanho do corpus gerado, mas torna a exploração inicial mais lenta.
mem: O uso atual de memória do processo PHP