Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 

README.md

callstack-detect

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.

Detection Approach

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:

  1. Module-range check — every return address must fall inside a mapped PE image. Addresses in heap or anonymous RX memory are immediately flagged (KernelAddressMismatch).

  2. CALL-preceded check — the bytes immediately preceding the return address in the image must constitute a valid CALL instruction encoding (0xE8 rel32 or 0xFF /2 r/m64). If the bytes do not match, the RA was written synthetically (SyntheticReturnAddress).

  3. Unwind geometry checkRtlLookupFunctionEntry retrieves the RUNTIME_FUNCTION entry for the function containing each return address. The UNWIND_INFO record 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).

Why SilentMoonwalk Is Hard to Catch (and Why Kernel-ETW Changes That)

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.

Usage

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 Support

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

Containment

Requires EXPLOIT_LAB_ACTIVE=1. collect_current_thread_stack() panics if the variable is absent.

Offensive Counterpart

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.

References

  • 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)