-
Notifications
You must be signed in to change notification settings - Fork 32
Fuzzing goblin (Rust:crab:!) project with Sydr and AFLplusplus (rus)
Из этой статьи вы узнаете о том, как применять инструмент гибридного фаззинга 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 начали находить аварийные завершения. Давайте дождёмся конца фаззинга.
Спустя 17 часов фаззинг завершился, и мы нашли 2 таймаута и 559 аварийных завершений!
Я знаю, вам хочется скорее применить casr для анализа аварийных завершений:). "Now don't be hasty, Master Meriadoc." (c) Treebeard.
Давайте сперва минимизируем выходной корпус:
# sydr-fuzz -c parse-afl++.toml 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
Вот какой вывод мы получили:
# export PATH=/root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/bin/:$PATH
Это уже сделано в Dockerfile. Нам нужно это изменение, чтобы sydr-fuzz использовал тот же самый тулчейн llvm, с помощью которого происходила сборка цели под покрытие. В результате получаем покрытие по исходному коду, которые можно смотреть.
Идея, которая стоит за предикатами безопасности, была кратко описана в гайде по фаззингу проекта 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 что-то нашёл. Давайте подождём когда проверка доработает до конца.
Проверка предикатов безопасности завершена. Мы нашли ещё одно аварийное завершение и теперь у нас их аж 560.
Наконец-то переходим к разбору аварийных завершений! Для этих целей я использую
casr с помощью сабкоманды sydr-fuzz casr
:
# sydr-fuzz -c parse-afl++.toml casr
Вы можете узнать больше о casr
из репозитория casr или из другого моего гайда.
Давайте посмотрим, что нам выдал casr:
После дедупликации мы получили 11 аварийных завершений, которые разбиты на 4 кластера. Также мы видим что в третьем кластере cl3
находится 5 аварийхных завершений с одинаковой строчкой падения. Окинув быстрым взором, все аварийные завершения уже были исправлены, кроме одного из кластера 4 cl4
. Давайте посмотрим отчёт.
Так, что тут у нас? Ясно, целочисленное переполнение. Честно говоря, сходу точно не скажешь, влияет ли оно на что-то ещё или нет, но в окресностях функции явно ничего плохого не произойдёт. Возможно стоит завести issue на этот счёт.
Внимательный читатель спросит меня: "А что там с двумя таймаутами?". Я их проверил. На них цель действительно долго работает, около минуты, а потом завершается, т.е. вечного цикла я не обнаружил. Кажется, нам нужна автоматизация анализа таких случаев, вы не находите?
В этой небольшой статье я попытался осветить некоторые интересные аспекты фаззинга проектов на языке Rust 🦀. Я показал как применять sydr-fuzz, как проводить минимизацию корпуса, собирать покрытие по исходному коду, проверять предикаты безопасности и сортировать аварийные завершения. Надеюсь, вам было полезно и интересно читать:).
Андрей Федотов