Функционально идентичный шеллкод-лоадер весит 71.7 КБ на C и 151.5 КБ на Rust - бинарь вырос вдвое. При этом, по данным исследования Rochester Institute of Technology (2023, цитируется в блоге Bishop Fox), автоматизированные средства анализа дают куда больше false negatives на Rust-бинарях, а Ghidra и IDA Free декомпилируют их в откровенную кашу по сравнению с аналогами на C/C++. Последние два года я переписываю лоадеры и C2-агенты на Rust и вижу эту картину в каждом проекте: размер растёт, но детект падает. Не магия языка - конкретные свойства компилятора и рантайма ломают привычные паттерны статического анализа. Ниже - разбор полного пайплайна: от OPSEC-конфигурации проекта до конкретных ограничений против продакшн-EDR.
Место в цепочке атаки: зачем red team строит кастомные агенты на Rust
Кастомный malware-агент - не самоцель, а звено цепочки: initial access -> лоадер -> имплант -> post-exploitation -> exfiltration. В терминах MITRE ATT&CK разработка агента - это Malware (T1587.001, Resource Development): подготовка инструментария до начала операции. Агент при выполнении дёргает Native API (T1106, Execution) для вызова Windows API, а в продвинутых сценариях - Process Hollowing (T1055.012, Defense Evasion / Privilege Escalation) для инъекции в легитимные процессы. Подробнее - в нашем материале про разработка red team инструментов.Почему не взять готовый фреймворк? Sliver, Havoc, Mythic - рабочие решения, но на каждый из них есть публичные сигнатуры. CrowdStrike Falcon и SentinelOne обновляют детекты на публичные C2 в течение дней после выхода релиза. На внутреннем пентесте против зрелой инфраструктуры с EDR-покрытием публичный агент - это алерт в SOC ещё на этапе initial access.
Кастомный агент решает конкретную задачу: его бинарь не совпадает ни с одной известной сигнатурой, а поведенческий профиль можно подогнать под целевую среду. Rust здесь даёт три преимущества перед C/C++, и они подтверждены данными:
Первое - отсутствие привязки к MSVC CRT. Rust статически линкует зависимости через LLVM, из import table исчезают характерные для C/C++ паттерны (
msvcrt.dll, ucrtbase.dll). Тривиальная классификация бинаря по импортам усложняется.Второе - сложность декомпиляции. Ownership-модель, pattern matching, монадические обработки
Result<T, E> генерируют код, который Ghidra превращает в нечитаемое месиво. Аналитику в SOC потребуется заметно больше времени на triage. По данным аналитиков PT Expert Security Center (Habr, 2025), сложность реверса Rust-бинарей обусловлена агрессивным инлайнингом, системой владения и абстракциями (трейты, макросы, pattern matching).инлайнинг - это оптимизация, при которой компилятор заменяет вызов функции непосредственно ее телом.
Третье - меньший overlap с известными семействами. Подавляющее большинство малвари по-прежнему написано на C/C++. Rust-бинарь не попадает в те же кластеры при машинном обучении, что снижает сигнатурный детект.
Контекст применения: внутренний пентест или red team engagement против инфраструктуры с EDR (CrowdStrike Falcon, SentinelOne, Elastic 8.x+). На внешнем пентесте без EDR-контроля оверинженеринг с кастомным Rust-агентом - стрельба из пушки по воробьям.
Rust vs C++ для malware: trade-off таблица
Прежде чем писать код - трезвая оценка. Rust offensive security - не серебряная пуля, и в ряде сценариев C++ остаётся предпочтительным выбором.
| Критерий | Rust | C/C++ | Комментарий |
|---|---|---|---|
| Размер бинаря (release) | 150–200 КБ (лоадер) | 50–80 КБ | Rust статически линкует std; уменьшается через opt-level = "z" + LTO |
| Import table | Минимальная (kernel32) | Включает CRT-зависимости | Меньше IoC при статическом анализе |
| Декомпиляция (Ghidra/IDA) | Плохая: monomorphization раздувает код | Хорошая: прямое соответствие исходнику | Основное преимущество для evasion |
| Сигнатурный детект (AV) | Низкий: мало overlap с известными семействами | Высокий: совпадение с CRT-паттернами | Актуально для signature-based движков |
| Поведенческий детект (EDR) | Одинаковый | Одинаковый | VirtualAlloc + CreateThread детектируется независимо от языка |
| Прямые syscalls | Через ntapi/windows-rs | Через inline ASM / SysWhispers | Оба варианта рабочие |
| Sleep obfuscation | Требует unsafe + ручная реализация | Нативно через Ekko/Foliage | C++ удобнее для продвинутых техник |
| Стабильность агента | Memory safety из коробки | Ручное управление памятью | Rust: меньше крашей в длительных операциях |
Когда Rust лучше: кастомный C2-агент с нуля, лоадер первого этапа (stager), инструменты для initial access - всё, где критично минимизировать статический детект и усложнить triage. Применимо: внутренний пентест, инфраструктура с EDR-покрытием.
Когда C++ лучше: sleep obfuscation уровня Ekko/Foliage, работа с ROP-цепочками, интеграция с существующим C2-фреймворком на C (Cobalt Strike BOF). В этих сценариях Rust создаёт дополнительное трение без выигрыша в evasion.
Когда язык не имеет значения: если EDR использует kernel-level телеметрию (ETW-TI в Elastic 8.x+, kernel callbacks в SentinelOne) - поведенческий детект одинаков для любого языка. Хоть на ассемблере пиши.
OPSEC-конфигурация: от toolchain до чистого бинаря
Требования к окружению
- ОС: Windows 10/11 или GNU/Linux (cross-компиляция через
x86_64-pc-windows-gnu) - Rust: nightly channel (обязательно для флагов
-Z). Установка:rustup default nightly - Target:
rustup target add x86_64-pc-windows-msvc(или-gnu) - RAM: минимум 4 ГБ (Rust-компилятор с LTO жрёт заметно больше, чем gcc/clang), рекомендуется 8 ГБ
- Зависимости: крейт
windowsдля WinAPI, опциональноntapiдля прямых syscalls - Режим работы: offline-сборка возможна после первого
cargo buildс кэшированным registry
Проект без следов: cargo.toml и флаги компилятора
📚 Часть контента скрыта. Этот материал доступен участникам сообщества с рангом One Level или выше
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме
strings - минимум читаемого текста.Строки в бинаре: шифрование и обход FLOSS
После очистки паник-строк остаются пользовательские: URL C2-сервера, пути, HTTP-заголовки. FLARE-FLOSS от Mandiant вытаскивает их за секунды. Решение - шифрование строк на этапе компиляции. Крейт
str_crypter зашифровывает строку в compile-time и расшифровывает при вызове: let url = sc!("https://c2.example.com", 20).unwrap() - в бинаре строка будет XOR-зашифрована с ключом 20. По данным автора крейта, результат устойчив к FLARE-FLOSS.Отдельная головная боль - сериализация. Крейт
serde, стандартный для JSON в Rust, записывает имена полей структур в секцию .rdata. Если C2-протокол описан как struct Task { command: String }, строка command останется в бинаре в чём мать родила. Альтернатива - C-style enums с ручной десериализацией через transmute:
Код:
#[repr(u32)]
pub enum Command {
Sleep = 1u32,
Shell,
Upload,
Undefined, // catch-all для невалидных кодов
}
impl Command {
pub fn from_u32(id: u32) -> Self {
// SAFETY: u32 гарантирован сигнатурой функции
unsafe { std::mem::transmute(id) }
}
}
u32-код команды, агент конвертирует через transmute - ни одного строкового литерала в release-бинаре. Для отладки: #[cfg(debug_assertions)] impl Display for Command { ... } - Display-реализация компилируется только в debug-сборке. Попытка вызвать println!("{}", command) в release без этого атрибута приведёт к ошибке компиляции. Компилятор сам становится страховкой от OPSEC-промахов - и вот это я считаю одним из самых недооценённых свойств Rust в offensive-разработке.Rust shellcode loader: минимальный пример с разбором
Базовый паттерн загрузки шеллкода в память не зависит от языка: выделить RW-память, скопировать payload, сменить права на RWX, передать управление. На Rust через крейт
windows (пример адаптирован из исследования Bishop Fox, 2025):
Код:
unsafe {
let addr = VirtualAlloc(Some(ptr::null_mut()), buf.len(),
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
ptr::copy(buf.as_ptr(), addr as *mut u8, buf.len());
VirtualProtect(addr, buf.len(), PAGE_EXECUTE_READWRITE,
&mut PAGE_PROTECTION_FLAGS(0));
let h = CreateThread(Some(ptr::null()), 0,
Some(std::mem::transmute(addr)),
Some(ptr::null()), THREAD_CREATION_FLAGS(0), Some(ptr::null_mut()));
WaitForSingleObject(h.unwrap(), INFINITE);
}
VirtualAllocсPAGE_READWRITE- выделение RW-памяти. Ничего подозрительного: приложения регулярно аллоцируют память.ptr::copy- запись шеллкода в собственный процесс. Для user-mode хуков невидима: нет межпроцессного взаимодействия.VirtualProtect -> PAGE_EXECUTE_READWRITE- вот тут начинается самое интересное. CrowdStrike Falcon перехватываетNtProtectVirtualMemoryчерез user-mode хук и помечает регион. SentinelOne дополнительно мониторит через kernel callbacks.CreateThreadс адресом в аллоцированной памяти - второй триггер. EDR коррелируетNtCreateThreadExс адресом, не принадлежащим загруженному модулю.
rust_dealloc / unwrap_failed, тогда как C-версия декомпилируется в почти читаемый код.Предусловия: техника работает без модификаций на Windows 10/11 без EDR. С активным CrowdStrike Falcon (≥6.x) или SentinelOne бинарь будет задетектирован поведенчески на связке VirtualProtect + CreateThread. Обход требует indirect syscalls или unhooking ntdll.
Где засветится Rust-агент: ограничения по вендорам EDR
Rust не защищает от поведенческого детекта. Язык меняет статику бинаря, но не последовательность API-вызовов. Разберём по конкретным продуктам - потому что обобщение «работает против EDR» без вендор-специфики это, мягко говоря, самообман.CrowdStrike Falcon (user-mode hooks + kernel callbacks). Falcon перехватывает функции
ntdll.dll через inline-хуки. Цепочка VirtualAlloc -> VirtualProtect(RWX) -> CreateThread триггерит практические методы независимо от языка. Обход: прямые или непрямые syscalls через крейт ntapi, минуя хукнутые функции. На Rust реализация direct syscalls через макрос asm! менее тривиальна, чем через SysWhispers3 на C, но функционально эквивалентна.SentinelOne (комбинированный подход). Кроме user-mode хуков использует kernel-level мониторинг через minifilter-драйверы. Прямые syscalls обходят user-mode слой, но kernel callbacks на
NtCreateThreadEx и NtMapViewOfSection остаются. Rust vs C++ здесь - без разницы.Elastic 8.x+ (kernel ETW-TI). Elastic Endpoint полагается на Event Tracing for Windows - Threat Intelligence provider на уровне ядра. Даже прямые syscalls детектируются, потому что телеметрия идёт от ядра, а не от хукнутых DLL. Против ETW-TI ни Rust, ни C++ преимуществ не дают - нужны техники уровня Disable or Modify Tools (T1685): патчинг ETW-провайдера или BYOVD.
Kaspersky EDR Expert. Собственный kernel-драйвер с callback'ами на основные операции. Детектирует shellcode injection на уровне ядра. Статический анализ Rust-бинарей у Kaspersky менее зрелый, чем для C/C++, но поведенческий модуль одинаково эффективен.
Общий паттерн: Rust снижает детект на этапе статического анализа (Masquerading, T1036 - бинарь не похож на известные семейства) и усложняет ручной реверс (Debugger Evasion, T1622 - аналитику нужно больше времени на разбор). Но на этапе поведенческого анализа - runtime-мониторинг - язык роли не играет: EDR видит API-вызовы, а не исходный код.
| Что Rust реально меняет | Что Rust не меняет |
|---|---|
| Сигнатурный match с известными семействами (снижает) | Поведенческий детект VirtualAlloc/CreateThread |
| Время ручного triage аналитиком (увеличивает) | Kernel-level телеметрию (ETW-TI, minifilters) |
| Overlap с CRT-паттернами в import table (убирает) | Детект sandbox-чеков и anti-debug (T1497.001, T1622) |
| Точность автоматизированной классификации (снижает) | Network-based детект C2-трафика |
Вокруг Rust в offensive-комьюнити сложился своего рода культ: пересаживаем лоадеры с C на Rust и ждём, что детект исчезнет. На практике я вижу обратное - команды тратят недели на борьбу с borrow checker вместо того, чтобы потратить это время на исследование детект-механики конкретного EDR в целевой инфраструктуре.
Rust даёт реальный выигрыш ровно в двух точках: снижение сигнатурного совпадения и усложнение triage. Остальное - direct syscalls, sleep obfuscation, ETW patching - реализуется на любом языке с inline assembly. Если агент детектируется по поведению, переписывание на Rust ничего не изменит:
VirtualProtect с RWX-правами триггерит одинаковый алерт на C, на Rust и на ассемблере.Реальная ценность Rust - в возможности написать агент с чистой статикой за разумное время, не утонув в buffer overflow собственного кода. Memory safety - это не про защиту жертвы, это про стабильность вашего импланта на третью неделю операции. Агент, который крашится из-за use-after-free в четыре утра, обходится дороже пары дней на освоение lifetime annotations.
А пока комьюнити спорит о выборе языка, EDR-вендоры наращивают kernel-level телеметрию. И никакой компилятор от этого не спасёт. Если хочешь отработать эту связку руками - на WAPT разбирают цепочку от лоадера до обхода EDR в нескольких модулях с лабами.
Последнее редактирование модератором: