Cr4ckm3 发表于 2018-3-19 16:07

如何分析虚拟机系列(1):新手篇VMProtect 1.81 Demo

本帖最后由 Cr4ckm3 于 2018-3-26 10:10 编辑

# 序言

(本贴也发布于看雪:https://bbs.pediy.com/thread-225262.htm)

关于 VMProtect,从其诞生到现在已经十几年。无数人投入精力进行研究,虚拟机基本结构已经基本明确了。

相关资料可以参考:
[[专题]汇集所有能帮助你对抗强壳的知识(VMP、SE、THEMIDA、Enigma)](https://bbs.pediy.com/thread-224537.htm)

顺便推一下我整理的一份虚拟化保护相关资料的列表,放在了Github上,https://github.com/lmy375/awesome-vmp 。

对于 VMProtect 和 Themida 的虚拟机结构,许多文章已经说的得很清楚。然而却少有文章具体的分析方法。
([[翻译]手把手静态分析FinSpy ](https://bbs.pediy.com/thread-224911.htm) 系列文章有细致介绍作者分析FinSpy VM 的分析过程与思考过程,是非常值得参考的。)

假如我们面对野生样本中的未知虚拟机,该如何入手,一步一步弄清虚拟机结构,提取字节码,进行代码还原?

本系列会分析多个不同类型的虚拟机样本(VMProtet, Code Virtualizer 甚至更多有趣的 VM 保护样本),向大家展示我自己针对虚拟机保护代码的分析方法。

本文是系列的第1篇,内容上没有什么很新的东西,主要是展示一下完整的分析过程。

本文中通过 Trace 提取虚拟指令的部分我个人觉得还算有趣,对虚拟机已经有了解的读者可以跳过其他废话直接看那一部分。

# 虚拟机简介

对于大多数虚拟机来说,其结构是相似的。

- **VM_DATA** 是虚拟机字节码,是虚拟机要解释执行的指令。
- **VM_EIP**,也可以叫 VPC 或者 vEIP ,比如 VMProtect 中的 ESI 寄存器。一般是指向 VM_DATA 中的某个地址,虚拟机每次从这里取出指令,并执行。
- **VM_CONTEXT** 虚拟机上下文,实际就是虚拟机寄存器数组。比如 VMProtect 中的 EDI 寄存器的地址,就是虚拟机寄存器数组的起始地址。
- **VM_STACK** 虚拟栈,栈式虚拟机实现起来方便,膨胀倍数高,是虚拟机保护的首选。虚拟栈就是临时进行数据交换。VMProtect 的 EBP 寄存器就是虚拟栈的栈顶指针。

一般虚拟机保护代码的执行过程是这样的:
1. 初始化虚拟机。保存物理寄存器到虚拟机CONTEXT中。
2. 从 VM_EIP 处取出指针,根据指令的 Opcode 跳转到 Handler 代码中
3. 执行 Handler,每条 Handler 完成不一样的功能,代表不同的指令操作。 一般来说是操作虚拟寄存器或虚拟栈,进行一些算术运算等。
4. 执行完 Handler 后,VM_EIP 会向后移动,指向下一条指令。回到第2步,解释执行下一条指令。
5. 执行完全部字节码后,虚拟机退出,将虚拟寄存器的值还原到物理寄存器中。

所以在分析虚拟机保护的过程,把握如下几个关键要素:
1. 关键数据结构的位置:虚拟栈在哪?虚拟寄存器在哪儿?
2. 解释循环位置:VM_EIP 在哪儿?如何取指令?如何跳向 Handler?
3. Handler 分析:有多少条 Handler?每条 Handler 都要完成什么样的工作?

# 实例分析 VMProtect 1.81 Demo

下面实例分析一个虚拟机保护的样本,展示一下分析思路。

为什么选这么古老的版本,而且还是 Demo 版。因为这个版本虚拟机的主体代码没有混淆,保留了完整且清晰的虚拟机结构,适合入门分析。

样本是对如下代码进行 VM 得到的。
```
sub_401000 proc near
mov   eax, dword_403000
add   eax, 12345678h
sub   eax, 12345678h
mov   dword_403000, eax
retn
sub_401000 endp
```

经验丰富的话应该对 VMProtect 虚拟机结构已经比较熟悉,这里并不会介绍新的东西,只是展示一下思路,给新人一点参考。

## 1. 初步结构

IDA 打开加保护后的文件,定位到0x401000位置,这是我们进行保护的代码位置。经过虚拟机保护,原本的代码已经不在了。

新的代码如下:
```
.text:00401000               push    offset byte_404781
.text:00401005               call    sub_40472C
```
CALL的目的地址已经不在`.text节`中,在新加的`.vmp0`中。也就是虚拟机新加入的代码了。

具体代码如下:
```
.vmp0:0040472C               push    esi ; 依次保存寄存器
.vmp0:0040472D               push    edi
.vmp0:0040472E               push    esp
.vmp0:0040472F               push    ebx
.vmp0:00404730               push    eax
.vmp0:00404731               push    edx
.vmp0:00404732               push    ebp
.vmp0:00404733               pushf
.vmp0:00404734               push    ecx
.vmp0:00404735               push    ds:dword_404649; 重定位偏移
.vmp0:0040473B               push    0
.vmp0:00404740               mov   esi,    ; 前面 push 的 offset byte_404781
.vmp0:00404744               mov   ebp, esp
.vmp0:00404746               sub   esp, 0C0h   ; 分配一下栈空间
.vmp0:0040474C               mov   edi, esp      ; 栈顶位置赋值给 edi
.vmp0:0040474E
.vmp0:0040474E loc_40474E:                           
.vmp0:0040474E               add   esi, ; 重定位
.vmp0:00404751
.vmp0:00404751 loc_404751:                           
.vmp0:00404751                                       
.vmp0:00404751               mov   al,    ; 从 esi 取出 1 字节
.vmp0:00404753               movzx   eax, al   
.vmp0:00404756               inc   esi         ; esi 字节
.vmp0:00404757               jmp   ds:off_40409C; 根据 esi 取出字节进行跳转
```
这段代码首先保存当前寄存器的值。(这与前面介绍了虚拟机初始化的过程是一致的)

然后进行分配栈空间,初始化 esi, edi, ebp 寄存器。这三个寄存器都是作什么的?

`mov   al, `从 esi 地址取出 1 字节,并根据这一字节进行跳转`jmp   ds:off_40409C`。这一过程很像前面介绍的虚拟机执行过程的第2步。

继续分析验证推断, esi 的值实际来自前面的 `mov   esi, `,进一步追溯实际来自`push    offset byte_404781`。IDA 查看一下 `byte_404781` 位置:

```
.vmp0:00404781 byte_404781   db 0Eh, 0E8h, 81h      
.vmp0:00404784               dd 5C765DA9h, 3E0A1A1Eh, 3A36262Eh, 602122Ah, 3000E822h
.vmp0:00404784               dd 16790040h, 34567869h, 1E7A1412h, 5678E836h, 97341234h
.vmp0:00404784               dd 5C066B4Dh, 1121973Eh, 4C3C0622h, 0EB161121h, 1E11F7EAh
.vmp0:00404784               dd 326B2020h, 78081577h, 36325C32h, 30006904h, 0ED0040h
.vmp0:00404784               dd 4382010h, 8342C24h, 0E1001Ch, 8 dup(0)
.vmp0:00404800               dd 200h dup(?)
```
初始有值的数据,且位于 `.vmp0` 节内。这很有可能就是 `VM_DATA` 也就是虚拟机字节码的内容(实际也确实是这样的)。那么 esi 寄存器的作用也就明确了,就是`VM_EIP`。

edi 和 ebp 是指向栈上的内存,具体是什么还不明确,继续分析。

前面那个`jmp   ds:off_40409C`跳转是个很典型的switch结构,IDA可以查看CFG图如下:



图中蓝色线条最为密集的部分就是 0x0404751 的代码,这部分代码前面已经分析过,是 esi 取字节并跳转。

可以看到跳转的目标很多,共有41个跳转目标。这时我们有充分的理由认为这个41个跳转目标就是 Handler 代码。

## 2. Handler 分析

接下来是比较枯燥的过程,要逐条分析每个 Handler。

因为要考虑地址寄存器宽度1字节、2字节、4字节,所以41条Handler中有许多指令功能是一致的,只是数据宽度不同。取几条比较典型的指令说明:

1. 立即数压栈

```
.vmp0:0040462B vPushImm4:                           
.vmp0:0040462B                                       
.vmp0:0040462B               mov   eax,
.vmp0:0040462D               sub   ebp, 4
.vmp0:00404630               lea   esi,
.vmp0:00404633               mov   , eax
.vmp0:00404636               jmp   loc_40400F
```
这条指令从 esi 地址取出 4 字节,然后 ebp - 4 后写入 内存处。 esi 指向的地方是 VM_DATA,因此取出的部分是指令中的固定数,即虚拟指令中的立即数。 ebp - 4 后再赋值的操作很像栈操作,先抬高栈顶,再写值。通过分析其他指令可以发现许多加减 ebp 然后读写值的情况。那么可以认定 ebp 就是虚拟栈栈顶指针。

2. 寄存器压栈

```
.vmp0:004045AF vPushReg4:                           
.vmp0:004045AF                                    
.vmp0:004045AF               and   al, 3Ch
.vmp0:004045B2               mov   edx,
.vmp0:004045B5               sub   ebp, 4
.vmp0:004045B8               mov   , edx
.vmp0:004045BB               jmp   loc_40400F
```
这条指令取 al 的后几位,作为 edi 寄存器的偏移,取出值后压入栈顶。al 是之前从 esi 地址中取出的值,也是指令的一部分。由该值作索引,从 edi 寻址取值。可以猜测 edi 就是 VM_CONTEXT。这里是将虚拟寄存器中值压入虚拟栈中。

3. 计算

```
.vmp0:00404000 vAdd4:                                 
.vmp0:00404000                                       
.vmp0:00404000               mov   eax,
.vmp0:00404003               add   , eax
.vmp0:00404006               pushf
.vmp0:00404007               pop   dword ptr
.vmp0:0040400A               jmp   loc_404751
```

这是一条比较明显的计算指令(加法指令)。

ebp + 0 是栈顶, ebp + 4 是次栈顶。 二者相加,保存在 。 eflag 值保存在 。
即先从栈中弹出两个数,相加后将结果压入栈中,再将eflag值压入栈中。

4. 其他

我们已经确定了:
- esi 取出的值来自指令,因此是立即数或者寄存器下标
- edi 指向VM_CONTEXT,读写就是操作虚拟寄存器
- ebp 指向虚拟栈顶,读写就是压栈、弹栈

根据这几点,很容易就可以将所有 Handler 的作用分析清楚。

## 3. 还原字节码

分析完所有 Handler 之后,就可以提取分析字节码了。

### 方法1:IDA 静态解析 VM_DATA

IDA 查看 VM_DATA 的内容如下:
```
004047810E E8 81 A9 5D 76 5C 1E1A 0A 3E 2E 26 36 3A 2A....]v\...>.&6:*
0040479112 02 06 22 E8 00 30 4000 79 16 69 78 56 34 12..."..0@.y.ixV4.
004047A114 7A 1E 36 E8 78 56 3412 34 97 4D 6B 06 5C 3E.z.6.xV4.4.Mk.\>
004047B197 21 11 22 06 3C 4C 2111 16 EB EA F7 11 1E 20.!.".<L!.......
004047C120 6B 32 77 15 08 78 325C 32 36 04 69 00 30 40   k2w..x2\26.i.0@
004047D100 ED 00 10 20 38 04 242C 34 08 1C 00 E1 00 00.... 8.$,4......
004047E100 00 00 00 00 00 00 0000 00 00 00 00 00 00 00................
```

跳转指令`jmp   ds:off_40409C`,跳转表就在0x0040409C处。如下:
```
.vmp0:0040409C handlers      dd offset vPushReg4   
.vmp0:004040A0               dd offset vShl2
.vmp0:004040A4               dd offset vPopReg4
.vmp0:004040A8               dd offset vPushImm2
.vmp0:004040AC               dd offset vPushReg4
.vmp0:004040B0               dd offset vNor2
.vmp0:004040B4               dd offset vPopReg4
.vmp0:004040B8               dd offset vShl1
.vmp0:004040BC               dd offset vPushReg4
.vmp0:004040C0               dd offset vWriteMemDs4
.vmp0:004040C4               dd offset vPopReg4
.vmp0:004040C8               dd offset vShr2
.vmp0:004040CC               dd offset vPushReg4
.vmp0:004040D0               dd offset vNor2
.vmp0:004040D4               dd offset vPopReg4 <---
.vmp0:004040D8               dd offset vPopEBP
.vmp0:004040DC               dd offset vPushReg4
....
```

VM_DATA 第1个字节是 0E。跳转的位置是 `0x40409C + 0x0E * 4 = 0x4040d4`

即上面的 vPopReg4 指令。Handler 代码如下:

```
.vmp0:00404058               and   al, 3Ch
.vmp0:0040405B               mov   edx,    ; 取栈顶值
.vmp0:0040405E               add   ebp, 4         ; 弹栈
.vmp0:00404061               mov   , edx ; 值写入VM_CONTEXT
.vmp0:00404064               jmp   loc_404751
```
`0x0E & 0x3C = 0x0C`。 4 字节一个寄存器,`0x0C/4 = 3`。因此这里是将栈中的值写入第3个寄存器(记为R3)。

整条指令就可以记为 `vPushReg4 R3`。

VM_DATA 下一个字节是 E8 ,跳转的目标在

```
.vmp0:0040443C               dd offset vPushImm4
```

Handler 代码如下:

```
.vmp0:0040462B vPushImm4:                           
.vmp0:0040462B                                       
.vmp0:0040462B               mov   eax,    ; 取4字节立即数
.vmp0:0040462D               sub   ebp, 4       ; 栈分析4字节空间
.vmp0:00404630               lea   esi, ; esi = esi + 4
.vmp0:00404633               mov   , eax ; 压入栈顶
.vmp0:00404636               jmp   loc_40400F
```
取 4 字节 `81 A9 5D 76`, 对应数字 0x765da981,这条指令就可以记作 `vPushImm4 0x765da981`

依次类推,通过编写IDAPython脚本,可以自动的解析 VM_DATA。最终还原出所有虚拟指令。

### 方法2: OD Trace 分析

静态分析确实可以还原出虚拟指令,但是动态运行可以更快更容易的得到结果。

我们已经分析出了每个 Handler 的位置。那么我们只需要确定每个 Handler 调用的序列,就可以还原出字节码。

这里我们使用 Ollydbg 2.0 的 Trace 功能(不使用 OD 1 的原因是 2 的 Trace 可以记录内存引用,功能更为强大)。

1. OD2 载入样本
2. 切换 E (Executable modules) 窗口,右键主模块,选择 `Limit run trace protocol to selected module`。表示 Trace 时记录该模块的所有地址,其他模块(如系统模块)不进行记录。
3. Options -> Run Trace -> 勾选 Remember memory ,记录内存
4. Trace -> Trace into 或 Ctrl + F11 开始 Trace
5. View -> Run trace 打开 Trace 窗口,右键 Log to file (勾选Add available contents 和 Separate columns with tabs)保存文件。

得到的 Trace 类似如下内容:

```
main      00401000      push    00404781      =00000000      EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF80, EBP=0019FF94, ESI=00401015, EDI=00401015
main      00401005      call    0040472C                EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF7C, EBP=0019FF94, ESI=00401015, EDI=00401015
main      0040472C      push    esi      =00000000      EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF78, EBP=0019FF94, ESI=00401015, EDI=00401015
main      0040472D      push    edi      =00000000      EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF74, EBP=0019FF94, ESI=00401015, EDI=00401015
main      0040472E      push    esp      =00000000      EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF70, EBP=0019FF94, ESI=00401015, EDI=00401015
main      0040472F      push    ebx      =00000000      EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF6C, EBP=0019FF94, ESI=00401015, EDI=00401015
main      00404730      push    eax      =00000000      EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF68, EBP=0019FF94, ESI=00401015, EDI=00401015
main      00404731      push    edx      =00000000      EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF64, EBP=0019FF94, ESI=00401015, EDI=00401015
main      00404732      push    ebp      =00000000      EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF60, EBP=0019FF94, ESI=00401015, EDI=00401015
main      00404733      pushfd      =00000000      EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF5C, EBP=0019FF94, ESI=00401015, EDI=00401015
main      00404734      push    ecx      =00000000      EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF58, EBP=0019FF94, ESI=00401015, EDI=00401015
main      00404735      push    dword ptr ds:      =00000000      EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF54, EBP=0019FF94, ESI=00401015, EDI=00401015
main      0040473B      push    0      =00000000      EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF50, EBP=0019FF94, ESI=00401015, EDI=00401015
main      00404740      mov   esi, dword ptr ss:      =00404781      EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF4C, EBP=0019FF94, ESI=00401015, EDI=00401015
main      00404744      mov   ebp, esp                EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF4C, EBP=0019FF94, ESI=00404781, EDI=00401015
main      00404746      sub   esp, 0C0                EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF4C, EBP=0019FF4C, ESI=00404781, EDI=00401015
main      0040474C      mov   edi, esp                EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FE8C, EBP=0019FF4C, ESI=00404781, EDI=00401015
main      0040474E      add   esi, dword ptr ss:      =00000000      EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FE8C, EBP=0019FF4C, ESI=00404781, EDI=0019FE8C
main      00404751      mov   al, byte ptr ds:      =0E      EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FE8C, EBP=0019FF4C, ESI=00404781, EDI=0019FE8C
main      00404753      movzx   eax, al                EAX=099B410E, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FE8C, EBP=0019FF4C, ESI=00404781, EDI=0019FE8C
main      00404756      inc   esi                EAX=0000000E, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FE8C, EBP=0019FF4C, ESI=00404781, EDI=0019FE8C
main      00404757      jmp   dword ptr ds:      =00404058      EAX=0000000E, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FE8C, EBP=0019FF4C, ESI=00404782, EDI=0019FE8C
...
```
每行的格式比较统一,因此可以很容易的写脚本进行处理。

根据前面人肉分析的结果,已经确定了每个Handler的地址。
```
0x0404000: 'vAdd4',
0x0404041: 'vNor2',
0x0404058: 'vPopReg4',
0x0404069: 'vReadMemSs4',
0x0404077: 'vShr4',
0x040408E: 'vRet',
....(等)
```
综合这些信息,我们逐条检查每一条Trace,如果地址是0x0404000,则说明在执行vAdd4指令,如果地址是0x0404041,则说明在执行vNor2指令,依次类推,写脚本处理,得到伪代码序列,如下:
```
# python dump_vmp1.81.py
vPopReg4
vPushImm4
vAdd4
vPopReg4
vPopReg4
vPopReg4
vPopReg4
vPopReg4
vPopReg4
vPopReg4
vPopReg4
vPopReg4
vPopReg4
...(等)
```
不过还有一点不足,比如`vPopReg4`指令,这里只知道指令类型,具体的操作数还是不知道的。根据前面的分析我们知道`vPopReg4`操作的寄存器是由 al & 0x3c 决定的。因此我们只要再查找下 Trace 中执行到 vPopReg4 时 eax 的值,就可以计算出操作寄存器的下标了。比如第1次调用 `vPopReg4` 时的 Trace 如下:
```
.vmp0:00404058 vPopReg4:   

main      00404058      and   al, 3C                EAX=0000000E, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FE8C, EBP=0019FF4C, ESI=00404782, EDI=0019FE8C
main      0040405B      mov   edx, dword ptr ss:      =00000000      EAX=0000000C, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FE8C, EBP=0019FF4C, ESI=00404782, EDI=0019FE8C
main      0040405E      add   ebp, 4                EAX=0000000C, ECX=00401015, EDX=00000000, EBX=00344000, ESP=0019FE8C, EBP=0019FF4C, ESI=00404782, EDI=0019FE8C
main      00404061      mov   dword ptr ds:, edx      =00000000      EAX=0000000C, ECX=00401015, EDX=00000000, EBX=00344000, ESP=0019FE8C, EBP=0019FF50, ESI=00404782, EDI=0019FE8C
main      00404064      jmp   00404751                EAX=0000000C, ECX=00401015, EDX=00000000, EBX=00344000, ESP=0019FE8C, EBP=0019FF50, ESI=00404782, EDI=0019FE8C
```
EAX 的值为 0x1e,那么操作的虚拟寄存器下标就是 (0x0e & 0x3c)/4 = 3,完整的指令应该是`vPopReg4 R3`。

再举个`vPushImm4`的例子,第1次调用 `vPushImm4` 的 Trace如下:
```
main      0040462B      mov   eax, dword ptr ds:      =765DA981      EAX=000000E8, ECX=00401015, EDX=00000000, EBX=00344000, ESP=0019FE8C, EBP=0019FF50, ESI=00404783, EDI=0019FE8C
main      0040462D      sub   ebp, 4                EAX=765DA981, ECX=00401015, EDX=00000000, EBX=00344000, ESP=0019FE8C, EBP=0019FF50, ESI=00404783, EDI=0019FE8C
main      00404630      lea   esi,       =5C      EAX=765DA981, ECX=00401015, EDX=00000000, EBX=00344000, ESP=0019FE8C, EBP=0019FF4C, ESI=00404783, EDI=0019FE8C
main      00404633      mov   dword ptr ss:, eax      =00000000      EAX=765DA981, ECX=00401015, EDX=00000000, EBX=00344000, ESP=0019FE8C, EBP=0019FF4C, ESI=00404787, EDI=0019FE8C
main      00404636      jmp   0040400F                EAX=765DA981, ECX=00401015, EDX=00000000, EBX=00344000, ESP=0019FE8C, EBP=0019FF4C, ESI=00404787, EDI=0019FE8C
```
调用`mov   eax, dword ptr ds:`后,EAX的值在下一条指令中体现,是`EAX=765DA981`,那么完整的指令是`vPushImm4 765DA981`

通过这种方法,可补全指令中的立即数和寄存器下标,可以得取的最终虚拟指令序列如下:

```
vPopReg4      R3
vPushImm4       0x765da981
vAdd4
vPopReg4      R7
vPopReg4      R6
vPopReg4      R2
vPopReg4      R15
vPopReg4      R11
vPopReg4      R9
vPopReg4      R13
vPopReg4      R14
vPopReg4      R10
vPopReg4      R4
vPopReg4      R0
vPopReg4      R1
vPopReg4      R8
vPushImm4       0x403000
vReadMemDs4
vPopReg4      R5
vPushImm4       0x12345678
vPushReg4       R5
vAdd4
vPopReg4      R7
vPopReg4      R13
vPushImm4       0x12345678
vPushReg4       R13
vPushEBP
vReadMemSs4
vNor4
vPopReg4      R1
vAdd4
vPopReg4      R15
vPushEBP
vReadMemSs4
vNor4
vPopReg4      R8
vPopReg4      R1
vPushReg4       R15
vPushEBP
vReadMemSs4
vNor4
vPopReg4      R5
vPushImmSx2   0xfffff7ea
vNor4
vPopReg4      R7
vPushReg4       R8
vPushReg4       R8
vNor4
vPopReg4      R12
vPushImmSx2   0x815
vNor4
vPopReg4      R12
vAdd4
vPopReg4      R12
vPopReg4      R13
vPushReg4       R1
vPushImm4       0x403000
vWriteMemDs4
vPushReg4       R0
vPushReg4       R4
vPushReg4       R8
vPushReg4       R14
vPushReg4       R1
vPushReg4       R9
vPushReg4       R11
vPushReg4       R13
vPushReg4       R2
vPushReg4       R7
vPushReg4       R0
vRet
```

结果和 VMP分析插件1.4 得到的结果是一样的。(本文虚拟指令的表示方式也参考了VMP分析插件1.4中表示方法。)

## 4. 还原x86代码

这部分一直都是十分困难的点。比较典型的方法是通过模板进行匹配收缩,VMP分析插件 1.4 就是这么做的。

不想收集模板,则可以利用编译优化的方法,见我之前写过的[[原创]通过编译优化进行VMP代码还原](https://bbs.pediy.com/thread-217588.htm)。不过这种方法准确性要比模板匹配差一些。

具体的自动化方法本文不再讨论。因为使用的示例程序比较简单,所以只简单人工还原一下。

注意虚拟机入口处的寄存器入栈顺序:

```
.vmp0:0040472C               push    esi
.vmp0:0040472D               push    edi
.vmp0:0040472E               push    esp
.vmp0:0040472F               push    ebx
.vmp0:00404730               push    eax
.vmp0:00404731               push    edx
.vmp0:00404732               push    ebp
.vmp0:00404733               pushf
.vmp0:00404734               push    ecx
.vmp0:00404735               push    ds:reloc
.vmp0:0040473B               push    0
```
根据这个还原:
```
vPopReg4      R3         ; R3 = 0   
vPushImm4       0x765da981   ;
vAdd4                        ;
vPopReg4      R7         ; R7 = eflag
vPopReg4      R6         ; R6 = 0x765da981
vPopReg4      R2         ; R2 = ECX
vPopReg4      R15          ; R15 = EFLAG
vPopReg4      R11          ; R11 = EBP
vPopReg4      R9         ; R9 = EDX
vPopReg4      R13          ; R13 = EAX
vPopReg4      R14          ; R14 = EBX
vPopReg4      R10          ; R10 = ESP
vPopReg4      R4         ; R4 = EDI
vPopReg4      R0         ; R0 = ESI
vPopReg4      R1         ; push/call 的返回地址 0x40100A
vPopReg4      R8         ; push 的值 即 vm_data 0x404781

// mov   eax, dword_403000
vPushImm4       0x403000
vReadMemDs4
vPopReg4      R5         ; R5 =

// add   eax, 12345678h
vPushImm4       0x12345678
vPushReg4       R5         ;
vAdd4                        ; R5 + 0x12345678
vPopReg4      R7         ; add_flag
vPopReg4      R13          ; R13 = R5 + 0x12345678

// sub   eax, 12345678h

// 关键计算公式如下:
// not(a) = nor(a, a)
// and(a, b) = nor(not(a), not(b))
// sub(a, b) = not(not(a) + b) = nor(not(a) + b, not(a) + b) = nor(nor(a, a) + b, nor(a, a) + b)
// 0xfffff7ea = not(0x815)
// not_flag(a) = nor_flag(a, a)
// and_flag(a, b) = nor_flag(a, not(b))
// sub_flag(a, b) = and(FFFFF7EA, not_flag(not(a) + b)) + and(0x815 , add_flag(not(a), b))

vPushImm4       0x12345678   ; 0x12345678 是 b
vPushReg4       R13          ; R13 是 a
vPushEBP
vReadMemSs4                  ; 栈顶2个 a
vNor4                        ; nor(a, a) = not(a)
vPopReg4      R1         ; R1 = not_flag(a)
vAdd4                        ; not(a) + b
vPopReg4      R15          ; R15 = add_flag(not(a) + b)
vPushEBP                     ;
vReadMemSs4                  ; 栈顶2个 not(a) + b
vNor4                        ; not(not(a)+b) = sub(a,b)
vPopReg4      R8         ; R8 = not_flag(not(a) + b)
vPopReg4      R1         ; R1 = sub(a,b) = R13 - 0x12345678
vPushReg4       R15          ; R15 = add_flag(not(a) + b)
vPushEBP                  
vReadMemSs4               
vNor4                        ; not(R15) = not(add_flag(not(a) + b))
vPopReg4      R5         ; flag - 无用
vPushImmSx2   0xfffff7ea   
vNor4                        ; nor(0xfffff7ea, R14) = and (0x815, R15) = and(0x815, add_flag(not(a) + b))
vPopReg4      R7         ; R7 = and(0x815, add_flag(not(a) + b)) sub_flag 右半部分
vPushReg4       R8         
vPushReg4       R8
vNor4                        ; not(R8) = not(not_flag(not(a) + b))
vPopReg4      R12          ; flag - 无用
vPushImmSx2   0x815      
vNor4                        ; nor(0x815, not(R8)) = and(0xfffff7ea, R8) = and(FFFFF7EA, not_flag(not(a) + b))
vPopReg4      R12          ; flag - 无用
vAdd4                        ; add
vPopReg4      R12          ; flag - 无用
vPopReg4      R13          ; R13 = and(FFFFF7EA, not_flag(not(a) + b)) + and(0x815 , add_flag(not(a), b)) = sub_flag(a, b)

// mov   dword_403000, eax
vPushReg4       R1
vPushImm4       0x403000   ; = R1
vWriteMemDs4

// ret
vPushReg4       R0         ; ESI
vPushReg4       R4         ; EDI
vPushReg4       R8         ; flag(无用)
vPushReg4       R14          ; EBX
vPushReg4       R1         ; RAX
vPushReg4       R9         ; EDX
vPushReg4       R11          ; EBP
vPushReg4       R13          ; sub_flag
vPushReg4       R2         ; ECX
vPushReg4       R7         ; flag(无用)
vPushReg4       R0         ; ESI(无用)
vRet
```
刚好可以和 vRet 指令衔接上。
```
.vmp0:0040408E vRet:                                 
.vmp0:0040408E                                       
.vmp0:0040408E               mov   esp, ebp
.vmp0:00404090               pop   edx ; 被后面的pop覆盖
.vmp0:00404091               pop   ebx ; 被后面的pop覆盖
.vmp0:00404092               pop   ecx ; R2
.vmp0:00404093               popf      ; R13 sub_flag
.vmp0:00404094               pop   ebp ; R11
.vmp0:00404095               pop   edx ; R9
.vmp0:00404096               pop   eax ; R1
.vmp0:00404097               pop   ebx ; R14
.vmp0:00404098               pop   esi ; 被后面的pop覆盖
.vmp0:00404099               pop   edi ; R4
.vmp0:0040409A               pop   esi ; R0
.vmp0:0040409B               retn
```

# 小结

本文以一个极弱的虚拟机 VMProtect 1.81 Demo 为例,完整的展示了一个虚拟机保护代码的分析过程。

从初步分析虚拟机结构、到提取Handler,并通过Trace提取虚拟指令序列,最后进行人肉进行代码还原。

作为第1篇,可能有意思的地方并不多。

首先选择的虚拟机版本太弱了,而且是虚拟机内部没有混淆和花指令的。对于正式版 VMProtect 虚拟机的解释执行过程都是添加了冗余指令的,这种情况该如何处理?Code Virtualizer 系列虚拟机内则有代码变形,又如何处理? VMProtect 3.x 已经没有解释循环,使用链式寻址代替,这样有什么方便的提取字节码的方法么?

后面的文章会一点点介绍,希望后面几篇可以让大家有更大的收获。


(文章涉及的样本、IDB文件、Trace文件、部分脚本见附件,密码123456)

Cr4ckm3 发表于 2018-3-22 14:17

linuxprobe 发表于 2018-3-21 20:47
我们现在用的是Vmware和KVM,你写的这些没涉及到这个啊。

这里提到的虚拟机实际是一种软件保护技术是对二进制代码进行加密保护用的 典型工具是VMProtectThemida 等 与VMWare 这种系统虚拟机是两回事儿

Cr4ckm3 发表于 2018-4-4 14:13

poisonbcat 发表于 2018-4-3 16:50
还是想请教一下,如果用VMProtect去保护比较大的动态数据的软件(比如网游),是不是软件的运行效率会大大 ...

是的 不夸张的说 一条x86指令经过虚拟保护后,可能要执行100多条指令才完成相同的功能,效率会十分明显的下降。

不过一般来说这种保护只保护在关键位置(比如用户认证代码)不是所有代码都加,因此整体运行的影响还好,一般是可以接受的。

(最后,头像不错 EVA赛高)

绝恋人间 发表于 2018-3-19 16:19

这个要好好学习,以后用得着

Liture 发表于 2018-3-19 16:20

哇,分析虚拟机代码。可以的,mark。{:1_927:}

绝恋人间 发表于 2018-3-19 16:27

请教楼主,左边那目录怎么弄的,求教哦。

tlf 发表于 2018-3-19 16:29

这个有点深奥了
加强学习

无影 发表于 2018-3-19 16:36

绝恋人间 发表于 2018-3-19 16:27
请教楼主,左边那目录怎么弄的,求教哦。
MD(MarkDown)



可以用这个https://www.52pojie.cn/thread-707633-1-1.html

shj2k 发表于 2018-3-19 17:04

收藏一下,有时间再研究

wudipilihuo 发表于 2018-3-19 17:40

感谢分享

StriveMario 发表于 2018-3-19 17:52

安心做一条会喊666的咸鱼了

wuailaomao 发表于 2018-3-19 20:30

收藏学习ing    先送个cb
页: [1] 2 3 4 5 6 7 8 9 10
查看完整版本: 如何分析虚拟机系列(1):新手篇VMProtect 1.81 Demo