Skip to content

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

Daniel Kuts edited this page Nov 7, 2025 · 26 revisions

Введение

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

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

Для демонстрации возможностей гибридного направленного фаззинга с Sydr-Fuzz будем использовать библиотеку xlnt.

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

Сборка производится с помощью cargo make, который автоматически собирает все фаззинг-цели и выполняет нужную подготовку для направленного фаззинга.

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

Для направленного фаззинга с LibAFL-DiFuzz необходимо подготовить:

  • Код фаззинг обертки
  • Скрипт сборки фаззинг обертки
  • Makefile.toml для сборки целей
  • config.toml с целевыми точками

Для нашего примера вполне подойдёт функция load и обёртка, используемая в этом гайде. Нам понадобится сборка с функцией main, что отражено в соответствующем скрипте сборки. Скрипт сборки - исполняемый файл, который используется в Makefile.toml для сборки целей с различной инструментацией (для фаззинга LibAFL-DiFuzz, для символьной интерпретации Sydr, для анализа аварийных завершений Casr, для сбора покрытия). Соответственно, скрипт сборки фаззинг-цели должен: осуществлять переход в репозиторий проекта, очищать артефакты предыдущей сборки, выполнять конфигурацию и сборку проекта и фаззинг-цели. Сборка различных целей в Makefile.toml определяется через переменные окружения CC/CXX/CFLAGS/CXXFLAGS, поэтому в скрипте сборки эти переменные не должны перезаписываться. Примеры различных скриптов сбокри для направленного фаззера: xlnt, cxxfilt.

Cпециальный Makefile.toml осуществляет сборку фаззинг-цели с различной инструментацией, а также производит предобработку для направленного фаззинга (построение графов и целевых последовательностей). Makefile.toml можно сгенерировать по шаблону при помощи скрипта gen_target.py (sydr/difuzz/template/gen_target.py), указав значения аргументов, специфичных для фаззинг цели:

  • -p/--project: название проекта
  • -s/--script: путь до скрипта сборки фаззинг цели
  • -t/--target-dir: путь до директории с исходным кодом проекта
  • -m/--main-path: путь до файла с реализацией функции main относительно target-dir (или абсолютный путь)
  • -b/--bin-path: путь до бинарного файла фаззинг-цели, получающегося в результате сборки, относительно target-dir
  • -a/--bin-args: аргументы для запуска бинарного файла (с "@@" вместо имени файла входных данных)
  • -c/--config-dir (опционально): директория для сохранения подготовленных конфигурационных файлов (по умолчанию ".")
  • -r/--rep-clone: (опционально) bash-команда для клонирования репозитория проекта
  • -v/--version: (опционально) bash-команда для перехода на определённый коммит в проекте
  • --mode: режим сборки (debug/release), по умолчанию release
  • --root: название функции, которая является точкой входа в программу (main)
  • -l/--lang: язык целевой программы (c/rust/go)

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

$ python3 gen_target.py -p xlnt -s directed_target/build_libafl_load.sh -r "git clone https://github.com/tfussell/xlnt" -v "git checkout 3a279fcaab3432bb851c7976d4591f9505c3462a" \
    -t xlnt -m /opt/StandaloneFuzzTargetMain.c -b build/load_libafl -a "@@" -c . --mode release --root main -l c

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

  • проверить корректность всех путей в [env], в частности путь до инструментов LibAFL-DiFuzz (DIFUZZ_DIR).
  • выставить нужные флаги для сборки фаззинг целей в [tasks.debug_unix], [tasks.casr_unix], [tasks.coverage_unix], [tasks.target_unix].

Последним этапом подготовки является задание целевых точек. При применении обычного гибридного фаззинга xlnt было найдено несколько крешей - возьмём некоторые из них в качестве целевых точек. Для этого добавим их в виде списка в следующий конфигурационный файл config.toml:

[[target]]
file = "/xlnt/source/detail/cryptography/compound_document.cpp"
line = 975

[[target]]
file = "/xlnt/source/detail/cryptography/compound_document.cpp"
line = 723

[[target]]
file = "/xlnt/source/detail/cryptography/compound_document.cpp"
line = 126

[[target]]
file = "/xlnt/source/detail/serialization/xlsx_consumer.cpp"
line = 2031

[[target]]
file = "/xlnt/source/detail/serialization/zstream.cpp"
line = 269

[[target]]
file = "/xlnt/source/utils/path.cpp"
line = 185

[[target]]
file = "/xlnt/source/worksheet/worksheet.cpp"
line = 1086

Сборка фаззинг-цели

Перейдём к сборке фаззинг цели. Команда OUT_DIR=/ cargo make all выполняет сборку целей debug, target, coverage и casr из Makefile.toml. С помощью переменной окружения OUT_DIR можно задать путь для сохранения собранных бинарных файлов. Стоит отметить, что при сборке целей под LibAFL-DiFuzz и покрытие, исходный код фаззинг-цели автоматически патчится при помощи скрипта insert_forkserver.py. Поэтому между запусками cargo make all, например, во время отладки, необходимо проверять и откатывать изменения в исходном коде (в частности в main_path - файле с функцией main).

Для проекта xlnt был подготовлен Docker-образ, в котором установлено всё необходимое окружение и выполняется сборка фаззинг-целей. Соответствующий докерфайл называется Dockerfile_libafl:

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

ARG SYDR_ARCHIVE="./sydr.zip"

WORKDIR /

# Clone target from GitHub.
RUN git clone https://github.com/tfussell/xlnt

WORKDIR /xlnt

# Checkout specified commit. It could be updated later.
RUN git checkout 3a279fcaab3432bb851c7976d4591f9505c3462a && git submodule update --init --recursive

# Copy build script and targets.
COPY save.cc load.cc ./

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

WORKDIR /directed_target

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

# Prepare seed corpus.
RUN mkdir /corpus && find /xlnt -name "*.xlsx" | xargs -I {} cp {} /corpus
RUN cp -r /corpus /save_corpus
RUN for file in /save_corpus/*; do sed -i '1s/^/\x00\x05/' $file; done

Все предварительно подготовленные файлы (Makefile.toml, скрипт сборки, config.toml) находятся в отдельной директории directed_target. Стоит обратить внимание, что для сборки докера понадобится архив sydr.zip, содержащий бинарные файлы и библиотеки, необходимые для инструментации LibAFL-DiFuzz. Соберём образ при помощи команды:

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

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

Рассмотрим подробнее каким образом в Makefile.toml производится сборка цели для LibAFL-DiFuzz. Сборка программы непосредственно для фаззера LibAFL-DiFuzz выполняется в несколько этапов. Сначала производится предварительная сборка с добавлением отладочной информации при помощи компиляторов wllvm/wllvm++. Далее - статический анализ полученного бинарного файла инструментом DiFuzz. Для запуска статического анализа программы необходимо указать путь до конфигурационного файла config.toml и до бинарного файла программы, полученного в результате сборки wllvm/wllvm++, а также аргументы, специфичные для анализа. Эти этапы описаны целью difuzz в файле Makefile.toml:

[tasks.difuzz_unix]
script_runner = "@shell"
script = '''
cd ${PROJECT_DIR}
export LLVM_COMPILER=clang
export CC=wllvm; export CXX=wllvm++
export CFLAGS="-g -fsanitize=address,integer,bounds,null,undefined,float-divide-by-zero"; export CXXFLAGS="$CFLAGS"
python3 ${DIFUZZ_DIR_ABS}/insert_forkserver.py -a insert -l c -f /opt/StandaloneFuzzTargetMain.c
python3 ${DIFUZZ_DIR_ABS}/insert_forkserver.py -a comment -l c -f /opt/StandaloneFuzzTargetMain.c
cd ${OUT_DIR_ABS}
${PROJECT_DIR}/build_libafl_load.sh
${DIFUZZ_DIR_ABS}/difuzz -c ${PROJECT_DIR}/config.toml -b ${EXAMPLE_DIR}/build/load_libafl -e ${OUT_DIR_ABS}/ets_load.toml ${DIFUZZ_ARGS}
${PROJECT_DIR}/build_libafl_save.sh
${DIFUZZ_DIR_ABS}/difuzz -c ${PROJECT_DIR}/config.toml -b ${EXAMPLE_DIR}/build/save_libafl -e ${OUT_DIR_ABS}/ets_save.toml ${DIFUZZ_ARGS}
'''

При запуске цели difuzz увидим следующий вывод от инструмента DiFuzz:

difuzz1 image

После этапа статического анализа создаётся вспомогательный конфигурационный файл ets.toml, а также сохраняются в формате DOT граф вызовов и ГПУ целевых функций программы вместе с их деревьями доминаторов. Файл ets.toml используется при повторной сборке программы инструментирующими компиляторами libafl_cc/libafl_cxx. Для сборки необходимо добавить вызов функции инициализации фаззинга в код функции main при помощи скрипта insert_forkserver.py, а также обработать ets.toml при помощи специального менеджера ETS_SHARED_MANAGER (обеспечивает параллельную компиляцию модулей, задействуя разделяемую память). Эти действия описаны целью target:

[tasks.target_unix]
script_runner = "@shell"
script = '''
cd ${PROJECT_DIR}
export CC=${LIBAFL_CC}
export CXX=${LIBAFL_CXX}
export CFLAGS="-g -fsanitize=address,integer,bounds,null,undefined,float-divide-by-zero"; export CXXFLAGS="$CFLAGS"
${ETS_SHARED_MANAGER} -a remove -n xlnt_load
${ETS_SHARED_MANAGER} -a create -n xlnt_load
${ETS_SHARED_MANAGER} -a parse -n xlnt_load -i ${OUT_DIR_ABS}/ets_load.toml
python3 ${DIFUZZ_DIR_ABS}/insert_forkserver.py -a uncomment -l c -f /opt/StandaloneFuzzTargetMain.c
export LIBAFL_SHARED_NAME="xlnt_load"
${PROJECT_DIR}/build_libafl_load.sh
mv ${EXAMPLE_DIR}/build/load_libafl ${OUT_DIR_ABS}/load_libafl
${ETS_SHARED_MANAGER} -a dump -n xlnt_load -o ${OUT_DIR_ABS}/ets_load.toml
${ETS_SHARED_MANAGER} -a remove -n xlnt_load
python3 ${DIFUZZ_DIR_ABS}/insert_forkserver.py -a remove -l c -f /opt/StandaloneFuzzTargetMain.c
'''
dependencies = ["difuzz"]

При компиляции увидим следующие логи (выставив значение переменной LIBAFL_DEBUG_PASS=2):

image

После запуска этих целей, а также цели debug (и, при необходимости, coverage, casr) сборка программы для направленного фаззинга готова.

Фаззинг

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

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

Получим следующий конфигурационный файл для обёртки load:

exit-on-time = 7200

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

[difuzz]
path = "/directed_target/sydr/difuzz/libafl_difuzz"
target = "/load_libafl @@"
args = "-j4 -l64 -i /corpus -e /ets_load.toml"
casr_bin = "/load_casr"

[cov]
target = "/load_cov @@"

В аргументах для [difuzz] в данном конфигурационном файле указывается:

  • -j4: параллельный запуск в 4 процесса
  • -l64: ограничение размера стека в 64Гб
  • -i /corpus: путь до начального корпуса фаззинга
  • -e /ets.toml: путь до файла с целевыми последовательностями

Подробнее о настройке LibAFL-DiFuzz в sydr-Fuzz в документации.

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

sydr-fuzz -c ./load-libafl.toml run

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

image

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

image

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

Также в процессе фаззинга можно увидеть подобные логи:

image

По ним можно увидеть количество файлов, которые были импортированы фаззером от Sydr. Эти файлы оказались полезны по одной из двух причин: либо они помогают в достижении целевых точек, либо открывают новое покрытие целевой программы.

По окончании анализа выводится общая статистика по всем процессам фаззера, а также список всех достигнутых целевых точек:

image

Здесь уже для каждой точки выводится минимальное время её достижения - по этим измерениям можно оценить метрику Time to Exposure (TTE).

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

Результатами направленного фаззинга являются objective-инпуты, которые приводят по крайней мере к одному из трёх состояний:

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

По окончании фаззинга производится минимизация и сортировка результатов. При минимизации из всех файлов с одинаковыми характеристиками для достижения целевых точек сохраняется только один -- файл, который был сгенерирован первым. Это позволяет измерять метрику TTE по минимизированному набору objective'ов. Опция конфига xmin позволяет отключать минимизацию путём установки в значение false. Это может быть полезно в случаях, когда требуется сохранить все objective-файлы.

Оставшиеся после минимизации файлы переименовываются в соответствии с их влиянием на целевую программу и сортируются по достигнутым целевым точкам. В результате сортировки в директории load-libafl-out формируются отдельные директории-кластеры для каждой из целевых точек. При этом если некоторый objective приводит к достижению сразу двух целевых точек, он будет добавлен в оба кластера. Название кластера содержит локацию целевой точки:

image

Здесь мы видим, что в результате запуска гибридного направленного фаззинга было достигнуто 6 целевых точек. При этом, к примеру, для compound_document.cpp:125 было найдено 2 креша, 1 objective, достигающий точки без креша, и 2 зависания.

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

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

Воспользуемся следующей командой:

$ sydr-fuzz -c load-libafl.toml cov-html
image image image

По полученному HTML-отчёту можно увидеть, к примеру, что точка compound_document.cpp:125 была покрыта:

image

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

При помощи следующей команды можно осуществить анализ полученных в результате фаззинга аварийных завершений программы с помощью Casr:

$ sydr-fuzz -c load-libafl.toml casr
image

Здесь уже рассматриваются только те objective'ы, которые приводят к крешу программы. В результате запуска Casr получим отдельную кластерную иерархию в директории load-libafl-out/casr с шестью кластерами, один из которых содержит PROBABLY_EXPLOITABLE креш:

image

Посмотрим отчёт для этого креша при помощи команды:

$ casr-cli load-libafl-out/casr/cl1/crash-4b9fda7296ce518d-10.casrep
image

Мы видим часть ASAN-отчёта и несколько строк кода вокруг ошибки -- похоже на неверно вычисленный адрес для записи. Составление таких отчётов позволяет существенно упростить анализ результатов фаззинга и сэкономить время.

Заключение

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

Clone this wiki locally