一种对抗Unbalance Stack反沙箱的解决方法
字数 1181 2025-08-24 20:49:22

对抗Unbalance Stack反沙箱技术的解决方法

1. 概述

沙箱是分析恶意代码的常用手段,而HOOK常用Windows API是观察恶意代码行为的最常用技术。然而恶意代码也演化出各类反沙箱机制,其中一种就是Unbalance Stack技术,它可以检测堆栈变化来判断是否被HOOK。

2. Unbalance Stack检查原理

Unbalance Stack检查可以侦测所有类型的用户模式函数钩取,特别是通过包裹原始函数来控制输入输出的类型。其执行流程如下:

  1. 在被调用函数局部变量以上的位置放置canaries(校验值)
  2. 函数调用完成后检查这些canaries
  3. 如果canaries与原始值一致,则没有被HOOK
  4. 如果canaries被修改,则说明目标函数被HOOK了

正常状态下的堆栈

[调用者栈帧]
[返回地址]
[参数]
[被调用函数局部变量]
[canaries位置] <- 校验点

被HOOK后的堆栈

[调用者栈帧]
[返回地址]
[参数]
[Hook函数局部变量]
[被调用函数局部变量]
[canaries位置] <- 校验点(已被破坏)

3. 解决方法概述

为应对这种检测,有以下几种方法:

  1. 使用内核级别的Hook
  2. 在堆中分配空间,作为栈使用
  3. 降低堆栈指针的值,使其低于Canaries的位置

方法3对于大多数Hook是可行的,但存在以下问题:

  • 不清楚当前函数栈帧在整个栈中的位置
  • 难以确定Hook时需要多少局部变量
  • 不知道目标函数需要多少堆栈空间

因此,本文提出结合方法2和3,在用户模式下绕过Unbalance检查。

4. 32位环境下的实现

4.1 准备工作

  1. 找到原始目标HOOK函数,修改其前12字节为:

    mov ebx, target_func_addr
    jmp stack_shifter
    
  2. 备份原始函数前12字节用于恢复

  3. 编写恢复函数,将原始函数恢复成原本状态

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. 关键点总结

  1. 堆栈迁移:通过降低堆栈指针并复制原始堆栈内容来绕过canaries检查
  2. 多线程安全:为每个调用分配专有的局部存储
  3. 调用约定处理
    • 32位区分__cdecl__stdcall
    • 64位统一使用fastcall
  4. 实现细节
    • 32位使用内联汇编
    • 64位需要单独汇编文件
    • 正确处理堆栈对齐
  5. 恢复机制:保留原始函数指令以便恢复

7. 注意事项

  1. 修改函数代码前需要修改内存保护属性(PAGE_EXECUTE_READWRITE)
  2. 堆栈复制范围需要根据实际情况调整
  3. 64位环境下不能使用__asm内联汇编
  4. 需要考虑多线程环境下的安全性

这种方法结合了堆栈指针调整和堆内存分配,有效绕过了Unbalance Stack检查,同时保持了较好的兼容性和安全性。

对抗Unbalance Stack反沙箱技术的解决方法 1. 概述 沙箱是分析恶意代码的常用手段,而HOOK常用Windows API是观察恶意代码行为的最常用技术。然而恶意代码也演化出各类反沙箱机制,其中一种就是Unbalance Stack技术,它可以检测堆栈变化来判断是否被HOOK。 2. Unbalance Stack检查原理 Unbalance Stack检查可以侦测所有类型的用户模式函数钩取,特别是通过包裹原始函数来控制输入输出的类型。其执行流程如下: 在被调用函数局部变量以上的位置放置canaries(校验值) 函数调用完成后检查这些canaries 如果canaries与原始值一致,则没有被HOOK 如果canaries被修改,则说明目标函数被HOOK了 正常状态下的堆栈 被HOOK后的堆栈 3. 解决方法概述 为应对这种检测,有以下几种方法: 使用内核级别的Hook 在堆中分配空间,作为栈使用 降低堆栈指针的值,使其低于Canaries的位置 方法3对于大多数Hook是可行的,但存在以下问题: 不清楚当前函数栈帧在整个栈中的位置 难以确定Hook时需要多少局部变量 不知道目标函数需要多少堆栈空间 因此,本文提出结合方法2和3,在用户模式下绕过Unbalance检查。 4. 32位环境下的实现 4.1 准备工作 找到原始目标HOOK函数,修改其前12字节为: 备份原始函数前12字节用于恢复 编写恢复函数,将原始函数恢复成原本状态 4.2 多线程安全考虑 使用以下结构保存原始函数的前12字节: 4.3 堆栈迁移函数实现 使用 __declspec(naked) 修饰声明裸函数: 对于 __stdcall 类型的函数,需要额外保存返回地址并清理参数(模拟RETN指令)。 5. 64位环境下的实现 5.1 准备工作 修改原始函数的操作码为: InsBackup 结构体变化为: 5.2 堆栈迁移函数实现 64位汇编实现(单独.asm文件): 6. 关键点总结 堆栈迁移 :通过降低堆栈指针并复制原始堆栈内容来绕过canaries检查 多线程安全 :为每个调用分配专有的局部存储 调用约定处理 : 32位区分 __cdecl 和 __stdcall 64位统一使用fastcall 实现细节 : 32位使用内联汇编 64位需要单独汇编文件 正确处理堆栈对齐 恢复机制 :保留原始函数指令以便恢复 7. 注意事项 修改函数代码前需要修改内存保护属性(PAGE_ EXECUTE_ READWRITE) 堆栈复制范围需要根据实际情况调整 64位环境下不能使用 __asm 内联汇编 需要考虑多线程环境下的安全性 这种方法结合了堆栈指针调整和堆内存分配,有效绕过了Unbalance Stack检查,同时保持了较好的兼容性和安全性。