Rust как язык программирования только набирает обороты и находит своих почитателей. Он не только предлагает множество надстроек для безопасности кода, но с недавнего времени еще и появился в ядре Linux. В этой статье мы посмотрим на него с "обратной" стороны, а именно попробуем пореверсить программу, написанную на Rust, и выяснить, что можно сделать, чтобы сделать ее анализ проще. Рассмотрим утилиты, приложения и плагины, а также напишем свой плагин для IDA Pro, Cutter и rizin, чтобы автоматически создать сигнатуры для исполняемого файла без отладочных символов. Поговорим о FLIRT-сигнатурах, их преимуществах и недостатках и о том, можно ли автоматизировать их создание. В качестве примера будем рассматривать задачу с BSidesSF CTF 2020 под названием rusty2. Создаем две версии исполняемого файла: один с отладочными символами и без strip, а второй без отладочных символов со strip. Второй вариант используется чаще всего в реальных проектах. Собирать будем с помощью менеджера пакетов cargo 1.63.0:  релизную версию —cargo build --release дебаг сборку — cargo build Использовался компилятор stable версии 1.63.0. В статье мы рассмотрим три плагина, два из который для IDA Pro: rust-reversing-helper idapro-rust Что-то свое? По заверению автора, этот плагин для IDA Pro помогает с деманглингом имен функций и изменением соглашений о вызове функций. Проверим работу плагина на сборке с отладочными символами. Деманглинг имен функций в IDA автоматический, поэтому проверим вторую функцию. До работы плагина мы имеем следующий вид программы: После его выполнения получаем такой вид: Функции, которые стали Library function, — измененные плагином. Например, функция core::panicking::assert_failed_usize_usize__1 до преобразования выглядела так: А после преобразования мы получаем следующий вид: В данном случае скрипт изменил функцию неверно, поскольку она должна принимать 4 агрумента, и к тому же не должна возвращать ничего. На сборке без отладочных символов скрипт закончил свою работу без каких либо изменений. В целом, плагин довольно интересный, но сильно помочь в реверсе Rust, на мой взгляд, не может. Плагин состоит из Rust-кода и трех скриптов на Python. Основная задача плагина — сбор информации о функциях: начало и конец кода, соглашение о вызовах, имена и т.д. Выводит информацию в JSON-формате. Показывать его работу особо смысла не имеется. Может помочь при реверсе, если необходимо, например, найти определенные функции по признакам или просто собрать информацию. Дизассемблеры, которые могут помочь в реверсе программ на Rust: IDA Pro Rizin/Cutter Ghidra Это выбор каждого. Просто посмотрим, как одна и та же функция main выглядит в каждом из них. IDA Pro: Rizin/Cutter: Ghidra: Если начать сравнивать, то можно увидеть, что в декомпиляторе HexRays есть проблема с определением адреса прыжка (JUMPOUT). Это не очень критично, но неприятно. Остальные декомпиляторы справились хорошо. FLIRT-сигнатуры — это база с соответствием имен функций и их первых байт кодов. Позволяет при реверсе приложения без отладочных символов определить часть функций, для которых первые байты кода совпадут с одной из сигнатур. Такая возможность существует в IDA Pro и Cutter. Ghidra пока такого не умеет. Казалось бы, очень хороший способ определять функции — создавать статический бинарный файл, содержащий нужные функции, извлекать сигнатуры и использовать где угодно. Но тут есть свои подводные камни. Из-за ограничения на количество первых записываемых байт сигнатуры могут подвергаться коллизиям, поэтому придется либо решать, какую функцию вы хотели бы оставить, либо убрать обе. В больших файлах может получаться большое количество коллизий, что уменьшит эффективность использования такого подхода. Еще можно добавить, что сигнатуры сильно зависят от оптимизаций, применяемых при компиляции или линковке. Чем более похожие оптимизации будут у целевого файла и того, откуда были сгенерированы сигнатуры, тем лучше будет результат распознавания. Тем не менее это очень удобный способ с ходу распознать часть функции в бинарном файле.  Все способы, описанные выше, имеют свое применение в определенных условиях. Но ведь можно не только пользоваться, но и создавать свое. Мы попробуем написать плагин для IDA Pro, Cutter и rizin, который будет автоматически создавать сигнатуры для исполняемых файлов или библиотек, написанных на языке Rust и собранных без отладочных символов. Чтобы создать сигнатуры, нужно скомпилировать свой бинарный файл с отладочными символами. А для этого нам нужно знать, какие функции этот файл должен содержать. Для начала посмотрим, какую информацию можно вынести из скомпилированного файла, написанного на Rust. С виду он выглядит как обычный исполняемый файл с кучей функций (около 700 в нашем примере). Самое интересное для нас находится в строках. Замечу, что чаще всего оказывается, что строки в Rust не заканчиваются нулем. Компилятор хранит большую часть строковых типов как ссылку на ее начало и длину. В некоторый случаях (как в типе String) еще ее максимальную вместимость до выделения большего объема памяти. И все же там можно увидеть очень много полезного, потому что строки для вывода ошибок строго прописаны. Например, можно найти путь до модуля или библиотеки на стороне создателя файла. А еще там прописаны названия используемых библиотек и их версии. Для нашего примера это будет выглядеть так: Имея эту информацию, мы можем посмотреть эти библиотеки в документации. Там есть раздел для функций, откуда можно взять их шаблоны и примеры использования. Постепенно у нас вырисовывается план, как будет работать наш плагин: Ищем строки в бинарной файле. Регулярным выражением выделяем библиотеки и их версии. В документации находим нужные библиотеки и информацию об их функциях. Создаем свой Rust-проект и пытаемся добавить в него как можно больше функций. Компилируем с отладочными символами. Генерируем сигнатуры для дальнейшего использования. Пройдемся по каждому пункту. Тут я буду писать про плагин для IDA Pro, но в целом для Cutter и rizin будет примерно такая же ситуация. Тут ничего сложного. С помощью средств idapython вытягиваем все строки в файле. Код выглядит так: С этим тоже трудностей не предвидится. Берем библиотеку re и с ее помощью достаем нужную нам информацию и преобразуем в множество (set), чтобы убрать повторяющийся элементы: Для запросов к документации будем использовать библиотеку requests. А для парсинга html страниц — BeautifulSoup4. Информация о библиотеке по ее версии можно найти по адресу https://docs.rs/{lib_name}/{lib_version}. А список функций - по адресу https://docs.rs/{lib_name}/{lib_version}/#functions. Потом с помощью html-парсера находим ссылки на сами функции. Эти действия вынесем в функцию: После этого мы можем посмотреть на шаблон функции и на примеры ее использования. В плагине будет использоваться два подхода. В нем определены переменные с наиболее распространенными типами в Rust. Каждый раз, когда проверяется информация о функции, плагин смотрит на ее шаблон, и, если у нее нет аргументов или аргументы имеют определенные типы, то мы можем просто вызвать ее внутри нашего проекта без дополнительных действий. Если аргументы имеют неизвестный для плагина вид, можно попытаться взять код из примера в документации, оформить его как функцию и вынести ее в отдельный модуль. А затем в нашем проекте вызвать ее. Так будем доставать код функции: В get_func_code функция check_template проверяет шаблон функции на наличие аргументов. Если они есть, смотрит их тип и решает, сможем ли мы у себя в проекте ее вызвать с аргументами. Дальше мы достаем код из примера использования и работаем уже с ним. Стоит добавить, какие типы аргументов будет поддерживать плагин, потому что кастомные и generic типы сложно вывести заранее. Поддерживаемые типы (будет mut в скобках если добавлены изменяемые и неизменяемый типы):  Если типы аргументов не поддерживаются, попытаемся взять пример из документации. При желании, можно добавить свои типы в глобальные переменные VARIABLES, DEFINES. Проект будем создавать с помощью команды cargo new {project_name} --lib --vcs none. С помощью нее мы получим готовый проект для создания библиотеки.  Теперь наша задача — добавить библиотеки в зависимости в Cargo.toml и добавить их в проект с помощью extern crate {crate_name}. Код для генерации Cargo.toml: Тут мы видим, что конечный результат компиляции должен быть статической и разделяемой библиотекой. Какая из них будет использоваться зависит от того, где сигнатуры будут использоваться. Еще мы добавляем в релизный профиль отладочные символы. Прежде чем продолжить, поговорим немного о возможных оптимизациях, применяемых в Rust. Это необходимо, поскольку может сильно повлиять на вид сигнатур. Для управления настройками компиляции и определения зависимостей cargo использует файл Cargo.toml. Самое интересное для нас из него — это настройки профилей по умолчанию. Основные профили — это dev и release. Профиль dev используется по умолчанию в сборке через cargo: cargo build. Профиль release используется в сборке с флагом --release.  Стандартные настройки для дебаг сборки: Стандартные настройки для релизной сборки: В целом, различия очевидны. В дебаг сборке включены макросы вроде debug_assert!, добавляются отладочные символы. А в релизной сборке это лишнее — там включены оптимизации. Рассмотрим основные флаги оптимизации, используемые в Cargo.toml: opt-level — определяет уровень оптимизации. Наиболее продвинутая оптимизация достигается при уровне 3 и отключается на уровне 0 lto (LLVM's link time optimizations) — отвечает за оптимизацию кода во время линковки. Для этого приходится проводить дополнительный анализ, из-за чего время сборки проекта может сильно возрасти. Как правило, в реальных проектах, эта опция будет включена. Шаблон у нас есть, код функций тоже. Теперь осталось грамотно вызвать все функции и правильно сгенерировать код библиотеки. Этот шаг довольно непростой. Как создать библиотеку и быть уверенным, что код в ней соберется как нужно? Для начала определимся со "скелетом", в который мы будем добавлять остальной код: Определяем модуль smth, в котором будет экспортируемая функция main. Внутри нее и будет основная логика библиотеки - определение переменных и вызовы функций. Еще тут не хватает добавления внешних библиотек. В наш "скелет" сначала добавим внешние крейты. Это и есть наши зависимости, описанные в Cargo.toml. Теперь хотелось бы убедиться, что наши вызовы или примеры кода, вынесенные в отдельные модули, могут компилироваться. Для этого будем добавлять по одному вызову функций в шаблон выше и проверять с помощью cargo check --release. Если команда выполняется успешно, мы оставляем этот вызов в конечный результат. Если нет, то мы игнорируем этот вызов и переходим к следующему. Код, выполняющий проверку и записывающий конечный результат в lib.rs приведен ниже. После этого мы вызываем cargo build --release и получаем библиотеку с отладочными символами. Дело за малым — создать из готовой библиотеки сигнатуры. В случае с IDA Pro 8.0 мы будем использовать ее инструментарий flair80. А именно pelf/sigmake для Linux или pld/sigmake для Windows. Сначала мы создаем .pat-файл, и с него с помощью sigmake создаем .sig-файл. При этом чаще всего с первого раза мы получим предупреждение о коллизиях. Для этого нам нужно удалить строки, начинающиеся с ";" с .exc-файла. После чего повторяем действия с sigmake и получаем flirt-сигнатуры для IDA Pro. В случае с Rizin / Cutter все намного проще. Нам нужно иметь установленный rizin и выполнить команду rizin -A -qc "zfc {target}/libname}.sig" {target}/{libname}, где libname — это скомпилированная разделяемая библиотека с отладочными символами. Посмотрим, как различается вид программы до и после применения полученных сигнатур. Тестировать будем на релизной версии файла. Вот как все было до применения сигнатур: А вот что мы получаем после: 158 функций из 772 распознались с помощью сигнатур. Вполне неплохой результат. Часть оставшихся функций — это аналоги распознанных функций для другого типа принимаемого значения. Правда, часть функций получит имя unknown_libname, и с этим пока не очень понятно что делать. К тому же мы отбрасываем все коллизии вместо выбора. Эту проблему можно решить руками, если их не особо много. В статье мы рассмотрели существующие инструменты, которые могут помочь при реверсе программ, написанный на языке Rust. Сделали вывод по каждому из них и написали собственный плагин на python для автоматического создания сигнатур, используя строки из бинарного файла. Исходные кода плагинов для остальных дизассемблеров можно найти по ссылке. В планах добавить парсинг документаций к библиотекам с других источников вроде github или сгенерированной с помощью rustdoc-gen.