初探堆栈欺骗之静态欺骗
字数 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

核心思路

  1. Hook Sleep函数
  2. 当beacon调用Sleep时进入自定义MySleep函数
  3. 找到返回地址的内存地址并保存
  4. 将返回地址重写为0,隐藏shellcode堆栈
  5. Sleep结束后恢复原始堆栈

关键代码分析

  1. Hook Sleep函数:
void hookSleep() {
    // 准备hook结构体
    // 将Sleep、MySleep和buffers传给fastTrampoline
    // 构造trampoline用于跳转
    // 修改addressToHook处的字节
}
  1. 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

核心思路

  1. 在beacon休眠前对计时器进行排队
  2. 用假的调用堆栈覆盖当前调用堆栈
  3. 恢复执行前恢复原始调用堆栈

两种模式

  1. 静态模式:
  • 模仿spoolsv.exe硬编码调用堆栈
  • 线程显示通过KERNELBASE!WaitForSingleObjectEx处于'Wait:UserRequest'状态
  1. 动态模式:
  • 枚举主机上所有可访问线程
  • 找到通过WaitForSingleObjectEx处于UserRequest状态的线程
  • 复制其堆栈用于休眠线程的克隆

关键代码分析

void MaskCallStack() {
    // 初始化上下文和句柄
    // 获取NtContinue函数地址
    // 设置定时器
    // 创建定时器并设置回调函数
    // 等待事件触发(堆栈被遮蔽)
    // 定时器结束后恢复堆栈
}

3.3 纤程技术实现

纤程特点

  • 用户级线程,切换由程序控制
  • 上下文包括寄存器状态和堆栈
  • 切换时不需内核参与,效率高

关键API

// 创建纤程
LPVOID lpFiber = CreateFiber(0, FiberFunc, NULL);

// 将当前线程转换为纤程
ConvertThreadToFiber(NULL);

// 切换到新创建的纤程
SwitchToFiber(lpFiber);

实现思路

  1. Hook Sleep函数
  2. 调用Sleep时转换到新纤程
  3. Sleep结束后转回shellcode执行的纤程

四、技术优缺点分析

4.1 ThreadStackSpoofer

优点

  • 实现简单直接
  • 有效隐藏shellcode堆栈
  • 可扩展其他功能(内存加密等)

缺点

  • 堆栈不可展开,人工分析易发现异常
  • 仅针对Sleep函数

4.2 CallStackMasker

优点

  • 动态模式更隐蔽
  • 模仿真实进程堆栈
  • 不易被自动化工具检测

缺点

  • 实现较复杂
  • 静态模式依赖硬编码堆栈

4.3 纤程技术

优点

  • 切换效率高
  • 完全控制堆栈上下文

缺点

  • 需要转换线程为纤程
  • 实现复杂度较高

五、防御与检测建议

5.1 防御措施

  • 监控关键API的hook行为(特别是Sleep)
  • 检查线程堆栈的完整性
  • 分析异常堆栈展开模式

5.2 检测方法

  • 检查线程返回地址是否在合法模块内
  • 验证堆栈帧链是否完整
  • 监控纤程创建和切换行为

六、扩展应用

这些技术不仅可用于Sleep混淆,还可应用于:

  • ETW/AMSI绕过
  • 内存加密保护
  • 反调试技术
  • 进程注入隐藏

通过组合使用这些技术,可以构建更强大的规避检测方案。

堆栈欺骗技术详解:静态欺骗与睡眠混淆 一、堆栈欺骗的背景与原理 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时会执行: 通过回溯rbp可以回溯整个堆栈 64位系统 : ebp成为通用寄存器,不再具有32位下的功能 使用.pdata区段和RUNTIME_ FUNCTION_ ENTRY结构管理堆栈 2.2 64位PE文件中的堆栈信息 x64 PE文件中的.pdata区段(x64独有)包含多个_ IMAGE_ RUNTIME_ FUNCTION_ ENTRY结构: _ UNWIND_ INFO结构: _ UNWIND_ CODE联合体: 三、静态欺骗技术实现 3.1 ThreadStackSpoofer实现 项目地址:https://github.com/mgeeky/ThreadStackSpoofer 核心思路 : Hook Sleep函数 当beacon调用Sleep时进入自定义MySleep函数 找到返回地址的内存地址并保存 将返回地址重写为0,隐藏shellcode堆栈 Sleep结束后恢复原始堆栈 关键代码分析 : Hook Sleep函数: MySleep函数处理堆栈: 效果对比 : 未欺骗时:完整显示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状态的线程 复制其堆栈用于休眠线程的克隆 关键代码分析 : 3.3 纤程技术实现 纤程特点 : 用户级线程,切换由程序控制 上下文包括寄存器状态和堆栈 切换时不需内核参与,效率高 关键API : 实现思路 : 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绕过 内存加密保护 反调试技术 进程注入隐藏 通过组合使用这些技术,可以构建更强大的规避检测方案。