Skip to content

Fuzzing goblin (Rust:crab:!) project with Sydr and AFLplusplus (rus)

Antwy edited this page Jan 11, 2023 · 24 revisions

Введение

Из этой статьи вы узнаете о том, как применять инструмент гибридного фаззинга sydr-fuzz для фаззинга проектов, написанных на языке Rust 🦀. Sydr-fuzz совмещает в себе преимущества Sydr (инструмента динамического символьного выполнения) и AFLplusplus. Sydr-fuzz также поддерживает другой движок фаззинга: libFuzzer. В этом гайде мы сосредоточимся на подготовке фаззинг-целей для AFLplusplus, Sydr, libFuzzer, а также для сбора покрытия по исходному коду. Мы проведём гибридный фаззинг, соберём покрытие по исходному коду. Мы воспользуемся нашим инструментом сортировки аварийных завершений сasr, и применим Sydr для проверки предикатов безопасности с целью поиска интересных ошибок технологиями символьного выполнения. Конечно, мы уделим особое внимание тому, что исследуемый нами проект написан на языке Rust 🦀!

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

Goblin – это отличная библиотека для парсинга различных исполняемых форматов файлов. Перед тем как я начну свой рассказ, отмечу наличие уже подготовленного для сборки docker контрейнера с необходимым окружением для фаззинга: цели для AFl++, Sydr, libFuzzer и сбора покрытия. Далее, мы будем использовать этот контейнер с небольшими модификациями.

Нам повезло, в goblin уже есть фаззинг-цели под libFuzzer. Фаззинг rust крэйтов libFuzzer'ом часто производят с помощью cargo fuzz. Вы можете изучить как использовать cargo fuzz из следующих ресурсов: Rust Fuzz Book. Сборка фаззинг-целей для libFuzzer'а осуществляется с помощью простой команды: cargo fuzz build -O. И вот, настало время моего первого совета. При сборке фаззинг цели используйте следующий флаг: RUSTFLAGS="-C panic=abort" cargo fuzz build -O. Исходя из документации, panic=abort позволит нам не тратить время на раскрутку стека на каждом аварийном завершении и позволит собрать более аккуратные и компактные стэктрейсы для дальнейшего анализа с помощью casr.

Хорошо, но мы собирались использовать AFL++ для фаззинга. Для этих целей есть cargo afl. Вы также можете ознакомиться с его использованием из Rust Fuzz Book. Сейчас нам необходимо собрать фаззинг-цели для AFL++ основываясь на готовых целях для libFuzzer. Это просто. Вот пример фаззинг цели для parse для AFL++.

#[macro_use]
extern crate afl;

fn main() {
    fuzz!(|data: &[u8]| {
        let _ = goblin::Object::parse(data);
    });
}

Как и для libFuzzer'а мы собираем цель с флагом -C panic=abort с помощью следующей команды: RUSTFLAGS="-C panic=abort" cargo afl build --release.

Теперь перейдём к сборке цели для Sydr. Тут тоже всё легко. Вот пример цели parse для Sydr.

extern crate goblin;

use std::env;
use std::fs::File;
use std::io::Read;

fn main() -> std::io::Result<()> {
    let args: Vec<String> = env::args().collect();
    if args.len() >= 2 {
        let filename = &args[1];
        let mut f = File::open(filename).expect("no file found");
        let metadata = std::fs::metadata(filename).expect("unable to read metadata");
        let mut data = vec![0; metadata.len() as usize];
        f.read(&mut data).expect("buffer overflow");
        let _ = goblin::Object::parse(&data);
    }

    Ok(())
}

Нам просто нужно прочитать входной файл как вектор u8 и передать его функции parse. Собираем с помощью команды: RUSTFLAGS="-C panic=abort" cargo build --release. Также, хочу отметить, что мы собираем релизную сборку, но хотим, чтобы проверки целочисленного переполнения overflow-checks были включены. Это очень полезно для символьного выполнения, т.к. эти проверки представляют собой условные переходы, которые могут быть инвертированы в процессе исследования путей символьным вычислителем, что позволит обнаружить ошибку. Вот пример Cargo.toml, но это также можно сделать и с помощью RUSTFLAGS.

[profile.release]
debug = true
panic = 'abort'
overflow-checks = true

И наконец, сборка цели для покрытия по исходному коду. Для этого мы будем использовать цель под Sydr и следующую команду сборки: RUSTFLAGS="-C instrument-coverage" cargo build.

Отлично, мы изучили, как подготовить все необходимые исполняемые файлы. Давайте соберём докер контейнер с окружением для фаззинга, используя следующие инструкции. Перед тем, как мы начнём сборку давайте поменяем версию goblin на коммит постарше в Dockerfile (например: git checkout 59ec2f3c57c53aa828b6a4cb4730d1efe3e43a05). Goblin – это хорошая библиотека, где новые найденные фаззингом аварийные завершения быстро исправляются, поэтому эти изменения помогут нам гарантированно что-то найти.

Фаззинг

Мы собираемся начать гибридный фаззинг с помощью sydr-fuzz, используя Sydr & AFL++. Я немного поправил parse-afl++.toml.

exit-on-time = 3600

[sydr]
target = "/sydr_parse @@"
jobs = 2

[aflplusplus]
target = "/afl_parse"
args = "-i /corpus"
jobs = 4

[cov]
target = "/cov_parse @@"

Давайте взглянем на эти изменения:

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

Я указал два экземпляра Sydr и 4 экземпляра AFL++. Настало время начать фаззинг:

# sydr-fuzz -c parse-afl++.toml run

После нескольких минут фаззинга, я заметил, что AFL++ и сам Sydr начали находить аварийные завершения. Давайте дождёмся конца фаззинга. sydr-crashes-found

Спустя 17 часов фаззинг завершился, и мы нашли 2 таймаута и 559 аварийных завершений! goblin-fuzz-end

Я знаю, вам хочется скорее применить casr для анализа аварийных завершений:). "Now don't be hasty, Master Meriadoc." (c) Treebeard.

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

# sydr-fuzz -c parse-afl++.toml cmin

goblin-cmin

afl-cmin сократил количество файлов в корпусе с 14994 до 1982. Отличный результат. Давайте соберём покрытие по исходному коду и применим предикаты безопасности перед анализом аварийных завершений.

Покрытие

sydr-fuzz предоставляет удобный способ сбора покрытия также и для проектов на языке rust. Давайте попробуем.

# sydr-fuzz -c parse.toml cov-export -- -format=lcov > parse.lcov
# genhtml --ignore-errors source -o parse_html parse.lcov

Вот какой вывод мы получили: sydr-fuzz-cov

# export PATH=/root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/bin/:$PATH

Это уже сделано в Dockerfile. Нам нужно это изменение, чтобы sydr-fuzz использовал тот же самый тулчейн llvm, с помощью которого происходила сборка цели под покрытие. В результате получаем покрытие по исходному коду, которые можно смотреть. lcov-report

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

Идея, которая стоит за предикатами безопасности, была кратко описана в гайде по фаззингу проекта xlnt. Сейчас давайте проверим предикаты безопасности. Результирующий корпус всё ещё достаточно большой, поэтому предлагаю для экономии времени проверить предикаты на подмножестве этого корпуса (скажем 256 файлов), используя 4 задачи для Sydr. Я провожу анализ на своём рабочем компьютере и ограничен 6 ядрами процессора и 32умя гигабайтами ОЗУ. Тут я должен сказать, что нам как раз пригодится опция сборки цели -C panic=abort, т.к у нас нет UBSAN&ASAN. Также, мы собрали цель с опцией overflow-checks = true. Без использования опции -C panic=abort цель просто тихо завершиться, и мы не узнаем о переполнении или какой-либо другой панике.

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

# sydr-fuzz  -c parse-afl++.toml security -j 4 --runs 256

По прошествии некоторого времени, Sydr что-то нашёл. Давайте подождём когда проверка доработает до конца. security-crash

Проверка предикатов безопасности завершена. Мы нашли ещё одно аварийное завершение и теперь у нас их аж 560. security-end

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

Наконец-то переходим к разбору аварийных завершений! Для этих целей я использую casr с помощью сабкоманды sydr-fuzz casr:

# sydr-fuzz  -c parse-afl++.toml casr

Вы можете узнать больше о casr из репозитория casr или из другого моего гайда.

Давайте посмотрим, что нам выдал casr: casr-results

После дедупликации мы получили 11 аварийных завершений, которые разбиты на 4 кластера. Также мы видим что в третьем кластере cl3 находится 5 аварийхных завершений с одинаковой строчкой падения. Окинув быстрым взором, все аварийные завершения уже были исправлены, кроме одного из кластера 4 cl4. Давайте посмотрим отчёт. casrep

Так, что тут у нас? Ясно, целочисленное переполнение. Честно говоря, сходу точно не скажешь, влияет ли оно на что-то ещё или нет, но в окресностях функции явно ничего плохого не произойдёт. Возможно стоит завести issue на этот счёт.

Внимательный читатель спросит меня: "А что там с двумя таймаутами?". Я их проверил. На них цель действительно долго работает, около минуты, а потом завершается, т.е. вечного цикла я не обнаружил. Кажется, нам нужна автоматизация анализа таких случаев, вы не находите?

Заключение

В этой небольшой статье я попытался осветить некоторые интересные аспекты фаззинга проектов на языке Rust 🦀. Я показал как применять sydr-fuzz, как проводить минимизацию корпуса, собирать покрытие по исходному коду, проверять предикаты безопасности и сортировать аварийные завершения. Надеюсь, вам было полезно и интересно читать:).


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