Skip to content
KitploitKITPLOIT
ToolsExploitsBlog
Log in
Submit
ToolsExploitsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

FeedsContactPrivacy© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
lacuna-rs — Rust crate for ghost-frame call-stack spoofing, runtime indirect syscalls, and APC injection on Windows x64. Provides SSN resolution, JIT stub emission, and VEH-based parameter encryption for EDR evasion. | Kitploit
Tools/GitHubGitHub/karkas66/lacuna-rs
ShellcodePost-ExploitationRed TeamingPayload DevelopmentBinary Exploitation
GitHubkarkas66/lacuna-rs

lacuna-rs

Rust crate for ghost-frame call-stack spoofing, runtime indirect syscalls, and APC injection on Windows x64. Provides SSN resolution, JIT stub emission, and VEH-based parameter encryption for EDR evasion.

View Repository
1817219 days agoReviewed by Kitploit

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

lacuna-rs

Ghost-frame call-stack spoofing + runtime indirect syscalls for Windows x64 — ported to Rust.

Ported from LACUNA Chain (lacuna_chain.c) by Mohamed Alzhrani (0xmaz).

lacuna-rs is a reusable Rust crate that provides the same primitives as the original C TTP, structured so it can be dropped into any Rust project — like wsyscall-rs or syscalls-rs, but with runtime SSN resolution, per-function syscall;ret targeting, and ghost-frame stack spoofing.


Critical: Frame-Pointer Requirement

The stack-spoof feature will silently fail if the consuming crate is not compiled with frame pointers.

The stack-stomping primitives in chain.rs locate the caller's return-address slot via mov rbp, {x} inline asm. This requires the RBP chain to be intact. Rust (and most release-mode compilers) omit frame pointers by default.

build.rs sets force-frame-pointers=yes for this crate's own codegen, but Cargo cannot propagate compiler flags to downstream crates. You must add this to your own project:

# .cargo/config.toml  (in YOUR crate, not in lacuna-rs)
[build]
rustflags = ["-C", "force-frame-pointers=yes"]

Without this, stomp_plant() will read garbage from RBP and either no-op (best case) or corrupt the stack (worst case). The crate has no way to detect at runtime whether frame pointers are enabled — it will simply not work.

If you only need the scanning, SSN resolution, or injection primitives (without stack spoofing), you can omit the stack-spoof feature and this requirement does not apply.


What it does

PrimitiveC functionRust module
PE section + export parsingpe_section(), pe_export()pe
.pdata ghost-region scanningscan_ghosts(), best_ghost()scan
Ghost-gadget discovery (jmp [rbx])scan_ghost_gadgets()scan
win32u NOP-gap finderwin32u_nop_gap()scan
BYOUD-MF anchor finderfind_mf_target()scan
SSN resolution (Hell's Gate / Halo's Gate)resolve_ssn()nt
Per-function syscall;ret locatorfind_func_syscall()nt
JIT indirect-syscall stub emissionalloc_stub()stub
Ghost-gadget stub redirect(in alloc_stub())stub
VEH + hardware-breakpoint param encryptionparam_encrypt_veh(), pcrypt_arm()veh
Chain-guard VEHchain_veh()veh
LACUNA chain constructionbuild_chain()chain
Stack stomp (BYOUD-RT)stomp_plant(), stomp_restore()chain
Chain walker (verify)lacuna_walk_chain()chain
Section-based APC injectiondo_inject_sapc()inject

Feature flags

[dependencies]
lacuna-rs = { version = "0.4", features = ["inject", "stack-spoof", "veh"] }
FeatureDescriptionRequires frame pointers?
syscalls (default)SSN resolution + JIT stub emissionNo
inject (default)Section-based APC injection (inject::inject_sapc)No
vehVEH + hardware-breakpoint parameter encryptionNo
stack-spoofLACUNA ghost-frame chain + stack stompYes
no-stdno_std mode (experimental)No

When no features are enabled, only the scanning/PE/NT layers are available.


Quick start

Scan for ghost regions

cargo run --example scan

Build + verify the ghost-frame chain

cargo run --example verify --features stack-spoof

Inject shellcode via section + APC

Injection proof Injection proof -- the payload executed inside the target process.

cargo run --example inject --features inject,stack-spoof,veh -- <pid> <sc.bin>

Add --verbose to enable VEH diagnostic output (stack dumps, register prints):

cargo run --example inject --features inject,stack-spoof,veh -- <pid> <sc.bin> --verbose

Thread scoring algorithm

inject_sapc ranks the target's threads. The strongest ranking signal is whether a thread owns a window:

  • Owns a window (GUI/message-pump thread) — EnumWindows + GetWindowThreadProcessId. A GUI thread parks in an alertable wait in its message loop, so a queued APC runs there first. +1000.
  • Cycle time — NtQueryInformationThread(ThreadCycleTime); falls back to ThreadTimes when the class is unavailable on the running build (see examples/tprobe.rs). Normalised and added.
  • CPU time — NtQueryInformationThread(ThreadTimes) — kernel + user time.
  • Suspend count — NtQueryInformationThread(ThreadSuspendCount). A suspended thread cannot run an APC: −2000 per suspend count.
  • Priority — NtQueryInformationThread(ThreadBasicInformation). Priority 8–10 gets +150; out-of-range priority gets −300.

Filtering. Suspended threads and threads parked in a non-alertable kernel wait — WrDelayExecution (Sleep/NtDelayExecution) or WrSuspended, read from NtQuerySystemInformation(SystemProcessInformation) — are dropped outright (they can never take an APC). The wait reason is otherwise diagnostic only: it is logged per candidate as State: / Reason: / Alert:, but it does not influence the score, because KWAIT_REASON does not encode alertability (an alertable and a non-alertable object wait report the same value — verified with examples/tprobe.rs-style probing).

The candidates are sorted (highest score, tie-break GUI then cycles) and truncated to MAX_APC_CANDIDATES (10) for enumeration/logging.

Enumerate ≠ inject. The number of threads listed is decoupled from the number injected. MAX_APC_CANDIDATES (10) controls how many are ranked and logged; APC_INJECT_THREADS (5) controls how many actually receive the APC.

Why inject to several, then retry: user mode cannot tell which thread is in an alertable wait, and a single chosen thread often never enters one, so the APC would never fire. inject_sapc therefore queues the APC to several threads and then re-queues in a short loop (~80 ms) until the shared gate word flips from 0 to 1 — i.e. until some target thread actually ran the payload. It logs whether that happened: payload claimed the gate (ran) vs gate NOT claimed.

Why run once: the same shellcode/context is queued to each thread, so without protection a non-reentrant payload would run concurrently and crash the host (measured: 3 simultaneous executions). The reentrancy gate (USE_REENTRANCY_GATE, default on) makes the payload body run exactly once regardless of how many threads (or how many retry rounds) take the APC (measured: exactly 1). Leave the gate on unless your payload is explicitly concurrency-safe.


Using as a library

Basic: scan + SSN resolution

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

Emit an indirect syscall stub for any NT API

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");
Download Tool