На SOC Forum 2025 наша команда выступила с докладом «Змеиная Rustамания», в котором помимо анализа трех вредоносов, написанных на Rust, мы дали несколько советов, упрощающих анализ вредоносов на этом языке. Сегодня мы более подробно пройдемся по тем рекомендациям, которые представили в докладе. Вы узнаете:

  • Как подобрать параметры оптимизации, с которыми были скомпилированы Rust-сэмплы.
  • Как искать основную логику программы в сэмплах, которые используют крейт Tokio.

Подбор параметров

Исполняемые файлы, скомпилированные компилятором rustc, являются статически слинкованными, поэтому в них содержится большое количество безымянных функций, что значительно затрудняет анализ.

Аналитик просматривает функции
Аналитик просматривает функции

Чтобы облегчить себе задачу, исследователи создают сигнатуры, которые помогают распознать библиотечные функции и отделить их от основного кода программы. Это неплохо работает с языками С и С++, но с Rust возникают проблемы. Компилятор rustc умеет хорошо оптимизировать код, поэтому в зависимости от параметров оптимизации скомпилированная программа может значительно измениться. И если создавать сигнатуры, не зная параметров, с которыми разработчик скомпилировал исследуемый файл, их эффективность может сильно снизиться. Например, на практике были случаи, когда сигнатуры, созданные с неправильными параметрами, распознавали десятки функций, а с правильно подобранными уже давали результат в несколько тысяч функций.

Одним из способов решения этой проблемы может быть подбор параметров оптимизации. Для того чтобы это сделать, нужно распознать в исследуемом файле несколько библиотечных функций, а после этого компилировать выбранный крейт с разными параметрами и каждый раз сравнивать получившиеся функции, например, с помощью Diaphora. Схематично этот процесс показан на рисунке ниже.

Схема процесса
Схема процесса

Этот процесс можно автоматизировать. Для этой задачи мы создали специальный скрипт, который можно скачать здесь. Используя скрипт rustbinsign для компиляции, idat (консольный вариант IDA) для извлечения функций и Diaphora для сравнения, наш скрипт выполнит всю рутинную работу за вас. В результате вы получите параметры оптимизации, при которых функции были максимально похожими.

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

Быстро найти функции, которые могут принадлежать какому-то крейту, можно с помощью информации, содержащейся в panic — формат ошибок в Rust, который содержит в себе путь к файлу и номер строки и столбца, где произошла ошибка. Удобно обработать эти данные может скрипт IDA_rust_metadata_finder. При выборе функций для сравнения нужно отдавать предпочтение наиболее крупным и ветвистым из них. Функции, которые содержат менее трех базовых блоков, могут не распознаться в Diaphora. Также эти функции не должны принадлежать std-крейтам, так как на них оптимизация не влияет и результаты будут недействительными.

К нашему скрипту также прилагается yara-правило, созданное исследователями из JPCERT, которое по строкам может «угадать» некоторые параметры оптимизации. Это правило не всегда работает, поэтому может быть использовано только для дополнительной проверки результатов работы основного скрипта.

После того как вы получите подобранные параметры оптимизации, нужно их правильно использовать для создания сигнатур. Для компиляции крейтов можно использовать скрипт rustbinsign. Он позволяет указать параметры оптимизации с помощью аргумента template. Кроме того, при компиляции стоит указать параметр full-compilation. Этим вы укажете, что помимо самого крейта нужно скомпилировать тесты, примеры и бенчмарки, которые есть в его репозитории, и уже из этого набора создавать сигнатуры. Это может быть полезно для того, чтобы решить проблемы lto и мономорфизации в Rust.

Tokio

Tokio — одно слово, а сколько проблем… Это крейт, который позволяет создавать асинхронные приложения на Rust, из-за чего очень часто используется в сэмплах вредоносов, которые работают с сетью.

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

Аналитик во время анализа
Аналитик во время анализа

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

Например, в Linux-системах для этого используется только три системных вызова:

Использование

Системный вызов

Создание рантайма Tokio

sys_epoll_create1

sys_epoll_ctl

Асинхронное исполнение

sys_epoll_wait

В Windows-системах есть подобный набор WinAPI:

Использование

WinAPI-функция

Создание рантайма Tokio

CreateIoCompletionPort

GetQueuedCompletionStatus

Асинхронное исполнение

GetQueuedCompletionStatus

PostQueuedCompletionStatus

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

Такой функциональностью, например, обладает Proximity browser в IDA. Он позволяет находить путь между двумя функциями с помощью пункта «Find path» в контекстном меню.

На примере ниже можно увидеть путь между функцией main и системным вызовом sys_epoll_create1. Второй функцией после main в этом графе является функция tokio::runtime::builder::Builder::build. Она используется для создания рантайма Tokio.

Функция build
Функция build

Подобным образом, но уже с помощью других функций можно находить, например, tokio::runtime::runtime::Runtime::block_on, с которой начинается асинхронное исполнение основного кода программы.

Функция block_on
Функция block_on

Помимо Proximity browser, в IDA начиная с версии 9.2 можно использовать интерактивный граф перекрестных ссылок. В примере ниже выведен такой граф с помощью пункта «Xrefs graph to» до функции CreateIoCompletionPort.

Граф перекрестных ссылок
Граф перекрестных ссылок

Примеры выше показывают, как можно сузить зону поиска основной программной логики. Дальше все зависит от вашей находчивости. Например, с помощью функции tokio::runtime::builder::Builder::build можно понять, в какой переменной будет храниться рантайм, и с помощью этого определить, где происходит настройка и дальнейший запуск асинхронного кода.

А если удалось найти block_on-функцию, то можно зайти внутрь нее и попытаться найти основной код. Чтобы там не заблудиться, можно скомпилировать для себя сэмпл с символами, который использует Tokio, и пройти путь от block_on-функции до основного кода программы. Как показывает практика, в обоих сэмплах путь будет очень похожим и можно найти корреляции, которые сориентируют в исследуемом сэмпле.

В дополнение ко всему, нужно понимать, как выглядит то, что мы ищем. Основной код в Tokio преобразуется в машину состояний, чтобы выполнять его асинхронно. После компиляции это может выглядеть как switch-case-выражение, ключом которого является структура в первом аргументе функции. Можно отмечать такие функции и более подробно их анализировать — возможно, именно там находится то, что вы ищете.

Пример основного кода
Пример основного кода

Заключение

Мы рассмотрели ряд рекомендаций и инструментов, которые могут облегчить вам задачу исследования сэмплов на Rust. Хотя эти подходы не универсальны и не гарантируют решения в каждой ситуации, расширение арсенала доступных средств повышает вероятность успешного исследования. Желаем вам (и нам) успехов в анализе Rust-вредоносов.