APP下载

基于API Hook的Stealth Loader局部堆问题解决

2021-02-27徐晓亭

现代计算机 2021年35期
关键词:调用内存程序

徐晓亭

(四川大学网络空间安全学院,成都 610207)

0 引言

鉴于当今恶意代码爆炸性增长趋势,国外安全厂商如卡巴斯基、VirusTotal等,和国内安全厂商如腾讯、微步等均推出了自动化判别未知恶意代码系统。API调用序列无疑是判别未知程序有无恶意行为的关键信息,其记录的完整与否直接影响判定结果。

Windows API可分为用户层和内核层,一个用户层API的调用将需要多个内核层API的配合才能完成,因而用户层API可以提供更加抽象的语义信息,相应的动态检测误报率和漏报率会更低。为了绕过动态检测,攻击者使用了API混淆技术来干扰用户层行为信息收集过程。

API混淆技术指在进行静态或动态分析恶意代码时,获取API地址对应的名称失败,从而可以隐藏它的真实意图,增加分析难度。Kawakoya等人[1]提出最新的 API混淆方案 Stealth Loader是该方向的最新成果,可击败大多数静态和动态反混淆技术。但本身也存在多种缺陷,如ntdll模块初始化、多个DLL之间局部堆共享、消息回调函数注册失败等。程斌林等人[2]提出的BinUnpack属于自动化脱壳方向的最新成果,可以检测到Stealth Loader,跟踪记录加壳后的程序调用的API,实现反混淆。

本文聚焦堆共享问题,依靠API Hook的原理解决此问题,使得其支持更加广泛的内存管理函数。将基于空闲链表的内存管理系统嵌入Stealth Loader中,删除加壳后程序的IAT,成功逃脱Bi⁃nUnpack的检测。

1 相关工作

1.1 API反混淆技术原理

根据上述对API混淆的定义,与之相对的反混淆就是将API地址与名称重新建立连接。现有的反混淆技术大多都是基于以下两个条件:

(1)IAT中的API地址可由DLL基址和DLL的EAT(Export Address Table,导出地址表)得出。

(2)DLL基址可通过Windows操作系统特定的数据结构获得。

基于以上两个条件,获取API名称的过程如算法1所示。

算法1获取API名称算法

其中A代表API地址,可由DLL基址与EAT获得;N代表API名称,可由DLL基址与ENT(Export Name Table,导出名称表)获得。dj代表一个API地址与其名称构成的元组(Aj,Nj)。Di代表由组成的集合,象征一个DLL中所有API函数。D代表由Di组成的集合,代表当前进程所有DLL。

1.2 最新的混淆技术与反混淆技术

Kawakoya等人提出Stealth Loader混淆技术,实现一套独立DLL管理系统,可绕过现有的大多数API反混淆技术。

它主要由exPEB、sLdrLoadDll、sLdrGetProc Address和BootStrap四部分组成。exPEB负责管理由sLdrLoadDll加载的DLL。sLdrLoadDll负责将DLL以Reflective Loading方式映射到内存中,并递归调用该函数与sLdrGetProcAddress函数修复DLL的IAT。sLdrGetProcAddress以API名称为参数,返回API地址。BootStrap负责解析程序所依赖的DLL,并调用sLdrLoadDll和sLdrGetProcAd⁃dress函数填充IAT。Stealth Loader还删除了DLL映射到内存中的DOS头、INT表、ENT表和调试节等信息,防止反混淆工具以这些信息为线索识别加载的模块。

程斌林等人[2]基于脱壳前后程序IAT地址不同的特性,提出Windows通用脱壳技术BinUn⁃pack,是自动化脱壳方向的最新研究成果。Bi⁃nUnpack利用Stealth Loader的API调用序列特征进行检测,即首先调用CreateFile而后调用内存分配API。但这种检测方法也不是无法绕过的,本文将基于空闲链表的内存管理系统与Stealth Loader相结合,并删除加壳后程序的IAT,达到绕过Bi⁃nUnpack检测的目的。

2 基于API Hook技术的方法设计

2.1 多个DLL之间局部堆共享问题

堆在系统安全领域是一种用于存储动态数据的内存空间。在Windows操作系统中,堆可分为私有堆(private heap)和局部堆(local heap)或称默认堆(default heap)。局部堆在Windows加载可执行程序时被创建,PE头结构中的SizeOfHeapRe⁃serve和SizeOfHeapCommit字段分别指明了需要保留和提交的局部堆空间大小。

Windows中对局部堆的使用主要有两种方式:一种是以Heap为前缀的通用堆管理函数,另一种是专用的局部堆管理函数,如LocalAlloc、Local⁃ReAlloc等。kernelbase动态链接库中的全局变量BHHT(base heap handle table)负责管理由专用局部堆函数分配的堆。

Stealth Loader先将三个必要模块重新加载到进程中,随后加载程序依赖的其他模块,修复模块之间的依赖关系。但出现kernelbase模块与其他模块的BHHT变量指针不一致的问题,即ker⁃nelbase中的BHHT指针指向自身,其他模块中的却指向Windows加载的kernelbase。随之而来的就是两kernelbase中的BHHT变量内容不相同,导致调用专用的局部堆管理函数时出现异常结果,如:LocalSize结果总是为0、GlobalReAlloc未成功扩展内存大小和GlobalLock锁定内存失败等,存在运行Crash的隐患。

2.2 API Hook技术

API Hook是一种通过劫持原有API控制流,改变执行结果的技术。虽然该技术实现方式存在很大差异,但原理都是修改控制流,使其经过回调函数。

2.3 解决方案

通过上文对该问题的分析,可知该问题的根源是由于两kernelbase中的BHHT变量内容不一致造成的。借助EAT Hook技术,替换写入BHHT变量的导出API函数地址为回调函数地址。在回调函数中,先执行原有的API函数,随后同步两kernelbase中的BHHT变量的内容。庆幸的是,与局部堆相关的Win32 API数量不是很多,且仅存在于kernelbase和kernel32两模块。借助静态分析工具IDA逆向分析kernelbase和kernel32,得到引用BHHT全局变量相关的API,如表1所示。

表1 两模块中读写BHHT API

3 方法实现

3.1 Stealth Loader系统设计

本小节将详细阐述Stealth Loader的设计实现过程。由于Kawakoya并没有公布相关源代码和具体实现细节,本文按照[1]中的框架重新实现。为了规避BinUnpack程序的检测,采用了以下两种方式:①实现基于空闲链表的内存管理系统,绕过其在API调用特征上的检测。②根据[2]中7.1节所述,无法作用于无IAT的程序。因此,将加壳后程序的IAT置空,致使BinUnpack脱壳失败。

3.1.1 修改PE头内存属性

从PEB中获取kernel32中基地址,进而依据PE格式得到VirtualProtect和VirtualAlloc两个导出函数地址。VirtualProtect修改PE头部的内存属性为可写,利用PE文件格式磁盘对齐与内存对齐的大小不同,可在PE头尾部0x200处存放一些重要信息,如字符串、exPEB的地址和一些必要的Windows系统API地址等。

3.1.2 内存分配

使用VirtualAlloc函数分配10 MB内存,用于存放程序所需要的外部模块和exPEB信息,并在此基础上使用空闲链表对内存进行管理。exPEB是由一个个Module结构体组成的,代表程序所依赖的外部模块,结构体的成员如表2所示。图1(a)是内存管理头部结构体,主要用于记录内存块的各种信息。内存分配算法如图1(b)所示,搜索空闲链表中的内存块,返回给用户。如果空闲块大于所需,需要进行拆分,并将剩余的内存链接至空闲链表中。内存回收算法如图1(c)所示,释放内存时为避免内存碎片过多,会进行适当的合并操作。

图1 内存管理算法

表2 Module结构体

3.1.3 解析外部模块

Windows系统下的每个进程都需要依赖ntdll、kernelbase和kernel32模块,因此首先将这些模块加载到内存中。保存表1中需要被Hook的原函数地址至内存镜像PE头0x200处,修改这些函数在Module结构体function数组中的值为回调函数地址。本文将function数组作为模块的EAT,替换其中的函数地址。对于kernelbase的回调函数来说,先执行对应的原函数,后将BHHT同步至Win⁃dows系统下的BHHT。对于kernel32的回调函数来说,也是先调用原函数,不同的是从Windows系统同步BHHT至Stealth Loader下。

根据PE文件格式,逐步寻找程序所依赖的外部函数和模块。调用sLdrLoadDll函数加载这些模块,sLdrGetProcAddress获取依赖函数的地址填充至模块的IAT中。

上述步骤已经将程序所有依赖外部函数填充至程序IAT中,最后跳转原始程序OEP(original entry point),执行真正的代码。

3.1.4 sLdrGetProcAddress和sLdrLoadDll函数

sLdrLoadDll函数按照Windows模块加载的顺序来进行搜索,以Reflective Loading[3]方式将这些模块加载到内存中,并注册至exPEB中。根据当前模块IAT,递归调用sLdrLoadDll和sLdrGetProc Address修复模块间依赖。

sLdrGetProcAddress的参数为模块基址B和函数名N或序号O。首先查找模块,比较B和exPEB内存中的Module的dst_base或src_base成员。然后定位函数名下标,如果第二个参数为函数名N,对N使用ror13函数得到N_hash,在模块的name_ror13数组中搜索,得到数组的下标I;如果第二个参数为O,下标I为O与ordinal_base的差。最后,返回函数地址functions[I]。

3.1.5 其他

一些分析工具依照PE头特征来识别加载的模块,在修复模块之间的依赖之后,删除模块的PE头是很有必要的。

BinUnpack检测程序最初的IAT是否包含正在调用的API地址,来判断是否新IAT以产生。如果未包含,则暗示新的IAT已产生,可进行脱壳操作。将IAT表从加壳后的程序中删除后,引起BinUnpack判断新IAT产生时机失误,脱壳失败。

3.2 Stealth Loader功能实验

Stealth Loader支持包括:ntdll、kernel32、ker⁃nelbase、gdi32、user32、shell32、shlwapi、ws2_32、wininet、winsock、crypt32 和 msvcrt,共 12 个 模块。这些模块中的API函数已经覆盖了恶意代码所需的各个方面。实验内容与[1]相同,分为两项:①API混淆功能测试。主要验证Stealth Loader是否可以击败各种动态和静态反混淆工具。②恶意代码测试。将二进制恶意代码作为输入,Stealth Loader输出具有功能相同且具有API反混淆能力恶意代码,在相关恶意软件分析平台检测其危险系数是否降低。

3.2.1 API混淆功能实验

选取相同的实验程序,包括:calc.exe、win⁃mine.exe、 notepad.exe、 cmd.exe、 regedt32.exe、tasklist.exe、taskmgr.exe、xcopy.exe和 ftp.exe。将它们作为Stealth Loader的输入,对输出的程序分别使用静态分析和动态分析验证是否达到API混淆的目的。

动静态分析。使用静态分析工具,包括IDA、Scylla、impscan和ldrmodules,动态分析工具,包括 Cuckoo沙箱[4]、traceapi[5]、min_apitracer[6],并增加对BinUnpack测试。在使用Cuckoo沙箱分析原始程序与加壳后的程序过程中,出现加壳后的程序无法在Cuckoo沙箱中执行的现象,造成前后分析结果相差较大。经过进一步的实验,加壳后的程序可以在真机或虚拟机环境中正常执行,这也可以证明Cuckoo沙箱反混淆失败。

实验结果如表4所示,可见重写的Stealth Loader达到了[1]中的效果。

表4 恶意代码实验结果

3.2.2 恶意代码实验

在3.2.1节的实验中,加壳后的程序无法在Cuckoo沙箱中运行,为了更加真实的测试重写Stealth Loader的效果,选择VirusTotal和微步两平台。

重复与[1]相同的实验,对从VirusShare平台收集到的283个原始样本加壳。首先对加壳后的程序逆向分析,确保原有功能保持不变。然后从中挑选10个不同类型的样本,上传至VirusTotal和微步两个恶意软件分析平台进行检测,结果如表5。

表3 API混淆功能实验结果

表5 函数分组测试

从表中可以得出以下结论:

(1)对于VirusTotal平台,Stealth Loader使得检测到威胁的反病毒引擎数量大大降低。

(2)除了Lokibot和CoinMiner两实验,其余实验中检测到威胁反病毒引擎数量均减少了一倍以上,且沙箱中运行的进程均Crash。

(3)两平台对恶意程序的分类出现偏差。

3.3 局部堆实验

kernelbase和kernel32模块中的专用局部堆管理函数按其功能可被分为两组,如表6所示。组一是测试内存管理函数是否可以正常使用,组二测试内存锁函数是否可以正常使用。

表6 内存管理函数实验结果

3.3.1 内存管理函数测试

使用下方的实验伪代码进行测试,包括内存分配、大小调整和释放三方面。

首先,前两行代码测试了Hook GlobalAlloc(kernelbase)的效果。若没有同步BHHT变量至Windows下 kernelbase,则 GlobalReAlloc(kernel32)将返回空。第四行代码测试Hook GlobalFree(kernel32)的效果,若没有同步变量内容至Stealth Loader下kernelbase中,result不为零。实验结果表7验证了EAT Hook技术已成功解决内存管理函数的使用问题。

表7 内存锁函数实验结果

3.3.2 内存锁函数测试

使用下方伪代码堆内存锁函数进行测试,包括加锁、解锁和获取锁的数量。

前两行伪码测试Hook LocalAlloc(kernelbase)函数的效果,分配0x40字节大小内存,并为其加锁。若没有同步BHHT至Windows下kernelbase,则 LocalFlags(kernel32)为 LMEM_INVALID_HANDLE,否则返回为 1(lock_count_first)和 0(lock_count_second)。实验结果表8验证了EAT Hook技术已成功解决内存锁函数使用问题。

4 结语

本文在重新实现Stealth Loader的基础上,结合了基于空闲链表的内存管理系统,并删除加壳后的IAT,以规避BinUnpack的检测。以EAT Hook技术为原型进行改进,修复了Stealth Loader的模块间局部堆问题。在3.2.1节中,实验对象出现了在沙箱环境下运行失败的情况,该情况并未在[1]中出现。未来将进一步修复Stealth Loader其他问题,并研究加壳后程序出现的反沙箱特性。

猜你喜欢

调用内存程序
给Windows添加程序快速切换栏
笔记本内存已经在涨价了,但幅度不大,升级扩容无须等待
“春夏秋冬”的内存
试论我国未决羁押程序的立法完善
“程序猿”的生活什么样
基于Android Broadcast的短信安全监听系统的设计和实现
英国与欧盟正式启动“离婚”程序程序
内存搭配DDR4、DDR3L还是DDR3?
利用RFC技术实现SAP系统接口通信
上网本为什么只有1GB?