
Фаззер для PHP-библиотек, управляемый покрытием краёв, который обнаруживает ошибки через крахи, тайм-ауты и предупреждения. Поддерживает управление корпусом, минимизацию крахов и отчёты о покрытии кода.
Эта библиотека реализует фаззер для PHP, который можно использовать для поиска ошибок в библиотеках (особенно в библиотеках парсинга) путём подачи им «случайных» входных данных. Обратная связь от инструментария покрытия рёбер используется для управления выбором «случайных» входных данных, чтобы посещались новые участки кода.
Phar (рекомендуется): Вы можете загрузить phar-пакет этой библиотеки со страницы релизов. Использование phar рекомендуется, так как это позволяет избежать конфликтов зависимостей с библиотеками, использующими PHP-Parser.
Composer: composer global require nikic/php-fuzzer
Сначала необходимо определить целевую функцию. Вот пример цели для поиска ошибок в microsoft/tolerant-php-parser:
<?php // target.php
/** @var PhpFuzzer\Config $config */
require 'path/to/tolerant-php-parser/vendor/autoload.php';
// Required: The target accepts a single input string and runs it through the tested
// library. The target is allowed to throw normal Exceptions (which are ignored),
// but Error exceptions are considered as a found bug.
$parser = new Microsoft\PhpParser\Parser();
$config->setTarget(function(string $input) use($parser) {
$parser->parseSourceFile($input);
});
// Optional: Many targets don't exhibit bugs on large inputs that can't also be
// produced with small inputs. Limiting the length may improve performance.
$config->setMaxLen(1024);
// Optional: A dictionary can be used to provide useful fragments to the fuzzer,
// such as language keywords. This is particularly important if these
// cannot be easily discovered by the fuzzer, because they are handled
// by a non-instrumented PHP extension function such as token_get_all().
$config->addDictionary('example/php.dict');
Фаззер запускается на корпусе начальных «интересных» входных данных, который можно, например, заполнить на основе существующих модульных тестов. Если корпус не указан, вместо него будет создан временный каталог корпуса.
# Запуск без начального корпуса
php-fuzzer fuzz target.php
# Запуск с начальным корпусом (один входной файл на файл)
php-fuzzer fuzz target.php corpus/
Если фаззинг был прерван, его можно позже возобновить, указав тот же каталог корпуса.
После обнаружения сбоя он записывается в файл crash-HASH.txt. Он предоставляется в том виде,
в котором был первоначально найден, что может быть излишне сложным и содержать фрагменты, не относящиеся
к сбою. Поэтому, вероятно, вам сначала потребуется уменьшить сбойный вход:
php-fuzzer minimize-crash target.php crash-HASH.txt
Это создаст последовательность всё более коротких файлов minimized-HASH.txt. Если вы хотите
быстро проверить трассировку исключений, создаваемую сбойным входом, вы можете использовать команду run-single:
php-fuzzer run-single target.php minimized-HASH.txt
Наконец, можно сгенерировать HTML-отчёт о покрытии кода, который показывает, какие блоки кода в цели были задействованы при выполнении входных данных из заданного корпуса:
php-fuzzer report-coverage target.php corpus/ coverage_dir/
Дополнительные параметры конфигурации можно посмотреть с помощью php-fuzzer --help.
Во время работы фаззер непрерывно отображает свой статус одной строкой вывода. Эта строка состоит из следующих частей в указанном порядке:
NEW или REDUCED: Действие, вызвавшее эту строку статуса. NEW означает, что в корпус был добавлен новый вход, а REDUCED — что существующая запись корпуса была заменена более коротким входом.run: N: Общее количество итераций фаззинга (выполнений цели) с момента запуска фаззера(N/с): Текущая скорость выполнения, измеряемая в запусках в секундуft: N: Общее количество уникальных признаков, обнаруженных на данный момент(N/с): Среднее количество новых признаков, обнаруживаемых в секунду с момента запуска фаззераcorp: N: Количество интересных входов, хранящихся в корпусе на данный момент(%s): Общий размер всех входов в корпусеlen: %d/%d: Первое число — длина (в байтах) текущего входа, вызвавшего действие, второе число — текущая максимально допустимая длина входаt: Общее прошедшее время с момента запуска фаззера, в секундахmem: Текущее использование памяти процессом PHPПо умолчанию фаззер обнаруживает три вида ошибок:
Error, выбрасываемые целью фаззинга. В то время как исключения Exception считаются нормальным результатом для
некорректного ввода, неперехваченные исключения Error всегда указывают на ошибку программирования. Чаще всего они вызываются
самим PHP, например при вызове метода на null.Error.pcntl_alarm() и асинхронного обработчика сигнала, который выбрасывает Error при
тайм-ауте.Примечательно, что ни одна из этих проверок не проверяет корректность вывода цели, они лишь определяют, что цель не ведёт себя вопиюще неправильно. Один из способов проверить корректность вывода — сравнить две разные реализации, которые должны давать идентичные результаты:
$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!');
}
});
Многие технические детали этого фаззера основаны на libFuzzer из проекта LLVM. Ниже описаны некоторые детали реализации.
Для эффективной работы фаззинга требуется обратная связь о путях выполнения кода при тестировании конкретного входного фаззинг-входа. Эта информация о покрытии собирается путём «инструментирования» цели фаззинга. Библиотека include-interceptor используется для преобразования кода всех подключаемых файлов на лету. Библиотека PHP-Parser используется для разбора кода и поиска всех мест, куда нужно вставить дополнительный инструментальный код.
Внутри каждого базового блока вставляется следующий код, где BLOCK_INDEX — уникальный для каждого блока целое число:
$___key = (\PhpFuzzer\FuzzingContext::$prevBlock << 28) | BLOCK_INDEX;
\PhpFuzzer\FuzzingContext::$edges[$___key] = (\PhpFuzzer\FuzzingContext::$edges[$___key] ?? 0) + 1;
\PhpFuzzer\FuzzingContext::$prevBlock = BLOCK_INDEX;
Это предполагает, что индекс блока имеет размер не более 28 бит, и подсчитывает количество пар (prev_block, cur_block),
наблюдаемых во время выполнения. Сгенерированный код, к сожалению, довольно дорог из-за необходимости работать с
неинициализированными счётчиками рёбер и использования статических свойств. В будущем можно было бы создать PHP-расширение,
которое может собирать информацию о покрытии гораздо эффективнее.
В некоторых случаях базовые блоки являются частью выражений, и тогда мы не можем легко вставить дополнительный код. В этих случаях мы вместо этого вставляем вызов метода, который содержит указанный выше код:
if ($foo && $bar) { ... }
// становится
if ($foo && \PhpFuzzer\FuzzingContext::traceBlock(BLOCK_INDEX, $bar)) { ... }
В будущем было бы полезно также инструментировать сравнения, чтобы можно было автоматически определять
словарные записи из сравнений, таких как $foo == "SOME_STRING".