
Windows x64向けのRustクレート: ghost-frame call-stack spoofing、runtime indirect syscalls、APC injectionを提供。SSN resolution、JIT stub emission、VEH-based parameter encryptionを提供し、EDR evasionを実現。
Windows x64 向けのゴーストフレーム・コールスタック・スプーフィング + ランタイム間接システムコール — Rust にポーティング。
Mohamed Alzhrani (0xmaz) による LACUNA Chain (lacuna_chain.c) からポーティング。
lacuna-rs は、元の C 実装の TTP と同じプリミティブを提供する再利用可能な Rust クレートであり、任意の Rust プロジェクトに組み込めるように構成されています — wsyscall-rs や syscalls-rs のようなものですが、ランタイム SSN 解決、関数ごとの syscall;ret ターゲティング、そしてゴーストフレーム・スタック・スプーフィングを備えています。
利用側のクレートがフレームポインタ付きでコンパイルされていない場合、
stack-spoof機能はサイレントに失敗します。
chain.rs 内のスタック踏みつけプリミティブは、mov rbp, {x} インラインアセンブリを介して呼び出し元のリターンアドレススロットを特定します。これには RBP チェーンが無傷であることが必要です。Rust(およびほとんどのリリースモードコンパイラ)はデフォルトでフレームポインタを省略します。
build.rs はこのクレート自身のコード生成に対して force-frame-pointers=yes を設定しますが、Cargo はコンパイラフラグを下流のクレートに伝播できません。 あなた自身のプロジェクトに以下を追加する必要があります:```toml
[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` |
| `win32u` NOP ギャップファインダー | `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 間接 syscall スタブ生成 | `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` |
---
## フィーチャーフラグ```toml
[dependencies]
lacuna-rs = { version = "0.4", 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
### ゴーストフレームチェーンをビルドして検証する```sh
cargo run --example verify --features stack-spoof
インジェクションの証明 -- ペイロードはターゲットプロセス内で実行されました。```sh
cargo run --example inject --features inject,stack-spoof,veh -- <sc.bin>
`--verbose` を追加して VEH 診断出力(スタックダンプ、レジスタ出力)を有効にします:```sh
cargo run --example inject --features inject,stack-spoof,veh -- <pid> <sc.bin> --verbose
inject_sapc はターゲットのスレッドをランク付けします。最も強力なランキングシグナルは、スレッドがウィンドウを所有しているかどうかです:
EnumWindows +
GetWindowThreadProcessId。GUIスレッドはメッセージループ内のアラータブルな待機で停止するため、キューに入れられたAPCはそこで最初に実行されます。+1000。NtQueryInformationThread(ThreadCycleTime)。実行中のビルドでクラスが利用できない場合は ThreadTimes にフォールバックします(examples/tprobe.rs を参照)。正規化して加算されます。NtQueryInformationThread(ThreadTimes) — カーネル時間 + ユーザー時間。NtQueryInformationThread(ThreadSuspendCount)。サスペンドされたスレッドはAPCを実行できません:サスペンドカウントごとに −2000。NtQueryInformationThread(ThreadBasicInformation)。優先度8〜10は +150、範囲外の優先度は −300。フィルタリング。 サスペンドされたスレッド、および非アラータブルなカーネル待機 — WrDelayExecution(Sleep/NtDelayExecution)または WrSuspended(NtQuerySystemInformation(SystemProcessInformation) から読み取り) — に停止しているスレッドは完全に除外されます(これらはAPCを決して受け取れません)。待機理由はそれ以外では診断専用です:候補ごとに State: / Reason: / Alert: としてログに記録されますが、スコアには影響しません。なぜなら KWAIT_REASON はアラータビリティをエンコードしないためです(アラータブルなオブジェクト待機と非アラータブルなオブジェクト待機は同じ値を報告します — examples/tprobe.rs 形式のプロービングで検証済み)。
候補はソートされ(最高スコア、タイブレークはGUI、次にサイクル)、列挙/ログ記録用に MAX_APC_CANDIDATES(10)に切り詰められます。
列挙 ≠ インジェクト。 リストされるスレッド数は、インジェクトされる数とは分離されています。
MAX_APC_CANDIDATES(10)はランク付けおよびログ記録される数を制御し、APC_INJECT_THREADS(5)は実際にAPCを受け取る数を制御します。なぜ複数にインジェクトしてから再試行するのか: ユーザーモードではどのスレッドがアラータブルな待機状態にあるかを判別できず、選択された単一のスレッドがアラータブルな待機に入らないことがよくあるため、APCは決して発火しません。したがって
inject_sapcはAPCを複数のスレッドにキューイングし、その後短いループ(約80 ms)で再キューイングして、共有ゲートワードが0から1に反転するまで — つまり、いずれかのターゲットスレッドが実際にペイロードを実行するまで — 繰り返します。それが発生したかどうかをログに記録します:payload claimed the gate(実行された)対gate NOT claimed。なぜ一度だけ実行するのか: 同じシェルコード/コンテキストが各スレッドにキューイングされるため、保護がなければ非リエントラントなペイロードが同時に実行され、ホストがクラッシュします(実測:3つの同時実行)。リエントランシーゲート(
USE_REENTRANCY_GATE、デフォルトでオン)により、ペイロード本体は、いくつのスレッド(または何回の再試行ラウンド)がAPCを受け取っても正確に一度だけ実行されます(実測:正確に1回)。ペイロードが明示的に並行実行安全でない限り、ゲートはオンのままにしてください。
use lacuna::{scan, nt, win::get_module};
let ntdll = get_module(b"ntdll.dll\0");
// Scan for ghost regions let mut ghosts = [scan::Ghost::default(); 512]; let n = scan::scan_ghosts(ntdll, &[b"NtAllocateVirtualMemory\0"], &mut ghosts); println!("{} ghost regions in ntdll", n);
// Resolve SSN + syscall;ret for a specific function let (ssn, syscall_ret) = nt::resolve(ntdll, b"NtOpenProcess\0"); println!("NtOpenProcess: ssn={:#x}, syscall;ret={:#x}", ssn, syscall_ret);
### 任意のNT API用の間接システムコールスタブを生成する```rust
use lacuna::{nt, stub, win::{get_module, HMODULE}};
let ntdll: HMODULE = get_module(b"ntdll.dll\0");
// Resolve SSN + the function's own syscall;ret address
let (ssn, syscall_ret) = nt::resolve(ntdll, b"NtAllocateVirtualMemory\0");
assert!(ssn != nt::SSN_INVALID && syscall_ret != 0);
// JIT-emit a stub: mov r10,rcx; mov eax,SSN; jmp [syscall;ret]
// (or jmp [ghost_gadget] -> JMP [RBX] -> syscall;ret if build_chain was called)
let stub = stub::make_stub(ssn, syscall_ret).expect("stub alloc failed");
// Cast to the matching function pointer type and call
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()) };
// Scan ntdll/kernelbase/wow64/win32u for ghost regions and construct // the six-layer fake call stack. Sets G_GHOST_GADGET so stubs route // through JMP [RBX] in a signed DLL. lacuna::chain::build_chain();
// Register VEH handlers (param encryption + chain guard) let _veh = lacuna::veh::VehGuard::register().expect("VEH registration failed");
// Optional: enable verbose diagnostics at runtime lacuna::veh::set_verbose(true);
### 機密性の高いシステムコールをスタック偽装 + パラメータ暗号化でラップする```rust
use lacuna::win::HANDLE;
use core::ptr;
let mut h_proc: HANDLE = ptr::null_mut();
let key: u64 = 0xCAFE_1337;
// Plant ghost frames (replaces return addresses with signed-DLL ghosts)
lacuna::chain::stomp_plant();
// Arm DR0 on the syscall;ret -- VEH will XOR-decrypt params at the boundary
lacuna::veh::pcrypt_arm(key, syscall_ret, true);
// Call through the indirect stub -- RIP lands inside ntdll at kernel entry
let _status = unsafe { open_proc(/* XOR-encrypted params */) };
// Always disarm before any non-protected call
lacuna::veh::pcrypt_disarm();
lacuna::chain::stomp_restore();
すべての API 呼び出しに完全な LACUNA 処理が必要なわけではない。鍵となる原則は、EDR がインジェクションやポストエクスプロイテーション活動として相関させる呼び出しを隠すことである。
これらは EDR がフックし相関させる「王冠の宝石」となる NT syscall である: