初探堆栈欺骗之静态欺骗
字数 1828 2025-08-03 16:43:50
堆栈欺骗技术详解:静态欺骗与睡眠混淆
一、堆栈欺骗的背景与原理
1.1 堆栈欺骗的必要性
当使用基本的shellcode loader加载CS的shellcode时,如果不对堆栈做任何处理,堆栈会呈现不干净的状态:
- 堆栈中存在大量未被解析的地址,这显然不正常
- AV/EDR会重点扫描这部分内存区域,可能导致loader被检测
1.2 系统调用的堆栈差异
正常程序调用链:
主程序模块 → kernel32.dll → ntdll.dll → syscall
- 当ring0执行结束返回ring3时,返回地址应在ntdll地址范围内
直接系统调用:
- 返回地址将在主程序模块内,不在ntdll范围内
- 这种异常容易被检测
二、堆栈基础知识
2.1 32位与64位堆栈差异
32位系统:
- 通过rbp指向堆栈开始位置
- 每次移动rbp时会执行:
push rbp mov rbp, rsp - 通过回溯rbp可以回溯整个堆栈
64位系统:
- ebp成为通用寄存器,不再具有32位下的功能
- 使用.pdata区段和RUNTIME_FUNCTION_ENTRY结构管理堆栈
2.2 64位PE文件中的堆栈信息
x64 PE文件中的.pdata区段(x64独有)包含多个_IMAGE_RUNTIME_FUNCTION_ENTRY结构:
typedef struct _IMAGE_RUNTIME_FUNCTION_ENTRY {
DWORD BeginAddress;
DWORD EndAddress;
union {
DWORD UnwindInfoAddress;
DWORD UnwindData;
} DUMMYUNIONNAME;
} RUNTIME_FUNCTION, *PRUNTIME_FUNCTION, _IMAGE_RUNTIME_FUNCTION_ENTRY, *_PIMAGE_RUNTIME_FUNCTION_ENTRY;
_UNWIND_INFO结构:
typedef struct _UNWIND_INFO {
UBYTE Version : 3;
UBYTE Flags : 5;
UBYTE SizeOfProlog;
UBYTE CountOfCodes;
UBYTE FrameRegister : 4;
UBYTE FrameOffset : 4;
UNWIND_CODE UnwindCode[1];
} UNWIND_INFO, *PUNWIND_INFO;
_UNWIND_CODE联合体:
typedef union _UNWIND_CODE {
struct {
UBYTE CodeOffset;
UBYTE UnwindOp : 4;
UBYTE OpInfo : 4;
};
USHORT FrameOffset;
} UNWIND_CODE, *PUNWIND_CODE;
三、静态欺骗技术实现
3.1 ThreadStackSpoofer实现
项目地址:https://github.com/mgeeky/ThreadStackSpoofer
核心思路:
- Hook Sleep函数
- 当beacon调用Sleep时进入自定义MySleep函数
- 找到返回地址的内存地址并保存
- 将返回地址重写为0,隐藏shellcode堆栈
- Sleep结束后恢复原始堆栈
关键代码分析:
- Hook Sleep函数:
void hookSleep() {
// 准备hook结构体
// 将Sleep、MySleep和buffers传给fastTrampoline
// 构造trampoline用于跳转
// 修改addressToHook处的字节
}
- MySleep函数处理堆栈:
void MySleep(DWORD dwMilliseconds) {
// 获取返回地址的内存地址
PVOID overwrite = _AddressOfReturnAddress();
// 保存原始返回地址
PVOID originalReturnAddress = *(PVOID*)overwrite;
// 重写返回地址为0
*(PVOID*)overwrite = 0;
// 执行原始Sleep
OriginalSleep(dwMilliseconds);
// 恢复原始返回地址
*(PVOID*)overwrite = originalReturnAddress;
}
效果对比:
- 未欺骗时:完整显示shellcode堆栈帧
- 启用欺骗后:堆栈展开到MySleep函数,后续shellcode帧被隐藏
额外功能:
- 可在Sleep期间更改shellcode内存属性
- 对shellcode内存区域进行加密
- 解除/重新hook ETW/AMSI
3.2 CallStackMasker实现
项目地址:https://github.com/Cobalt-Strike/CallStackMasker
核心思路:
- 在beacon休眠前对计时器进行排队
- 用假的调用堆栈覆盖当前调用堆栈
- 恢复执行前恢复原始调用堆栈
两种模式:
- 静态模式:
- 模仿spoolsv.exe硬编码调用堆栈
- 线程显示通过KERNELBASE!WaitForSingleObjectEx处于'Wait:UserRequest'状态
- 动态模式:
- 枚举主机上所有可访问线程
- 找到通过WaitForSingleObjectEx处于UserRequest状态的线程
- 复制其堆栈用于休眠线程的克隆
关键代码分析:
void MaskCallStack() {
// 初始化上下文和句柄
// 获取NtContinue函数地址
// 设置定时器
// 创建定时器并设置回调函数
// 等待事件触发(堆栈被遮蔽)
// 定时器结束后恢复堆栈
}
3.3 纤程技术实现
纤程特点:
- 用户级线程,切换由程序控制
- 上下文包括寄存器状态和堆栈
- 切换时不需内核参与,效率高
关键API:
// 创建纤程
LPVOID lpFiber = CreateFiber(0, FiberFunc, NULL);
// 将当前线程转换为纤程
ConvertThreadToFiber(NULL);
// 切换到新创建的纤程
SwitchToFiber(lpFiber);
实现思路:
- Hook Sleep函数
- 调用Sleep时转换到新纤程
- Sleep结束后转回shellcode执行的纤程
四、技术优缺点分析
4.1 ThreadStackSpoofer
优点:
- 实现简单直接
- 有效隐藏shellcode堆栈
- 可扩展其他功能(内存加密等)
缺点:
- 堆栈不可展开,人工分析易发现异常
- 仅针对Sleep函数
4.2 CallStackMasker
优点:
- 动态模式更隐蔽
- 模仿真实进程堆栈
- 不易被自动化工具检测
缺点:
- 实现较复杂
- 静态模式依赖硬编码堆栈
4.3 纤程技术
优点:
- 切换效率高
- 完全控制堆栈上下文
缺点:
- 需要转换线程为纤程
- 实现复杂度较高
五、防御与检测建议
5.1 防御措施
- 监控关键API的hook行为(特别是Sleep)
- 检查线程堆栈的完整性
- 分析异常堆栈展开模式
5.2 检测方法
- 检查线程返回地址是否在合法模块内
- 验证堆栈帧链是否完整
- 监控纤程创建和切换行为
六、扩展应用
这些技术不仅可用于Sleep混淆,还可应用于:
- ETW/AMSI绕过
- 内存加密保护
- 反调试技术
- 进程注入隐藏
通过组合使用这些技术,可以构建更强大的规避检测方案。