Skip to content

Fuzzing xlnt project with sydr fuzz for fun and profit (rus)

Andrey Fedotov edited this page Jan 10, 2023 · 11 revisions

Введение

"Syrio says that every hurt is a lesson, and every lesson makes you better"

― George R. R. Martin, A Song of Ice and Fire

Данная статья повещена фаззингу проектов с открытым исходным кодом. Существует много зарекомендовавших себя фаззеров (libFuzzer, AFLplusplus, Honggfuzz, и.т.д.) и ещё больше хорошо написанных гайдов. Тем не менее, я бы хотел показать, как применять гибридный подход к фаззингу (сочетание фаззинга и символьного выполнения) для тестирования программ с открытым исходным кодом. Для этих целей я буду использовать наш фаззер sydr-fuzz, который сочетает в себе мощь Sydr - инструмент динамического символьного выполнения и libFuzzer - эволюционный движок фаззинга, учитывающий обратную связь по покрытию кода и работающий в том же процессе, что и анализируемая программа. Мы узнаем не только, как подготовить фаззинг-цели, проводить гибридный фаззинг, а также познакомимся с инструментом кластеризации и дедупликации аварийных завершений casr, узнаем, как получать отчёты о покрытии кода и применять Sydr в режиме проверки предикатов безопасности для поиска интересных ошибок, используя технологии символьного выполнения.

Задача поиска единого примера для демонстрации всего спектра технологий довольно сложная. Для этих целей отлично подходит проект xlnt. Отлично, мы готовы начать наше славное путешествие по поиску багов!

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

"It is no easy thing to slay a dragon, but it can be done."

― George R.R. Martin, A Song of Ice and Fire

Первое, что нам стоит сделать - это найти функцию или фрагмент кода для фаззинга. Для сложных проектов, таких как (suricata, postgresql, nginx, и т.д.) - это сложная задача. Для её выполнения необходимо хорошее понимание кода проекта и сборочной системы. К счастью, наш проект - это библиотека.

Так, что такое xlnt? Xlnt - это современная C++ библиотека для работы с XLSX форматом. Давайте посмотрим на API. Может, мы сможем найти какую-нибудь функцию, которая загружает .xlsx файл?

В xlnt.hpp включён очень интересный заголовочный файл:

#include <xlnt/workbook/workbook.hpp>

В этом файле много интересных функций, которые работают с xlsx файлами. Мне понравилась вот эта функция:

/// <summary>
/// Interprets byte vector data as an XLSX file and sets the content of this
/// workbook to match that file.
/// </summary>
void load(const std::vector<std::uint8_t> &data);

Да, это определённо то, что нам нужно! Эта функция рассматривает байтовый вектор как XLSX файл. Другими словами функция разбирает xlsx файл, а входные параметры функции хорошо подходят под интерфейс libFuzzer'а. Хорошо, у нас есть целевая функция, теперь нужен план действий:

  • Создаём фаззинг-цель для libFuzzer'а. Если Вы не знакомы с libFuzzer'ом, то советую посмотреть этот гайд.
  • Создаём фаззинг-цель для Sydr и для сбора покрытия. Для этих целей Вам просто нужно добавить main функцию, которая читает содержимое входного файла и отправляет его в функцию LLVMFuzzerTestOneInput.
  • Собираем исполняемые файлы для libFuzzer'а, Sydr'а и сбора покрытия по коду.
  • Подготавливаем входной корпус.

Xlnt уже добавлен в oss-sydr-fuzz. Поэтому Вы можете просто склонировать этот репозиторий, построить docker контейнер и следовать инструкциям.

Далее я постараюсь описать основные моменты для подготовки проекта под фаззинг в oss-sydr-fuzz. Процесс фаззинга происходит в docker контейнере, что позволяет обеспечить удобство и воспроизводимость. Давайте посмотрим на Dockerfile для xlnt:

FROM sweetvishnya/ubuntu20.04-sydr-fuzz

MAINTAINER Alexey Vishnyakov

# 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 d88c901faa539f9272a81ba0bab72def70ca18d7 && git submodule update --init --recursive

# Copy build script and targets.
COPY load_fuzzer.cc load_sydr.cc build.sh ./

# Build fuzz targets.
RUN ./build.sh

WORKDIR ..
# Prepare seed corpus.
RUN mkdir /corpus && find /xlnt -name "*.xlsx" | xargs -I {} cp {} /corpus

Мы используем базовый образ с установленным clang-14. Dockerfile содержит команды, которые клонируют репозиторий xlnt, фиксируют коммит для воспроизводимости, запускают скрипт сборки и подготавливают изначальный корпус. Обратим внимание на build.sh скрипт. Этот скрипт собирает три исполняемых файла для libFuzzer'а, Sydr'а и сбора покрытия по коду. Я не буду приводить всё содержимое скрипта из-за его размера (66 строк). Основная идея - пересобрать xlnt библиотеку с разными флагами (c санитайзерами+libFuzzer, без санитайзеров, с llvm-cov инструментацией):

# libFuzzer
cmake -D STATIC=ON -D TESTS=OFF \
    -DCMAKE_CXX_COMPILER=clang++ \
    -DCMAKE_CXX_FLAGS="-g -fsanitize=fuzzer-no-link,address,bounds,integer,undefined,null,float-divide-by-zero" \
    ..

# Sydr
cmake -DSTATIC=ON -D TESTS=OFF \
    -DCMAKE_CXX_COMPILER=clang++ \
    -DCMAKE_CXX_FLAGS=-g \
    ..

# Coverage
cmake -DSTATIC=ON -D TESTS=OFF \
    -DCMAKE_CXX_COMPILER=clang++ \
    -DCMAKE_CXX_FLAGS="-fprofile-instr-generate -fcoverage-mapping" \
    ..

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

#include <xlnt/xlnt.hpp>
#include <libstudxml/parser.hxx>

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
    std::vector<uint8_t> v_data(data, data + size);
    xlnt::workbook excelWorkbook;
    try
    {
        excelWorkbook.load(v_data);
    }
    catch (const xlnt::exception& e)
    {
        return 0;
    }
    catch (const xml::parsing& e)
    {
        return 0;
    }
    return 0;
}

Здесь мы выполняем два очевидных действия: создаём вектор с данными и загружаем книгу (workbook). Очень важная вещь при фаззинге C++ кода - это обработка исключений. Некоторые исключения можно рассматривать как ошибки (unhandled exceptions), а некоторые нет. Определённо, не обработанные исключения от стандартной библиотеки можно рассматривать как ошибки. Исключения от библиотек, которые используются вашей целью (внутренние библиотеки), тоже могут считаться ошибками. Такие исключения должны быть обработаны самой целевой функцией, над которой происходит фаззинг. А вот внешние (документированные) исключения должны быть обработаны в LLVMFuzzerTestOneInput, как Вы видите на примере xlnt. Также можете посмотреть на фаззинг-цель для Sydr и покрытия.

Перед началом фаззинга стоит подготовить корпус входных данных. Обычно внутри проекта есть немного файлов с необходимым форматом данных. Мы уже видели комаду в Dockerfile, которая создаёт изначальный корпус:

# Prepare seed corpus.
RUN mkdir /corpus && find /xlnt -name "*.xlsx" | xargs -I {} cp {} /corpus

Ну чтож, мы уже подготовили контейнер для фаззинга, давайте начинать!

Фаззинг

"The night is dark and full of terrors."

― George R.R. Martin, A Song of Ice and Fire

Перед тем как запустить sydr-fuzz нам нужно составить простой конфигурационный файл в toml формате. Ниже приводится конфигурационный файл sydr-fuzz.toml для xlnt:

exit-on-time = 7200

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

[libfuzzer]
path = "/load_fuzzer"
args = "-rss_limit_mb=8192 -timeout=10 -jobs=1000 -workers=8 /corpus"

[cov]
target = "/load_cov @@"

Давайте на него посмотрим:

exit-on-time - опциональный параметр, который принимает значение времени в секундах. Если в течение этого времени (2 часа в нашем случае) нет прироста покрытия, то фаззинг автоматически останавливается.

[sydr] таблица может содержать следующие параметры:

args - строка аргументов для запуска Sydr. Рекомендуется использовать опцию -s для равномерной обработки входных файлов из корпуса Sydr'ом.

target - строка запуска фаззинг-цели. Вместо входного файла используем @@.

jobs - число экземпляров Sydr для одновременного запуска. Значение по умолчанию 1.

[libfuzzer] таблица содержит аргументы для libFuzzer'а.

[cov] таблица содержит строку запуска для исполняемого файла, собранного с инструментацией llvm-cov.

В итоге мы запускаем фаззинг в 8 потоков libFuzzer и 2 экземпляра Sydr. Процесс фаззинга закончится, если покрытие (cov:) не будет увеличиваться в течении двух часов или libFuzzer найдёт 1000 аварийных завершений/oom/тайм-аутов. Отлично, давайте запустим docker контейнер:

$ sudo docker run --privileged --network host -v /etc/localtime:/etc/localtime:ro --rm -it -v $PWD:/fuzz oss-sydr-fuzz-xlnt /bin/bash

Перейдём в директорию /fuzz:

# cd /fuzz

И наконец запустим фаззинг:

# sydr-fuzz run

start-sydr-fuzz

Сперва sydr-fuzz мёржит изначальные директории с корпусами в корпус проекта. Давайте взглянем на логи. После шага с мёржем стартанул фаззинг. Мы видим приятно раскаршенные логи libFuzzer'а, который уже успел найти аварийное завершение! Настоящий segmentation fault, шикарно! Также мы видим информацию о работе Sydr. reloads{unique} показывают, сколько входных файлов от Sydr было полезно для libFuzzer'а (libFuzzer загрузил эти входы в свой корпус). Файлы от всех инстансов Sydr копируются в корпус проекта. Один файл может быть загружен несколькими инстансами libFuzzer'а, поэтому мы также считаем уникальные файлы. Мы обновляем статистику reloads по таймеру, чтобы видеть пользу от Sydr в реальном времени, а информация о количестве сгенеринованных входных данных обновляется после каждого завершения Sydr. Давайте подождём, когда закончится фаззинг и снова посмотрим на логи.

end-sydr-fuzz

Исходя из логов, 1000 заданий для libFuzzer'а завершились через 25 минут и нашли 5 аварийных завершений (отличающихся по данным файла). За это время Sydr успел отработать 17 раз и сгенерировать 2579 файлов (26 из них оказались полезными). Перед тем, как мы начнём собирать покрытие по коду и запускать проверку предикатов безопасности, давайте минимизируем корпус проекта:

$ sydr-fuzz cmin

Отлично, теперь у нас минимизированный корпус из 185 файлов. Теперь можно собирать покрытие и запускать проверку предикатов безопасности.

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

"Never do what they expect"

― George R.R. Martin, A Song of Ice and Fire

Для сбора покрытия в lcov формате воспользуемся следующей командой:

# sydr-fuzz cov-export -- -format=lcov > load.lcov

sydr-fuzz-cov

После получения .lcov файла создаём html отчёт:

# genhtml -o load-html load.lcov

cov-html

Отлично, теперь мы можем видеть, какие участки кода удалось покрыть с помощью sydr-fuzz. Самое время приступить к проверке предикатов безопасности.

Предикаты безопасности

"Never forget what you are, the rest of the world will not. Wear it like armor and it can never be used to hurt you."

Tyrion Lannister"

― George R.R. Martin, A Song of Ice and Fire

Идея использования символьного выполнения для целенаправленного поиска ошибок не нова. Мы используем Sydr и sydr-fuzz целенаправленно для поиска ошибок после того, как процесс фаззинга завершился. Сперва я должен рассказать, как устроены предикаты безопасности в Sydr. В Sydr мы используем предикаты безопасности для поиска следующих типов ошибок:

  • целочисленное переполнение
  • выход за границы буфера
  • и некоторые другие

Целочисленное переполнение - это очень интересная ситуация. Иногда это нормально, например, когда идут какие-то хэширующие вычисления, но иногда такого рода переполнения могут приводить к серьёзным последствиям (переполнение буфера). Также в бинарном коде очень сложно понять, является ли целочисленное переполнение действительно ошибкой. Например, целочисленное переполнение это абсолютно нормальная ситуация при работе с большими числами. Для решения данной проблемы мы работаем с подмножеством арифметических инструкций (мы их называем источниками). Когда случается переполнение в этих инструкциях, мы проверяем, используется ли переполненное значение дальше в:

  • аргументах функций
  • разыменованиях указателей
  • условиях ветвлений

Этот подход помогает уменьшить число ложных срабатываний на строне Sydr, но они всё ещё могут происходить. Помните, мы собирали фаззинг-цель для Sydr с отладочной информацией? Для символьной интерепретации Sydr использует только бинарный код, но с помощью отладочной информации мы можем отобразить найденные срабатывания на исходный код. После этого мы можем проверить, истинное это срабатывание или нет, путём запуска фаззинг-цели для libFuzzer'а с санитайзерами на входном файле от Sydr, сравнив строчки срабатывания санитайзера с предупрежнениями от предикатов безопасности. Таким образом, мы боремся с ложными срабатываниями. Теперь нам нужно как-то встроить проверку предикатов безопасности в пайплайн фаззинга, здесь вступает в игру sydr-fuzz. Проверка предикатов безопасности достаточно ресурсоёмкая задача - тяжелее, чем расширение покрытия с помощью символьного выполнения. Таким образом, нам бы хотелось не тратить зря вычислительные ресурсы. Идея заключается в том, чтобы запускать предикаты безопасности на хорошо профаженном и минимизированном корпусе входных файлов.

Так, мне кажется я уже поведал Вам об идеи, которая стоит за предикатами безопастости. Давайте уже скорее запустим sydr-fuzz! У нас есть минимизированный корпус после фаззинга, поэтому просто запускаем sydr-fuzz security в 8 потоков.

$ sydr-fuzz security --jobs 8

security-start

После нескольких запусков sydr-fuzz нашёл целочисленные переполнения, которые приводят нашу фаззинг-цель к аварийному завершению, круто!!!

security-crash

Давайте подождём, пока sydr-fuzz завершит свою работу. Ого, после проверки предикатов безопасности у нас появилось 126 аварийных завершений. Для меня это слишком много, чтобы просматривать каждое вручную, хочется какой-то автоматизации. Сабкоманда casr - это именно то что мне нужно;).

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

"Fear cuts deeper than swords. The man who fears losing has already lost."

― George R.R. Martin, A Song of Ice and Fire

Давайте запустим сортировку аварийных завершений следующей командной:

$ sydr-fuzz casr

casr

Сперва создаются отчёты об аварийных завершениях (.casrep) для каждого крэша. Для этого мы используем фаззинг-цель от libFuzzer'а с санитайзерами. Основной компонент отчёта для сортировки - это стек вызовов. Стек вызовов используюется при сравнении аварийных завершений. Следующим шагом идёт дедупликация отчётов с одинаковыми стеками вызовов. После запускается алгоритм кластеризации отчётов. Затем в полученные кластеры к имеющимся отчётам копируются входные данные, а в отчётах обновляется некоторая информация. На последнем этапе для каждого кластеризованного отчёта запускается caesar, который используется для получения отчёта на исполняемом файле без санитайзеров. Это может быть полезно в рамках анализа, посмотреть, как происходит аварийное завершение (если оно происходит), в отсутствии санитайзеров.

NOTE: casr доступен на github. caesar заменён на casr-gdb.

casr-clusters

Отлично, в директории проекта casr у нас получилось 7 кластеров и 12 аварийных завершений. В одном из кластеров у нас находится три аварийных завершения. В итоге мы имеем 7 аварийных завершений для просмотра, что звучит гораздо лучше, чем 126. Давайте взглянем на один из отчётов. Для этого я используют средство просмотра casr-cli.

$ casr-cli ./sydr-fuzz-out/casr/cl5/crash-sydr_2cb2768bb7bd020f982362a74344d8e133d4089e_int_overflow_14_signed.casrep

casr-cli

Мы видим часть asan отчёта и строчки кода, где произошло срабатывание. Предикаты безопасности помогли нам найти интересную ошибку, где std::vector создаётся с очень большим размером. На мой взгляд, использование таких средств сортировки аварийных завершений существенно сокращает время анализа найденных срабатываний.

Заключение

В этой статье я рассказал Вам о том, как мы используем фаззинг и символьное выполнение для анализа проектов с открытым исходным кодом. Я также хочу заметить, что современные инструменты символьного выполнения (Sydr, Fuzzolic, и.т.д.) работают совместно с фаззерами, делая процесс фаззинга немного эффективнее:). Интересные методы символьного выполнения, такие как предикаты безопасности, помогают найти новые аварийные завершения, которые могут быть пропущены в процессе фаззинга. Сортировка аварийных завершений - это неотъемлемая часть анализа кода. Без неё очень сложно анализировать каждое аварийное завершение через gdb. Я надеюсь, что статья была для Вас интересной и Вы узнали что-то новое:)


Андрей Федотов