
Fuzzer guidato dalla copertura degli edge per librerie PHP che rileva bug tramite crash, timeout e avvisi. Supporta la gestione del corpus, la minimizzazione dei crash e i report di copertura del codice.
Questa libreria implementa un fuzzer per PHP, che può essere usato per trovare bug in librerie (in particolare librerie di parsing) fornendo loro input "casuali". Il feedback dalla strumentazione della copertura dei bordi viene utilizzato per guidare la scelta degli input "casuali", in modo tale che vengano visitati nuovi percorsi di codice.
Phar (consigliato): Puoi scaricare un pacchetto phar di questa libreria dalla pagina delle release. L'uso del phar è consigliato, perché evita conflitti di dipendenza con librerie che usano PHP-Parser.
Composer: composer global require nikic/php-fuzzer
Prima di tutto, è necessaria una definizione della funzione target. Ecco un esempio di target per trovare bug in microsoft/tolerant-php-parser:
<?php // target.php
/** @var PhpFuzzer\Config $config */
require 'path/to/tolerant-php-parser/vendor/autoload.php';
// Obbligatorio: Il target accetta una singola stringa di input e la passa alla libreria testata.
// Al target è consentito lanciare normali Exception (che vengono ignorate),
// ma gli Error sono considerati un bug trovato.
$parser = new Microsoft\PhpParser\Parser();
$config->setTarget(function(string $input) use($parser) {
$parser->parseSourceFile($input);
});
// Opzionale: Molti target non mostrano bug su input grandi che non possono anche essere
// prodotti con input piccoli. Limitare la lunghezza può migliorare le prestazioni.
$config->setMaxLen(1024);
// Opzionale: Un dizionario può essere usato per fornire frammenti utili al fuzzer,
// come parole chiave del linguaggio. Questo è particolarmente importante se questi
// non possono essere facilmente scoperti dal fuzzer, perché sono gestiti
// da una funzione di estensione PHP non strumentata come token_get_all().
$config->addDictionary('example/php.dict');
Il fuzzer viene eseguito su un corpus di input iniziali "interessanti", che possono ad esempio essere seminati basandosi su test unitari esistenti. Se non viene specificato alcun corpus, verrà creata una directory temporanea per il corpus.
# Esecuzione senza corpus iniziale
php-fuzzer fuzz target.php
# Esecuzione con corpus iniziale (un input per file)
php-fuzzer fuzz target.php corpus/
Se il fuzzing viene interrotto, può essere ripreso in seguito specificando la stessa directory del corpus.
Una volta trovato un crash, viene scritto in un file crash-HASH.txt. Viene fornito nella forma
in cui è stato originariamente trovato, che potrebbe essere inutilmente complessa e contenere frammenti non
rilevanti per il crash. Pertanto, è probabile che tu voglia prima ridurre l'input che causa il crash:
php-fuzzer minimize-crash target.php crash-HASH.txt
Questo produrrà una sequenza di file minimized-HASH.txt successivamente più piccoli. Se vuoi
controllare rapidamente la traccia dell'eccezione prodotta per un input crash, puoi usare il comando run-single:
php-fuzzer run-single target.php minimized-HASH.txt
Infine, è possibile generare un report di copertura del codice HTML, che mostra quali blocchi di codice nel target vengono colpiti durante l'esecuzione di input da un determinato corpus:
php-fuzzer report-coverage target.php corpus/ coverage_dir/
Ulteriori opzioni di configurazione possono essere visualizzate con php-fuzzer --help.
Mentre il fuzzer è in esecuzione, riporta il suo stato continuamente utilizzando una singola riga di output. Questa riga comprende queste parti nel seguente ordine:
NEW o REDUCED: L'azione che ha attivato questa riga di stato. NEW indica che un nuovo input è stato aggiunto al corpus, mentre REDUCED indica che una voce esistente del corpus è stata sostituita con un input più corto.run: N: Il numero totale di iterazioni di fuzzing (esecuzioni del target) eseguite dall'avvio del fuzzer(N/s): La velocità di esecuzione corrente, misurata in esecuzioni al secondoft: N: Il numero totale di caratteristiche uniche scoperte finora(N/s): Il numero medio di nuove caratteristiche scoperte al secondo dall'avvio del fuzzercorp: N: Il numero di input interessanti attualmente memorizzati nel corpus(%s): La dimensione totale di tutti gli input nel corpuslen: %d/%d: Il primo numero è la lunghezza (in byte) dell'input corrente che ha attivato l'azione, il secondo numero è la lunghezza massima consentita corrente per l'inputt: Il tempo totale trascorso dall'avvio del fuzzer, in secondiIl fuzzer rileva tre tipi di bug per impostazione predefinita:
Error lanciate dal target del fuzzing. Mentre le eccezioni Exception sono considerate un risultato normale per
input malformati, le eccezioni Error non catturate indicano sempre un errore di programmazione. Sono più comunemente prodotte da
PHP stesso, ad esempio quando si chiama un metodo su null.Error.pcntl_alarm() e un gestore di segnale asincrono che lancia un Error al
timeout.Nota che nessuno di questi controlla se l'output del target è corretto, determinano solo che il target non si comporta in modo eccessivamente scorretto. Un modo per verificare la correttezza dell'output è confrontare due diverse implementazioni che dovrebbero produrre risultati identici:
$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!');
}
});
Molti dei dettagli tecnici di questo fuzzer sono basati su libFuzzer del progetto LLVM. Quanto segue descrive alcuni dettagli implementativi.
Per funzionare in modo efficiente, il fuzzing richiede un feedback riguardo ai percorsi di codice eseguiti durante il test di un particolare input di fuzzing. Questo feedback di copertura viene raccolto "strumentando" il target del fuzzing. La libreria include-interceptor viene utilizzata per trasformare il codice di tutti i file inclusi al volo. La libreria PHP-Parser viene utilizzata per analizzare il codice e trovare tutti i punti in cui è necessario inserire codice di strumentazione aggiuntivo.
All'interno di ogni blocco di base, viene inserito il seguente codice, dove BLOCK_INDEX è un intero univoco per blocco:
$___key = (\PhpFuzzer\FuzzingContext::$prevBlock << 28) | BLOCK_INDEX;
\PhpFuzzer\FuzzingContext::$edges[$___key] = (\PhpFuzzer\FuzzingContext::$edges[$___key] ?? 0) + 1;
\PhpFuzzer\FuzzingContext::$prevBlock = BLOCK_INDEX;
Questo presuppone che l'indice del blocco sia al massimo di 28 bit e conta il numero di coppie (prev_block, cur_block)
osservate durante l'esecuzione. Il codice generato è purtroppo piuttosto costoso, a causa della necessità di gestire
conteggi di archi non inizializzati e dell'uso di proprietà statiche. In futuro, sarebbe possibile creare un'estensione
PHP in grado di raccogliere il feedback di copertura in modo molto più efficiente.
In alcuni casi, i blocchi di base fanno parte di espressioni, nel qual caso non possiamo facilmente inserire codice aggiuntivo. In questi casi inseriamo invece una chiamata a un metodo che contiene il codice sopra:
if ($foo && $bar) { ... }
// diventa
if ($foo && \PhpFuzzer\FuzzingContext::traceBlock(BLOCK_INDEX, $bar)) { ... }
In futuro, sarebbe vantaggioso strumentare anche i confronti, in modo da poter determinare automaticamente
le voci del dizionario da confronti come $foo == "SOME_STRING".
Gli input di fuzzing sono considerati "interessanti" se contengono nuove caratteristiche non osservate con altri input già presenti nel corpus. Questa libreria utilizza conteggi di hit di archi a grana grossa come caratteristiche:
ft = (approx_hits << 56) | (prev_block << 28) | cur_block
Il conteggio approssimativo degli hit riduce il conteggio effettivo a 8 categorie (basate su AFL):
0: 0 hit
1: 1 hit
2: 2 hit
3: 3 hit
4: 4-7 hit
5: 8-15 hit
6: 16-127 hit
7: >=128 hit
Pertanto, ogni input è associato a un insieme di interi che rappresentano le caratteristiche. Inoltre, ha un insieme di "caratteristiche uniche", che sono caratteristiche non viste in nessun altro input del corpus al momento del test dell'input.
Se un input ha caratteristiche uniche, viene aggiunto al corpus (NEW). Se un input B è stato creato mutando un input A, ma l'input B è più corto e ha tutte le caratteristiche uniche dell'input A, allora A viene sostituito da B nel corpus (REDUCE).
Ad ogni iterazione, viene scelto un input casuale dal corpus corrente e poi mutato utilizzando una sequenza di mutatori. I seguenti mutatori (presi da libFuzzer) sono attualmente implementati:
EraseBytes: Rimuove un numero di byte.InsertByte: Inserisce un nuovo byte casuale.InsertRepeatedBytes: Inserisce un byte casuale ripetuto più volte.ChangeByte: Sostituisce un byte con un byte casuale.ChangeBit: Capovolge un singolo bit.ShuffleBytes: Mescola una piccola sottostringa.ChangeASCIIInt: Cambia un intero ASCII incrementando/decrementando/raddoppiando/dimezzando.ChangeBinInt: Cambia un intero binario aggiungendo una piccola quantità casuale.CopyPart: Copia parte della stringa in un'altra parte, sovrascrivendo o inserendo.CrossOver: Incrocia con un'altra voce del corpus con più strategie.AddWordFromManualDictionary: Inserisce o sovrascrive con una parola dal dizionario (se presente).La mutazione è soggetta a un vincolo di lunghezza massima. Mentre una lunghezza massima complessiva può essere specificata dal target
(setMaxLength()), il fuzzer esegue anche un controllo automatico della lunghezza (--len-control-factor). La lunghezza massima
è inizialmente impostata a un valore molto basso e poi aumentata di log(maxlen) ogni volta che non è stata eseguita alcuna azione (NEW o REDUCE)
per gli ultimi len_control_factor * log(maxlen) esecuzioni.
Maggiore è il fattore di controllo della lunghezza, più aggressivamente il fuzzer esplorerà input brevi prima di consentire input più lunghi. Ciò riduce significativamente la dimensione del corpus generato, ma rende l'esplorazione iniziale più lenta.
mem: L'utilizzo corrente di memoria del processo PHP