关于 VMProtect,从其诞生到现在已经十几年。无数人投入精力进行研究,虚拟机基本结构已经基本明确了。
相关资料可以参考:
[专题][Fight Against Big Four]汇集所有能帮助你对抗强壳的知识(VMP、SE、THEMIDA、Enigma)
顺便推一下我整理的一份虚拟化保护相关资料的列表,放在了Github上,https://github.com/lmy375/awesome-vmp 。
对于 VMProtect 和 Themida 的虚拟机结构,许多文章已经说的得很清楚。然而却少有文章具体的分析方法。
([翻译]手把手静态分析FinSpy 系列文章有细致介绍作者分析FinSpy VM 的分析过程与思考过程,是非常值得参考的。)
假如我们面对野生样本中的未知虚拟机,该如何入手,一步一步弄清虚拟机结构,提取字节码,进行代码还原?
本系列会分析多个不同类型的虚拟机样本(VMProtet, Code Virtualizer 甚至更多有趣的 VM 保护样本),向大家展示我自己针对虚拟机保护代码的分析方法。
本文是系列的第1篇,内容上没有什么很新的东西,主要是展示一下完整的分析过程。
本文中通过 Trace 提取虚拟指令的部分我个人觉得还算有趣,对虚拟机已经有了解的读者可以跳过其他废话直接看那一部分。
对于大多数虚拟机来说,其结构是相似的。
一般虚拟机保护代码的执行过程是这样的:
所以在分析虚拟机保护的过程,把握如下几个关键要素:
下面实例分析一个虚拟机保护的样本,展示一下分析思路。
为什么选这么古老的版本,而且还是 Demo 版。因为这个版本虚拟机的主体代码没有混淆,保留了完整且清晰的虚拟机结构,适合入门分析。
样本是对如下代码进行 VM 得到的。
经验丰富的话应该对 VMProtect 虚拟机结构已经比较熟悉,这里并不会介绍新的东西,只是展示一下思路,给新人一点参考。
IDA 打开加保护后的文件,定位到0x401000位置,这是我们进行保护的代码位置。经过虚拟机保护,原本的代码已经不在了。
新的代码如下:
CALL的目的地址已经不在.text节
中,在新加的.vmp0
中。也就是虚拟机新加入的代码了。
具体代码如下:
这段代码首先保存当前寄存器的值。(这与前面介绍了虚拟机初始化的过程是一致的) 然后进行分配栈空间,初始化 esi, edi, ebp 寄存器。这三个寄存器都是作什么的?
mov al, [esi]
从 esi 地址取出 1 字节,并根据这一字节进行跳转jmp ds:off_40409C[eax*4]
。这一过程很像前面介绍的虚拟机执行过程的第2步。
继续分析验证推断, esi 的值实际来自前面的 mov esi, [esp+30h]
,进一步追溯实际来自push offset byte_404781
。IDA 查看一下 byte_404781
位置:
初始有值的数据,且位于 .vmp0
节内。这很有可能就是 VM_DATA
也就是虚拟机字节码的内容(实际也确实是这样的)。那么 esi 寄存器的作用也就明确了,就是VM_EIP
。
edi 和 ebp 是指向栈上的内存,具体是什么还不明确,继续分析。
前面那个jmp ds:off_40409C[eax*4]
跳转是个很典型的switch结构,IDA可以查看CFG图如下:
图中蓝色线条最为密集的部分就是 0x0404751 的代码,这部分代码前面已经分析过,是 esi 取字节并跳转。
可以看到跳转的目标很多,共有41个跳转目标。这时我们有充分的理由认为这个41个跳转目标就是 Handler 代码。
接下来是比较枯燥的过程,要逐条分析每个 Handler。
因为要考虑地址寄存器宽度1字节、2字节、4字节,所以41条Handler中有许多指令功能是一致的,只是数据宽度不同。取几条比较典型的指令说明:
立即数压栈
这条指令从 esi 地址取出 4 字节,然后 ebp - 4 后写入 [ebp] 内存处。 esi 指向的地方是 VM_DATA,因此取出的部分是指令中的固定数,即虚拟指令中的立即数。 ebp - 4 后再赋值的操作很像栈操作,先抬高栈顶,再写值。通过分析其他指令可以发现许多加减 ebp 然后读写值的情况。那么可以认定 ebp 就是虚拟栈栈顶指针。
寄存器压栈
这条指令取 al 的后几位,作为 edi 寄存器的偏移,取出值后压入栈顶。al 是之前从 esi 地址中取出的值,也是指令的一部分。由该值作索引,从 edi 寻址取值。可以猜测 edi 就是 VM_CONTEXT。这里是将虚拟寄存器中值压入虚拟栈中。
计算
这是一条比较明显的计算指令(加法指令)。
ebp + 0 是栈顶, ebp + 4 是次栈顶。 二者相加,保存在 [ebp + 4]。 eflag 值保存在 [ebp + 0]。
即先从栈中弹出两个数,相加后将结果压入栈中,再将eflag值压入栈中。
其他
我们已经确定了:
分析完所有 Handler 之后,就可以提取分析字节码了。
IDA 查看 VM_DATA 的内容如下:
跳转指令jmp ds:off_40409C[eax*4]
,跳转表就在0x0040409C处。如下:
VM_DATA 第1个字节是 0E。跳转的位置是 0x40409C + 0x0E * 4 = 0x4040d4
即上面的 vPopReg4 指令。Handler 代码如下:
0x0E & 0x3C = 0x0C
。 4 字节一个寄存器,0x0C/4 = 3
。因此这里是将栈中的值写入第3个寄存器(记为R3)。
整条指令就可以记为 vPushReg4 R3
。
VM_DATA 下一个字节是 E8 ,跳转的目标在
Handler 代码如下:
取 4 字节 81 A9 5D 76
, 对应数字 0x765da981,这条指令就可以记作 vPushImm4 0x765da981
依次类推,通过编写IDAPython脚本,可以自动的解析 VM_DATA。最终还原出所有虚拟指令。
静态分析确实可以还原出虚拟指令,但是动态运行可以更快更容易的得到结果。
我们已经分析出了每个 Handler 的位置。那么我们只需要确定每个 Handler 调用的序列,就可以还原出字节码。
这里我们使用 Ollydbg 2.0 的 Trace 功能(不使用 OD 1 的原因是 2 的 Trace 可以记录内存引用,功能更为强大)。
得到的 Trace 类似如下内容:
每行的格式比较统一,因此可以很容易的写脚本进行处理。
根据前面人肉分析的结果,已经确定了每个Handler的地址。
综合这些信息,我们逐条检查每一条Trace,如果地址是0x0404000,则说明在执行vAdd4指令,如果地址是0x0404041,则说明在执行vNor2指令,依次类推,写脚本处理,得到伪代码序列,如下:
不过还有一点不足,比如vPopReg4
指令,这里只知道指令类型,具体的操作数还是不知道的。根据前面的分析我们知道vPopReg4
操作的寄存器是由 al & 0x3c 决定的。因此我们只要再查找下 Trace 中执行到 vPopReg4 时 eax 的值,就可以计算出操作寄存器的下标了。比如第1次调用 vPopReg4
时的 Trace 如下:
EAX 的值为 0x1e,那么操作的虚拟寄存器下标就是 (0x0e & 0x3c)/4 = 3,完整的指令应该是vPopReg4 R3
。
再举个vPushImm4
的例子,第1次调用 vPushImm4
的 Trace如下:
调用mov eax, dword ptr ds:[esi]
后,EAX的值在下一条指令中体现,是EAX=765DA981
,那么完整的指令是vPushImm4 765DA981
通过这种方法,可补全指令中的立即数和寄存器下标,可以得取的最终虚拟指令序列如下:
结果和 VMP分析插件1.4 得到的结果是一样的。(本文虚拟指令的表示方式也参考了VMP分析插件1.4中表示方法。)
这部分一直都是十分困难的点。比较典型的方法是通过模板进行匹配收缩,VMP分析插件 1.4 就是这么做的。
不想收集模板,则可以利用编译优化的方法,可以参考我之前写过的[原创]通过编译优化进行VMP代码还原。不过这种方法准确性要比模板匹配差一些。
具体的自动化方法本文不再讨论。因为使用的示例程序比较简单,所以只简单人工还原一下。
注意虚拟机入口处的寄存器入栈顺序:
根据这个还原:
刚好可以和 vRet 指令衔接上。
本文以一个极弱的虚拟机 VMProtect 1.81 Demo 为例,完整的展示了一个虚拟机保护代码的分析过程。
从初步分析虚拟机结构、到提取Handler,并通过Trace提取虚拟指令序列,最后进行人肉进行代码还原。
作为第1篇,可能有意思的地方并不多。
首先选择的虚拟机版本太弱了,而且是虚拟机内部没有混淆和花指令的。对于正式版 VMProtect 虚拟机的解释执行过程都是添加了冗余指令的,这种情况该如何处理?Code Virtualizer 系列虚拟机内则有代码变形,又如何处理? VMProtect 3.x 已经没有解释循环,使用链式寻址代替,这样有什么方便的提取字节码的方法么?
后面的文章会一点点介绍,希望后面几篇可以让大家有更大的收获。
(文章涉及的样本、IDB文件、Trace文件、部分脚本见附件,密码123456)
2018/03/18
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2018-4-9 18:12
被穆恩编辑
,原因: