随着移动互联网的快速发展,软件的逆向分析与正向保护始终处于“矛”与“盾”的相互博弈之中。利用代码混淆技术生成的软件集合具有较高的异构性,增加了漏洞攻击的挑战性
[1]。其中,代码虚拟化混淆是一种高级且复杂的混淆技术。它采用自定义指令集架构,将原始指令转化为虚拟字节码,并通过虚拟机在进程层面对虚拟指令进行解释执行。由于虚拟字节码无法直接通过传统X86指令集理解,且不同的虚拟机解释器支持的字节码指令集各不相同,每条字节码的功能与具体实现也存在差异,这极大地增加了针对虚拟化混淆程序进行静态和动态分析的难度。
近年来,越来越多的恶意软件利用代码虚拟化混淆技术逃避检测
[2]。分析此类样本时,通常需先进行反混淆处理,而提取虚拟指令是反混淆的第1步。现有方法主要基于模式匹配,对部分指令操作码进行硬编码,但受限于虚拟指令的内部实现,无法识别超出匹配规则的指令。通过提取虚拟字节码解释执行过程中的汇编语法特征,存在约束设定不准确的问题,易导致虚拟指令语义识别错误。静态分析方法在处理虚拟分支时也存在局限性,因为动态获取虚拟指针寄存器VIP和滚动密钥寄存器VKEY的值是必要条件。由于虚拟机结构的变化,大多数依赖虚拟机结构假设的方法受到阻碍,仅支持单一版本的VMProtect(VMP)
[3]2.x或3.x,难以实现跨版本应用。
虚拟机执行过程主要包括取指、译码、分派和执行4个步骤。根据组织方式的不同,虚拟机解释器可分为分派式结构(Decode-Dispatch Based Interpretation)和穿线式结构(Threaded Interpretation),如
图1所示。
分派式结构在VMP3之前被广泛采用,其标志性的特征是存在一个中心译码—分派循环(Central Decode-Dispatch Loop)。分派函数(Dispatcher)作为一个特殊的Handler,充当调度器的角色。虚拟机引擎在执行完初始化模块后会进入Dispatcher,并调用各个Handler。每个Handler执行完毕后,控制流跳转回Dispatcher,继续循环执行。
穿线式结构在VMP3及后续版本中被应用。该结构移除了中心译码—分派循环,将Dispatcher合并到每个Handler中,并根据执行顺序将Handler串联起来。当前Handler执行完成后,控制流直接跳转至下一个Handler。穿线式结构减少了控制转移次数,充分利用了现代CPU对间接跳跃的预测特性,提高了虚拟机解释器的效率。同时,这一结构使得一些根据循环结构定位虚拟机的反混淆工具
[4-5]受到严重阻碍。
当前,针对代码虚拟化保护的正向应用和逆向工程研究十分密集,现有工作在VMP反混淆方面取得了一些进展,但仍存在虚拟指令识别不准确、静态分析无法解析分支跳转、无法跨大版本应用等挑战。文献[
4]提出通过编译器代码优化的反混淆方法,针对的是VMP2.x,不能直接应用于VMP3.x。文献[
5]提出的VMP虚拟指令逆向分析算法,将虚拟指令集进行简化和宽归类,但在还原后的指令上只简单地分成4类,缺少对虚拟指令细节和寄存器的具体描述。VMP分析插件
[6]采用基于模式匹配的方法,对部分指令操作码进行了硬编码,故受虚拟指令内部实现限制,需人工补充规则,且无法应用于VMP3.x。NoVmpy
[7]只能针对VMP3.x的虚拟化混淆进行处理,缺少对2.x版本的支持,且存在因无法解析跳转分支目标崩溃的情况
[8]。
为解决上述问题,提出一种基于符号执行的虚拟指令提取方法。通过对Handler进行符号执行
[9],生成状态表达式作为从字节码到虚拟指令的中间表示,基于启发式规则提取出具有完整语义的虚拟指令。在符号执行过程中引入具体值,动态获取当前Handler的虚拟寄存器与本地寄存器的映射关系,支持虚拟分支跳转解析。设计的Handler语义分析方法,适用于VMP2.x和VMP3.x两种不同的典型虚拟机结构所保护的二进制程序。实验结果表明,所提方法对比VMP分析插件和NoVmpy,虚拟指令识别率提升了26.72个百分点,准确率提升了41.09个百分点。
1 指令跟踪记录
在基于语义特征的虚拟化代码反混淆中,获取受保护程序的动态执行信息是首要环节。通常意义上的执行跟踪包含程序在运行过程中的动态指令的有限序列。执行跟踪的记录可以使用QEMU
[10]、Qiling
[11]等进行模拟执行,或者利用IDA等动态调试器生成。
指令动态跟踪阶段负责收集与虚拟化保护程序执行相关的运行时数据,采用动态二进制插桩的方法获得执行跟踪,并设计了一种结构体扩展的指令表示方法,以保存更为丰富的动态信息。在一个执行跟踪中,一个指令可能对应多条记录。为方便读取和处理,设计了长度为24字节的change结构体来存放每条记录,包含4个成员,指令计数number、指令操作类型flags、指令地址address和指令数据data。
指令计数number:该字段存储指令下标,表示从入口点开始执行的第几条指令。
指令操作类型flags:该字段表示当前change的操作类型,即寄存器/内存的读/写操作。
指令地址address:若flags对应开始记录指令标志,则address存放开始记录指令的地址;若flags对应内存操作标志,则address存放指令读写内存的地址;若flags对应寄存器操作标志(即非内存操作),则address存放寄存器的编号。
指令数据data:若flags对应开始记录指令标志,则data存放指令的长度;若flags对应寄存器或者内存操作标志,则data存放读/写的值。
2 虚拟指令提取
对指令跟踪记录进行离线分析,根据虚拟机结构及跳转规则生成Handler集合,之后动态符号执行
[12]对Handler进行语义分析,生成状态表达式,最后采用启发式规则提取出相应的虚拟指令。
尽管VMP2.x和VMP3.x虚拟机结构不同,但实际上虚拟字节码定义并无显著区别,主要是Handler的连接方式及指令内部实现方式发生了改变,只需在分析指令跟踪记录时进行分别处理,生成Handler集合,之后通过符号执行对Handler进行分析以及提取虚拟指令的方法通用
[13]。
2.1 Handler集合生成
由于一个虚拟化保护的程序中可能含有多个虚拟机结构,定义单个虚拟机为VM_Stub。为尽可能提高代码覆盖率,构造不同的输入以得到多个指令跟踪,分析得到VM_Stub集合。在VM_Stub中,按照一定规则划分出Handler,每个Handler由几十条真实指令组成。将所有Trace中提取出的Handler视为集合,根据虚拟指针寄存器VIP和Handler地址即可确认唯一Handler。针对两种虚拟机结构分别设计了相应的Handler划分算法。
2.1.1 VMP2.x:Handler划分
由于VMP2.x中存在以Dispatcher为核心的中心循环—译码结构,通过定位Dispatcher,即可定位Handler表。虚拟机引擎在执行完初始化模块后,就会进入Dispatcher,从中调用Handler,执行完成后会跳转回Dispatcher继续循环执行,Dispatcher在执行流图中的结构如
图2所示。可知,Dispatcher是与之存在双向边的节点数最多的特殊Handler。遍历所有基本块,在当前基本块的后继节点中寻找指向当前基本块的节点个数,个数最多的节点即为Dispatcher。Dispatcher将不同指令跳转到不同分支执行,通常用switch+case实现。
因此,对于VMP2.x,遍历Dispatcher的循环寻找Handler,生成集合。由于Handler存在调度器的所有循环中,只需遍历Dispatcher循环,除自身外的节点均属于一个Handler。
2.1.2 VMP3.x:Handler划分
在VMP3.x中,中心循环—译码结构被移除,需要通过定位虚拟机入口,确定当前VM_Stub的开始部分。虚拟机入口存在以下特征:Jmp addr、Push imm1、Call imm2。其中:第1条指令将程序的执行流从代码段跳转至VMP壳段;第2条指令将立即数imm1入栈,作为滚动加密密钥,在后续译码过程中多次使用;第3条指令控制虚拟指令寄存器VIP指向VM_Entry,立即数imm2即为VM_Entry的首地址。
在VM_Stub中,第一个Handler定义为虚拟机进入模块VM_Entry,最后一个Handler定义为虚拟机退出模块VM_Exit,中间的Handler则通过分析跳转连接形成序列。Handler间的跳转存在push reg, ret和jmp reg两种形式。其中,寄存器reg的值是下一个Handler的地址,通过在虚拟机的记录Stub.trace中匹配以上2种形式可以进行Handler的划分和识别。
在Handler内,虚拟指令指针寄存器VIP和滚动密钥寄存器VKEY对应的本地寄存器不变,在符号执行时,可将当前指令的VIP和VKEY的输出状态作为下一条指令的初始状态。当Handler执行到最后一条指令时,跳转后寄存器映射关系可能发生变化,通过反向遍历当前块指令,确定VIP和VKEY的具体值,保存并传递给下一个Handler新的VIP和VKEY所对应的本地寄存器。
退出虚拟机前,进行寄存器等上下文环境恢复,并通过ret语句返回。故虚拟机退出模块VM_Exit的识别,需要判断当前Handler的结尾是否包含一定数量的pop指令和ret指令。
2.2 Handler语义分析
现有工作在进行Handler语义分析时大多采用匹配关键汇编指令的方法,但如果虚拟机实现某条指令时采取其他等价形式,这类方法就会失效。由此,提出基于符号执行的Handler语义分析方法。通过符号执行,对Handler进行语义分析,生成符号表达式,并保留了关键寄存器的值。在符号执行层面上,只关注状态的转换,忽略不相关的细节,比如虚拟化混淆引入的大量冗余指令
[14]。
在虚拟机进入模块VM_Entry,首先需要完成寄存器状态初始化,然后按序对每条指令进行动态符号执行,得到VM_Entry执行后的状态表达式。为优化符号执行的表达式结果,在运行时读取了内存地址中当前存放的值。其他的Handler与之类似。
在分析Handler语义前,获取当前VM_Stub的虚拟指令指针寄存器VIP和滚动密钥寄存器VKEY。通过对Stub.trace进行符号执行,记录符号执行前后的寄存器状态表达式。VIP的变化范围是确定的整型量。而VKEY多次参与译码例程,状态表达式中出现次数最多的寄存器即为VKEY。确定VIP和VKEY对应的真实寄存器后,遍历Handler集合。第1个Handler为虚拟机进入模块VM_Entry,主要负责VM_Context的准备工作,例如将寄存器环境保存到堆栈。需注意,当VIP或VKEY不再为具体的整型数值时,说明虚拟寄存器的映射已发生变化,需重点分析虚拟跳转指令。
在一个Handler中,由于指令按序执行,控制转移指令没有实际上的作用,属于无用指令,但是在符号执行引擎中会产生一个分支。为简化后续分析,需要将Handler中的跳转指令进行转换,例如call指令转换为push addr。得到初始状态后,对Handler进行符号执行,得到输出状态表达式。
2.3 虚拟指令识别
以X86指令系统为基础,定义VMP虚拟指令集由以下4类构成:数据传送、算术运算、逻辑运算和移位以及控制转移虚拟指令,如
表1所示。
每个虚拟指令在功能上与X86指令等价,故符号执行得到的表达式结果在语义上是相同的,设计一种基于启发式规则的虚拟指令识别算法。根据对虚拟堆栈指针、字节码指针和内存的不同操作区分Handler类别,具体流程如
图3所示。
按照内存操作数的大小、运算符等规则将当前Handler与候选X86指令进行匹配。由于VMP是基于堆栈的虚拟机架构,只需重点关注部分寄存器和内存值,对符号执行结果进行输出过滤。如下:
--引用第三方内容--
EDX = EDX_init -> 0x38
ESI = ESI_init -> ESI_init + 0x4
@32[ESP_init + 0x38] = @32[ESI_init]
@32[ESP_init + 0xFFFFFFFC] = EBP_init + 0x513F1
以上述结果为例,ESI充当虚拟栈指针寄存器VSP,堆栈指针加4,说明为pop dword操作,将栈顶的值存入ESP所维护的VM_Context中,EDX存放虚拟寄存器偏移量。由此可得,该Handler对应的虚拟指令为vmp_pop,操作数为dreg[0x38]。
3 实验与评估
原型系统基于Intel Pin框架
[15]实现指令跟踪记录模块,对虚拟化保护程序进行动态二进制插桩,得到Trace文件。使用Miasm实现虚拟指令提取模块,通过符号执行引擎,将Trace划分出Handler集合,并生成状态表达式,根据执行前后状态变化,进一步识别Handler语义,提取出虚拟指令。
为测试和评估算法,构建虚拟机混淆和反混淆环境,对测试用例进行实验。通过分析提取出的虚拟指令在给定对应输入下的执行逻辑是否正确,以及输出结果是否与虚拟化保护前的原程序一致,来验证算法的正确性。分别虚拟机保护前后、反混淆后的指令数量的比较,对化简效果进行有效性评估,并与VMP分析插件、NoVmpy进行对比实验。
3.1 测试环境
实验软硬件环境为:Intel i9-13900HX 2.20 GHz处理器,32 GB RAM,Windows 11操作系统,VS2022编译环境,GCC8.1.0编译器,Intel Pin-3.21.98484,VMProtect版本v3.5.0。此外,用C/C++语言实现待保护的原始代码,用Python语言实现虚拟指令提取算法。
测试集包括VMP2.13.8和VMP3.5.0分别虚拟化保护后的5个混淆程序,分别是构造函数simpALU、冒泡排序算法Bubble_sort、快速排序算法Quick_sort和两种哈希算法RShash、DEKhash。保护的代码部分主要是关键算法,参考用于测试混淆算法的公开数据集,排序算法常见于数据库查询结果排序等场景,哈希算法主要应用于数据的加密和校验。
3.2 正确性测试与评估
正确性是指在逆向过程中,提取的虚拟指令片段的执行逻辑是否与原指令相同。根据应用程序常见方法的内部逻辑特点及类型,构建simpALU函数作为测试用例,用汇编指令实现加法、减法、与或4种运算操作并返回结果。指定虚拟化保护的位置区间,测试代码如下。
int simpALU(int x, int y, int z){
_asm{
mov eax, x
mov ecx, y
mov edx, z
//VMProtectBegin("funcA");
add eax, ecx
sub edx, eax
and ecx, edx
or eax, ecx
//VMProtectEnd();
mov x, eax
mov y, ecx
mov z, edx
}
printf("%d %d %d", x, y, z);
return 0;
}
关闭编译优化选项,生成Release版本的32 位可执行程序,使用VMP进行虚拟化保护。为排除其他干扰,设置选项中内存保护等针对文件的保护状态值为否,得到虚拟化混淆程序。通过指令跟踪记录和Handler集合生成,共有3 528 个运行指令,提取出61 个Handler集合。经Handler语义分析,最终生成的虚拟指令片段(部分)如下所示。
12:vmp_pushd reg[0x1c]
13:vmp_pushd reg[0x38]
14:vmp_add d[sp+4],d[sp]
通过分析第14条虚拟指令vmp_add对应的Handler符号执行结果,虚拟堆栈指针VSP存放在本地寄存器EDI中,@32[EDI_init + 0x4]保存虚拟堆栈上两数相加的结果,@32[EDI_init]保存标志位,与汇编指令add的定义一致。结合栈平衡原理和寄存器映射,可以从虚拟指令片段还原出初始代码中的“add eax, ecx”原始汇编指令,其他3条指令同理。如上所述,根据本算法分析出的虚拟指令,可从语义和结果上验证其正确性。
3.3 有效性测试与评估
为验证本文算法的有效性,从多种角度进行分析评估,
表2为静态信息,
表3为对混淆后程序的动态分析结果。根据
表3,VMProtect虚拟化混淆后程序的汇编指令数膨胀约500~2 500 倍。经本算法分析后,有效识别了Handler,生成的虚拟指令具备良好可读性,数量简化到原始指令的50~60倍。因此,基于虚拟指令对样本作进一步分析,能够显著降低逆向还原的难度。
3.3.1 VMP分析插件对比实验
VMP分析插件是zdhysd开发的一款Ollydbg插件。定义虚拟指令提取数量为分析后的指令片段中不重复的虚拟指令数(含未知指令),虚拟指令识别率为识别出的虚拟指令(非未知指令)占虚拟指令数的百分比,虚拟指令准确率为识别正确的虚拟指令占识别出的虚拟指令数的百分比。最新的VMP分析插件v1.4(VAP)对于VMP2.13.8的支持并不完善,尽管可以识别出虚拟机,但无法正常识别Handler,为体现对比效果,手动修改虚拟指令配置文件,得到VMP分析插件_patched(VAPP)。
表4结果显示,本文方法相较于VMP分析插件在虚拟指令数、虚拟指令提取数、虚拟指令识别率及准确率上均有明显提升,其中:虚拟指令识别率最多提升了62.5个百分点,平均提升了46.16个百分点;虚拟指令准确率最多提高了100个百分点,平均提高了82.18个百分点。对于VAPP,在以上指标中与本方法表现相当,但该工具的使用需在Ollydbg中调试进行,过程较为繁琐,而本方法虚拟指令提取任务的自动化和独立脚本运行。
3.3.2 NoVmpy对比实验
NoVmpy是一个开源反混淆项目,提供了IDA插件的使用形式,也可以直接执行脚本。将本文方法与NoVmpy分别应用于VMP3.5.0虚拟混淆后的测试程序,从虚拟指令提取数、识别率和准确率上进行对比。
在IDA中使用NoVmpy插件时,search可以找到虚拟机入口地址,letsgo生成虚拟指令时存在无法响应的情况。分析源代码发现,NoVmpy有时不能成功解析jmp的跳转目标,导致程序运行超时。针对该问题,引入本文方法记录的VM指令的分支信息(即vmp_jmp的跳转地址)用于辅助NoVmpy,称为NoVmpy-dyn。
由
表5可知,本文方法相较于NoVmpy(Npy)在虚拟指令片段的完整性上有明显优势,即使为后者增加了分支地址作为跳转的辅助信息,其虚拟指令数仍少于本文方法。在虚拟指令提取及识别率上,本文方法比NoVmpy最多提高了18.2个百分点,平均提高了7.28个百分点,这也受到NoVmpy未将混淆程序运行时的指令执行完全的影响,NoVmpy-dyn(Npyd)则较好地解决了该问题。从虚拟指令准确率上看,本文方法和NoVmpy对于已识别出的虚拟指令,均能够对其进行正确语义表示。
需要说明的是,为设定一致的虚拟指令类型标准,对一些虚拟指令的表示方法进行了归纳统一,例如:push_reg4、push_sp4、push_imm4均判断为一条虚拟指令vmp_pushd。整理后的虚拟指令仅表现操作码及操作数大小,操作数类型则由虚拟指令操作数单独表征。除一般的虚拟指令外,本文方法提取出的虚拟指令还包括虚拟机入指令口vmp_entry和出口指令vmp_exit,而NoVmpy及NoVmpy-dyn所提取出的虚拟指令只有vmp_exit,没有vmp_entry。表格中的虚拟指令提取数已分别减去上述特殊的虚拟指令。
4 结束语
由于代码虚拟化混淆的复杂性,静态分析技术在逆向还原代码时面临显著挑战。现有的反混淆方法存在虚拟指令识别不准确、虚拟分支解析失效、无法跨版本应用等问题。本文提出的虚拟指令提取方法,通过对Handler进行符号执行,生成状态表达式,并基于启发式规则进行筛选,得到具有完整语义的虚拟指令。在符号执行过程中引入具体值,动态获取Handler的虚拟寄存器与本地寄存器的映射关系,支持两种典型虚拟机结构下的分支解析。在实验部分,本文实现的原型系统与VMP分析插件和NoVmpy相比,虚拟指令识别率提高了26.72个百分点,准确率提升了41.09个百分点,优化了分支跳转的处理。实验结果表明,该方法能够准确提取虚拟指令流,确保虚拟指令语义正确,并减少逆向分析中的指令数量,从而增强对混淆程序的理解。然而,在指令跟踪过程中使用Intel Pin进行动态二进制插桩会增加程序开销。未来可考虑使用支持处理器级指令记录的CPU,在硬件层面直接记录运行时指令,以提升动态追踪效率。