
Fuzzer guiado por cobertura de bordes para bibliotecas PHP que detecta errores mediante fallos, tiempos de espera y advertencias. Admite gestión de corpus, minimización de fallos e informes de cobertura de código.
Esta biblioteca implementa un fuzzer para PHP, que se puede usar para encontrar errores en bibliotecas (particularmente bibliotecas de análisis sintáctico) alimentándolas con entradas "aleatorias". La retroalimentación de la instrumentación de cobertura de aristas se utiliza para guiar la selección de entradas "aleatorias", de modo que se visiten nuevas rutas de código.
Phar (recomendado): Puede descargar un paquete phar de esta biblioteca desde la página de lanzamientos. Se recomienda usar el phar porque evita conflictos de dependencias con bibliotecas que usan PHP-Parser.
Composer: composer global require nikic/php-fuzzer
Primero, es necesario definir la función objetivo. Aquí hay un ejemplo de objetivo para encontrar errores en microsoft/tolerant-php-parser:
<?php // target.php
/** @var PhpFuzzer\Config $config */
require 'path/to/tolerant-php-parser/vendor/autoload.php';
// Requerido: El objetivo acepta una única cadena de entrada y la ejecuta a través de la
// biblioteca probada. Se permite que el objetivo lance Excepciones normales (que se ignoran),
// pero las excepciones de Error se consideran un error encontrado.
$parser = new Microsoft\PhpParser\Parser();
$config->setTarget(function(string $input) use($parser) {
$parser->parseSourceFile($input);
});
// Opcional: Muchos objetivos no presentan errores en entradas grandes que no se puedan
// producir también con entradas pequeñas. Limitar la longitud puede mejorar el rendimiento.
$config->setMaxLen(1024);
// Opcional: Se puede usar un diccionario para proporcionar fragmentos útiles al fuzzer,
// como palabras clave del lenguaje. Esto es particularmente importante si estos
// no pueden ser descubiertos fácilmente por el fuzzer, porque son manejados
// por una función de extensión PHP no instrumentada como token_get_all().
$config->addDictionary('example/php.dict');
El fuzzer se ejecuta contra un corpus de entradas iniciales "interesantes", que pueden por ejemplo ser sembradas basándose en pruebas unitarias existentes. Si no se especifica un corpus, se creará un directorio de corpus temporal en su lugar.
# Ejecutar sin corpus inicial
php-fuzzer fuzz target.php
# Ejecutar con corpus inicial (una entrada por archivo)
php-fuzzer fuzz target.php corpus/
Si el fuzzing se interrumpe, se puede reanudar más tarde especificando el mismo directorio de corpus.
Una vez que se encuentra un fallo, se escribe en un archivo crash-HASH.txt. Se proporciona en la
forma en que se encontró originalmente, que puede ser innecesariamente compleja y contener fragmentos
no relevantes para el fallo. Por lo tanto, es probable que desee reducir primero la entrada del fallo:
php-fuzzer minimize-crash target.php crash-HASH.txt
Esto producirá una secuencia de archivos minimized-HASH.txt sucesivamente más pequeños. Si desea
verificar rápidamente el rastro de excepción producido para una entrada de fallo, puede usar el comando
run-single:
php-fuzzer run-single target.php minimized-HASH.txt
Finalmente, es posible generar un informe de cobertura de código HTML, que muestra qué bloques de código en el objetivo se ejecutan al procesar entradas de un corpus dado:
php-fuzzer report-coverage target.php corpus/ coverage_dir/
Además, las opciones de configuración se pueden mostrar con php-fuzzer --help.
Mientras el fuzzer está en ejecución, informa su estado continuamente usando una sola línea de salida. Esta línea comprende estas partes en el siguiente orden:
NEW o REDUCED: La acción que desencadenó esta línea de estado. NEW indica que se agregó una nueva entrada al corpus, mientras que REDUCED indica que una entrada existente del corpus fue reemplazada por una entrada más corta.run: N: El número total de iteraciones de fuzzing (ejecuciones del objetivo) realizadas desde que se inició el fuzzer(N/s): La velocidad de ejecución actual, medida en ejecuciones por segundoft: N: El número total de características únicas descubiertas hasta ahora(N/s): El número promedio de nuevas características descubiertas por segundo desde que se inició el fuzzercorp: N: El número de entradas interesantes almacenadas actualmente en el corpus(%s): El tamaño total de todas las entradas en el corpuslen: %d/%d: El primer número es la longitud (en bytes) de la entrada actual que desencadenó la acción, el segundo número es la longitud máxima permitida actualEl fuzzer por defecto detecta tres tipos de errores:
Error lanzadas por el objetivo de fuzzing. Mientras que las excepciones Exception se consideran un resultado normal para entrada mal formada, las excepciones Error no capturadas siempre indican un error de programación.Error.pcntl_alarm() y un manejador de señales asíncrono que lanza un Error al agotar el tiempo de espera.Notablemente, ninguno de estos verifica si la salida del objetivo es correcta, solo determinan que el objetivo no se comporta de manera gravemente incorrecta. Una forma de verificar la corrección de la salida es comparar dos implementaciones diferentes que se supone producen 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!');
}
});
Muchos de los detalles técnicos de este fuzzer se basan en libFuzzer del proyecto LLVM. A continuación se describen algunos de los detalles de implementación.
Para funcionar eficientemente, el fuzzing requiere retroalimentación sobre las rutas de código que se ejecutaron al probar una entrada de fuzzing particular. Esta retroalimentación de cobertura se recopila "instrumentando" el objetivo de fuzzing. La biblioteca include-interceptor se utiliza para transformar el código de todos los archivos incluidos sobre la marcha. La biblioteca PHP-Parser se utiliza para analizar el código y encontrar todos los lugares donde se necesita insertar código de instrumentación adicional.
Dentro de cada bloque básico, se inserta el siguiente código, donde BLOCK_INDEX es un entero único por bloque:
$___key = (\PhpFuzzer\FuzzingContext::$prevBlock << 28) | BLOCK_INDEX;
\PhpFuzzer\FuzzingContext::$edges[$___key] = (\PhpFuzzer\FuzzingContext::$edges[$___key] ?? 0) + 1;
\PhpFuzzer\FuzzingContext::$prevBlock = BLOCK_INDEX;
Esto asume que el índice del bloque es de a lo sumo 28 bits y cuenta el número de pares (prev_block, cur_block)
que se observan durante la ejecución. El código generado es desafortunadamente bastante costoso, debido a la
necesidad de manejar contadores de aristas no inicializados y al uso de propiedades estáticas. En el futuro, sería
posible crear una extensión de PHP que pueda recopilar la retroalimentación de cobertura de manera mucho más eficiente.
En algunos casos, los bloques básicos son parte de expresiones, en cuyo caso no podemos insertar fácilmente código adicional. En estos casos, en su lugar insertamos una llamada a un método que contiene el código anterior:
if ($foo && $bar) { ... }
// se convierte en
if ($foo && \PhpFuzzer\FuzzingContext::traceBlock(BLOCK_INDEX, $bar)) { ... }
En el futuro, sería beneficioso instrumentar también las comparaciones, para poder determinar automáticamente
entradas de diccionario a partir de comparaciones como $foo == "SOME_STRING".
Las entradas de fuzzing se consideran "interesantes" si contienen nuevas características que no se han observado con otras entradas que ya forman parte del corpus. Esta biblioteca utiliza recuentos de aciertos de aristas de grano grueso como características:
ft = (approx_hits << 56) | (prev_block << 28) | cur_block
El recuento de aciertos aproximado reduce el recuento real de aciertos a 8 categorías (basadas en AFL):
0: 0 aciertos
1: 1 acierto
2: 2 aciertos
3: 3 aciertos
4: 4-7 aciertos
5: 8-15 aciertos
6: 16-127 aciertos
7: >=128 aciertos
Por lo tanto, cada entrada se asocia con un conjunto de enteros que representan características. Además, tiene un conjunto de "características únicas", que son características no vistas en ninguna otra entrada del corpus en el momento en que se probó la entrada.
Si una entrada tiene características únicas, entonces se agrega al corpus (NEW). Si una entrada B se creó mutando una entrada A, pero la entrada B es más corta y tiene todas las características únicas de A, entonces A se reemplaza por B en el corpus (REDUCE).
En cada iteración, se elige una entrada aleatoria del corpus actual y luego se muta usando una secuencia de mutadores. Actualmente se implementan los siguientes mutadores (tomados de libFuzzer):
EraseBytes: Elimina una cantidad de bytes.InsertByte: Inserta un nuevo byte aleatorio.InsertRepeatedBytes: Inserta un byte aleatorio repetido varias veces.ChangeByte: Reemplaza un byte por un byte aleatorio.ChangeBit: Voltea un solo bit.ShuffleBytes: Baraja una pequeña subcadena.ChangeASCIIInt: Cambia un entero ASCII incrementando/decrementando/doblando/reduciendo a la mitad.ChangeBinInt: Cambia un entero binario agregando una pequeña cantidad aleatoria.CopyPart: Copia parte de la cadena en otra parte, ya sea sobrescribiendo o insertando.CrossOver: Cruza con otra entrada del corpus con múltiples estrategias.AddWordFromManualDictionary: Inserta o sobrescribe con una palabra del diccionario (si existe).La mutación está sujeta a una restricción de longitud máxima. Si bien el objetivo puede especificar una longitud
máxima general (setMaxLength()), el fuzzer también realiza un control automático de longitud (--len-control-factor).
La longitud máxima se establece inicialmente en un valor muy bajo y luego se incrementa en log(maxlen) cada vez
que no se ha realizado ninguna acción (NEW o REDUCE) durante las últimas len_control_factor * log(maxlen) ejecuciones.
Cuanto mayor sea el factor de control de longitud, más agresivamente explorará el fuzzer entradas cortas antes de permitir entradas más largas. Esto reduce significativamente el tamaño del corpus generado, pero hace que la exploración inicial sea más lenta.
t: El tiempo total transcurrido desde que se inició el fuzzer, en segundosmem: El uso actual de memoria del proceso PHP