
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.
Spoofing de pilha de chamadas com ghost-frame + syscalls indiretas em runtime 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 que a TTP original em C, estruturada para poder ser inserida em qualquer projeto Rust — como wsyscall-rs ou syscalls-rs, mas com resolução de SSN em runtime, direcionamento de syscall;ret por função e spoofing de pilha com ghost-frame.
A feature
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 do endereço de retorno do chamador via inline asm mov rbp, {x}. Isto requer que a cadeia RBP esteja intacta. O Rust (e a maioria dos compiladores em modo release) omite frame pointers por padrão.
O build.rs define force-frame-pointers=yes para a codegen desta própria crate, mas o Cargo não consegue propagar flags de compilador para crates dependentes. Deve adicionar isto ao seu próprio projeto:```toml
[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). O crate não tem como detectar em tempo de execução se os frame pointers estão habilitados — simplesmente não funcionará.
Se você precisa apenas do scanning, da resolução de SSN ou das primitivas de injeção (sem stack spoofing), você pode omitir a feature `stack-spoof` e este requisito não se aplica.
---
## O que ele faz
| Primitiva | Função C | Módulo Rust |
|---|---|---|
| Análise de seção PE + exportação | `pe_section()`, `pe_export()` | `pe` |
| Varredura de região fantasma `.pdata` | `scan_ghosts()`, `best_ghost()` | `scan` |
| Descoberta de ghost-gadget (`jmp [rbx]`) | `scan_ghost_gadgets()` | `scan` |
| Localizador de lacuna NOP em `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 de syscall indireta JIT | `alloc_stub()` | `stub` |
| Redirecionamento de stub de ghost-gadget | (em `alloc_stub()`) | `stub` |
| Criptografia de parâmetros VEH + hardware-breakpoint | `param_encrypt_veh()`, `pcrypt_arm()` | `veh` |
| VEH de chain-guard | `chain_veh()` | `veh` |
| Construção de cadeia LACUNA | `build_chain()` | `chain` |
| Stack stomp (BYOUD-RT) | `stomp_plant()`, `stomp_restore()` | `chain` |
| Chain walker (verificação) | `lacuna_walk_chain()` | `chain` |
| Injeção APC baseada em seção | `do_inject_sapc()` | `inject` |
---
## Feature flags```toml
[dependencies]
lacuna-rs = { version = "0.4", features = ["inject", "stack-spoof", "veh"] }
| Recurso | 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 | Criptografia de parâmetros VEH + hardware-breakpoint | Não |
| stack-spoof | Cadeia de ghost-frame LACUNA + stack stomp | Sim |
| no-std | Modo no_std (experimental) | Não |
Quando nenhum recurso está habilitado, apenas as camadas de scanning/PE/NT estão disponíveis.
cargo run --example scan
### Compilar + verificar a cadeia de ghost-frame```sh
cargo run --example verify --features stack-spoof
Prova de injeção -- o payload foi executado dentro do processo alvo.```sh
cargo run --example inject --features inject,stack-spoof,veh -- <sc.bin>
Adicione `--verbose` para habilitar a saída de diagnóstico do VEH (despejos de pilha, impressões de registradores):```sh
cargo run --example inject --features inject,stack-spoof,veh -- <pid> <sc.bin> --verbose
inject_sapc classifica as threads do alvo. O sinal de classificação mais forte é
se uma thread possui uma janela:
EnumWindows +
GetWindowThreadProcessId. Uma thread de GUI estaciona em uma espera alertável em seu
loop de mensagens, então um APC enfileirado executa lá primeiro. +1000.NtQueryInformationThread(ThreadCycleTime); recorre a
ThreadTimes quando a classe não está disponível na build em execução (veja
examples/tprobe.rs). Normalizado e adicionado.NtQueryInformationThread(ThreadTimes) — tempo de kernel + usuário.NtQueryInformationThread(ThreadSuspendCount). Uma thread suspensa
não pode executar um APC: −2000 por contagem de suspensão.NtQueryInformationThread(ThreadBasicInformation). Prioridade
8–10 recebe +150; prioridade fora do intervalo recebe −300.Filtragem. Threads suspensas e threads estacionadas em uma espera de kernel não alertável —
WrDelayExecution (Sleep/NtDelayExecution) ou WrSuspended, lidas de
NtQuerySystemInformation(SystemProcessInformation) — são descartadas de imediato (elas
nunca podem receber um APC). O motivo da espera é, fora isso, apenas diagnóstico: é
registrado por candidato como State: / Reason: / Alert:, mas não
influencia a pontuação, porque KWAIT_REASON não codifica alertabilidade (uma
espera de objeto alertável e uma não alertável reportam o mesmo valor — verificado com
sondagem no estilo de examples/tprobe.rs).
Os candidatos são ordenados (maior pontuação, desempate por GUI e depois ciclos) e
truncados para MAX_APC_CANDIDATES (10) para enumeração/logging.
Enumerar ≠ injetar. O número de threads listadas é desacoplado do número injetado.
MAX_APC_CANDIDATES(10) controla quantas são classificadas e registradas;APC_INJECT_THREADS(5) controla quantas realmente recebem o APC.Por que injetar em várias, depois tentar novamente: o modo usuário não consegue dizer qual thread está em uma espera alertável, e uma única thread escolhida frequentemente nunca entra em uma, então o APC nunca dispararia.
inject_sapcportanto enfileira o APC para várias threads e então reenfileira em um loop curto (~80 ms) até que a palavra de gate compartilhada mude de 0 para 1 — ou seja, até que alguma thread alvo realmente execute o payload. Ele registra se isso aconteceu:payload claimed the gate(executou) vsgate NOT claimed.Por que executar uma vez: o mesmo shellcode/contexto é enfileirado para cada thread, então sem proteção um payload não reentrante executaria concorrentemente e travaria o host (medido: 3 execuções simultâneas). O gate de reentrância (
USE_REENTRANCY_GATE, ativado por padrão) faz o corpo do payload executar exatamente uma vez independentemente de quantas threads (ou quantas rodadas de retry) recebam o APC (medido: exatamente 1). Deixe o gate ativado a menos que seu payload seja explicitamente seguro para concorrência.
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);