一种对抗Unbalance Stack反沙箱的解决方法
字数 1181 2025-08-24 20:49:22
对抗Unbalance Stack反沙箱技术的解决方法
1. 概述
沙箱是分析恶意代码的常用手段,而HOOK常用Windows API是观察恶意代码行为的最常用技术。然而恶意代码也演化出各类反沙箱机制,其中一种就是Unbalance Stack技术,它可以检测堆栈变化来判断是否被HOOK。
2. Unbalance Stack检查原理
Unbalance Stack检查可以侦测所有类型的用户模式函数钩取,特别是通过包裹原始函数来控制输入输出的类型。其执行流程如下:
- 在被调用函数局部变量以上的位置放置canaries(校验值)
- 函数调用完成后检查这些canaries
- 如果canaries与原始值一致,则没有被HOOK
- 如果canaries被修改,则说明目标函数被HOOK了
正常状态下的堆栈
[调用者栈帧]
[返回地址]
[参数]
[被调用函数局部变量]
[canaries位置] <- 校验点
被HOOK后的堆栈
[调用者栈帧]
[返回地址]
[参数]
[Hook函数局部变量]
[被调用函数局部变量]
[canaries位置] <- 校验点(已被破坏)
3. 解决方法概述
为应对这种检测,有以下几种方法:
- 使用内核级别的Hook
- 在堆中分配空间,作为栈使用
- 降低堆栈指针的值,使其低于Canaries的位置
方法3对于大多数Hook是可行的,但存在以下问题:
- 不清楚当前函数栈帧在整个栈中的位置
- 难以确定Hook时需要多少局部变量
- 不知道目标函数需要多少堆栈空间
因此,本文提出结合方法2和3,在用户模式下绕过Unbalance检查。
4. 32位环境下的实现
4.1 准备工作
-
找到原始目标HOOK函数,修改其前12字节为:
mov ebx, target_func_addr jmp stack_shifter -
备份原始函数前12字节用于恢复
-
编写恢复函数,将原始函数恢复成原本状态
4.2 多线程安全考虑
使用以下结构保存原始函数的前12字节:
typedef struct _InsBackup {
BYTE original[12]; // 原始指令备份
DWORD funcAddr; // 目标函数地址
DWORD paramCount; // 参数数量
DWORD stackSize; // 堆栈大小
} InsBackup;
4.3 堆栈迁移函数实现
使用__declspec(naked)修饰声明裸函数:
StackShifter:
push ebp
mov ebp, esp
sub esp, 0x1000 ; 降低ESP值,绕过Unbalance Stack Canaries
push eax
push ebx
push ecx
push edx
push esi
push edi
; 分配局部存储
call RequestLocalMem ; 返回结构体指针在eax中
mov edi, eax ; 保存结构体指针
; 计算原始ESP值
mov eax, ebp
add eax, 8 ; 跳过保存的ebp和返回地址
mov [edi].data, eax ; 保存原始ESP
; 计算堆栈复制范围
mov ecx, [ebx].stackSize
mov esi, [edi].data
mov edi, [edi].stack
; 迁移堆栈
mov esp, edi ; 设置新堆栈
; 复制堆栈内容
rep movsb ; 复制堆栈
; 堆栈对齐处理
sub esp, 4
mov [ebp-4], esp ; 保存局部变量
; 重新Hook函数
mov eax, [ebx].funcAddr
mov [esp+0x20], eax ; 设置返回地址
mov eax, 1 ; 函数类型为__cdecl
; 恢复寄存器
pop edi
pop esi
pop edx
pop ecx
pop ebx
pop eax
leave
ret
对于__stdcall类型的函数,需要额外保存返回地址并清理参数(模拟RETN指令)。
5. 64位环境下的实现
5.1 准备工作
修改原始函数的操作码为:
mov rbx, count
jmp stack_shifter
InsBackup结构体变化为:
typedef struct _InsBackup64 {
BYTE original[12]; // 原始指令备份
DWORD64 funcAddr; // 目标函数地址
DWORD paramCount; // 参数数量
DWORD stackSize; // 堆栈大小
} InsBackup64;
5.2 堆栈迁移函数实现
64位汇编实现(单独.asm文件):
StackShifter64:
push rbp
mov rbp, rsp
sub rsp, 0x1000 ; 降低RSP值
push rax
push rbx
push rcx
push rdx
push rsi
push rdi
push r8
push r9
push r10
push r11
push r12
push r13
push r14
push r15
; 获取函数信息
mov rcx, rbx ; 函数索引
call GetFuncInfo ; 返回InsBackup64结构指针在rax中
mov rdi, rax ; 保存结构体指针
; 计算原始RSP值
mov rax, rbp
add rax, 8 ; 跳过保存的rbp和返回地址
mov [rdi].data, rax ; 保存原始RSP
; 计算堆栈复制范围
mov rcx, [rdi].stackSize
mov rsi, [rdi].data
mov rdi, [rdi].stack
; 迁移堆栈
mov rsp, rdi ; 设置新堆栈
; 复制堆栈内容
rep movsb ; 复制堆栈
; 设置返回地址
mov rax, [rdi].funcAddr
mov [rsp+0x80], rax ; 设置返回地址
; 恢复寄存器
pop r15
pop r14
pop r13
pop r12
pop r11
pop r10
pop r9
pop r8
pop rdi
pop rsi
pop rdx
pop rcx
pop rbx
pop rax
leave
ret
6. 关键点总结
- 堆栈迁移:通过降低堆栈指针并复制原始堆栈内容来绕过canaries检查
- 多线程安全:为每个调用分配专有的局部存储
- 调用约定处理:
- 32位区分
__cdecl和__stdcall - 64位统一使用fastcall
- 32位区分
- 实现细节:
- 32位使用内联汇编
- 64位需要单独汇编文件
- 正确处理堆栈对齐
- 恢复机制:保留原始函数指令以便恢复
7. 注意事项
- 修改函数代码前需要修改内存保护属性(PAGE_EXECUTE_READWRITE)
- 堆栈复制范围需要根据实际情况调整
- 64位环境下不能使用
__asm内联汇编 - 需要考虑多线程环境下的安全性
这种方法结合了堆栈指针调整和堆内存分配,有效绕过了Unbalance Stack检查,同时保持了较好的兼容性和安全性。