Karkas66/lacuna-rs

GitHub: Karkas66/lacuna-rs

将 LACUNA Chain 的 ghost-frame 调用栈欺骗与间接 syscall 技术移植为 Rust 库,帮助红队工具绕过 EDR 的调用栈检测。

Stars: 17 | Forks: 2

# lacuna-rs **适用于 Windows x64 的 Ghost-frame 调用栈欺骗 + 运行时间接 syscalls —— 移植至 Rust。** 由 Mohamed Alzhrani (0xmaz) 编写的 [LACUNA Chain](https://github.com/MazX0p/LACUNA-Chain) (`lacuna_chain.c`) 移植而来。 `lacuna-rs` 是一个可复用的 Rust crate,提供了与原始 C TTP 相同的原语,并进行了结构化处理,使其可以直接集成到任何 Rust 项目中——就像 `wsyscall-rs` 或 `syscalls-rs` 一样,但增加了**运行时 SSN 解析**、**针对每个函数的 `syscall;ret` 定位**以及 **ghost-frame 栈欺骗**。 ## 关键:帧指针要求 `chain.rs` 中的栈覆写(stack-stomping)原语通过 `mov rbp, {x}` 内联汇编定位调用者的返回地址槽。这要求 RBP 链保持完整。Rust(以及大多数 release 模式的编译器)默认会省略帧指针。 **`build.rs` 会为本 crate 自身的代码生成设置 `force-frame-pointers=yes`,但 Cargo 无法将编译器标志传递给下游 crate。** 你必须将以下内容添加到你自己的项目中: ``` # .cargo/config.toml(在你的 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` | | Ghost-gadget 发现 (`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 stub 生成 | `alloc_stub()` | `stub` | | Ghost-gadget 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` | ## Feature flags ``` [dependencies] lacuna-rs = { version = "0.1", features = ["inject", "stack-spoof", "veh"] } ``` | Feature | 描述 | 需要帧指针? | |---|---|---| | `syscalls` (默认) | SSN 解析 + JIT stub 生成 | 否 | | `inject` (默认) | 基于节区的 APC 注入 (`inject::inject_sapc`) | 否 | | `veh` | VEH + 硬件断点参数加密 | 否 | | `stack-spoof` | LACUNA ghost-frame 链 + 栈覆写 | **是** | | `no-std` | `no_std` 模式 (实验性) | 否 | 当没有启用任何特性时,仅扫描/PE/NT 层可用。 ## 快速开始 ### 扫描幽灵区域 ``` cargo run --example scan ``` ### 构造 + 验证 ghost-frame 链 ``` cargo run --example verify --features stack-spoof ``` ### 通过节区 + APC 注入 shellcode ![注入验证](https://raw.githubusercontent.com/Karkas66/lacuna-rs/main/assets/lacuna.png) 使用真实 implant 的注入示例 - C2 处于离线状态,但 Shellcode 已执行 ``` cargo run --example inject --features inject,stack-spoof,veh -- ``` 添加 `--verbose` 以启用 VEH 诊断输出(栈转储,寄存器打印): ``` cargo run --example inject --features inject,stack-spoof,veh -- --verbose ``` #### 线程评分算法 与其向目标进程中的每个线程排队 APC(当太多线程同时收到通知时,可能会导致进程崩溃),`inject_sapc` 使用了一种评分算法来选择最佳的 `MAX_APC_THREADS` (5) 个候选者: - **周期时间** — `NtQueryInformationThread(ThreadCycleTime)` — 主要活动指标 - **CPU 时间** — `NtQueryInformationThread(ThreadTimes)` — 内核 + 用户时间 - **挂起计数** — `NtQueryInformationThread(ThreadSuspendCount)` — 未挂起的线程获得 +300 加成;挂起的线程每次挂起计数扣除 150 - **优先级** — `NtQueryInformationThread(ThreadBasicInformation)` — 优先级为 8-10 的线程获得 +150 加成;超出范围的优先级受到 -100 惩罚 完全空闲的线程(周期时间和 CPU 时间均为零)将被完全跳过。 其余候选者按分数排序(分数最高者优先,分数相同者按周期数决胜) 并截断至前 5 名。 ## 作为库使用 ### 基础:扫描 + SSN 解析 ``` 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 生成间接 syscall stub ``` 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()) }; ``` ### 构建 ghost-frame 链 + 注册 VEH ``` // 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); ``` ### 使用栈欺骗 + 参数加密包装敏感 syscall ``` 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(); ``` ## OPSEC:什么应该隐藏在 ghost frames 之后 并非每个 API 调用都需要完整的 LACUNA 处理。核心原则是:**隐藏那些会被 EDR 关联为注入或后渗透活动的调用**。 ### 必须位于间接 syscall + ghost frames + 参数加密之后 这些是 EDR 会进行 hook 和关联的“皇冠上的明珠”级别的 NT syscalls: | 调用 | 敏感原因 | |---|---| | `NtOpenProcess` | 打开指向另一个进程的句柄 —— 注入的第一步 | | `NtCreateSection` + `NtMapViewOfSection` (远程) | 基于节区的注入特征 | | `NtWriteVirtualMemory` | 跨进程写入 —— 经典的注入指示器 | | `NtCreateThreadEx` | 远程线程创建 —— 最高信号的注入原语 | | `NtQueueApcThread` | APC 注入 —— 高信号 | | `NtProtectVirtualMemory` | RWX 权限更改 —— shellcode 暂存指示器 | | `NtAllocateVirtualMemory` (远程) | 远程分配 —— 注入前奏 | | `NtSetInformationThread` | 线程隐藏 (`HideFromDebugger`) —— 规避指示器 | 对于以上每一项: 1. 使用 `nt::resolve(ntdll, b"NtXxx\0")` 进行解析 2. 使用 `stub::make_stub(ssn, syscall_ret)` 生成 stub 3. 使用 `veh::pcrypt_arm(key, syscall_ret, true)` 对敏感参数进行 XOR 加密 4. 将调用包裹在 `chain::stomp_plant()` 和 `chain::stomp_restore()` 之间 ### 可以是直接的(不需要 ghost frames) | 调用 | 安全原因 | |---|---| | `NtQueryInformationThread` | 仅查询,极少被 hook,无跨进程写入 | | `NtDelayExecution` | 睡眠 —— 良性的,每个应用程序都会使用 | | `NtClose` | 句柄关闭 —— 良性的,极其常见 | | `GetModuleHandleA` / `GetProcAddress` | 模块解析 —— 不是 syscall,无法被 EDR 用户态 hook | | `CreateToolhelp32Snapshot` / `Thread32First` / `Thread32Next` | 线程枚举 —— kernel32,不是 ntdll syscall | | `OpenThread` / `CloseHandle` | 标准句柄操作 —— kernel32 | | `GetThreadContext` / `SetThreadContext` | DR0 所需 —— kernel32,仅限自身进程 | ### 核心原则 1. **解析** 任何 NT syscall 使用 `nt::resolve(ntdll, b"NtXxx\0")` —— 返回 `(SSN, syscall_ret_VA)`。 2. **生成** JIT stub 使用 `stub::make_stub(ssn, syscall_ret)` —— 返回可调用的函数指针。 3. **转换** stub 通过 `core::mem::transmute` 转换为适当的 `extern "system" fn(...)` 类型。 4. **(仅针对敏感调用)** 使用 `chain::build_chain()` 构建 ghost-frame 链,并在调用前后使用 `chain::stomp_plant()` / `chain::stomp_restore()`。 5. **(仅针对敏感调用)** 在调用前使用 `veh::pcrypt_arm(key, syscall_ret, true)` 启用参数加密,并在调用后使用 `veh::pcrypt_disarm()`。 6. **始终禁用** VEH,然后再进行非保护调用 —— 在未启用的 syscall 上发生杂散的 DR0 命中会导致崩溃。 7. **所有输出字符串** 必须包裹在 `lc!()` 中 —— crate 强制要求这一点,并且 VEH 诊断受 `set_verbose(true)` 控制,默认关闭。 stub 会自动处理 `mov r10, rcx` / `mov eax, SSN` / `jmp [syscall;ret]` 序列。如果注册了 ghost gadget,stub 将通过 `JMP [RBX]` 进行路由,实现双重用途的执行重定向 + 零特征桥接帧。 ## 文章覆盖范围验证 [LACUNA Chain 文章](https://0xmaz.me/posts/LACUNA-Chain-Ghost-Frames-defeats-All-EDR-layers-of-call-stack-based-detection/)中的全部 9 项关键贡献均已在代码中体现: | # | 文章概念 | 代码位置 | 描述 | |---|---|---|---| | 1 | **BYOUD-Gap** (零 `.pdata` 修改) | `chain.rs` -- ghost frame 链构造 | 利用 `RUNTIME_FUNCTION` 条目之间的间隙;展开器将它们视为叶帧 (RSP += 8) | | 2 | **ETW-Ti APC 窗口攻击** | `inject.rs` -- `NtDelayExecution` 可警告状态耗尽 | 通过操纵线程的可警告状态,控制 ETW-Ti APC 栈快照何时触发 | | 3 | **BYOUD 上下文中的参数加密** | `veh.rs` -- `pcrypt_arm()`, `param_encrypt_veh()` | 在暂存时对 syscall 参数进行 XOR 加密;在 `syscall` 指令处的硬件断点 VEH 内进行解密 | | 4 | **Win32u NOP 间隙链 + Ghost Gadget** | `scan.rs` -- `win32u_nop_gap()`, `scan_ghost_gadgets()` | `win32u.dll` 中的 1,242 个 NOP 间隙提供了白名单叶帧;位于 `ntdll+0xFC47B` 处的 `JMP [RBX]` ghost gadget | | 5 | **kernelbase 语义 Ghost 邻近** | `chain.rs` -- 靠近 `VirtualProtect` 的 `L2_kbase` 层 | 在 `VirtualProtect` 入口点结束的 231 字节 ghost frame —— 与真实的 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 Ghost 邻近** | `chain.rs` -- `L1_wow64` 层, `scan.rs` -- 目标 `Wow64PrepareForException` | 在 `Wow64PrepareForException` 入口处结束的 91 字节 ghost frame —— 链的第四个语义层 | | 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 窗口攻击控制快照时机 | | 参数检查 | **规避** | 硬件断点 VEH 解密 | | 内核回调 | **部分** | 句柄操作仍会触发 `ObRegisterCallbacks` | ## 为什么不直接使用 `syscalls-rs`? `syscalls-rs` 和 `wsyscall-rs` 根据特定 Windows 版本的 ntdll 生成**编译时 SSN 表**。如果目标机器运行的是不同的版本,SSN 将是错误的,syscall 会失败(或触发 EDR 启发式检测)。 `lacuna-rs` 通过直接读取 ntdll 的 stubs,在**运行时**解析 SSN,并定位到**函数自身的 `syscall;ret` 指令**,从而确保在内核入口处 RIP 位于 ntdll 内部——击败了 EDR 的“SSN 不匹配”和“来自无后备内存的间接 syscall”启发式检测。 此外,`lacuna-rs` 提供了 `syscalls-rs` 不具备的 **ghost-frame 栈欺骗**链。 ## 构建要求 - **目标:** `x86_64-pc-windows-msvc` (或 `x86_64-pc-windows-gnu`) - **Rust 版本:** 2021 - **依赖项:** `litcrypt2` (编译时字符串混淆) ### 帧指针设置(仅针对 `stack-spoof`) `stack-spoof` 特性需要帧指针。`build.rs` 会自动为本 crate 设置 `force-frame-pointers=yes`,但**消费方 crate**也必须在其 `.cargo/config.toml` 中进行设置: ``` # .cargo/config.toml(在 consuming crate 中) [build] rustflags = ["-C", "force-frame-pointers=yes"] ``` ### litcrypt2 字符串混淆 crate 和示例中的所有字符串字面量都包裹在 `lc!()` 宏中,这会在编译时对它们进行加密,并在运行时进行解密。加密密钥从 `LITCRYPT_ENCRYPT_KEY` 环境变量中读取;如果未设置,litcrypt2 会自动生成一个随机密钥。 要为可重现构建固定密钥: ``` set LITCRYPT_ENCRYPT_KEY=your-secret-key cargo build ``` ## 架构 ``` lacuna-rs/ ├── Cargo.toml ├── build.rs # Sets force-frame-pointers for stack-spoof ├── src/ │ ├── lib.rs # Crate root + re-exports + scan_all() + litcrypt2 setup │ ├── win.rs # Win32/NT FFI bindings (no_std-compatible) │ ├── pe.rs # PE section + export parsing │ ├── scan.rs # .pdata ghost-region + gadget scanning │ ├── nt.rs # SSN resolution + syscall;ret targeting │ ├── stub.rs # JIT indirect-syscall stub emission │ ├── veh.rs # VEH + hardware-breakpoint param encryption │ ├── chain.rs # LACUNA ghost-frame chain + stomp │ └── inject.rs # Section-based APC injection ├── examples/ │ ├── scan.rs # lacuna.exe scan │ ├── verify.rs # lacuna.exe verify │ └── inject.rs # lacuna.exe inject └── .cargo/ └── config.toml # force-frame-pointers + windows-msvc target ``` ## 完全披露 本项目得到了 AI 聊天机器人的积极协助。我有机会测试了一个全新的、非常强大的开源模型(由 Z.ai (Zhipu AI) 开发的 GLM-5.2),并想给它一个具有挑战性的任务。 经验教训:如果人类知道结果应该是什么样子,它就能很好地工作。如果不知道……那就只能产出垃圾了。 ## 许可证 MIT
标签:DNS 反向解析, Hpfeeds, Rust, 免杀技术, 可视化界面, 底层开发, 暴力破解检测, 系统调用, 网络流量审计, 通知系统