toneillcodes/UnwindRaven

GitHub: toneillcodes/UnwindRaven

UnwindRaven 是一个 Windows x64 攻击性研究框架,通过构建数据驱动的合成调用栈来绕过基于栈遍历的 EDR 检测与调试器分析。

Stars: 62 | Forks: 5

# UnwindRaven

## 概述 UnwindRaven 是一个 Windows x64 攻击性研究框架,它在线程启动时构建**完全合成的调用栈**,使得新创建的线程——在遍历栈的调试器、EDR 传感器和内核回调看来——仿佛是由一系列已知且受信任的系统栈帧合法调用的。 ### 前期技术 UnwindRaven 站在两项基础性的 x64 调用栈操控研究的肩膀上。 **WithSecure Labs 的 [VulcanRaven](https://github.com/WithSecureLabs/CallStackSpoofer)** —— 在其 [*Spoofing Call Stacks to Confuse EDRs*](https://labs.withsecure.com/blog/spoofing-call-stacks-to-confuse-edrs) 博客文章中有详细记录 —— 建立了 UnwindRaven 核心直接继承的基础原语集:在运行时通过 `CalculateFunctionStackSize()` 解析目标函数的 `UNWIND_INFO` 记录,使用解码出的栈帧大小分配正确数量的栈空间,通过 `PushToStack` 将合成的返回地址推入该分配区,并安装一个 Vectored Exception Handler 来吸收当假链耗尽时产生的终端访问违例。这三个调用链配置文件(`--wmi`、`--rpc`、`--svchost`)是从针对 lsass 句柄操作的实时 SysMon 进程访问事件中收集的。这种硬编码是一个刻意的 PoC 限制:每个配置文件都嵌入了仅针对 Windows 10 21H2 (build 19044.1706) 验证的特定版本偏移量,这意味着目标 OS 构建的任何更改都需要重新编译和重新测量。 **klezVirus 的 [BYOUD — *Bring Your Own Unwind Data*](https://github.com/klezVirus/BYOUD)** 从相反的方向解决了这个问题。BYOUD 并不构建一个对遍历栈者*看起来*正确的合成栈,而是操控栈遍历器*查询*的 PE 异常目录元数据——重写真实数据,而不是伪造证据。它的七种技术涵盖了元数据颠覆的全谱系:原地 `UNWIND_INFO` 篡改(`UNWIND_DATA_TAMPER`)、捐赠栈帧的 `UnwindData` RVA 替换(`UNWIND_DATA_HIJACK`)、劫持 `.pdata` 条目以覆盖 shellcode 地址范围(`RT_FUNCTION_HIJACK`)、向异常目录追加全新的 `RUNTIME_FUNCTION` 和 `UNWIND_INFO` 记录(`RT_FUNCTION_INJECT`),以及通过 `RtlAddFunctionTable` 进行动态注册的三个变体,它们具有不同的调试器和 EDR 可见性特征。核心洞见——即 `UNWIND_INFO` 不是只读编译器产物,而是可以在运行时编写、替换或扩展的活动数据结构——正是 *"Bring Your Own"* 框架所捕捉的含义。 ### UnwindRaven 的定位 UnwindRaven 采用了 VulcanRaven 经过实战检验的原语(`CalculateFunctionStackSize`、`PushToStack`、VEH 清理),并使栈帧链变得**完全数据驱动**。纯文本的 blueprint 文件取代了编译时内置的配置文件,配套的 `BlueprintCallstack` 收集器直接从实时进程快照中获取这些 blueprint——使得该工具无需重新编译即可立即跨 Windows 版本移植。从 BYOUD 中,UnwindRaven 继承了将 PE 异常目录视为一等运行时数据源的准则:每个合成栈帧的精确栈占用空间都是通过读取目标模块现有的 `UNWIND_INFO` 记录得出的,而不是依赖硬编码的大小或偏移量。一个轻量级的集成 PE 加载器完善了该框架,使得测试载荷可以直接针对合成栈进行演练,保持研究工作流完全自成体系。 核心流程如下:在调用入口点之前,UnwindRaven 会: 1. 解析描述所需假调用链的 **blueprint 文件**(module → exported symbol → in-function offset)。 2. 通过解析该函数的 PE `UNWIND_INFO` 元数据来解析每个栈帧的真实栈占用空间。 3. 分配连续的合成栈,将正确的返回地址和寄存器保存值推入其中,并构建一个指向 `RSP`/`RIP` 为此假链顶部的伪造 `CONTEXT` 记录。 4. 手动映射 payload DLL,解析其导入,应用重定位,设置节权限,并在伪造的线程上启动它。 5. 安装 **Vectored Exception Handler**,拦截当合成返回链最终解退时产生的任何访问违例,优雅地将线程重定向到 `RtlExitUserThread`。 结果是一个由任何依赖于 `StackWalk64` 或 `RtlVirtualUnwind` 的观察者所看到的线程栈,看起来就像它起源于合法的 Windows 执行路径。 ## 功能 | 功能 | 详情 | |---|---| | 基于 blueprint 的栈构建 | 通过纯文本 blueprint 文件实现完全数据驱动的伪造栈帧链 | | UNWIND\_INFO 精确栈帧大小计算 | 解析 `UWOP_PUSH_NONVOL`、`ALLOC_SMALL`、`ALLOC_LARGE`、`SET_FPREG` 和 `PUSH_MACHFRAME` 操作码 | | 手动 PE 加载器 | 映射节,应用基址重定位,解析 IAT,调整节权限 —— 不使用 `LoadLibrary` | | 基于 VEH 的线程清理 | Vectored Exception Handler 捕获终端 AV 并干净地退出线程 | | WOW64 感知分析器 | 实时分析模式支持原生 x64 和 32 位 (WOW64) 目标线程 | | Blueprint 收集工具 | `BlueprintCallstack` 直接从任何运行中的进程捕获导出栈帧的 blueprint | | Symbol-server 集成 | 开箱即用配置为 `https://msdl.microsoft.com/download/symbols` | | MSVC 优化构建 | 使用 `/Oy-` 保证存在帧指针;发出汇编列表以供分析 | ## 工作原理 ``` ┌──────────────────────────────────────────────────────────────────────┐ │ UnwindRaven Flow │ │ │ │ blueprint.txt UnwindRaven.exe payload.dll │ │ ┌───────────┐ ┌────────────┐ ┌────────────┐ │ │ │ntdll.dll │ parse │ Load │ map │ DllMain │ │ │ │NtWaitFor… │ ───────► │ Blueprint │ ────────► │ (payload) │ │ │ │kernel32 │ │ │ └────────────┘ │ │ │... │ │ Resolve │ │ │ └───────────┘ │ Frames │ Each frame's stack size │ │ │ (UNWIND_ │ calculated from PE metadata │ │ │ INFO) │ │ │ │ │ │ │ │ Build │ Synthetic stack allocated; │ │ │ Synthetic │ return addresses + saved │ │ │ Stack │ regs pushed per frame │ │ │ │ │ │ │ Map PE │ Manual loader: sections, │ │ │ (no │ relocs, IAT, permissions │ │ │ LoadLib) │ │ │ │ │ │ │ │ Spoof │ SetThreadContext on a │ │ │ Thread │ suspended thread — RSP/RIP │ │ │ Context │ point into synthetic stack │ │ │ │ │ │ │ VEH │ AV on unwind → redirected │ │ │ Installed │ to RtlExitUserThread │ │ └────────────┘ │ └──────────────────────────────────────────────────────────────────────┘ ``` ## 合成栈伪造 ### 栈帧解析 对于 blueprint 中的每个条目,`BuildSyntheticStackFromBlueprint`(位于 `src/synthetic_stack.c` 中)定位当前进程中的目标模块(或在 `needLoad == 1` 时按需加载),并通过 `GetProcAddress` 或手动导出表遍历(`GetRvaFromName`)解析函数地址。 然后通过解析 PE 的异常目录来确定函数的精确栈使用情况。`CalculateFunctionStackSize` 解码每一个 `UNWIND_CODE` 记录: | 操作码 | 计算的效果 | |---|---| | `UWOP_PUSH_NONVOL` | +8 字节(一次 GPR 压栈) | | `UWOP_ALLOC_SMALL` | +(info+1)×8 字节 | | `UWOP_ALLOC_LARGE` | slot 1: info×8; slot 2: 原始 DWORD | | `UWOP_SET_FPREG` | 建立帧指针,无大小变化 | | `UWOP_PUSH_MACHFRAME` | +40 或 +48 字节(硬件异常帧) | ### 栈布局 单个连续分配(`VirtualAlloc`,`PAGE_READWRITE`)保存整个合成栈。`PushToStack` 从分配区的底部向低地址遍历,放置: - 每个栈帧的**返回地址**(解析出的导出 VA + 函数内偏移量) - 该栈帧的 `UNWIND_INFO` 要求的**非易失性寄存器保存** 在压入所有栈帧后,`InitializeFakeThreadState` 清零一个 `CONTEXT` 结构并设置: ``` ctx.Rsp = ctx.Rip = ``` 然后在恢复挂起的线程之前对其调用 `SetThreadContext`。 ### VEH 清理 一旦 payload 完成并且合成返回链遍历到了真实栈的末尾,就会触发访问违例。安装的 VEH(位于 `src/veh.c` 的 `VehCallback`)检查 `EXCEPTION_ACCESS_VIOLATION` 并将 `RIP` 重定向到 `RtlExitUserThread(0)`,确保线程干净地退出而不会导致宿主进程崩溃。为了避免可检测的导入,`RtlExitUserThread` 是在运行时通过手动遍历 `ntdll.dll` 的导出表(`GetLocalProcAddressManual`)来解析的。 ## Blueprint 收集器 `BlueprintCallstack`(`tools/blueprint-callstack.c`)是配套工具,用于**从展现出所需表观调用链的合法 Windows 进程中捕获真实的调用栈 blueprint**。 ### 工作原理 1. 使用 `PROCESS_QUERY_INFORMATION | PROCESS_VM_READ` 权限打开目标进程。 2. 挂起目标线程并捕获其 `CONTEXT`。 3. 调用 `StackWalk64` + `SymFromAddr` 枚举栈帧。 4. 对于每个栈帧,执行**手动导出表遍历**(`GetExportRva`)以验证该符号确实被导出 —— 未导出的栈帧将被静默跳过,因为它们无法跨进程边界重构(没有稳定的 RVA)。 5. 将模块路径规范化为标准的 `%SystemRoot%\System32\…` 格式。 6. 以 blueprint 格式为每个导出栈帧输出一行。 ### 用法 ``` # 从一个进程的任意 thread 进行 Harvest(自动选择第一个 thread) BlueprintCallstack.exe --pid [--out blueprint.txt] # 从特定的 thread 进行 Harvest BlueprintCallstack.exe --thread [--out blueprint.txt] ``` ### Blueprint 格式 输出的每一行(以及 UnwindRaven 使用的输入)都遵循这种竖线分隔的模式: ``` ||<0xOffset>| ``` | 字段 | 描述 | |---|---| | `module_path` | DLL 的标准绝对路径(System32 规范化) | | `exported_function` | 精确的导出符号名称 | | `0xOffset` | 距离导出起始处的字节偏移量(十六进制) | | `needLoad` | `1` = UnwindRaven 必须 `LoadLibrary` 此模块;`0` = 已经存在 | **Blueprint 片段示例:** ``` C:\Windows\System32\ntdll.dll|NtWaitForSingleObject|0x14|0 C:\Windows\System32\kernel32.dll|WaitForSingleObjectEx|0x8E|0 C:\Windows\System32\kernelbase.dll|WaitForSingleObjectEx|0x4C|0 C:\Windows\System32\kernel32.dll|BaseThreadInitThunk|0x14|0 C:\Windows\System32\ntdll.dll|RtlUserThreadStart|0x21|0 ``` ## 构建说明 ### 前置条件 | 需求 | 版本 / 备注 | |---|---| | Visual Studio | 2019 或 2022(Desktop C++ workload) | | CMake | 3.20 或更高版本 | | Windows SDK | 10.0.19041.0 或更高版本 | | Windows Driver Kit (WDK) | 需要 WDK 提供的 `dbghelp.dll`(x64 Debuggers 路径) | | 符号缓存目录 | `C:\Symbols`(首次运行时自动创建) | ### 克隆 ``` git clone https://github.com/toneillcodes/UnwindRaven.git cd UnwindRaven ``` ### 配置与构建 仓库附带了一个便捷脚本: ``` .\build.ps1 ``` 或者使用 CMake 和 Visual Studio generator 手动执行: ``` cmake -B build -G "Visual Studio 17 2022" -A x64 cmake --build build --config Release ``` 二进制文件放在 `bin\` 目录中: ``` bin\ UnwindRaven.exe UnwindRavenHellow.exe BlueprintCallstack.exe dbghelp.dll ← copied from WDK at post-build ``` ### 关键编译器标志 | 标志 | 用途 | |---|---| | `/Oy-` | **禁用省略帧指针** —— 准确遍历栈所需 | | `/Zi` / `/Z7` | PDB 中的完整调试信息 / 内嵌调试信息 | | `/FAs` `/FAcs` | 发出带注释的汇编列表以供分析 | | `/DEBUG` | 链接时调试信息 | ## 目录结构 ``` UnwindRaven/ │ ├── cmake/ # CMake helper modules │ ├── examples/ │ ├── UnwindRaven.c # Main example: blueprint load → stack spoof → payload run │ └── UnwindRaven-Hellow.c # Minimal "hello world" loader example │ ├── include/ # Public headers │ ├── unwindraven_core.h │ ├── stack_blueprint.h │ └── synthetic_stack.h │ ├── src/ # Core static library sources │ ├── common.c # EnableDebugPrivilege, module/image helpers │ ├── stack_blueprint.c # Blueprint file parsing (LoadBlueprintFile) │ ├── synthetic_stack.c # Frame resolution, UNWIND_INFO parsing, stack construction │ ├── pe_loader.c # Manual PE mapper: sections, relocations, IAT, permissions │ ├── veh.c # VEH installation and AV → RtlExitUserThread redirect │ └── unwindraven_core.c # High-level orchestration (MapPayloadImage, StartSpoofedLoaderThread) │ ├── tools/ │ └── blueprint-callstack.c # Blueprint Harvester utility │ ├── CMakeLists.txt ├── build.ps1 # PowerShell build helper └── LICENSE # GPL-3.0 ``` ## 示例用法 ### 1 — 捕获 Blueprint 针对任何展现出你想要模仿的调用链的进程运行 `BlueprintCallstack`。等待用户输入的 `notepad.exe` 是一个很好的例子: ``` # 查找 Notepad 的 PID $pid = (Get-Process notepad).Id # Harvest 并保存 .\bin\BlueprintCallstack.exe --pid $pid --out .\blueprints\notepad_wait.txt ``` 示例输出: ``` [+] Harvesting blueprint from PID 4812, TID 7640 C:\Windows\System32\win32u.dll|NtUserGetMessage|0x14|0 C:\Windows\System32\user32.dll|GetMessageW|0x4A|0 C:\Windows\System32\kernel32.dll|BaseThreadInitThunk|0x14|0 C:\Windows\System32\ntdll.dll|RtlUserThreadStart|0x21|0 [+] Total frames walked: 7 [+] Exported frames listed: 4 ``` ### 2 — 运行 UnwindRaven ``` .\bin\UnwindRaven.exe --load-blueprint .\blueprints\notepad_wait.txt .\payload\my_payload.dll ``` ``` [!] Blueprint loaded (4 entries). Press ENTER here to continue and build synthetic stack... [+] Frame 0: user32.dll!GetMessageW+0x4A (stack size: 0x38) [+] Frame 1: kernel32.dll!BaseThreadInitThunk+0x14 (stack size: 0x28) [+] Frame 2: ntdll.dll!RtlUserThreadStart+0x21 (stack size: 0x10) [!] Payload mapped successfully at 0x00007FF9A3C40000. Press ENTER to continue and start the spoofed loader thread... [+] Spoofed loader thread started. ``` ### 3 — 验证假栈 将 WinDbg 或 x64dbg 附加到宿主进程,并使用 `k` 检查新创建线程的栈。显示的栈帧将与 blueprint 精确匹配,真实分配的任何痕迹都将不可见。 ## 限制 - **仅限 x64。** 合成栈引擎和 UNWIND\_INFO 解析器专门针对 `IMAGE_FILE_MACHINE_AMD64`。不支持 ARM64 或 x86(32 位)。 - **仅限导出栈帧。** Blueprint 条目必须引用导出的符号。非导出函数没有稳定的跨进程 RVA,无法重构。 - **最多 64 个栈帧。** `MAX_STACK_FRAMES` 定义为 64。包含更多条目的 blueprint 将被截断。 - **最大合成栈大小:0x3000 字节。** 累计 `UNWIND_INFO` 栈帧大小超过此限制的 blueprint 将在构建时失败。 - **需要 WDK 的 `dbghelp.dll`。** 构建系统在构建后从 WDK x64 Debuggers 路径复制 `dbghelp.dll`。Windows SDK 版本的 `dbghelp.dll` 不足以支持收集器中的 `StackWalk64`。 - **不支持链式解退。** 使用链式 `UNWIND_INFO` 记录(常见于深度优化的系统 DLL 中)的函数可能会被低估栈大小。 - **ASLR / 模块可用性。** 如果模块布局或导出集不同,在一台机器上捕获的 blueprint 可能无法直接移植到另一机器上。始终在目标机器上重新收集或在使用前验证模块的存在。 - **防病毒 / EDR 检测。** 虽然调用栈伪造可以击败栈检查启发式方法,但其他遥测源(内核 ETW 回调、父进程关系、手动映射 DLL 中的 IAT 异常)仍然处于活跃状态。UnwindRaven 仅解决栈向量。 ## 许可证 UnwindRaven 在 **GNU General Public License v3.0** 下发布。 全文请见 [`LICENSE`](LICENSE)。
标签:云资产清单, 内核回调绕过, 安全意识培训, 客户端加密, 端点可见性, 调用栈欺骗, 逆向工程