
Фаззер для 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: Общее прошедшее время с момента запуска фаззера, в секундахПо умолчанию фаззер обнаруживает три вида ошибок:
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".
Входные фаззинг-данные считаются «интересными», если они содержат новые признаки, которые не наблюдались у других входов, уже находящихся в корпусе. Эта библиотека использует крупнозернистые счётчики попаданий рёбер в качестве признаков:
ft = (approx_hits << 56) | (prev_block << 28) | cur_block
Приблизительное количество попаданий сводит реальное количество попаданий к 8 категориям (на основе AFL):
0: 0 попаданий
1: 1 попадание
2: 2 попадания
3: 3 попадания
4: 4–7 попаданий
5: 8–15 попаданий
6: 16–127 попаданий
7: >=128 попаданий
Таким образом, каждый вход связывается с набором целых чисел, представляющих признаки. Кроме того, у него есть набор «уникальных признаков» — признаков, не наблюдавшихся ни в одном другом входе корпуса на момент тестирования данного входа.
Если вход имеет уникальные признаки, он добавляется в корпус (NEW). Если вход B был создан путём мутации входа A, но вход B короче и имеет все уникальные признаки входа A, то A заменяется на B в корпусе (REDUCE).
На каждой итерации из текущего корпуса выбирается случайный вход, а затем мутируется с помощью последовательности мутаторов. В настоящее время реализованы следующие мутаторы (взяты из libFuzzer):
EraseBytes: Удаление нескольких байтов.InsertByte: Вставка нового случайного байта.InsertRepeatedBytes: Вставка случайного байта, повторённого несколько раз.ChangeByte: Замена байта на случайный байт.ChangeBit: Переворот одного бита.ShuffleBytes: Перемешивание небольшой подстроки.ChangeASCIIInt: Изменение ASCII-целого числа путём увеличения/уменьшения/удвоения/уполовинивания.ChangeBinInt: Изменение двоичного целого числа путём добавления небольшой случайной величины.CopyPart: Копирование части строки в другую часть, либо с перезаписью, либо со вставкой.CrossOver: Скрещивание с другой записью корпуса с несколькими стратегиями.AddWordFromManualDictionary: Вставка или замена словом из словаря (если он есть).Мутация ограничена максимальной длиной. Хотя общая максимальная длина может быть задана целью (setMaxLength()), фаззер также выполняет автоматическое управление длиной (--len-control-factor). Максимальная длина
изначально устанавливается на очень низкое значение, а затем увеличивается на log(maxlen) каждый раз, когда не было совершено никаких действий (NEW или REDUCE) за последние len_control_factor * log(maxlen) запусков.
Чем выше коэффициент управления длиной, тем агрессивнее фаззер будет исследовать короткие входы, прежде чем разрешить более длинные. Это значительно уменьшает размер генерируемого корпуса, но замедляет начальное исследование.
mem: Текущее использование памяти процессом PHP