
Fuzzer guidé par la couverture d'arêtes pour bibliothèques PHP qui détecte les bugs via des plantages, des dépassements de temps et des avertissements. Prend en charge la gestion du corpus, la minimisation des plantages et les rapports de couverture de code.
Cette bibliothèque implémente un fuzzer pour PHP, qui peut être utilisé pour trouver des bugs dans des bibliothèques (notamment les bibliothèques d'analyse syntaxique) en leur fournissant des entrées « aléatoires ». Les retours de l'instrumentation de couverture par arête sont utilisés pour guider le choix des entrées « aléatoires », de manière à visiter de nouveaux chemins de code.
Phar (recommandé) : Vous pouvez télécharger un package phar de cette bibliothèque depuis la page des versions. L'utilisation du phar est recommandée car elle évite les conflits de dépendances avec les bibliothèques utilisant PHP-Parser.
Composer : composer global require nikic/php-fuzzer
Tout d'abord, une définition de la fonction cible est nécessaire. Voici un exemple de cible pour trouver des bugs dans microsoft/tolerant-php-parser :
<?php // target.php
/** @var PhpFuzzer\Config $config */
require 'path/to/tolerant-php-parser/vendor/autoload.php';
// Required: The target accepts a single input string and runs it through the tested
// library. The target is allowed to throw normal Exceptions (which are ignored),
// but Error exceptions are considered as a found bug.
$parser = new Microsoft\PhpParser\Parser();
$config->setTarget(function(string $input) use($parser) {
$parser->parseSourceFile($input);
});
// Optional: Many targets don't exhibit bugs on large inputs that can't also be
// produced with small inputs. Limiting the length may improve performance.
$config->setMaxLen(1024);
// Optional: A dictionary can be used to provide useful fragments to the fuzzer,
// such as language keywords. This is particularly important if these
// cannot be easily discovered by the fuzzer, because they are handled
// by a non-instrumented PHP extension function such as token_get_all().
$config->addDictionary('example/php.dict');
Le fuzzer est exécuté sur un corpus d'entrées initiales « intéressantes », qui peuvent par exemple être générées à partir de tests unitaires existants. Si aucun corpus n'est spécifié, un répertoire de corpus temporaire sera créé à la place.
# Run without initial corpus
php-fuzzer fuzz target.php
# Run with initial corpus (one input per file)
php-fuzzer fuzz target.php corpus/
Si le fuzzing est interrompu, il peut être repris ultérieurement en spécifiant le même répertoire de corpus.
Une fois qu'un crash a été trouvé, il est écrit dans un fichier crash-HASH.txt. Il est fourni sous la forme où il a été trouvé initialement, ce qui peut être inutilement complexe et contenir des fragments non pertinents pour le crash. Ainsi, vous souhaiterez probablement réduire l'entrée qui provoque le crash en premier lieu :
php-fuzzer minimize-crash target.php crash-HASH.txt
Cela produira une séquence de fichiers minimized-HASH.txt de plus en plus petits. Si vous souhaitez vérifier rapidement la trace d'exception produite pour une entrée provoquant un crash, vous pouvez utiliser la commande run-single :
php-fuzzer run-single target.php minimized-HASH.txt
Enfin, il est possible de générer un rapport de couverture de code HTML, qui montre quels blocs de code dans la cible sont touchés lors de l'exécution d'entrées d'un corpus donné :
php-fuzzer report-coverage target.php corpus/ coverage_dir/
De plus, les options de configuration peuvent être affichées avec php-fuzzer --help.
Pendant son exécution, le fuzzer rapporte son état en continu sur une seule ligne de sortie. Cette ligne comprend les parties suivantes dans cet ordre :
NEW ou REDUCED : L'action qui a déclenché cette ligne de statut. NEW indique qu'une nouvelle entrée a été ajoutée au corpus tandis que REDUCED indique qu'une entrée existante du corpus a été remplacée par une entrée plus courte.run: N : Le nombre total d'itérations de fuzzing (exécutions de cible) effectuées depuis le démarrage du fuzzer.(N/s) : La vitesse d'exécution actuelle, mesurée en runs par seconde.ft: N : Le nombre total de caractéristiques uniques découvertes jusqu'à présent.(N/s) : Le nombre moyen de nouvelles caractéristiques découvertes par seconde depuis le démarrage du fuzzer.corp: N : Le nombre d'entrées intéressantes actuellement stockées dans le corpus.(%s) : La taille totale de toutes les entrées dans le corpus.len: %d/%d : Le premier nombre est la longueur (en octets) de l'entrée actuelle qui a déclenché l'action, le second nombre est la longueur maximale autorisée actuelle.t : Le temps total écoulé depuis le démarrage du fuzzer, en secondes.mem : L'utilisation mémoire actuelle du processus PHP.Le fuzzer détecte par défaut trois types de bugs :
Error levées par la cible de fuzzing. Alors que les exceptions Exception sont considérées comme un résultat normal pour une entrée malformée, les exceptions Error non capturées indiquent toujours une erreur de programmation. Elles sont le plus souvent produites par PHP lui-même, par exemple lors de l'appel d'une méthode sur null.Error.pcntl_alarm() et un gestionnaire de signal asynchrone qui lève une exception Error au délai dépassé.Notamment, aucun de ces contrôles ne vérifie si la sortie de la cible est correcte, ils déterminent seulement que la cible ne se comporte pas de manière gravement erronée. Une façon de vérifier la correction de la sortie est de comparer deux implémentations différentes censées produire des résultats identiques :
$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!');
}
});
La plupart des détails techniques de ce fuzzer sont basés sur libFuzzer du projet LLVM. Ce qui suit décrit certains détails d'implémentation.
Pour fonctionner efficacement, le fuzzing nécessite un retour sur les chemins de code qui ont été exécutés lors du test d'une entrée de fuzzing particulière. Ce retour de couverture est collecté en « instrumentant » la cible de fuzzing. La bibliothèque include-interceptor est utilisée pour transformer le code de tous les fichiers inclus à la volée. La bibliothèque PHP-Parser est utilisée pour analyser le code et trouver tous les endroits où du code d'instrumentation supplémentaire doit être inséré.
Dans chaque bloc de base, le code suivant est inséré, où BLOCK_INDEX est un entier unique par bloc :
$___key = (\PhpFuzzer\FuzzingContext::$prevBlock << 28) | BLOCK_INDEX;
\PhpFuzzer\FuzzingContext::$edges[$___key] = (\PhpFuzzer\FuzzingContext::$edges[$___key] ?? 0) + 1;
\PhpFuzzer\FuzzingContext::$prevBlock = BLOCK_INDEX;