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 的同时,构建出合理且展开一致的调用栈。
分配内存

写入 shellcode

创建执行线程

### 限制与已知约束
这个项目主要是为了学习而做的,所以我想可能有些地方可以做得更好 :)
- 不支持 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++, 免杀技术, 数据擦除, 暴力破解检测, 汇编, 端点可见性, 系统底层, 调用栈伪造