虚拟机架构
基于栈的(Stack-based)
基于栈的虚拟机主要通过栈来执行操作,比如两个数字20和7相加可以用如下方式进行:
步骤:
- POP 20
- POP 7
- ADD 20,7,result
- PUSH result
基于栈的优点:
- 操作数由栈指针隐式寻址,虚拟机不需要显式地知道操作数地址
- 平均指令长度更短
基于寄存器的(Register-based)
基于寄存器的虚拟机的指令的操作数是在指令中显式寻址,也就是说指令需要包含操作数的地址(寄存器),比如两个数字5和30相加可以用如下方式进行:
步骤:
- ADD R1, R2, R3 ; # R1和R2的值相加,并将结果存到R3 Z中
基于寄存器的优点:
- 没有压入和弹出栈的开销,VM指令执行得更快
- 可以存储表达式的计算结果到寄存器中,当相同的表达式再次出现时可以直接使用,减少了再次计算的开销
VMProtect 1.81 Demo分析
VMP 1.81 demo的虚拟机架构是基于栈的,未经处理的代码如下
1 | .text:00401000 sub_401000 proc near ; CODE XREF: start↓p |
经VMP虚拟化后的代码如下
1 | .text:00401000 sub_401000 proc near ; CODE XREF: start↓p |
VMP结构
- VM_DATA 虚拟机字节码
- VM_EIP 指向虚拟机字节码的某个地址,在VMP中为ESI
- VM_CONTEXT 虚拟机上下文,在VMP中为EDI
- VM_STACK 虚拟栈,在VMP中EBP就是虚拟栈的栈顶指针
vm_entry
虚拟机的入口,代码如下
1 | .vmp0:0040472C push esi |
可以看到虚拟机入口先保存了各种寄存器,并为虚拟栈和虚拟机上下文开辟了空间,然后读取vm_data准备进入vm_handler。
vm_handler
所有的handler入口存储在一张表中,根据vm opcode即可进入对应的handler,下面分析几个handler
立即数压栈
1 | .vmp0:0040462B vPushImm4: ; CODE XREF: vm_entry+2B↓j |
寄存器压栈
1 | .vmp0:004045AF vPushReg4: ; CODE XREF: vm_entry+2B↓j |
寄存器出栈
1 | .vmp0:00404058 vPopReg4: ; CODE XREF: vm_entry+2B↓j |
读取内存
1 | .vmp0:00404069 vReadMemSs4: ; CODE XREF: vm_entry+2B↓j |
写入内存
1 | .vmp0:004045D8 vWriteMemSs4: ; CODE XREF: vm_entry+2B↓j |
返回(退出虚拟机)
1 | .vmp0:0040408E vRet: ; CODE XREF: vm_entry+2B↓j |
从上面可以看出所有需要抬升栈顶的handler结束后都不是直接返回到vm_dispatcher,而是走到了loc_40400F,代码如下
1 | .vmp0:0040400F loc_40400F: ; CODE XREF: vm_entry-6D9↓j |
VMP在执行的过程中会判断虚拟栈空间是否足够,如果不够会动态开辟栈空间,这也就解释了为什么一开始只分配了0xC0的栈空间。
vm_data
站在巨人的肩膀上,直接贴大佬的分析
1 | vPopReg4 R3 ; R3 = 0 |
vm_data一开始先将vm_entry压入栈中的寄存器全部存到vm_context,由此也能确定vm_context与原始寄存器的对应关系,vm_data的结束部分也与vRet可以对应得上。
VMProtect 1.8 分析
由于没有找到VMP 1.81 Demo,只找到了由其加壳过的程序,指令的丰富程度不够,于是找到了VMP 1.8正式版进行分析,未经处理的代码如下
1 | .text:00401000 sub_401000 proc near ; CODE XREF: start↓p |
经VMP虚拟化后的代码如下
1 | .text:00401000 sub_401000 proc near ; CODE XREF: start↓p |
可以看到比demo版本抽象了很多
vm_dispatcher
正式版的虚拟机部分做了比较多的混淆,通过调试器的trace功能找到指令执行次数最多的序列
1 | ; base = 0x1E0000 |
可以看出这是一个比较典型的dispatcher,并且在箭头部分对handler的地址进行了解密和进入,写个脚本处理一下handler table方便分析
1 | from idaapi import * |
vm_handler
由于加了混淆,静态分析会变得比较痛苦,通过trace记录可以减轻人工分析的复杂程度,举个例子
1 | 0x1e5f20 cmc |
将注意力集中在ebp有关的部分就能很快确定关键指令,通过分析可知该handler实现的是相加功能。接下来挑几个demo中没看到的讲
跳转(设置vEIP)
1 | 0x1e6d28 adc si, 0xc8c6 |
vm_data
vm_handler中对vm_data做了一些的加解密处理,如果静态分析vm_data有一定的工作量,这里只需要分析trace就好了,execution-trace-viewer脚本如下
1 | from yapsy.IPlugin import IPlugin |
分析出来的流程如下
1 | vPopReg4 R1 = 0xffde0000 |
jcc的目标地址直接通过eflags和一个数算出来了,懵逼,之后再看看。
参考
STACK BASED VS REGISTER BASED VIRTUAL MACHINE ARCHITECTURE, AND THE DALVIK VM
如何分析虚拟机系列(1):新手篇VMProtect 1.81 Demo
如何分析虚拟机(2):进阶篇 VMProtect 2.13.8
execution-trace-viewer x64dbg trace分析工具