Skip to content

Directed fuzzing for goblin project with sydr‐fuzz (LibAFL‐DiFuzz backend) (rus)

headshog edited this page Sep 25, 2025 · 11 revisions

Введение

В этой статье рассматривается методика направленного фаззинга Rust-кода с использованием интерфейса Sydr-Fuzz, построенного на базе фаззера LibAFL-DiFuzz. Инструмент Sydr-Fuzz обеспечивает удобный способ запуска гибридного фаззинга, комбинируя динамическое символьное выполнение на основе инструмента Sydr с современными фаззерами. Кроме того, Sydr-Fuzz поддерживает минимизацию корпуса, сбор покрытия в удобно читаемых форматах, обнаружение ошибок через проверку предикатов безопасности и анализ сбоев с помощью Casr.

Следующим шагом в развитии Sydr-Fuzz стала поддержка направленного фаззинга с использованием инструмента LibAFL-DiFuzz. Этот подход позволяет сосредоточить анализ на выбранных точках кода, что делает его особенно полезным для детального исследования отдельных компонентов. LibAFL-DiFuzz, построеный на модульной архитектуре LibAFL, предоставляет возможность указывать одну или несколько целевых точек, определяющих направление анализа. Для корректной работы требуется этап статической предобработки программы, на котором строятся специальные метрики, используемые в процессе фаззинга. В ходе анализа LibAFL-DiFuzz отслеживает текущее состояние выполнения программы и управляет распределением энергии входов, увеличивая шансы достижения целевых точек.

Для демонстрации возможностей гибридного направленного фаззинга LibAFL-DiFuzz в рамках инструмента Sydr-Fuzz будем использовать библиотеку goblin, написанную на языке Rust.

Подготовка фаззинг-цели

Для подготовки фаззинг цели необходимо подготовить целевые точки, которые требуется достичь и проанализировать, а также одна или несколько оберток, позволяющих достичь этих точек с помощью фаззинга. Для демонстрации возьмем функции goblin::Object::parse и goblin::elf::Elf::parse и обёртки, одну используемую в этом гайде и для elf::Elf::parse.

Далее необходимо подготовить специальный файл сборки цели Makefile.toml. Makefile.toml можно сгенерировать по шаблону при помощи скрипта gen_target.py, указав значения аргументов, специфичных для фаззинг цели. Чтобы посмотреть необходимые аргументы, можно выполнить следующую команду:

$ python3 gen_target.py -h

В полученном файле определены цели сборки для символьного исполнения Sydr (debug), для фаззинга LibAFL-DiFuzz (target), а также дополнительные цели сборки для анализа покрытия (coverage) и анализа аварийных завершений (casr). Конкретное описание файла Makefile.toml будет в секции "Сборка для LibAFL-DiFuzz". А сейчас про целевые точки и сборку фаззинг цели.

Целевые точки указываются в файле config.toml в следующем формате:

[[target]]
file = "/goblin/src/archive/mod.rs"
line = 130

[[target]]
file = "/goblin/src/archive/mod.rs"
line = 164

[[target]]
file = "/goblin/src/archive/mod.rs"
line = 342

[[target]]
file = "/goblin/src/pe/debug.rs"
line = 172

Фаззинг цель для удобства мы собираем в Docker-образ, в котором будет установлено необходимое окружение и будут собраны фаззинг цели. Рассмотрим файл Dockerfile_libafl:

ARG BASE_IMAGE="sydr/ubuntu22.04-sydr-fuzz"
FROM $BASE_IMAGE

ARG SYDR_ARCHIVE="./sydr.zip"

WORKDIR /

# Only for goblin.
RUN rustup override set nightly-2025-05-01

# Clone target from GitHub.
RUN git clone https://github.com/m4b/goblin.git

WORKDIR /goblin

# Checkout specified commit. It could be updated later.
RUN git checkout 59ec2f3c57c53aa828b6a4cb4730d1efe3e43a05

COPY build-config/Cargo.toml fuzz/

COPY sydr_*.rs fuzz/fuzz_targets/

# Copy LibAFL-DiFuzz target template.
COPY directed_target /directed_target

WORKDIR /directed_target

# Build goblin for LibAFL-DiFuzz.
ADD ${SYDR_ARCHIVE} ./
RUN unzip -o ${SYDR_ARCHIVE} && rm ${SYDR_ARCHIVE}
RUN OUT_DIR=/ cargo make all

WORKDIR /

# Create corpus.
RUN git clone https://github.com/JonathanSalwan/binary-samples /corpus && \
rm -rf /corpus/anti-disassembler && rm /corpus/MIT_LICENSE && rm /corpus/README.md

# Set SYDR_LLVM_COV env.
ENV SYDR_LLVM_COV=/usr/bin/llvm-cov-20

Сначала клонируется проект Goblin с GitHub, затем фиксируем определенную версию для анализа. Затем копируем в Docker необходимые файлы. Далее обратим внимание, что необходим специальный архив ${SYDR_ARCHIVE}. Это архив, содержащий бинарные файлы и библиотеки, необходимые для работы LibAFL-DiFuzz, путь до него установлен по умолчанию как ./sydr.zip, но путь до него можно указать во время сборки докера. Команда OUT_DIR=/ cargo make all выполняет сборку целей difuzz, target, debug, coverage и casr из Makefile.toml. Теперь соберём образ при помощи команды:

$ sudo docker build --build-arg SYDR_ARCHIVE="difuzz.zip" -t oss-sydr-fuzz-libafl-goblin -f ./Dockerfile_libafl .

Сборка для LibAFL-DiFuzz

Рассмотрим содержимое файла Makefile.toml. В нем задаются цели сборки, а также зависимости между ними, а для каждой цели можно задать скрипт, выполняющий ту или иную семантику. В нашем случае первым делом выполняется статический анализ программы:

[tasks.difuzz_unix]
script_runner = "@shell"
script = '''
python3 ${DIFUZZ_DIR_ABS}/insert_forkserver.py -a insert -l rust -f ${EXAMPLE_DIR}/fuzz_targets/sydr_parse.rs
python3 ${DIFUZZ_DIR_ABS}/insert_forkserver.py -a insert -l rust -f ${EXAMPLE_DIR}/fuzz_targets/sydr_parse_elf.rs
python3 ${DIFUZZ_DIR_ABS}/insert_forkserver.py -a comment -l rust -f ${EXAMPLE_DIR}/fuzz_targets/sydr_parse.rs
python3 ${DIFUZZ_DIR_ABS}/insert_forkserver.py -a comment -l rust -f ${EXAMPLE_DIR}/fuzz_targets/sydr_parse_elf.rs

cd ${EXAMPLE_DIR}
export CARGO_TARGET_DIR="${EXAMPLE_DIR}/target"
cargo clean
export RUSTC=${RUST_DIR_ABS}/libafl_rustc
RUSTFLAGS="--emit=llvm-bc -C debuginfo=2 -C debug-assertions=yes -C opt-level=0 -C target-cpu=native" cargo build --bin sydr_parse
llvm-link-18 -o=${EXAMPLE_DIR}/target/debug/sydr_parse.bc ${EXAMPLE_DIR}/target/debug/deps/*.bc
${DIFUZZ_DIR_ABS}/difuzz-rust -r sydr_parse::main -c ${PROJECT_DIR}/config.toml -b ${EXAMPLE_DIR}/target/debug/sydr_parse.bc -e ${OUT_DIR_ABS}/ets_parse.toml ${DIFUZZ_ARGS}

cargo clean
RUSTFLAGS="--emit=llvm-bc -C debuginfo=2 -C debug-assertions=yes -C opt-level=0 -C target-cpu=native" cargo build --bin sydr_parse_elf
llvm-link-18 -o=${EXAMPLE_DIR}/target/debug/sydr_parse_elf.bc ${EXAMPLE_DIR}/target/debug/deps/*.bc
${DIFUZZ_DIR_ABS}/difuzz-rust -r sydr_parse_elf::main -c ${PROJECT_DIR}/config.toml -b ${EXAMPLE_DIR}/target/debug/sydr_parse_elf.bc -e ${OUT_DIR_ABS}/ets_parse_elf.toml ${DIFUZZ_ARGS}
'''

Изначально скрипт insert_forkserver.py вставляет вызов функции инициализации фаззинга в код функции main, а далее комментирует его, так как пока что этот вызов нам не нужен на этапе статического анализа. Потом собираем бинарный файл фаззинг цели через Cargo со специальными аргументами в RUSTFLAGS. Одна из опций - --emit=llvm-bc сохраняет LLVM-IR модули программы. Затем с помощью инструмента llvm-link-18 все эти модули мы объединяем в один общий модуль, необходимый для статического анализа. Далее инструменом DiFuzz-Rust выполняется статический анализ (строкой ниже llvm-link-18).

Запустим цель difuzz и увидим следующий вывод от инструмента DiFuzz-Rust:

Screenshot 2025-09-23 at 13 07 29 Screenshot 2025-09-23 at 13 07 58

После анализа на выходе будут получены файлы ets_parse.toml и ets_parse_elf.toml, а также граф вызовов и ГПУ целевых функций программы вместе с их деревьями доминаторов в формате .DOT. Файлы ets_parse*.toml_ используются при повторной сборке программы инструментирующим компилятором libafl_rustc.

Теперь выполним сборку фаззинг цели с инструментацией, необходимой для фаззера LibAFL-DiFuzz:

[tasks.target_unix]
script_runner = "@shell"
script = '''
cd ${EXAMPLE_DIR}
export RUSTC=${RUST_DIR_ABS}/libafl_rustc
export LIBAFL_PROJECT_DIR="goblin"
${ETS_SHARED_MANAGER} -a remove -n parse
${ETS_SHARED_MANAGER} -a remove -n parse_elf
${ETS_SHARED_MANAGER} -a create -n parse
${ETS_SHARED_MANAGER} -a create -n parse_elf
${ETS_SHARED_MANAGER} -a parse -n parse -i ${OUT_DIR_ABS}/ets_parse.toml
${ETS_SHARED_MANAGER} -a parse -n parse_elf -i ${OUT_DIR_ABS}/ets_parse_elf.toml
python3 ${DIFUZZ_DIR_ABS}/insert_forkserver.py -a uncomment -l rust -f ${EXAMPLE_DIR}/fuzz_targets/sydr_parse.rs
python3 ${DIFUZZ_DIR_ABS}/insert_forkserver.py -a uncomment -l rust -f ${EXAMPLE_DIR}/fuzz_targets/sydr_parse_elf.rs

export CARGO_TARGET_DIR="${EXAMPLE_DIR}/target"
export LIBAFL_SHARED_NAME="parse"
cargo clean
cargo build --bin sydr_parse
mv ${EXAMPLE_DIR}/target/debug/sydr_parse ${OUT_DIR_ABS}/parse_libafl_target
export LIBAFL_SHARED_NAME="parse_elf"
cargo clean
cargo build --bin sydr_parse_elf
mv ${EXAMPLE_DIR}/target/debug/sydr_parse_elf ${OUT_DIR_ABS}/parse_elf_libafl_target

${ETS_SHARED_MANAGER} -a dump -n parse -o ${OUT_DIR_ABS}/ets_parse.toml
${ETS_SHARED_MANAGER} -a dump -n parse_elf -o ${OUT_DIR_ABS}/ets_parse_elf.toml
${ETS_SHARED_MANAGER} -a remove -n parse
${ETS_SHARED_MANAGER} -a remove -n parse_elf

python3 ${DIFUZZ_DIR_ABS}/insert_forkserver.py -a remove -l rust -f ${EXAMPLE_DIR}/fuzz_targets/sydr_parse.rs
python3 ${DIFUZZ_DIR_ABS}/insert_forkserver.py -a remove -l rust -f ${EXAMPLE_DIR}/fuzz_targets/sydr_parse_elf.rs
'''
dependencies = ["difuzz"]

Здесь мы указываем путь до нашего компилятора libafl_rustc, выставляем значение переменной LIBAFL_PROJECT_DIR, необходимой для инструментации, куда нужно указать имя директории/поддиректории проекта, которые будут проинструментированы. Специальный менеджер ETS_SHARED_MANAGER обеспечивает параллельную компиляцию модулей, задействуя разделяемую память. С помощью него мы обрабатываем файл ets.toml, заполняя необходимую информацию, а также создаем окружение для компиляции цели. Скрипт insert_forkserver.py раскомментировывает вызов функции инициализации фаззинга. Теперь можно приступать к сборке цели! Указываем имя окружения в переменную LIBAFL_SHARED_NAME и просто собираем цель через Cargo, НО только указав аргумент --bin TARGET_BIN с именем бинарного файла цели. В конце удаляется окружение компиляции и удаляется вызов инициализации фаззера.

Выставив переменную окружения LIBAFL_DEBUG_PASS=2, скомпилируем нашу фаззинг цель. Увидим следующий вывод, означающий, что инструментация была выполнена на примере одной из функций.

Screenshot 2025-09-23 at 13 13 59

Цели debug, coverage, casr делаются проще, их можно посмотреть в том же Makefile.toml.

Фаззинг

Чтобы запустить гибридный направленный фаззинг через Sydr-Fuzz для двух оберток необходимо составить два конфигурационных файла parse-libafl.toml и parse_elf-libafl.toml. Файлы должены содержать:

  • параметр exit-on-time, задающий время до завершения фаззинга при отсутствии нового покрытия (опционально),
  • таблицу [sydr] с указанием аргументов Sydr (args, jobs) и строки запуска целевой программы (target),
  • таблицу [difuzz] с указанием пути до фаззера LibAFL-DiFuzz (path), его аргументов (args), строки запуска целевой программы (target) и пути до бинарного файла программы (если необходимо выполнить анализ аварийных завершений), собранного для анализа инструментом Casr (casr_bin),
  • а также таблицу [cov] с указанием строки запуска целевой программы (target) для сбора покрытия (опционально).

Например, для parse конфигурационный файл будет выглядеть так:

exit-on-time = 7200

[sydr]
args = "--wait-jobs -s 90 -j2"
target = "/sydr_parse @@"
jobs = 2

[difuzz]
path = "/directed_target/sydr/difuzz/libafl_difuzz"
target = "/parse_libafl_target @@"
args = "-j4 --sync-limit 200 --sync-jobs 2 --panic-analysis rust -l64 -i /corpus -e /ets_parse.toml"
casr_bin = "/casr_parse"

[cov]
target = "/cov_parse @@"

В данном конфиге важно обратить внимание на опцию фаззера --panic-analysis rust. Эта опция включает обработку ошибок panic! в Rust, то есть при включенной опции инпуты, приводящие к панике, будут считаться как crash.

Запускаем гибридный направленный фаззинг через Sydr-Fuzz, указав путь до конфигурационного файла parse-libafl.toml:

$ sydr-fuzz -c parse-libafl.toml run

Фаззинг начался, процессы LibAFL-DiFuzz запускаются и начинают отправлять статистику по текущему состоянию:

Screenshot 2025-09-23 at 12 16 51

Примерно сразу начинает появляться информация о доостигнутых целевых точках, указанных в файле config.toml:

Screenshot 2025-09-23 at 12 17 38

В информации о достижении точки указывается время её достижения. Если целевые точки были достигнуты по несколько раз - это означает, что фаззер смог сгенерировать несколько инпутов, достигающих целевую точку.

Иногда в логах появляются следующие записи:

Screenshot 2025-09-23 at 12 18 51

Они означают, что LibAFL-DiFuzz смог (или не смог) импортировать N новых файлов, сгенерированных Sydr, в свой корпус.

Наконец, по завешении анализа выводится информация о минимальном времени достижения каждой точки, а также информация о времени поиска ошибок и по сортировке крешей:

Screenshot 2025-09-23 at 12 19 40

Анализ результатов

Результатом направленного фаззинга становится набор objective-инпутов. Каждый такой инпут характеризуется тем, что вызывает у целевой программы как минимум одно из следующих состояний:

  • аварийное завершение (crash),
  • достижение заданной точки в коде (target),
  • зависание (timeout).

Постобработка результатов включает два этапа:

  1. Минимизация: Для группы инпутов, приводящих к одинаковым результатам (например, к одной и той же целевой точке), сохраняется только самый первый сгенерированный файл. Это позволяет точно измерять метрику TTE (Time To Exposure) на основе репрезентативной выборки. Минимизацию можно отключить через опцию конфигурации xmin = false, что полезно для сохранения полного набора данных.
  2. Кластеризация: Оставшиеся после минимизации инпуты группируются (кластеризуются) по достигнутым целевым точкам. Для каждой точки создается отдельная директория, название которой указывает на её локацию в коде. Важно, что если один инпут приводит к срабатыванию нескольких точек, он будет присутствовать в каждом из соответствующих кластеров. Структурно выходная директория с кластерами выглядит так:
Screenshot 2025-09-23 at 16 41 15

В результате гибридного направленного фаззинга были достигнуты 8 целевых точек. Были созданы директории difuzz/crashes_TARGET_NAME:TARGET_LINE, куда были скопированы входные файлы, достигающие эти точки. Видно, что были найдены 3 crash_* входных файла (с аварийными завершениями) и 8 target_* файлов (просто достигающие целевую точку).

Сбор покрытия

В направленном фаззинге сбор покрытия производится не только по итоговому корпусу, но и по всем objective-файлам, оставшимся после минимизации. Это позволяет также увидеть покрытие кода, содержащего целевые точки (и ошибки).

Сбор покрытия через Sydr-fuzz запускается с помощью команды:

$ sydr-fuzz -c parse-libafl.toml cov-html

Получаем следующий лог:

Screenshot 2025-09-23 at 12 28 32

И в HTML-отчете можно убедиться, что целевая точка, например, /goblin/src/elf/mod.rs:365 была достигнута:

Screenshot 2025-09-23 at 12 33 25

Сортировка аварийных завершений

Анализ крешей, полученных в результате фаззинга, может быть выполнен с помощью инструмента Casr. Для этого нужно запустить следующую команду:

$ sydr-fuzz -c parse-libafl.toml casr

Здесь уже анализируются только те objective'ы, которые приводят к крешу программы. В результате запуска Casr получим отдельную кластерную иерархию в директории parse-libafl-out/casr. Запускать анализ имеет смысл, когда получилось найти не только инпуты, достигающие те или иные точки, но и инпуты-креши (после завершения фаззинга они начинаются на crash_*. Запустив анализ на goblin, получим следующий вывод:

Screenshot 2025-09-23 at 16 49 01

В итоге получили 2 кластера, всего 3 креша с типом RUST_PANIC.

Заключение

В этой статье был представлен подход к гибридному направленному фаззингу Rust-кода, объединяющий фаззер LibAFL-DiFuzz и символьный интерпретатор Sydr через инструмент Sydr-Fuzz. Подробно рассматривается этап подготовки целевой программы, продемонстрированный на примере проекта goblin. Показано, что с помощью Sydr-Fuzz можно автоматизировать ключевые этапы работы: непосредственно направленный фаззинг, последующую минимизацию и кластеризацию результатов, сбор покрытия и анализ аварийных завершений утилитой Casr.

Clone this wiki locally