vgeorgiev90/CallStackSpoof

GitHub: vgeorgiev90/CallStackSpoof

该项目是一个使用 C++ 和 x64 汇编实现的 Windows x64 调用栈伪装教育工具,通过解析 PE 展开元数据构建符合规则的合成栈帧来隐藏真实调用来源。

Stars: 35 | Forks: 8

# Windows x64 调用栈伪装 ### 概述 该项目是一个使用 C++ 和 x64 汇编构建的 Windows x64 调用栈伪装实现,充分考虑了 PE 异常元数据和展开信息。 该项目的目的是教育性的:深入理解 Windows 如何在运行时构建、展开和验证调用栈,并为高级 Windows 内部原理和漏洞利用开发主题做准备。 该实现不依赖于硬编码的栈布局或脆弱的启发式方法,而是从目标模块中解析真实的展开元数据,并构建符合 Windows x64 展开规则的合成调用栈。 ### 主要功能 - 感知展开信息的伪造栈构建 - 解析异常目录 (IMAGE_DIRECTORY_ENTRY_EXCEPTION) - 处理链式展开信息 - 计算准确的栈帧大小 - 动态生成伪造栈帧 - 从真实的导出函数构建合成栈帧 - 支持函数内偏移(prolog 后伪装) - 运行时 gadget 发现 - 使用展开元数据安全检查过滤 gadget - 汇编级的栈转移与恢复 - 无崩溃地恢复原始执行流 - 多参数 API 调用 ### 高层架构 ``` CallStackSpoof() ├─ Parsing system DLL to extract all potential jump gadgets ├─ Parse spoof targets function unwind information ├─ Build synthetic frames info array ├─ Select safe jump gadget ├─ Prepare a structure that holds all info about spoof targets and api to be called ├─ Invoke Spoof() (assembly) │ ├─ Save non-volatile registers │ ├─ Construct fake frames on stack │ ├─ Jump to target API │ └─ Restore stack & registers └─ Return to original caller ``` ### 技术细节 Windows x64 展开解析 该项目手动解析: - RUNTIME_FUNCTION - UNWIND_INFO - UNWIND_CODE 处理的展开操作包括: - UWOP_PUSH_NONVOL - UWOP_ALLOC_SMALL - UWOP_ALLOC_LARGE - UWOP_SAVE_NONVOL - UWOP_SAVE_NONVOL_FAR - UWOP_PUSH_MACHFRAME - UWOP_SAVE_XMM128 - UWOP_SAVE_XMM128_far 每个展开序列都被转换为: - 栈帧大小 - 保存的非易失性寄存器偏移量 - 帧指针使用检测 目前,使用 UWOP_SET_FPREG 的函数会被显式检测并排除,以避免不正确的帧重建。 合成栈帧构建 每个伪装栈帧包括: - 正确的返回 RIP(prolog 之后或自定义偏移量) - 准确的栈保留大小 用于安全展开的终止帧 栈帧完全按照 Windows 的预期进行堆叠,允许诸如栈遍历器或调试器等工具观察到看似合理的调用栈。 跳转 Gadget 选择 该项目动态扫描可执行节以查找(稍作修改后即可使用任何其他非易失性寄存器): ``` jmp qword ptr [rbx] ``` 每个候选 gadget 都通过以下方式进行验证: - 定位其所属的封闭函数 - 解析其展开信息 - 拒绝具有不安全 save-nonvolatile 偏移量的 gadget - 拒绝基于帧指针的函数 仅使用展开安全的 gadget。 汇编跳板(伪装) 汇编例程执行以下操作: - 保存所有非易失性寄存器 - 提取原始返回地址 - 直接在栈上构建合成栈帧 - 跳转至目标 API - 恢复原始栈和寄存器 - 安全地将执行返回给原始调用者 这避免了 call 指令,并保持对栈布局的完全控制。 ### 示例用法 项目中包含了一个简单的概念验证,以演示如何在实践中使用调用栈伪装机制。 该 PoC 执行标准的动作序列: 1. 使用 `NtAllocateVirtualMemory` 分配可执行内存 2. 使用 `WriteProcessMemory` 写入 payload 3. 使用 `CreateThread` 执行 payload 所有 API 调用都通过伪装机制执行,因此观察到的调用栈看起来像是源自合法的 Windows 线程初始化例程。 对于此示例,使用了两个伪装栈帧: - `RtlUserThreadStart` - `BaseThreadInitThunk` 如果需要,可以添加额外的栈帧。 #### 准备伪装目标 每个伪装栈帧都使用 `SPOOF_TARGET` 结构进行描述。 该结构定义了: - 要伪装的函数地址 - 该函数所在的模块 距离函数开头的可选偏移量(用于落在 prolog 之后) ``` LPCSTR SpoofName1 = "RtlUserThreadStart"; LPCSTR SpoofName2 = "BaseThreadInitThunk"; SPOOF_TARGET SpoofApi1 = { 0 }; SPOOF_TARGET SpoofApi2 = { 0 }; ``` 首先,获取所需模块的句柄并解析函数地址: ``` // Handles to the modules that are required HMODULE hMod = GetModuleHandleA("ntdll.dll"); HMODULE hMod2 = GetModuleHandleA("kernel32.dll"); // Prepare information about the spoof targets SpoofApi1.funcAddress = (ULONG_PTR)GetProcAddress(hMod, SpoofName1); SpoofApi1.hModule = hMod; SpoofApi1.offsetFromStart = 0x21; SpoofApi2.funcAddress = (ULONG_PTR)GetProcAddress(hMod2, SpoofName2); SpoofApi2.hModule = hMod2; SpoofApi2.offsetFromStart = 0x14; ``` 选择这些偏移量是为了确保执行在函数 prolog 之后恢复,从而与合法调用栈中观察到的实际返回地址相匹配。 #### 配置伪装调用链 一旦准备好伪装目标,指向它们的指针就会被存储在一个数组中。 该数组定义了在运行时将要构建的合成调用栈。 ``` // Store them in an array for syntetic frame preparations spoofArray[0] = &SpoofApi1; spoofArray[1] = &SpoofApi2; apiCallInfo.spoofFramesCount = 2; apiCallInfo.spoofFramesTargetsArray = spoofArray; ``` 数组的顺序对应于栈帧在栈上出现的顺序。 #### 指定目标 API 在调用伪装之前,必须将目标 API 信息填充到 `API_CALL_INFO` 结构中。 这包括: - 要执行的函数地址 - 它期望的参数数量 ``` apiCallInfo.pFuncAddr = pNtAllocateVirtualMemory; apiCallInfo.apiFuncArgsCount = 6; ``` 该结构的 `retVal` 成员是有意未初始化的,因为它会在内部被填充为伪装调用的返回值。 #### 执行伪装调用 准备好所有必要信息后,即可使用 `CallStackSpoof` 执行伪装的 API 调用。 参数作为可变参数传递,并会自动放入正确的寄存器和栈位置。 ``` PVOID addr = NULL; SIZE_T size = sizeof(shellcode); CallStackSpoof( &apiCallInfo, // NtAllocateVirtualMemory (uint64_t)(HANDLE)-1, // ProcessHandle (uint64_t)&addr, // *BaseAddress (uint64_t)0, // ZeroBits (uint64_t)&size, // RegionSize (uint64_t)(MEM_COMMIT | MEM_RESERVE), // AllocationType (uint64_t)PAGE_EXECUTE_READWRITE // Protect ); ``` 调用完成后,可以通过 `retVal` 字段访问返回值: ``` status = (NTSTATUS)(ULONG_PTR)apiCallInfo.retVal; printf("[+] Allocation: %p, status: 0x%08X\n", addr, status); ``` 后续的 API 调用(如 `WriteProcessMemory` 和 `CreateThread`)通过仅更新目标函数地址和参数数量来重用相同的伪装调用链。 #### 生成的调用栈(简化的) 在调试器或栈遍历工具下观察时,生成的调用栈类似于以下内容: ``` 0:000> ~0 k Call Site ntdll!NtAllocateVirtualMemory+0x3 ntdll!RtlQueryCriticalSectionOwner+0xd2 <-- jump gadget KERNEL32!BaseThreadInitThunk+0x14 ntdll!RtlUserThreadStart+0x21 ``` 这展示了该项目如何在执行任意 API 的同时,构建出合理且展开一致的调用栈。 分配内存 ![分配内存](https://static.pigsec.cn/wp-content/uploads/repos/cas/d0/d04a46630f1da3ac79bb1c1c6bc921b651a3df78a043146bb74a84ddf49e4a81.png) 写入 shellcode ![写入 shellcode](https://static.pigsec.cn/wp-content/uploads/repos/cas/33/339a68acfa98952a13630874be731da606644b5a753573230c0f4f1e69cba88f.png) 创建执行线程 ![创建线程](https://static.pigsec.cn/wp-content/uploads/repos/cas/2c/2c16933ed7ccbbe6a52a0e5b43075cde7b28d4bf65818de0ea662f7e743eb31e.png) ### 限制与已知约束 这个项目主要是为了学习而做的,所以我想可能有些地方可以做得更好 :) - 不支持 UWOP_SET_FPREG 栈帧 - 无法通过合成栈帧进行异常安全展开 - 没有 CFG 或 CET 兼容性 - 仅支持基于 gadget 的转移(暂不支持 epilog 伪装) ### 待办 / 未来工作 - 全面支持 UWOP_SET_FPREG - 支持FrameOffset大于栈帧大小的 UWOP_SAVE_NONVOL - 异常安全的伪装栈帧 - 感知 CFG 的返回目标 - 针对许多其他 API 的广泛测试 #### 安全与道德声明 本项目仅供教育和研究目的使用。 它旨在: - 理解 Windows x64 内部原理 - 学习展开元数据和调用栈机制 - 为高级防御和进攻性安全研究做准备 请勿在未经授权的环境中使用此代码。 ### 致谢与参考 - 一如既往地感谢 [MalDev Academy](https://maldevacademy.com/),其令人难以置信的内容为该项目提供了总体灵感 - 感谢来自 [susMdT](https://github.com/susMdT) 的精彩[博客文章](https://dtsec.us/2023-09-15-StackSpoofin/)(及其代码库),探讨了 x64 调用栈伪装的概念和实现细节。 - [HulkOps — x64 调用栈伪装](https://hulkops.gitbook.io/blog/red-team/x64-call-stack-spoofing) 对 Windows x64 调用栈伪装核心思想的清晰且结构良好的解释,这有助于建立对该技术的初步概念性理解。 - 最初的 [klezVirus — SilentMoonwalk](https://github.com/klezVirus/SilentMoonwalk) 项目,作为对比研究和更广泛背景的参考。
标签:C++, 免杀技术, 数据擦除, 暴力破解检测, 汇编, 端点可见性, 系统底层, 调用栈伪造