0x1 概述

沙箱是分析恶意代码的常用手段,对常用Windows API的HOOK是观察恶意代码行为的最常用手段。但道高一尺魔高一丈,恶意代码也慢慢演化出各类反沙箱机制,如直接从NTDLL调用原生Windows API来规避HOOK,或者如本文,使用Unbalance Stack技巧来检测堆栈。本文给出一种方法来规避Unbalance Stack的检查。

0x2 Unbalance Stack检查

这种检查方式可以用于侦测所有类型的用户模式函数钩取,特别是通过包裹原始函数来控制输入输出的这种类型,其执行流程类似这样:

由于执行了新的外层函数HookRoutine,在执行OriginalFunction时堆栈必然比原本高,因此,我们可以在被调用函数局部变量以上的位置放置canaries,在函数调用完成后对原本的canaries进行检查,如果和原始canaries一致,则没有被HOOK,否则目标函数被HOOK了。
正常的状态:

如果原始函数被HOOK:

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

  1. 使用内核级别的Hook
  2. 在堆中分配空间,作为栈使用
  3. 降低堆栈指针的值,使其低于Canaries的位置
    实际上只使用方法3对于大多数Hook是可行的,但是有一个明显的问题,我们并不清楚当前函数栈帧在整个栈中的位置,也很难给出Hook时需要多少局部变量,更不知道目标函数需要多少堆栈空间用于存储局部变量,如果直接降低堆栈指针,在原始堆栈上进行HOOK并尝试绕过Unbalance检查,可能会导致堆栈溢出,因此此处给出一种方法,结合2、3两条,在用户模式下绕过Unbalance检查。

0x3 32位执行环境下的检查绕过:

32位执行环境下函数调用约定大致分为两种:cdecl和stdcall,因此要根据目标函数的类型进行堆栈迁移的分类。我们先找到原始目标HOOK函数,将其前12字节修改为:

并在相应位置上填入对应的地址,这里选用FF直接跳转是避免相对跳转增加程序复杂度,其中stack_function是我们要Hook的函数。StackShfter是栈迁移函数,要根据目标函数的调用原型来选择两个不同的版本。然后对原始函数进行数据备份(恢复用),后将HookIns写入stack_function起始址。(注意修改函数时要修改目标内存的Protection,否则会造成访问违规)。
同时,也要编写对应的恢复函数,将原始函数恢复成原本的状态。考虑到程序的多线程安全性,使用以下结构保存原始函数的前12字节:

下面进行最关键的步骤:堆栈迁移。
由于是32位的执行环境,因此直接__declspec(naked)修饰声明裸函数:

这里逐一解释重点内容:
首先在代码执行前,ebx中存储的是目标函数的地址,而且只有目标函数原型为__cdecl时,才会调用当前这个栈迁移函数。
第6行,将ESP值降低,绕过Unbalance Stack Canaries。
第9行:此处调用RequestLocalMem函数,分配当前调用专有的局部存储,也是为了多线程安全,该局部存储的结构很简单:

data为动态开辟的内存空间(其实没必要,但是防止日后有需求),临时存储ESP或原始EBP,返回地址(__stdcall中需要)。Stack指向新开辟堆栈的末端(因为栈是反方向生长的,所以要将指针指向末端,但实际上不是最后一个字节,因为堆栈要对齐,详情请参考Intel开发手册)。Func指向目标函数。
由于目前只需要保存ESP的值,因此只需申请4个字节的data空间即可。
第11行:暂时将该结构体的首地址信息保存在edi寄存器中。
第15-18行:计算原始ESP的值,用于函数调用完成后进行堆栈恢复。
第19-20行:计算堆栈复制范围,此处简单粗暴的将调用者的整个栈帧直接复制过来。
第22-23行:迁移堆栈。
第24-28行:复制堆栈内容。(其实此处使用rep movs指令更高效)
第30,32行:由于堆栈对齐的需要,预留了4字节在EBP指向的位置,也是为了保存局部变量的值。
第34-36行:由于只有类型为__cdecl的函数才会使用此堆栈迁移,所以自然知道函数类型为1(__cdecl),重新Hook函数。
然后后面的代码重新回收堆栈空间,还原EAX寄存器(返回值),完成。
__stdcall类型的函数HOOK和此类类似,只是需要保存返回地址然后清理参数(相当于模拟了RETN指令)。

0x4 64位环境下的检查绕过:

64位应用程序由于参数传递问题,而且不再使用RBP寄存器寻址局部变量,因此实现和32位稍有不同:
这里,调整插入原始函数的操作码为:

这里的count是函数的索引,即指明到底是那个函数,针对64位应用,InsBackup结构体也有所变化:(此处的设计主要是为了让上面HookIns占的空间尽可能小)

由于64位应用程序会优先使用RCX,RDX,R8,R9寄存器传递前4个参数,而实际上在目标函数内部,首先要做的还是将RCX,RDX,R8,R9的值放到栈中,与剩余的参数相邻放置,即RSP+4为第一个参数,RSP+8为第二个参数……,因此只需复制这部分区域即可(简单的实现)。
另外,由于64位函数调用均为fastcall类型,所以堆栈迁移实现较为简单,64位已经不允许使用__asm关键字内联汇编,所以将栈迁移函数汇编写入asm文件中:

与32位的占迁移基本类似:
第25行即从InsBackup结构中获取索引所对应函数的地址和参数数目。
该方法认为子函数不会使用RBP寄存器(大多数情况可能是这样的)
初出茅庐,简单写写。问题肯定存在,往各位大神看到多加指正,大家一起学习一起进步!
参考资料:
https://github.com/shmoocon/proceedings-2017/blob/master/belay/06_defeating_sandbox_evasion.md

点击收藏 | 2 关注 | 2
登录 后跟帖