
Kantenabdeckungsgesteuerter Fuzzer für PHP-Bibliotheken, der Fehler durch Abstürze, Timeouts und Warnungen erkennt. Unterstützt Corpus-Verwaltung, Crash-Minimierung und Code-Abdeckungsberichte.
Diese Bibliothek implementiert einen Fuzzer für PHP, der verwendet werden kann, um Fehler in Bibliotheken (insbesondere Parsing-Bibliotheken) zu finden, indem ihnen "zufällige" Eingaben zugeführt werden. Feedback aus der Kantenabdeckungsinstrumentierung wird verwendet, um die Auswahl der "zufälligen" Eingaben so zu steuern, dass neue Codepfade besucht werden.
Phar (empfohlen): Sie können ein Phar-Paket dieser Bibliothek von der Releases-Seite herunterladen. Die Verwendung des Phars wird empfohlen, da dies Abhängigkeitskonflikte mit Bibliotheken vermeidet, die PHP-Parser verwenden.
Composer: composer global require nikic/php-fuzzer
Zunächst ist eine Definition der Zielfunktion erforderlich. Hier ist ein Beispielziel zum Auffinden von Fehlern in microsoft/tolerant-php-parser:
<?php // target.php
/** @var PhpFuzzer\Config $config */
require 'path/to/tolerant-php-parser/vendor/autoload.php';
// Erforderlich: Das Ziel akzeptiert einen einzelnen Eingabestring und führt ihn durch die getestete
// Bibliothek. Das Ziel darf normale Exceptions werfen (die ignoriert werden),
// aber Error-Exceptions werden als gefundener Fehler betrachtet.
$parser = new Microsoft\PhpParser\Parser();
$config->setTarget(function(string $input) use($parser) {
$parser->parseSourceFile($input);
});
// Optional: Viele Ziele zeigen keine Fehler bei großen Eingaben, die nicht auch mit
// kleinen Eingaben erzeugt werden können. Die Begrenzung der Länge kann die Leistung verbessern.
$config->setMaxLen(1024);
// Optional: Ein Wörterbuch kann verwendet werden, um dem Fuzzer nützliche Fragmente bereitzustellen,
// wie z.B. Sprachschlüsselwörter. Dies ist besonders wichtig, wenn diese
// nicht leicht vom Fuzzer entdeckt werden können, weil sie von einer
// nicht instrumentierten PHP-Erweiterungsfunktion wie token_get_all() verarbeitet werden.
$config->addDictionary('example/php.dict');
Der Fuzzer wird gegen ein Korpus anfänglicher "interessanter" Eingaben ausgeführt, die beispielsweise auf der Grundlage vorhandener Unit-Tests bereitgestellt werden können. Wenn kein Korpus angegeben wird, wird stattdessen ein temporäres Korpusverzeichnis erstellt.
# Ohne initiales Korpus ausführen
php-fuzzer fuzz target.php
# Mit initialem Korpus ausführen (eine Eingabe pro Datei)
php-fuzzer fuzz target.php corpus/
Wenn das Fuzzing unterbrochen wird, kann es später durch Angabe desselben Korpusverzeichnisses fortgesetzt werden.
Sobald ein Absturz gefunden wurde, wird er in eine Datei crash-HASH.txt geschrieben. Er wird in der Form
bereitgestellt, in der er ursprünglich gefunden wurde, die unnötig komplex sein und für den Absturz irrelevante
Fragmente enthalten kann. Daher sollten Sie die Absturzeingabe zuerst reduzieren:
php-fuzzer minimize-crash target.php crash-HASH.txt
Dies erzeugt eine Folge von zunehmend kleineren minimized-HASH.txt-Dateien. Wenn Sie schnell den
Ausnahmeverlauf für eine Absturzeingabe überprüfen möchten, können Sie den Befehl run-single verwenden:
php-fuzzer run-single target.php minimized-HASH.txt
Schließlich ist es möglich, einen HTML-Codeabdeckungsbericht zu erstellen, der zeigt, welche Codeblöcke im Ziel getroffen werden, wenn Eingaben aus einem bestimmten Korpus ausgeführt werden:
php-fuzzer report-coverage target.php corpus/ coverage_dir/
Zusätzliche Konfigurationsoptionen können mit php-fuzzer --help angezeigt werden.
Während der Fuzzer läuft, meldet er seinen Status kontinuierlich in einer einzigen Ausgabezeile. Diese Zeile umfasst die folgenden Teile in der angegebenen Reihenfolge:
NEW oder REDUCED: Die Aktion, die diese Statuszeile ausgelöst hat. NEW zeigt an, dass eine neue Eingabe zum Korpus hinzugefügt wurde, während REDUCED anzeigt, dass ein vorhandener Korpus-Eintrag durch eine kürzere Eingabe ersetzt wurde.run: N: Die Gesamtzahl der Fuzzing-Iterationen (Zielausführungen) seit dem Start des Fuzzers(N/s): Die aktuelle Ausführungsgeschwindigkeit, gemessen in Läufen pro Sekundeft: N: Die Gesamtzahl der bisher entdeckten einzigartigen Merkmale(N/s): Die durchschnittliche Anzahl neuer Merkmale, die seit dem Start des Fuzzers pro Sekunde entdeckt wurdencorp: N: Die Anzahl der derzeit im Korpus gespeicherten interessanten Eingaben(%s): Die Gesamtgröße aller Eingaben im Korpuslen: %d/%d: Die erste Zahl ist die Länge (in Bytes) der aktuellen Eingabe, die die Aktion ausgelöst hat, die zweite Zahl ist die derzeit maximal zulässige Eingabelänget: Die insgesamt vergangene Zeit seit dem Start des Fuzzers in Sekundenmem: Die derzeitige Speichernutzung des PHP-ProzessesDer Fuzzer erkennt standardmäßig drei Arten von Fehlern:
Error-Exceptions, die vom Fuzzing-Ziel ausgelöst werden. Während Exception-Exceptions als normales Ergebnis für
fehlerhafte Eingaben betrachtet werden, weisen nicht abgefangene Error-Exceptions immer auf einen Programmierfehler hin. Sie werden am häufigsten von PHP
selbst erzeugt, z.B. wenn eine Methode auf null aufgerufen wird.Error-Exceptions umwandelt.pcntl_alarm() und einem asynchronen Signalhandler realisiert, der bei
Timeout einen Error auslöst.Bemerkenswerterweise überprüft keiner dieser Punkte, ob die Ausgabe des Ziels korrekt ist, sie stellen nur fest, dass das Ziel sich nicht eklatant falsch verhält. Eine Möglichkeit, die Korrektheit der Ausgabe zu überprüfen, besteht darin, zwei verschiedene Implementierungen zu vergleichen, die identische Ergebnisse liefern sollen:
$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!');
}
});
Viele der technischen Details dieses Fuzzers basieren auf libFuzzer aus dem LLVM-Projekt. Im Folgenden werden einige Implementierungsdetails beschrieben.
Um effizient zu arbeiten, benötigt Fuzzing Rückmeldungen über die Codepfade, die während des Testens einer bestimmten Fuzzing-Eingabe ausgeführt wurden. Dieses Abdeckungs-Feedback wird durch "Instrumentierung" des Fuzzing-Ziels gesammelt. Die include-interceptor-Bibliothek wird verwendet, um den Code aller eingebundenen Dateien on-the-fly zu transformieren. Die PHP-Parser-Bibliothek wird verwendet, um den Code zu parsen und alle Stellen zu finden, an denen zusätzlicher Instrumentierungscode eingefügt werden muss.
In jedem Basisblock wird der folgende Code eingefügt, wobei BLOCK_INDEX eine eindeutige, pro-Block ganze Zahl ist:
$___key = (\PhpFuzzer\FuzzingContext::$prevBlock << 28) | BLOCK_INDEX;
\PhpFuzzer\FuzzingContext::$edges[$___key] = (\PhpFuzzer\FuzzingContext::$edges[$___key] ?? 0) + 1;
\PhpFuzzer\FuzzingContext::$prevBlock = BLOCK_INDEX;