
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 mediante ghost-frame + syscalls indirectas en tiempo de ejecución para Windows x64 — portado a Rust.
Portado desde LACUNA Chain (lacuna_chain.c) por Mohamed Alzhrani (0xmaz).
lacuna-rs es un crate de Rust reutilizable que proporciona las mismas primitivas que el TTP original en C, estructurado para poder integrarse en cualquier proyecto Rust — como wsyscall-rs o syscalls-rs, pero con resolución de SSN en tiempo de ejecución, apuntado a syscall;ret por función y suplantación de pila mediante ghost-frame.
La característica
stack-spooffallará silenciosamente si el crate consumidor no se compila con punteros de marco.
Las primitivas de stack-stomping en chain.rs localizan la ranura de dirección de retorno del llamador mediante el ensamblado en línea mov rbp, {x}. 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 este crate, pero Cargo no puede propagar los flags del compilador a los crates dependientes. Debes añadir esto a tu propio proyecto:```toml
[build] rustflags = ["-C", "force-frame-pointers=yes"]
Sin esto, `stomp_plant()` leerá basura de RBP y o bien no hará nada (en el mejor de los casos) o corromperá la pila (en el peor de los casos). El 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 el spoofing de pila), puedes omitir la característica `stack-spoof` y este requisito no aplica.
---
## Qué hace
| Primitiva | Función C | Módulo Rust |
|---|---|---|
| Análisis de secciones PE + exportaciones | `pe_section()`, `pe_export()` | `pe` |
| Escaneo de regiones fantasma en `.pdata` | `scan_ghosts()`, `best_ghost()` | `scan` |
| Descubrimiento de ghost-gadgets (`jmp [rbx]`) | `scan_ghost_gadgets()` | `scan` |
| Buscador de huecos NOP en `win32u` | `win32u_nop_gap()` | `scan` |
| Buscador de anclas 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 indirecta JIT | `alloc_stub()` | `stub` |
| Redirección de stub de ghost-gadget | (en `alloc_stub()`) | `stub` |
| Cifrado de parámetros con VEH + breakpoint de hardware | `param_encrypt_veh()`, `pcrypt_arm()` | `veh` |
| VEH de protección de cadena | `chain_veh()` | `veh` |
| Construcción de cadena LACUNA | `build_chain()` | `chain` |
| Stack stomp (BYOUD-RT) | `stomp_plant()`, `stomp_restore()` | `chain` |
| Recorredor de cadena (verificación) | `lacuna_walk_chain()` | `chain` |
| Inyección APC basada en secciones | `do_inject_sapc()` | `inject` |
---
## Indicadores de características```toml
[dependencies]
lacuna-rs = { version = "0.4", features = ["inject", "stack-spoof", "veh"] }
| Característica | Descripción | ¿Requiere punteros de marco? |
|---|---|---|
syscalls (predeterminado) | Resolución de SSN + emisión de stub JIT | No |
inject (predeterminado) | Inyección APC basada en secciones (inject::inject_sapc) | No |
veh | Cifrado de parámetros VEH + punto de interrupción por hardware | No |
stack-spoof | Cadena de marcos fantasma LACUNA + stack stomp | 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
### Construir + verificar la cadena de marcos fantasma```sh
cargo run --example verify --features stack-spoof
Prueba de inyección -- el payload se ejecutó dentro del proceso objetivo.```sh
cargo run --example inject --features inject,stack-spoof,veh -- <sc.bin>
Añade `--verbose` para habilitar la salida de diagnóstico de VEH (volcados de pila, impresiones de registros):```sh
cargo run --example inject --features inject,stack-spoof,veh -- <pid> <sc.bin> --verbose
inject_sapc clasifica los hilos del objetivo. La señal de clasificación más fuerte es
si un hilo posee una ventana:
EnumWindows +
GetWindowThreadProcessId. Un hilo GUI se estaciona en una espera alertable en su
bucle de mensajes, por lo que una APC en cola se ejecuta allí primero. +1000.NtQueryInformationThread(ThreadCycleTime); recurre a
ThreadTimes cuando la clase no está disponible en la compilación en ejecución (ver
examples/tprobe.rs). Normalizado y sumado.NtQueryInformationThread(ThreadTimes) — tiempo de kernel + usuario.NtQueryInformationThread(ThreadSuspendCount). Un hilo
suspendido no puede ejecutar una APC: −2000 por conteo de suspensión.NtQueryInformationThread(ThreadBasicInformation). Prioridad
8–10 obtiene +150; prioridad fuera de rango obtiene −300.Filtrado. Los hilos suspendidos y los hilos estacionados en una espera de kernel
no alertable — WrDelayExecution (Sleep/NtDelayExecution) o WrSuspended, leídos de
NtQuerySystemInformation(SystemProcessInformation) — se descartan directamente (nunca
pueden tomar una APC). El motivo de espera por lo demás es solo diagnóstico: se
registra por candidato como State: / Reason: / Alert:, pero no
influye en la puntuación, porque KWAIT_REASON no codifica la alertabilidad (una
espera de objeto alertable y una no alertable reportan el mismo valor — verificado con
sondeo estilo examples/tprobe.rs).
Los candidatos se ordenan (mayor puntuación, desempate GUI luego ciclos) y se
truncan a MAX_APC_CANDIDATES (10) para enumeración/registro.
Enumerar ≠ inyectar. El número de hilos listados está desacoplado del número inyectados.
MAX_APC_CANDIDATES(10) controla cuántos se clasifican y registran;APC_INJECT_THREADS(5) controla cuántos reciben realmente la APC.Por qué inyectar a varios, luego reintentar: el modo usuario no puede saber qué hilo está en una espera alertable, y un único hilo elegido a menudo nunca entra en una, por lo que la APC nunca se dispararía.
inject_sapcpor lo tanto encola la APC a varios hilos y luego reencola en un bucle corto (~80 ms) hasta que la palabra de puerta compartida cambie de 0 a 1 — es decir, hasta que algún hilo objetivo realmente ejecute el payload. Registra si eso ocurrió:payload claimed the gate(ejecutado) vsgate NOT claimed.Por qué ejecutar una vez: el mismo shellcode/contexto se encola a cada hilo, por lo que sin protección un payload no reentrante se ejecutaría concurrentemente y bloquearía el host (medido: 3 ejecuciones simultáneas). La puerta de reentrancia (
USE_REENTRANCY_GATE, activada por defecto) hace que el cuerpo del payload se ejecute exactamente una vez independientemente de cuántos hilos (o cuántas rondas de reintento) tomen la APC (medido: exactamente 1). Deja la puerta activada a menos que tu payload sea explícitamente seguro para concurrencia.
use lacuna::{scan, nt, win::get_module};
let ntdll = get_module(b"ntdll.dll\0");