
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ängeDer 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;
Dies setzt voraus, dass der Blockindex höchstens 28 Bit groß ist und zählt die Anzahl der (prev_block, cur_block)-Paare,
die während der Ausführung beobachtet werden. Der erzeugte Code ist leider recht teuer, aufgrund der Notwendigkeit, mit
nicht initialisierten Kantenzählungen umzugehen, und der Verwendung von statischen Eigenschaften. In Zukunft wäre es möglich, eine PHP
Erweiterung zu erstellen, die das Abdeckungs-Feedback viel effizienter sammeln kann.
In einigen Fällen sind Basisblöcke Teil von Ausdrücken, in denen wir nicht einfach zusätzlichen Code einfügen können. In diesen Fällen fügen wir stattdessen einen Aufruf einer Methode ein, die den obigen Code enthält:
if ($foo && $bar) { ... }
// wird zu
if ($foo && \PhpFuzzer\FuzzingContext::traceBlock(BLOCK_INDEX, $bar)) { ... }
In Zukunft wäre es vorteilhaft, auch Vergleiche zu instrumentieren, sodass wir automatisch
Wörterbucheinträge aus Vergleichen wie $foo == "SOME_STRING" bestimmen können.
Fuzzing-Eingaben gelten als "interessant", wenn sie neue Merkmale enthalten, die mit anderen Eingaben, die bereits Teil des Korpus sind, nicht beobachtet wurden. Diese Bibliothek verwendet grobkörnige Kantentrefferzahlen als Merkmale:
ft = (approx_hits << 56) | (prev_block << 28) | cur_block
Die ungefähre Trefferzahl reduziert die tatsächliche Trefferzahl auf 8 Kategorien (basierend auf AFL):
0: 0 Treffer
1: 1 Treffer
2: 2 Treffer
3: 3 Treffer
4: 4-7 Treffer
5: 8-15 Treffer
6: 16-127 Treffer
7: >=128 Treffer
Somit ist jede Eingabe mit einer Menge von ganzen Zahlen verknüpft, die Merkmale darstellen. Zusätzlich hat sie eine Menge von "einzigartigen Merkmalen", die zum Zeitpunkt des Testens der Eingabe in keinem anderen Korpus-Eingabemerkmal gesehen wurden.
Wenn eine Eingabe einzigartige Merkmale aufweist, wird sie zum Korpus hinzugefügt (NEW). Wenn eine Eingabe B durch Mutation einer Eingabe A erstellt wurde, aber Eingabe B kürzer ist und alle einzigartigen Merkmale von Eingabe A aufweist, dann wird A im Korpus durch B ersetzt (REDUCE).
In jeder Iteration wird eine zufällige Eingabe aus dem aktuellen Korpus ausgewählt und dann mit einer Sequenz von Mutatoren mutiert. Die folgenden Mutatoren (aus libFuzzer) sind derzeit implementiert:
EraseBytes: Entfernt eine Anzahl von Bytes.InsertByte: Fügt ein neues zufälliges Byte ein.InsertRepeatedBytes: Fügt ein zufälliges Byte mehrmals wiederholt ein.ChangeByte: Ersetzt ein Byte durch ein zufälliges Byte.ChangeBit: Kippt ein einzelnes Bit.ShuffleBytes: Mischt einen kleinen Teilstring.ChangeASCIIInt: Ändert eine ASCII-Ganzzahl durch Inkrementieren/Dekrementieren/Verdoppeln/Halbieren.ChangeBinInt: Ändert eine binäre Ganzzahl durch Hinzufügen eines kleinen zufälligen Betrags.CopyPart: Kopiert einen Teil des Strings in einen anderen Teil, entweder durch Überschreiben oder Einfügen.CrossOver: Kreuzt mit einem anderen Korpus-Eintrag mit mehreren Strategien.AddWordFromManualDictionary: Fügt ein Wort aus dem Wörterbuch ein oder überschreibt damit (falls vorhanden).Die Mutation unterliegt einer maximalen Längenbeschränkung. Während eine maximale Gesamtlänge vom Ziel angegeben werden kann
(setMaxLength()), führt der Fuzzer auch eine automatische Längensteuerung durch (--len-control-factor). Die maximale Länge
wird anfänglich auf einen sehr niedrigen Wert gesetzt und dann um log(maxlen) erhöht, wenn in den letzten len_control_factor * log(maxlen) Läufen keine Aktion (NEW oder REDUCE) stattgefunden hat.
Je höher der Längensteuerungsfaktor, desto aggressiver erkundet der Fuzzer kurze Eingaben, bevor er längere Eingaben zulässt. Dies reduziert die Größe des erzeugten Korpus erheblich, verlangsamt jedoch die anfängliche Erkundung.
tmem: Die derzeitige Speichernutzung des PHP-Prozesses