Skip to content
KitploitKITPLOIT
工具漏洞利用博客
Log in
提交
工具漏洞利用博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

订阅源联系隐私© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
lacuna-rs — 用于Windows x64上的幽灵帧调用栈欺骗、运行时间接系统调用和APC注入的Rust crate。提供SSN解析、JIT存根生成和基于VEH的参数加密,用于绕过EDR。 | Kitploit
工具/GitHubGitHub/karkas66/lacuna-rs
Shellcode后渗透利用红队Payload 开发二进制利用
GitHubkarkas66/lacuna-rs

lacuna-rs

用于Windows x64上的幽灵帧调用栈欺骗、运行时间接系统调用和APC注入的Rust crate。提供SSN解析、JIT存根生成和基于VEH的参数加密,用于绕过EDR。

查看仓库
1817219天前Kitploit 审核通过

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

lacuna-rs

适用于 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(以及大多数 release 模式编译器)默认省略帧指针。

build.rs 为本 crate 自身的代码生成设置了 force-frame-pointers=yes,但 Cargo 无法将编译器标志传播到下游 crate。 你必须在你自己的项目中添加以下内容:```toml

.cargo/config.toml (in YOUR crate, not in lacuna-rs)

[build] rustflags = ["-C", "force-frame-pointers=yes"]

如果没有这个,`stomp_plant()` 将从 RBP 读取垃圾数据,要么什么都不做(最好的情况),要么破坏栈(最坏的情况)。该 crate 无法在运行时检测帧指针是否启用——它只会直接不工作。

如果你只需要扫描、SSN 解析或注入原语(不需要栈欺骗),可以省略 `stack-spoof` feature,此要求便不适用。

---

## 功能

| 原语 | C 函数 | Rust 模块 |
|---|---|---|
| PE 节区 + 导出解析 | `pe_section()`, `pe_export()` | `pe` |
| `.pdata` 幽灵区域扫描 | `scan_ghosts()`, `best_ghost()` | `scan` |
| 幽灵 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 间接系统调用桩生成 | `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` |

---

## Feature 标志```toml
[dependencies]
lacuna-rs = { version = "0.4", features = ["inject", "stack-spoof", "veh"] }
功能描述需要帧指针?
syscalls(默认)SSN 解析 + JIT 存根生成否
inject(默认)基于节的 APC 注入(inject::inject_sapc)否
vehVEH + 硬件断点参数加密否
stack-spoofLACUNA 幽灵帧链 + 栈覆盖是
no-stdno_std 模式(实验性)否

当未启用任何功能时,仅扫描/PE/NT 层可用。


快速开始

扫描幽灵区域```sh

cargo run --example scan

### 构建并验证幽灵帧链```sh
cargo run --example verify --features stack-spoof

通过 section + APC 注入 shellcode

注入证明 注入证明——payload 在目标进程内执行。```sh cargo run --example inject --features inject,stack-spoof,veh -- <sc.bin>

添加 `--verbose` 以启用 VEH 诊断输出(堆栈转储、寄存器打印):```sh
cargo run --example inject --features inject,stack-spoof,veh -- <pid> <sc.bin> --verbose

线程评分算法

inject_sapc 对目标线程进行排名。最强的排名信号是线程是否拥有一个窗口:

  • 拥有窗口(GUI/消息泵线程)—— EnumWindows + GetWindowThreadProcessId。GUI 线程在其消息循环中停驻于可告警等待状态,因此排队的 APC 会首先在那里运行。+1000。
  • 周期时间 —— NtQueryInformationThread(ThreadCycleTime);当运行版本上该类不可用时回退到 ThreadTimes(参见 examples/tprobe.rs)。归一化后累加。
  • CPU 时间 —— NtQueryInformationThread(ThreadTimes) —— 内核 + 用户时间。
  • 挂起计数 —— NtQueryInformationThread(ThreadSuspendCount)。被挂起的线程无法运行 APC:每次挂起计数 −2000。
  • 优先级 —— NtQueryInformationThread(ThreadBasicInformation)。优先级 8–10 获得 +150;超出范围的优先级获得 −300。

过滤。 被挂起的线程以及停驻在不可告警内核等待中的线程 —— WrDelayExecution(Sleep/NtDelayExecution)或 WrSuspended,从 NtQuerySystemInformation(SystemProcessInformation) 读取 —— 会被直接丢弃(它们永远无法接受 APC)。等待原因在其他情况下仅用于诊断:它会按候选线程记录为 State: / Reason: / Alert:,但不会影响评分,因为 KWAIT_REASON 并不编码可告警性(可告警和不可告警的对象等待报告相同的值 —— 已通过 examples/tprobe.rs 风格的探测验证)。

候选线程会被排序(最高分优先,平局时先 GUI 后周期)并截断至 MAX_APC_CANDIDATES(10)用于枚举/日志记录。

枚举 ≠ 注入。 列出的线程数与注入的线程数是解耦的。MAX_APC_CANDIDATES(10)控制有多少线程被排名和记录;APC_INJECT_THREADS(5)控制实际有多少线程接收 APC。

为什么要注入多个线程然后重试: 用户模式无法判断哪个线程处于可告警等待中,而单个选定的线程往往永远不会进入该状态,因此 APC 永远不会触发。inject_sapc 因此将 APC 排队到多个线程,然后在短循环(约 80 毫秒)中重新排队,直到共享门控字从 0 翻转为 1 —— 即直到某个目标线程实际运行了载荷。它会记录是否发生了这种情况:payload claimed the gate(已运行)对比 gate NOT claimed。

为什么只运行一次: 相同的 shellcode/上下文被排队到每个线程,因此如果没有保护,非可重入载荷会并发运行并使主机崩溃(实测:3 次同时执行)。可重入门控(USE_REENTRANCY_GATE,默认开启)使载荷主体恰好运行一次,无论有多少线程(或多少轮重试)接受了 APC(实测:恰好 1 次)。除非你的载荷明确是并发安全的,否则请保持门控开启。


作为库使用

基础:扫描 + SSN 解析```rust

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 生成间接系统调用存根```rust
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```rust

// 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);

### 使用栈欺骗 + 参数加密包装敏感系统调用```rust
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:哪些调用需要隐藏在幽灵帧之后

并非每个 API 调用都需要完整的 LACUNA 处理。关键原则是:隐藏那些会被 EDR 关联为注入或后渗透活动的调用。

必须置于间接系统调用 + 幽灵帧 + 参数加密之后

这些是 EDR 会挂钩并关联的“皇冠明珠”级 NT 系统调用:

调用为何敏感
NtOpenProcess打开另一个进程的句柄——注入的第一步
NtCreateSection + NtMapViewOfSection(远程)基于节的注入特征
NtWriteVirtualMemory跨进程写入——经典注入指标
NtCreateThreadEx远程线程创建——信号最强的注入原语
NtQueueApcThreadAPC 注入——高信号
NtProtectVirtualMemoryRWX 权限变更——shellcode 暂存指标
NtAllocateVirtualMemory(远程)远程分配——注入前奏
NtSetInformationThread线程隐藏(HideFromDebugger)——规避指标

对于其中每一个:

  1. 使用 nt::resolve(ntdll, b"NtXxx\0") 解析
  2. 使用 stub::make_stub(ssn, syscall_ret) 生成桩
  3. 使用 veh::pcrypt_arm(key, syscall_ret, true) 对敏感参数进行 XOR 加密
  4. 将调用包裹在 chain::stomp_plant() 与 chain::stomp_restore() 之间

可以直接调用(无需幽灵帧)

调用为何安全
NtQueryInformationThread仅查询,很少被挂钩,无跨进程写入
NtDelayExecution睡眠——良性,每个应用程序都在使用
NtClose句柄关闭——良性,极其常见
GetModuleHandleA / GetProcAddress模块解析——不是系统调用,无法被 EDR 用户态钩子挂钩
CreateToolhelp32Snapshot / Thread32First / Thread32Next线程枚举——kernel32,不是 ntdll 系统调用
OpenThread / CloseHandle标准句柄操作——kernel32
GetThreadContext / SetThreadContextDR0 所需——kernel32,仅限自身进程

关键原则

下载工具