
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 actualt: El tiempo total transcurrido desde que se inició el fuzzer, en segundosmem: El uso actual de memoria del proceso PHPEl 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.