Фаззинг Java с Jazzer
Ноябрь 02, 2023
В одной из предыдущих статей мы рассказывали о лучших практиках проведения фаззинг-тестирования для повышения безопасности программного продукта. Выбор подходящего фаззера зависит от ваших целей (есть простые фаззеры для быстрой проверки, есть мощные решения для приложений с повышенными требованиями к безопасности) и языка программирования.
Для Java существуют свои фаззеры, и в этой статье расскажем про один из них — Jazzer.
- Зачем нужны специальные фаззеры для Java
- Как устроен Jazzer
- Как тестировать код с помощью Jazzer
- Заключение
Зачем нужны специальные фаззеры для Java
Чтобы выполнять фаззинг Java-кода, необходимы специфичные инструменты и подход. Привычные средства для фаззинга нативного кода в данном случае не подойдут. Поскольку Java-код управляется виртуальной машиной джавы (JVM), некоторые метрики фаззинга удобнее фиксировать не из выполняемого Java-кода (непосредственно), а с использованием функций JVM.
Как устроен Jazzer
Для фаззинга с использованием Jazzer создается специальный класс, использующий API фаззера и вызывающий метод, который является целью фаззинга. Например:
package com.example;
import com.code_intelligence.jazzer.api.FuzzedDataProvider;
import org.apache.commons.text.StringSubstitutor;
public class CommonsTextFuzzer {
public static void fuzzerTestOneInput(FuzzedDataProvider data) {
try {
StringSubstitutor.createInterpolator().replace(data.consumeAsciiString(20));
} catch (java.lang.IllegalArgumentException
| java.lang.ArrayIndexOutOfBoundsException ignored) {
}
}
}
Метод fuzzerInitialize()
можно использовать, когда необходимо произвести единичную инициализацию перед началом фаззинга, например открыть сокет. Метод fuzzerTestOneInput()
— это место, где будет происходить фаззинг. Метод, вызывающий парсер данных, который мы хотим тестировать, нужно вызывать из метода fuzzerTestOneInput()
и использовать data.consume
для передачи входных данных определенного типа.
Для сбора информации о покрытии при фаззинге виртуальная машина Java, в которой исполняется код специального класса для фаззинга, запускается с Java-агентом, реализующим функции инструментации по аналогии с нативным фаззингом.
Для мутации входных данных Jazzer использует libFuzzer. Про это важно помнить, поскольку мы можем настраивать работу как Jazzer, так и libFuzzer: опции запуска применяются либо к Jazzer, либо к libFuzzer в зависимости от префикса.
Как тестировать код с помощью Jazzer
Подготовка кода
Чтобы тестирование было эффективным, необходимо провести тщательную подготовку. В первую очередь нужно точно определить код, который будет тестироваться, а также понять, какие методы и классы задействованы в работе парсера данных, который мы будем фаззить.
Затем нужно определить точку входа для фаззинга — метод, в который будут передаваться входные данные от фаззера. Именно этот метод будет вызван из метода fuzzerTestOneInput в специальном классе для фаззинга. Следует отметить, что этот метод, как и весь парсер данных, должен быть кодом без состояния (stateless code), то есть он должен не менять свое поведение при последовательных вызовах и не зависеть от событий, предшествовавших его выполнению. Почему это важно?
Фаззеру необходимо получать информацию о том, какой код был выполнен при обработке очередного набора данных. Этот показатель должен зависеть только от входных данных, чтобы фаззер мог корректно планировать дальнейшую мутацию входных данных. Если поведение кода будет зависеть от прочих факторов, фаззер не сможет отличить значимый эффект от фантомного при мутации данных.
Подготовка образцов данных
Второй важный этап — подготовка набора образцов входных данных. С этого набора начинается процесс фаззинг тестирования. Для повышения эффективности важно, чтобы этот набор данных давал хорошее покрытие на интересующем нас коде и не имел дубликатов (образцов входных данных, приводящих к выполнению одного и того же кода).
Также образцы данных, если это возможно, следует минимизировать. Например, если можно отрезать N байт от входного файла, и при этом содержание кода, задействованного для его обработки, не изменится, нужно использовать урезанный образец.
Запуск Jazzer
Произвести запуск фаззера можно с помощью следующей команды:
jazzer --cp=target.jar:other.jar --target_class=com.target.parser.class --reproducer_path=repro --trace=all:gep -rss_limit_mb=10240 -use_value_profile=1 -print_final_stats=1 -- in_out
Опции с префиксом «--»
предназначены для настройки Jazzer, опции с префиксом «–»
— для libFuzzer. В этой команде, помимо стандартных опций запуска, есть дополнительные. Рассмотрим их подробнее.
Полезные параметры фаззера
Параметр | Назначение |
---|---|
Стандартные параметры | |
--cp
|
Путь до jar файлов, содержащих необходимые нам классы |
--target_class
|
Класс, содержащий метод fuzzerTestOneInput()
|
--reproducer_path
|
Путь до каталога с автоматически сгенерированными Java-файлами для воспроизведения найденной ошибки |
--in_out
|
Путь до каталога, где содержится набор образцов входных данных. Туда же будут записываться файлы с входными данными, которые привели к приросту покрытия или обнаружению ошибки |
Дополнительные / специальные параметры | |
--trace
|
Выбор датчиков для отслеживания определенных действий, например, сравнения, деления целых чисел или операторов switch |
-rss_limit_mb (Опция libFuzzer)
|
Ограничение используемой памяти для обработки входных данных. Превышение этого лимита будет считаться ошибкой в тестируемой программе |
-use_value_profile (Опция libFuzzer)
|
Позволяет значительно улучшить прирост покрытия за счет дополнительных датчиков в операциях сравнения |
-print_final_stats (Опция libFuzzer)
|
Дополнительная статистика |
Кроме того, полезно указывать при запуске фаззера параметры --instrumentation_includes
и --instrumentation_excludes
, чтобы ограничить область видимости фаззера интересующим нас кодом. В противном случае, фаззер может начать работать на прирост покрытия в коде, который нас не интересует, и в итоге тестирование будет менее эффективным. При этом фаззер может «потеряться» в коде большого и не интересующего нас компонента.
Еще одна продвинутая практика — использование хуков на методах. Jazzer позволяет использовать пользовательские хуки, чтобы отслеживать частные ошибки или выходить из методов, где фаззер может «заблудиться». Такие хуки можно включить опцией --custom_hooks
.
У Jazzer также есть полезный режим Autofuzz, который позволяет фаззить произвольные методы без необходимости вручную создавать классы-цели фаззинга. Вместо этого Jazzer попытается сгенерировать подходящие и разнообразные входные данные для указанных методов, используя только общедоступные функции API, присутствующие в classpath.
Сбор покрытия после фаззинга
После фаззинг-тестирования полезно посмотреть итоговое покрытие, чтобы оценить «глубину» фаззинга. Для этого можно воспользоваться опцией фаззера --coverage_dump=<file>
. При использовании такой опции фаззер создаст файл с информацией о покрытии в формате JaCoCo, затем из этого файла можно сформировать, например, html-отчет о покрытии, используя jacococli. Еще один способ — создать обертку для запуска парсера с определенными входными данными и запустить парсинг всего, что сохранилось в каталоге in_out после фаззинга. Использовать при этом можно JCov, JaCoCo, или другой удобный инструмент для сбора покрытия.
Заключение
Jazzer — полезный инструмент, который можно использовать в комбинации с другими фаззерами для комплексного анализа безопасности приложения.
Axiom JDK применяет в том числе Jazzer в рамках фаззинг-тестирования своих продуктов. Помимо фаззинга, наши технологии перед каждым релизом проходят через регрессионные тесты, статический, динамический и структурный анализ и другие испытания в рамках контроля качества.
Хотите узнать больше о полезных инструментах для анализа безопасности ПО? Подписывайтесь на нашу рассылку, присоединяйтесь к нашему Telegram-каналу и узнавайте о новых статьях первыми!