Disclaimer. This research is published for educational and defensive purposes only. I do not endorse the use of this technique for unauthorized access to any computer system. Always obtain explicit written authorization before testing. If you use this on systems you don't own, that's on you, and it's illegal.
What is this?
A Rust Proof of Concept that produces a fully legitimate call stack at the exact moment an indirect syscall executes, while remaining compliant with Intel CET (Control-flow Enforcement Technology) hardware enforcement.
Every frame in the resulting call stack belongs to a signed Windows module. No synthetic frames, no fabricated unwind data. The enum function is genuinely on the stack because we genuinely called it.
The technique in one paragraph
A thread pool worker callback manually unwinds its own frame and redirects execution to EnumSystemLocalesEx. The enum function calls our callback, which unwinds again and redirects to a syscall; ret trampoline inside ntdll.dll. CET compliance is achieved through a jmp-based context switch (bypassing the shadow stack entirely) combined with RDSSPQ/INCSSPQ reconciliation to consume stale shadow stack entries left by the call into user_mode_continue.
Execution flow
Three phases, same thread:
Thread pool worker captures its context, unwinds one frame to find the TP internals' return address, then redirects to
EnumSystemLocalesEx. TheEmbeddedContextpointer is passed throughTEB.ArbitraryUserPointer.Enum callback (first invocation) retrieves the context from TEB, unwinds its own frame, sets up the syscall registers (
RAX=SSN,RIP= trampoline, args in registers/stack), and jumps to thesyscall; retgadget inntdll.dll.Enum callback (second invocation, if needed) restores overwritten stack slots, restores TEB, returns 0 to stop the enumeration.
The call stack at syscall time
ntdll!NtProtectVirtualMemory+12 <- `syscall; ret` trampoline
kernelbase!Internal_EnumSystemLocales+348 <- real frame
kernelbase!EnumSystemLocalesEx+1F <- real frame
ntdll!TppWorkpExecuteCallback+4D0 <- thread pool internals
ntdll!TppWorkerThread+801 <- thread pool internals
kernel32!BaseThreadInitThunk+17
ntdll!RtlUserThreadStart+2C
Every frame: ntdll.dll, kernelbase.dll, or kernel32.dll. No unbacked memory.
CET compliance
Intel CET introduces a hardware-enforced shadow stack: an independent copy of return addresses that the CPU compares against on every ret. Mismatch = #CP fault = process dead. This kills traditional stack spoofing.
This PoC takes a different route:
jmpinstead ofretfor context switches.jmpdoes not interact with the shadow stack at all: no pop, no comparison, no fault.RDSSPQ/INCSSPQreconciliation to consume stale entries. Becauseuser_mode_continueis a real function (#[inline(never)]), thecallthat enters it pushes a return address onto the shadow stack. Since wejmpaway instead ofret-ing, that entry becomes stale.INCSSPQadvances the SSP past it, realigning the shadow stack with the normal stack before the nextret.- Same binary works on CET and non-CET systems. If CET is off,
RDSSPQreturns 0 and the reconciliation is skipped entirely.
Build
Requires Rust stable (or nightly if you want -Z ehcont-guard). The project targets x86_64-pc-windows-msvc.
cargo build --release
CET and CFG flags are already configured in .cargo/config.toml:
[target.x86_64-pc-windows-msvc]
rustflags = [
"-C", "control-flow-guard=yes",
"-C", "link-arg=/CETCOMPAT",
"-C", "link-arg=/force:guardehcont",
"-C", "link-arg=/guard:ehcont",
# "-Z", "ehcont-guard", # requires nightly; uncomment if using rustc nightly
"-C", "link-arg=/DYNAMICBASE",
"-C", "link-arg=/NXCOMPAT",
"-C", "link-arg=/HIGHENTROPYVA",
]
What the PoC does
The demo uses ZwProtectVirtualMemory (via the spoofed chain) to:
- Change the protection of
kernelbase!GetEraNameCountedStringtoPAGE_EXECUTE_WRITECOPY - Write 64 NOP bytes (
0x90) over the function - Restore the original protection
The victim function is arbitrary. The point is that the syscall executes through a fully spoofed, CET-compliant call chain.
A note on return values
The syscall's NTSTATUS (in RAX) cannot be recovered after the trampoline returns to the enum function. The PoC uses sentinel values (0xFFFFFFFF) to detect success: if the kernel overwrites the sentinel with the real old_protect value, the syscall succeeded.
Intentional simplifications
This PoC is not a weapon. It would not survive basic static analysis by any halfway decent EDR.
| Aspect | PoC approach | Production approach |
|---|---|---|
| Module resolution | GetModuleHandleW + GetProcAddress |
PEB walking, hash-based lookup |
| SSN resolution | Hardcoded per-OS table | Dynamic via stub parsing (Hell's Gate, etc.) |
| Enum function | Single (EnumSystemLocalesEx) |
Random selection from 39-function basket |
| Trampoline | Scans target Zw function only |
Any ntdll function |
| Strings | Plaintext | Encrypted / obfuscated |
The value is in the architecture, not in the operational stealth.
Prior art and acknowledgments
This work builds on research by people far more talented than me:
- SilentMoonwalk & BYOUD by klezVirus
- ThreadStackSpoofer by mgeeky
- Unwinder by Kudaes
- LoudSunRun by susMdT
- VulcanRaven by WithSecure Labs
- Indirect Syscalls by @modexpblog (MDSec)
- SysWhispers by @jthuraisamy
- SysWhispers3 by klezVirus
- Hell's Gate by am0nsec & smelly__vx
- Tartarus' Gate by trickster0
Research by Elastic Security Labs, Bill Demirkapi, taintedbits.com, and Synacktiv on CET internals was also instrumental.
Full article
For the detailed technical writeup covering the full technique, debugging war stories, and detection countermeasures, see this blog post.
Comments