
Rust-Crate für Ghost-Frame Call-Stack Spoofing, indirekte Syscalls zur Laufzeit und APC-Injektion auf Windows x64. Bietet SSN-Auflösung, JIT-Stub-Erzeugung und VEH-basierte Parameter-Verschlüsselung zur EDR-Umgehung.
Ghost-Frame-Call-Stack-Spoofing + Laufzeit-Indirekte-Syscalls für Windows x64 — nach Rust portiert.
Portiert von LACUNA Chain (lacuna_chain.c) von Mohamed Alzhrani (0xmaz).
lacuna-rs ist eine wiederverwendbare Rust-Crate, die dieselben Primitive wie das ursprüngliche C-TTP bereitstellt, so strukturiert, dass sie in jedes Rust-Projekt eingebunden werden kann — wie wsyscall-rs oder syscalls-rs, aber mit Laufzeit-SSN-Auflösung, pro-Funktion syscall;ret-Targeting und Ghost-Frame-Stack-Spoofing.
Das
stack-spoof-Feature wird stillschweigend fehlschlagen, wenn die konsumierende Crate nicht mit Frame-Pointern kompiliert wird.
Die Stack-Stomping-Primitive in chain.rs lokalisieren den Return-Address-Slot des Aufrufers über mov rbp, {x} Inline-Asm. Dies erfordert, dass die RBP-Kette intakt ist. Rust (und die meisten Release-Mode-Compiler) lassen Frame-Pointer standardmäßig weg.
build.rs setzt force-frame-pointers=yes für die eigene Codegen dieser Crate, aber Cargo kann Compiler-Flags nicht an nachgelagerte Crates weitergeben. Sie müssen dies zu Ihrem eigenen Projekt hinzufügen:```toml
[build] rustflags = ["-C", "force-frame-pointers=yes"]
Ohne dies liest `stomp_plant()` Müll aus RBP und führt entweder zu einem No-Op (im besten Fall) oder beschädigt den Stack (im schlimmsten Fall). Das Crate hat keine Möglichkeit, zur Laufzeit zu erkennen, ob Frame Pointer aktiviert sind — es wird einfach nicht funktionieren.
Wenn Sie nur die Scanning-, SSN-Auflösungs- oder Injection-Primitiven benötigen (ohne Stack Spoofing), können Sie das Feature `stack-spoof` weglassen, und diese Anforderung entfällt.
---
## Was es tut
| Primitiv | C-Funktion | Rust-Modul |
|---|---|---|
| PE-Section- + Export-Parsing | `pe_section()`, `pe_export()` | `pe` |
| `.pdata` Ghost-Region-Scanning | `scan_ghosts()`, `best_ghost()` | `scan` |
| Ghost-Gadget-Erkennung (`jmp [rbx]`) | `scan_ghost_gadgets()` | `scan` |
| `win32u` NOP-Gap-Finder | `win32u_nop_gap()` | `scan` |
| BYOUD-MF Anchor-Finder | `find_mf_target()` | `scan` |
| SSN-Auflösung (Hell's Gate / Halo's Gate) | `resolve_ssn()` | `nt` |
| Per-Funktion `syscall;ret`-Locator | `find_func_syscall()` | `nt` |
| JIT-Indirect-Syscall-Stub-Emittierung | `alloc_stub()` | `stub` |
| Ghost-Gadget-Stub-Redirect | (in `alloc_stub()`) | `stub` |
| VEH + Hardware-Breakpoint-Param-Verschlüsselung | `param_encrypt_veh()`, `pcrypt_arm()` | `veh` |
| Chain-Guard-VEH | `chain_veh()` | `veh` |
| LACUNA-Chain-Konstruktion | `build_chain()` | `chain` |
| Stack Stomp (BYOUD-RT) | `stomp_plant()`, `stomp_restore()` | `chain` |
| Chain Walker (Verifizierung) | `lacuna_walk_chain()` | `chain` |
| Section-basierte APC-Injection | `do_inject_sapc()` | `inject` |
---
## Feature-Flags```toml
[dependencies]
lacuna-rs = { version = "0.4", features = ["inject", "stack-spoof", "veh"] }
| Funktion | Beschreibung | Erfordert Frame Pointer? |
|---|---|---|---|
| syscalls (Standard) | SSN-Auflösung + JIT-Stub-Emittierung | Nein |
| inject (Standard) | Abschnittsbasierte APC-Injektion (inject::inject_sapc) | Nein |
| veh | VEH + Hardware-Breakpoint-Parameter-Verschlüsselung | Nein |
| stack-spoof | LACUNA Ghost-Frame-Kette + Stack Stomp | Ja |
| no-std | no_std-Modus (experimentell) | Nein |
Wenn keine Features aktiviert sind, stehen nur die Scanning-/PE-/NT-Ebenen zur Verfügung.
cargo run --example scan
### Die Ghost-Frame-Kette erstellen und verifizieren```sh
cargo run --example verify --features stack-spoof
Injektionsnachweis -- die Payload wurde im Zielprozess ausgeführt.```sh
cargo run --example inject --features inject,stack-spoof,veh -- <sc.bin>
Fügen Sie `--verbose` hinzu, um die VEH-Diagnoseausgabe zu aktivieren (Stack-Dumps, Registerausgaben):```sh
cargo run --example inject --features inject,stack-spoof,veh -- <pid> <sc.bin> --verbose
inject_sapc bewertet die Threads des Ziels. Das stärkste Ranking-Signal ist,
ob ein Thread ein Fenster besitzt:
EnumWindows +
GetWindowThreadProcessId. Ein GUI-Thread parkt in einem alertable Wait in seiner
Message-Loop, sodass ein eingereihter APC dort zuerst ausgeführt wird. +1000.NtQueryInformationThread(ThreadCycleTime); fällt auf
ThreadTimes zurück, wenn die Klasse im laufenden Build nicht verfügbar ist (siehe
examples/tprobe.rs). Normalisiert und addiert.NtQueryInformationThread(ThreadTimes) — Kernel- + User-Zeit.NtQueryInformationThread(ThreadSuspendCount). Ein suspendierter
Thread kann keinen APC ausführen: −2000 pro Suspend-Anzahl.NtQueryInformationThread(ThreadBasicInformation). Priorität
8–10 erhält +150; Priorität außerhalb des Bereichs erhält −300.Filterung. Suspendierte Threads und Threads, die in einem nicht-alertable Kernel-
Wait parken — WrDelayExecution (Sleep/NtDelayExecution) oder WrSuspended, gelesen aus
NtQuerySystemInformation(SystemProcessInformation) — werden direkt verworfen (sie
können niemals einen APC annehmen). Der Wait-Grund ist ansonsten nur diagnostisch: Er wird
pro Kandidat als State: / Reason: / Alert: protokolliert, beeinflusst aber nicht
die Bewertung, da KWAIT_REASON die Alertability nicht kodiert (ein
alertable und ein nicht-alertable Object-Wait melden denselben Wert — verifiziert mit
examples/tprobe.rs-artiger Probing).
Die Kandidaten werden sortiert (höchste Punktzahl, Tie-Break GUI, dann Zyklen) und
auf MAX_APC_CANDIDATES (10) für Enumeration/Logging gekürzt.
Enumerieren ≠ Injizieren. Die Anzahl der aufgelisteten Threads ist entkoppelt von der Anzahl der injizierten.
MAX_APC_CANDIDATES(10) steuert, wie viele bewertet und protokolliert werden;APC_INJECT_THREADS(5) steuert, wie viele tatsächlich den APC erhalten.Warum in mehrere injizieren und dann wiederholen: Der User-Mode kann nicht erkennen, welcher Thread sich in einem alertable Wait befindet, und ein einzelner ausgewählter Thread tritt oft nie in einen ein, sodass der APC niemals ausgelöst würde.
inject_sapcreiht den APC daher in mehrere Threads ein und reiht ihn dann in einer kurzen Schleife (~80 ms) erneut ein, bis das gemeinsame Gate-Wort von 0 auf 1 umschlägt — d. h. bis ein Ziel-Thread tatsächlich die Payload ausgeführt hat. Es protokolliert, ob dies geschehen ist:payload claimed the gate(ausgeführt) vs.gate NOT claimed.Warum nur einmal ausführen: Derselbe Shellcode/Kontext wird in jeden Thread eingereiht, sodass ohne Schutz eine nicht-reentrante Payload gleichzeitig ausgeführt würde und den Host zum Absturz bringen würde (gemessen: 3 gleichzeitige Ausführungen). Das Reentrancy-Gate (
USE_REENTRANCY_GATE, standardmäßig aktiviert) sorgt dafür, dass der Payload-Body genau einmal ausgeführt wird, unabhängig davon, wie viele Threads (oder wie viele Wiederholungsrunden) den APC annehmen (gemessen: genau 1). Lassen Sie das Gate aktiviert, es sei denn, Ihre Payload ist explizit nebenläufigkeitssicher.
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);