Add files via upload

This commit is contained in:
Tiziano Marra
2026-07-12 16:34:41 +02:00
committed by GitHub
parent b1d2bca325
commit 08f946bd73
5 changed files with 1100 additions and 0 deletions
+124
View File
@@ -0,0 +1,124 @@
# CET-Compliant Callstack Spoofing via Thread Pool Enum Callback Trampolining
> **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:
1. **Thread pool worker** captures its context, unwinds one frame to find the TP internals' return address, then redirects to `EnumSystemLocalesEx`. The `EmbeddedContext` pointer is passed through `TEB.ArbitraryUserPointer`.
2. **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 the `syscall; ret` gadget in `ntdll.dll`.
3. **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:
- **`jmp` instead of `ret`** for context switches. `jmp` does not interact with the shadow stack at all: no pop, no comparison, no fault.
- **`RDSSPQ`/`INCSSPQ` reconciliation** to consume stale entries. Because `user_mode_continue` is a real function (`#[inline(never)]`), the `call` that enters it pushes a return address onto the shadow stack. Since we `jmp` away instead of `ret`-ing, that entry becomes stale. `INCSSPQ` advances the SSP past it, realigning the shadow stack with the normal stack before the next `ret`.
- **Same binary works on CET and non-CET systems.** If CET is off, `RDSSPQ` returns 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`.
```bash
cargo build --release
```
CET and CFG flags are already configured in [`.cargo/config.toml`](.cargo/config.toml):
```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:
1. Change the protection of `kernelbase!GetEraNameCountedString` to `PAGE_EXECUTE_WRITECOPY`
2. Write 64 NOP bytes (`0x90`) over the function
3. 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](https://github.com/am0nsec/HellsGate), 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](https://github.com/klezVirus/SilentMoonwalk) & [BYOUD](https://github.com/klezVirus/BYOUD) by klezVirus
- [ThreadStackSpoofer](https://github.com/mgeeky/ThreadStackSpoofer) by mgeeky
- [Unwinder](https://github.com/Kudaes/Unwinder) by Kudaes
- [LoudSunRun](https://github.com/susMdT/LoudSunRun) by susMdT
- [VulcanRaven](https://github.com/WithSecureLabs/CallStackSpoofer) by WithSecure Labs
- [Indirect Syscalls](https://www.mdsec.co.uk/2020/12/bypassing-user-mode-hooks-and-direct-invocation-of-system-calls-for-red-teams/) by @modexpblog (MDSec)
- [SysWhispers](https://github.com/jthuraisamy/SysWhispers) by @jthuraisamy
- [SysWhispers3](https://github.com/klezVirus/SysWhispers3) by klezVirus
- [Hell's Gate](https://github.com/am0nsec/HellsGate) by am0nsec & smelly__vx
- [Tartarus' Gate](https://github.com/trickster0/TartarusGate) 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]().
## License
This project is released for educational and research purposes. See the disclaimer at the top.