
Crate Rust per ghost-frame call-stack spoofing, runtime indirect syscalls e APC injection su Windows x64. Fornisce risoluzione SSN, emissione di stub JIT e crittografia dei parametri basata su VEH per evasione di EDR.
Ghost-frame call-stack spoofing + runtime indirect syscalls per Windows x64 — portato in Rust.
Portato da LACUNA Chain (lacuna_chain.c) di Mohamed Alzhrani (0xmaz).
lacuna-rs è una crate Rust riutilizzabile che fornisce le stesse primitive del TTP originale in C, strutturata in modo da poter essere inserita in qualsiasi progetto Rust — come wsyscall-rs o syscalls-rs, ma con risoluzione runtime degli SSN, targeting syscall;ret per funzione e ghost-frame stack spoofing.
La feature
stack-spooffallirà silenziosamente se la crate che la utilizza non è compilata con i frame pointer.
Le primitive di stack-stomping in chain.rs individuano lo slot dell'indirizzo di ritorno del chiamante tramite l'asm inline mov rbp, {x}. Questo richiede che la catena RBP sia intatta. Rust (e la maggior parte dei compilatori in modalità release) omette i frame pointer per impostazione predefinita.
build.rs imposta force-frame-pointers=yes per la codegen di questa crate, ma Cargo non può propagare i flag del compilatore alle crate a valle. Devi aggiungere questo al tuo progetto:```toml
[build] rustflags = ["-C", "force-frame-pointers=yes"]
Senza questo, `stomp_plant()` leggerà dati spazzatura da RBP e o non farà nulla (caso migliore) o corromperà lo stack (caso peggiore). Il crate non ha modo di rilevare a runtime se i frame pointer sono abilitati — semplicemente non funzionerà.
Se hai bisogno solo dello scanning, della risoluzione SSN o delle primitive di injection (senza stack spoofing), puoi omettere la feature `stack-spoof` e questo requisito non si applica.
---
## Cosa fa
| 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.4", features = ["inject", "stack-spoof", "veh"] }
| Funzionalità | Descrizione | Richiede frame pointer? |
|---|---|---|
syscalls (predefinito) | Risoluzione SSN + emissione stub JIT | No |
inject (predefinito) | Iniezione APC basata su sezione (inject::inject_sapc) | No |
veh | VEH + cifratura parametri con breakpoint hardware | No |
stack-spoof | Catena ghost-frame LACUNA + stack stomp | Sì |
no-std | Modalità no_std (sperimentale) | No |
Quando nessuna funzionalità è abilitata, sono disponibili solo i livelli scanning/PE/NT.
cargo run --example scan
### Compila e verifica la catena ghost-frame```sh
cargo run --example verify --features stack-spoof
Prova di iniezione -- il payload è stato eseguito all'interno del processo target.```sh
cargo run --example inject --features inject,stack-spoof,veh -- <sc.bin>
Aggiungi `--verbose` per abilitare l'output diagnostico VEH (dump dello stack, stampe dei registri):```sh
cargo run --example inject --features inject,stack-spoof,veh -- <pid> <sc.bin> --verbose
inject_sapc classifica i thread del target. Il segnale di ranking più forte è
se un thread possiede una finestra:
EnumWindows +
GetWindowThreadProcessId. Un thread GUI si blocca in un'attesa alertable nel suo
message loop, quindi un APC accodato viene eseguito lì per primo. +1000.NtQueryInformationThread(ThreadCycleTime); ricade su
ThreadTimes quando la classe non è disponibile sulla build in esecuzione (vedi
examples/tprobe.rs). Normalizzato e sommato.NtQueryInformationThread(ThreadTimes) — tempo kernel + user.NtQueryInformationThread(ThreadSuspendCount). Un thread
sospeso non può eseguire un APC: −2000 per conteggio sospensione.NtQueryInformationThread(ThreadBasicInformation). Priorità
8–10 ottiene +150; priorità fuori range ottiene −300.Filtraggio. I thread sospesi e i thread bloccati in un'attesa kernel non
alertable — WrDelayExecution (Sleep/NtDelayExecution) o WrSuspended, letti da
NtQuerySystemInformation(SystemProcessInformation) — vengono scartati del tutto (non
possono mai ricevere un APC). Il motivo dell'attesa è altrimenti solo diagnostico: viene
registrato per ogni candidato come State: / Reason: / Alert:, ma non
influenza il punteggio, perché KWAIT_REASON non codifica l'alertability (un'attesa
alertable e una non alertable su un oggetto riportano lo stesso valore — verificato con
probing in stile examples/tprobe.rs).
I candidati vengono ordinati (punteggio più alto, spareggio GUI poi cicli) e
troncati a MAX_APC_CANDIDATES (10) per enumerazione/logging.
Enumerare ≠ iniettare. Il numero di thread elencati è disaccoppiato dal numero iniettati.
MAX_APC_CANDIDATES(10) controlla quanti vengono classificati e registrati;APC_INJECT_THREADS(5) controlla quanti ricevono effettivamente l'APC.Perché iniettare su diversi, poi riprovare: lo user mode non può determinare quale thread sia in un'attesa alertable, e un singolo thread scelto spesso non ne entra mai una, quindi l'APC non verrebbe mai eseguito.
inject_sapcquindi accoda l'APC a diversi thread e poi riaccoda in un breve loop (~80 ms) finché la gate word condivisa non passa da 0 a 1 — cioè finché qualche thread target non ha effettivamente eseguito il payload. Registra se ciò è avvenuto:payload claimed the gate(eseguito) vsgate NOT claimed.Perché eseguire una sola volta: lo stesso shellcode/contesto viene accodato a ogni thread, quindi senza protezione un payload non reentrante verrebbe eseguito concorrentemente e manderebbe in crash l'host (misurato: 3 esecuzioni simultanee). Il gate di reentrancy (
USE_REENTRANCY_GATE, attivo di default) fa sì che il corpo del payload venga eseguito esattamente una volta indipendentemente da quanti thread (o quanti round di retry) prendono l'APC (misurato: esattamente 1). Lascia il gate attivo a meno che il tuo payload non sia esplicitamente concurrency-safe.
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);