Defender-side kernel-ETW call-stack collector and unwind metadata validator. Counter-tooling for SilentMoonwalk-class call-stack spoofing.
Lab-only. Requires EXPLOIT_LAB_ACTIVE=1.
Modern EDR products (Elastic, CrowdStrike, Microsoft Defender for Endpoint) collect thread call stacks at the moment of sensitive API calls using kernel-ETW providers. The 2024–2025 Elastic Security Labs research ("Call Stacks: No More Free Passes", "Doubling Down: Detecting In-Memory Threats with Kernel ETW") showed that kernel-ETW collection happens at syscall entry — precisely when return-address stomping is active — and that the captured stack can be validated against PE image unwind metadata to detect fabricated frames.
This crate implements that defender-side validation:
-
Module-range check — every return address must fall inside a mapped PE image. Addresses in heap or anonymous RX memory are immediately flagged (
KernelAddressMismatch). -
CALL-preceded check — the bytes immediately preceding the return address in the image must constitute a valid
CALLinstruction encoding (0xE8rel32 or0xFF /2r/m64). If the bytes do not match, the RA was written synthetically (SyntheticReturnAddress). -
Unwind geometry check —
RtlLookupFunctionEntryretrieves theRUNTIME_FUNCTIONentry for the function containing each return address. TheUNWIND_INFOrecord describes the RSP/RBP delta for that function's frame. A spoofed frame often has incorrect geometry because the attacker copied the gadget address without replicating the full frame layout (MismatchedUnwind).
SilentMoonwalk (klezVirus, Waldo-irc, 2022–2024) is designed to defeat userland stack walkers: it selects gadgets that ARE preceded by CALL instructions, so rule 2 above passes. It also tries to replicate the frame geometry, which can defeat rule 3 for simple single-frame spoofs.
Kernel-ETW changes the detection surface in two ways. First, the stack is captured in kernel context at syscall entry, before any userland thread-suspension that could restore a real stack. The spoof is active and visible. Second, kernel-ETW provides the full 64-frame unwind chain, not just the top few frames visible to userland samplers — complex SilentMoonwalk implementations that fake only the top 2–3 frames leave inconsistent unwind transitions deeper in the chain.
The remaining gap is a fully unwind-consistent multi-frame spoof. See
tools/rust/callstack-spoof/spoof_vs_detect_matrix.md
for the current spoof vs. detect coverage matrix.
use callstack_detect::{assert_lab_env, collect_current_thread_stack, validate_call_stack, ValidationResult};
assert_lab_env().expect("lab required");
let capture = collect_current_thread_stack();
match validate_call_stack(&capture) {
ValidationResult::Valid => println!("stack appears genuine"),
ValidationResult::Spoofed(ind) => println!("spoof detected: {ind:?}"),
ValidationResult::Unknown => println!("insufficient metadata to validate"),
}For per-frame detail:
use callstack_detect::{unwind_validator::validate_unwind_chain};
let findings = validate_unwind_chain(&capture.frames);
for f in &findings {
println!("frame {}: {:?} — {}", f.frame_index, f.finding, f.detail);
}| Platform | Behavior |
|---|---|
| Windows x64 | Full: RtlCaptureStackBackTrace, RtlLookupFunctionEntry, CALL-preceded check |
| Linux / macOS | CI stub: collect_current_thread_stack returns empty; validate_call_stack returns Unknown |
Requires EXPLOIT_LAB_ACTIVE=1. collect_current_thread_stack() panics if the
variable is absent.
The offensive crate is tools/rust/callstack-spoof/. That
crate implements the SilentMoonwalk gadget-selection and RA-replacement attack.
The two crates together form a red/blue pairing for measuring detection coverage.
tools/rust/callstack-spoof/spoof_vs_detect_matrix.md
documents which spoof variants this detector catches and which it misses.
- Elastic Security Labs: "Call Stacks: No More Free Passes" (2024)
- Elastic Security Labs: "Doubling Down: Detecting In-Memory Threats with Kernel ETW" (2025)
- SilentMoonwalk: klezVirus, Waldo-irc (2022–2024)
- Microsoft PE/COFF specification: Exception Handling (x64)