- What: New framework BYOUD targets Windows unwind metadata to bypass Intel CET
- Impact: Researchers explore new methods to bypass control-flow enforcement
Fantastic unwind information and where to find them Contents Fantastic unwind information and where to find them Foreword This is the third and final installment in a series of posts on stack spoofing research that I presented at Black Hat Europe 2025. The first two posts covered Stack Moonwalking++ and Callback Hell , which explored techniques for spoofing call stacks in pre-CET environments. However, Intel CET (Control-flow Enforcement Technology) fundamentally breaks those approaches. The shadow stack maintained by CET catches any attempt to modify return addresses, making traditional stack spoofing techniques obsolete on modern systems. This post presents BYOUD (Bring Your Own Unwinding Data), a new framework that works within CET’s constraints by targeting a different layer entirely: Windows unwind metadata. The techniques described here were developed to answer a simple question: can we spoof call stacks without touching return addresses at all? Overview Intel CET and Hardware Stack Protection (HSP) have fundamentally altered the landscape of stack spoofing techniques. Traditional approaches such as Stack Moonwalking that rely on modifying return addresses are rendered ineffective. CET’s shadow stack detects these modifications immediately and raises a Control Protection exception. CET enforces return flow integrity, while Windows stack unwinding operates independently through exception metadata. This separation presents a new attack surface. This post introduces BYOUD (Bring Your Own Unwinding Data), a CET-compliant stack spoofing framework that manipulates Windows unwind information without modifying return addresses. In this blog, I’ll present three main variants plus a JIT-specific approach, demonstrating how malware can present fully unwindable, legitimate-looking call stacks while remaining compliant with Intel CET, even when executing from memory. TL;DR (aka I Don’t Want to Read 8000 Words About Pointer Arithmetic) I was told at Black Hat that this topic was “too complicated” and I was “unable to make it simpler.” I have chosen to interpret this as a compliment. Here is my attempt at simple. Explain everything and the meaning drowns in detail; simplify too much and the meaning disappears. Every running program can be described as a call tree. Each function call is a node, and the path from the root to any currently executing function is exactly what you see in a call stack. If you have any intuition at all for how a debugger shows you “who called what,” you already understand the structure: 1 2 3 4 main() └─ do_the_needful() └─ maybe_do_maybe_dont() └─ ReadFile() <- you are here What makes this tree reconstructable at runtime is that each node stores its return address (the location to jump back to when it finishes) on the stack, at a fixed offset from the current stack pointer. That offset is the frame size : the amount of stack space the function allocated in its prologue. If you know the frame size of any node, you can find its parent. If you know the frame size of every node along a path, you can walk the entire tree from any leaf back to the root. 1 2 3 4 5 RSP ──► [ current frame ] frame size = 0x80 [ return addr ] ← points to parent [ parent frame ] frame size = 0x200 [ return addr ] ← points to grandparent [ grandparent frame ] ...and so on This is precisely what a stack walker does. It reads frame sizes from .pdata unwind metadata, hops from frame to frame, and reconstructs the call tree one node at a time. BYOUD is a set of techniques that exploits this exact arithmetic. Once you know the cumulative frame size from the root of the tree down to some node N (i.e., the total stack distance from the thread’s entry point to N) then for any child of N, the distance is simply: 1 distance(child) = distance(parent) + child_frame_size This means you can craft a fake subtree. Pick any chunk in the real call tree that you want to hide and an API you want to call. Manipulate the frame size of the API (or forward frame) to equal exactly the distance from the BaseThreadInitThunk to the frame calling the API + the frame size of the API. The hidden portion of the tree simply does not exist from an unwinding perspective. 1 2 3 4 5 6 7 Real tree: What the walker sees: RtlUserThreadStart RtlUserThreadStart └─ BaseThreadInitThunk └─ BaseThreadInitThunk └─ [CRT main] - [HIDDEN] └─ shellcode() - [HIDDEN] └─ TargetWindowsApi() └─ TargetWindowsApi() The remaining 8000 words explain how to make the Windows unwinder agree with this fiction manipulating a few data structures, and why this is CET compatible. You were warned. The Dragon in the Room: Hardware Stack Protection Microsoft Corporation initiated the deployment of user-mode Hardware Stack Protection (HSP) with Windows 10 20H1. HSP represents an exploit mitigation technology aimed at preventing the corruption of return addresses on the stack. Underpinned by silicon-based architecture, HSP leverages Intel’s Control Flow Enforcement Technology (CET) and AMD...