
Rust crate pour le spoofing de pile d'appels ghost-frame, les appels système indirects à l'exécution, et l'injection APC sur Windows x64. Fournit la résolution SSN, l'émission de stubs JIT, et le chiffrement de paramètres basé sur VEH pour l'évasion EDR.
Usurpation de pile d'appels par ghost-frame + syscalls indirects à l'exécution pour Windows x64 — porté en Rust.
Porté depuis LACUNA Chain (lacuna_chain.c) par Mohamed Alzhrani (0xmaz).
lacuna-rs est une crate Rust réutilisable qui fournit les mêmes primitives que le TTP C original, structurée pour pouvoir être intégrée dans n'importe quel projet Rust — comme wsyscall-rs ou syscalls-rs, mais avec résolution des SSN à l'exécution, ciblage syscall;ret par fonction, et usurpation de pile par ghost-frame.
La fonctionnalité
stack-spooféchouera silencieusement si la crate consommatrice n'est pas compilée avec les frame pointers.
Les primitives de stack-stomping dans chain.rs localisent l'emplacement de l'adresse de retour de l'appelant via l'assembleur inline mov rbp, {x}. Cela nécessite que la chaîne RBP soit intacte. Rust (et la plupart des compilateurs en mode release) omet les frame pointers par défaut.
build.rs définit force-frame-pointers=yes pour la génération de code de cette crate elle-même, mais Cargo ne peut pas propager les flags du compilateur aux crates en aval. Vous devez ajouter ceci à votre propre projet :```toml
[build] rustflags = ["-C", "force-frame-pointers=yes"]
Sans cela, `stomp_plant()` lira des données corrompues depuis RBP et soit ne fera rien (meilleur cas), soit corrompra la pile (pire cas). Le crate n'a aucun moyen de détecter à l'exécution si les frame pointers sont activés — cela ne fonctionnera tout simplement pas.
Si vous n'avez besoin que du scanning, de la résolution SSN ou des primitives d'injection (sans stack spoofing), vous pouvez omettre la feature `stack-spoof` et cette exigence ne s'applique pas.
---
## Ce qu'il fait
| Primitive | Fonction C | Module Rust |
|---|---|---|
| Analyse des sections PE + exports | `pe_section()`, `pe_export()` | `pe` |
| Scan des ghost-regions `.pdata` | `scan_ghosts()`, `best_ghost()` | `scan` |
| Découverte de ghost-gadgets (`jmp [rbx]`) | `scan_ghost_gadgets()` | `scan` |
| Recherche de NOP-gap `win32u` | `win32u_nop_gap()` | `scan` |
| Recherche d'ancre BYOUD-MF | `find_mf_target()` | `scan` |
| Résolution SSN (Hell's Gate / Halo's Gate) | `resolve_ssn()` | `nt` |
| Localisateur `syscall;ret` par fonction | `find_func_syscall()` | `nt` |
| Émission de stub d'indirect-syscall JIT | `alloc_stub()` | `stub` |
| Redirection de stub ghost-gadget | (dans `alloc_stub()`) | `stub` |
| Chiffrement des paramètres VEH + hardware-breakpoint | `param_encrypt_veh()`, `pcrypt_arm()` | `veh` |
| VEH chain-guard | `chain_veh()` | `veh` |
| Construction de chaîne LACUNA | `build_chain()` | `chain` |
| Stack stomp (BYOUD-RT) | `stomp_plant()`, `stomp_restore()` | `chain` |
| Parcours de chaîne (vérification) | `lacuna_walk_chain()` | `chain` |
| Injection APC basée sur les sections | `do_inject_sapc()` | `inject` |
---
## Feature flags```toml
[dependencies]
lacuna-rs = { version = "0.4", features = ["inject", "stack-spoof", "veh"] }
| Fonctionnalité | Description | Nécessite les pointeurs de frame ? |
|---|---|---|
syscalls (par défaut) | Résolution SSN + émission de stub JIT | Non |
inject (par défaut) | Injection APC basée sur les sections (inject::inject_sapc) | Non |
veh | VEH + chiffrement des paramètres par point d'arrêt matériel | Non |
stack-spoof | Chaîne de ghost-frames LACUNA + stack stomp | Oui |
no-std | Mode no_std (expérimental) | Non |
Lorsqu'aucune fonctionnalité n'est activée, seules les couches scanning/PE/NT sont disponibles.
cargo run --example scan
### Construire + vérifier la chaîne de trames fantômes```sh
cargo run --example verify --features stack-spoof
Preuve d'injection -- la charge utile s'est exécutée à l'intérieur du processus cible.```sh
cargo run --example inject --features inject,stack-spoof,veh -- <sc.bin>
Ajoutez `--verbose` pour activer la sortie de diagnostic VEH (dumps de pile, impressions de registres) :```sh
cargo run --example inject --features inject,stack-spoof,veh -- <pid> <sc.bin> --verbose
inject_sapc classe les threads de la cible. Le signal de classement le plus fort est
de savoir si un thread possède une fenêtre :
EnumWindows +
GetWindowThreadProcessId. Un thread GUI se met en attente alertable dans sa
boucle de messages, donc un APC mis en file d'attente s'y exécute en premier. +1000.NtQueryInformationThread(ThreadCycleTime) ; se rabat sur
ThreadTimes lorsque la classe n'est pas disponible sur la build en cours (voir
examples/tprobe.rs). Normalisé et ajouté.NtQueryInformationThread(ThreadTimes) — temps noyau + utilisateur.NtQueryInformationThread(ThreadSuspendCount). Un thread
suspendu ne peut pas exécuter d'APC : −2000 par compteur de suspension.NtQueryInformationThread(ThreadBasicInformation). Une priorité
8–10 obtient +150 ; une priorité hors plage obtient −300.Filtrage. Les threads suspendus et les threads en attente noyau non alertable —
WrDelayExecution (Sleep/NtDelayExecution) ou WrSuspended, lus depuis
NtQuerySystemInformation(SystemProcessInformation) — sont écartés d'emblée (ils
ne peuvent jamais prendre un APC). La raison d'attente est sinon purement
diagnostique : elle est journalisée par candidat sous la forme State: / Reason: / Alert:, mais elle n'
influence pas le score, car KWAIT_REASON n'encode pas l'alertabilité (une
attente d'objet alertable et non alertable rapportent la même valeur — vérifié avec
un sondage de style examples/tprobe.rs).
Les candidats sont triés (score le plus élevé, départage GUI puis cycles) et
tronqués à MAX_APC_CANDIDATES (10) pour l'énumération/la journalisation.
Énumérer ≠ injecter. Le nombre de threads listés est découplé du nombre injectés.
MAX_APC_CANDIDATES(10) contrôle combien sont classés et journalisés ;APC_INJECT_THREADS(5) contrôle combien reçoivent réellement l'APC.Pourquoi injecter vers plusieurs, puis réessayer : le mode utilisateur ne peut pas déterminer quel thread est en attente alertable, et un seul thread choisi n'y entre souvent jamais, donc l' APC ne se déclencherait jamais.
inject_sapcmet donc l'APC en file d'attente vers plusieurs threads puis remet en file d'attente dans une courte boucle (~80 ms) jusqu'à ce que le mot de barrière partagé bascule de 0 à 1 — c'est-à-dire jusqu'à ce qu'un thread cible ait réellement exécuté la charge utile. Il journalise si cela s'est produit :payload claimed the gate(exécuté) vsgate NOT claimed.Pourquoi exécuter une seule fois : le même shellcode/contexte est mis en file d'attente vers chaque thread, donc sans protection une charge utile non réentrante s'exécuterait concurremment et ferait planter l'hôte (mesuré : 3 exécutions simultanées). La barrière de réentrance (
USE_REENTRANCY_GATE, activée par défaut) fait que le corps de la charge utile s'exécute exactement une fois quel que soit le nombre de threads (ou le nombre de cycles de nouvelle tentative) qui prennent l'APC (mesuré : exactement 1). Laissez la barrière activée sauf si votre charge utile est explicitement sûre en concurrence.
use lacuna::{scan, nt, win::get_module};
let ntdll = get_module(b"ntdll.dll\0");