吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 30023|回复: 162
收起左侧

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

    [复制链接]
Cr4ckm3 发表于 2018-3-19 16:07
本帖最后由 Cr4ckm3 于 2018-3-26 10:10 编辑

序言

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

关于 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 提取虚拟指令的部分我个人觉得还算有趣,对虚拟机已经有了解的读者可以跳过其他废话直接看那一部分。

虚拟机简介

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

  • 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, [esp+30h]   ; 前面 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, [ebp+0]  ; 重定位
.vmp0:00404751
.vmp0:00404751 loc_404751:                            
.vmp0:00404751                                        
.vmp0:00404751                 mov     al, [esi]   ; 从 esi 取出 1 字节
.vmp0:00404753                 movzx   eax, al    
.vmp0:00404756                 inc     esi         ; esi 字节
.vmp0:00404757                 jmp     ds:off_40409C[eax*4]  ; 根据 esi 取出字节进行跳转

这段代码首先保存当前寄存器的值。(这与前面介绍了虚拟机初始化的过程是一致的)

然后进行分配栈空间,初始化 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: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[eax*4]跳转是个很典型的switch结构,IDA可以查看CFG图如下:



ida.png

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

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

2. Handler 分析

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

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

  1. 立即数压栈
.vmp0:0040462B vPushImm4:                             
.vmp0:0040462B                                        
.vmp0:0040462B                 mov     eax, [esi]
.vmp0:0040462D                 sub     ebp, 4
.vmp0:00404630                 lea     esi, [esi+4]
.vmp0:00404633                 mov     [ebp+0], eax
.vmp0:00404636                 jmp     loc_40400F

这条指令从 esi 地址取出 4 字节,然后 ebp - 4 后写入 [ebp] 内存处。 esi 指向的地方是 VM_DATA,因此取出的部分是指令中的固定数,即虚拟指令中的立即数。 ebp - 4 后再赋值的操作很像栈操作,先抬高栈顶,再写值。通过分析其他指令可以发现许多加减 ebp 然后读写值的情况。那么可以认定 ebp 就是虚拟栈栈顶指针。

  1. 寄存器压栈
.vmp0:004045AF vPushReg4:                             
.vmp0:004045AF                                      
.vmp0:004045AF                 and     al, 3Ch
.vmp0:004045B2                 mov     edx, [edi+eax]
.vmp0:004045B5                 sub     ebp, 4
.vmp0:004045B8                 mov     [ebp+0], edx
.vmp0:004045BB                 jmp     loc_40400F

这条指令取 al 的后几位,作为 edi 寄存器的偏移,取出值后压入栈顶。al 是之前从 esi 地址中取出的值,也是指令的一部分。由该值作索引,从 edi 寻址取值。可以猜测 edi 就是 VM_CONTEXT。这里是将虚拟寄存器中值压入虚拟栈中。

  1. 计算
.vmp0:00404000 vAdd4:                                 
.vmp0:00404000                                        
.vmp0:00404000                 mov     eax, [ebp+0]
.vmp0:00404003                 add     [ebp+4], eax
.vmp0:00404006                 pushf
.vmp0:00404007                 pop     dword ptr [ebp+0]
.vmp0:0040400A                 jmp     loc_404751

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

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

  1. 其他

我们已经确定了:

  • esi 取出的值来自指令,因此是立即数或者寄存器下标
  • edi 指向VM_CONTEXT,读写就是操作虚拟寄存器
  • ebp 指向虚拟栈顶,读写就是压栈、弹栈

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

3. 还原字节码

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

方法1:IDA 静态解析 VM_DATA

IDA 查看 VM_DATA 的内容如下:

00404781  0E E8 81 A9 5D 76 5C 1E  1A 0A 3E 2E 26 36 3A 2A  ....]v\...>.&6:*
00404791  12 02 06 22 E8 00 30 40  00 79 16 69 78 56 34 12  ..."..0@.y.ixV4.
004047A1  14 7A 1E 36 E8 78 56 34  12 34 97 4D 6B 06 5C 3E  .z.6.xV4.4.Mk.\>
004047B1  97 21 11 22 06 3C 4C 21  11 16 EB EA F7 11 1E 20  .!.".<L!.......
004047C1  20 6B 32 77 15 08 78 32  5C 32 36 04 69 00 30 40   k2w..x2\26.i.0@
004047D1  00 ED 00 10 20 38 04 24  2C 34 08 1C 00 E1 00 00  .... 8.$,4......
004047E1  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................

跳转指令jmp     ds:off_40409C[eax*4],跳转表就在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, [ebp+0]   ; 取栈顶值
.vmp0:0040405E                 add     ebp, 4         ; 弹栈
.vmp0:00404061                 mov     [edi+eax], edx ; 值写入VM_CONTEXT[al]
.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, [esi]   ; 取4字节立即数
.vmp0:0040462D                 sub     ebp, 4       ; 栈分析4字节空间
.vmp0:00404630                 lea     esi, [esi+4] ; esi = esi + 4
.vmp0:00404633                 mov     [ebp+0], 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        [0019FF7C]=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        [0019FF74]=00000000        EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF78, EBP=0019FF94, ESI=00401015, EDI=00401015
main        0040472D        push    edi        [0019FF70]=00000000        EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF74, EBP=0019FF94, ESI=00401015, EDI=00401015
main        0040472E        push    esp        [0019FF6C]=00000000        EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF70, EBP=0019FF94, ESI=00401015, EDI=00401015
main        0040472F        push    ebx        [0019FF68]=00000000        EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF6C, EBP=0019FF94, ESI=00401015, EDI=00401015
main        00404730        push    eax        [0019FF64]=00000000        EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF68, EBP=0019FF94, ESI=00401015, EDI=00401015
main        00404731        push    edx        [0019FF60]=00000000        EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF64, EBP=0019FF94, ESI=00401015, EDI=00401015
main        00404732        push    ebp        [0019FF5C]=00000000        EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF60, EBP=0019FF94, ESI=00401015, EDI=00401015
main        00404733        pushfd        [0019FF58]=00000000        EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF5C, EBP=0019FF94, ESI=00401015, EDI=00401015
main        00404734        push    ecx        [0019FF54]=00000000        EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF58, EBP=0019FF94, ESI=00401015, EDI=00401015
main        00404735        push    dword ptr ds:[404649]        [00404649]=00000000        EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF54, EBP=0019FF94, ESI=00401015, EDI=00401015
main        0040473B        push    0        [0019FF4C]=00000000        EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF50, EBP=0019FF94, ESI=00401015, EDI=00401015
main        00404740        mov     esi, dword ptr ss:[esp+30]        [0019FF7C]=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:[ebp]        [0019FF4C]=00000000        EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FE8C, EBP=0019FF4C, ESI=00404781, EDI=0019FE8C
main        00404751        mov     al, byte ptr ds:[esi]        [00404781]=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:[eax*4+40409C]        [004040D4]=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:[ebp]        [0019FF4C]=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:[eax+edi], edx        [0019FE98]=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:[esi]        [00404783]=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, [esi+4]        [00404787]=5C        EAX=765DA981, ECX=00401015, EDX=00000000, EBX=00344000, ESP=0019FE8C, EBP=0019FF4C, ESI=00404783, EDI=0019FE8C
main        00404633        mov     dword ptr ss:[ebp], eax        [0019FF4C]=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:[esi]后,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代码还原。不过这种方法准确性要比模板匹配差一些。

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

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

.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 = [0x403000]

// 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     ; [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)


vmp1.81_passwd_123456.zip

44.64 KB, 下载次数: 141, 下载积分: 吾爱币 -1 CB

点评

这个更棒!建议射,,呸~ H大已设精! 嘿嘿~  发表于 2018-3-20 14:58

免费评分

参与人数 71吾爱币 +78 热心值 +69 收起 理由
wwwio + 1 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
软硬兼嗜 + 1 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
junty + 1 谢谢@Thanks!
isgoodtime + 1 + 1 谢谢@Thanks!
tiger_oil + 1 + 1 谢谢@Thanks!
枫笑九洲 + 1 + 1 谢谢@Thanks!
fla3537 + 1 + 1 谢谢@Thanks!
purplewall + 1 + 1 我很赞同!
GNUBD + 1 + 1 谢谢@Thanks!
c66d88 + 1 + 1 文章内容好不说,连文章的字体颜色都是护眼色,周到
dongkou + 1 谢谢@Thanks!
ScareCrowL + 2 + 1 谢谢@Thanks!
baixin7599 + 1 + 1 我很赞同!
dreamHacker + 1 我很赞同!
wangdonghui + 1 + 1 我很赞同!
chkds + 1 + 1 谢谢@Thanks!
TSF + 1 + 1 用心讨论,共获提升!
socky + 1 + 1 谢谢@Thanks!
Jiangyf + 1 + 1 我很赞同!
笙若 + 1 + 1 谢谢@Thanks!
cxh159 + 1 + 1 谢谢@Thanks!
未知了 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
zy1234 + 1 + 1 我很赞同!
heisexiaobai + 1 + 1 鼓励转贴优秀软件安全工具和文档!
ReGive + 1 + 1 我很赞同!
Lminexy + 1 + 1 谢谢@Thanks!,感觉好流弊啊……
nyfyzkl + 1 + 1 我很赞同!
nanmobei + 1 + 1 我很赞同!
peter_king + 1 谢谢@Thanks!
vison.v + 1 + 1 我很赞同!
xiaoxi2011 + 1 + 1 谢谢@Thanks!
天才知道33 + 1 + 1 我很赞同!
二逼159 + 1 + 1 谢谢@Thanks!
noth + 1 + 1 谢谢@Thanks!
Ricoxxx + 1 + 1 谢谢@Thanks!
tztt3033 + 1 用心讨论,共获提升!
lookerJ + 1 + 1 用心讨论,共获提升!
MaxMadcc + 1 + 1 谢谢@Thanks!
wmsuper + 2 + 1 我很赞同!
jimeng1020 + 1 + 1 谢谢@Thanks!
virusPPP + 1 + 1 我很赞同!
acelite + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
ilikethis2 + 1 + 1 我很赞同!
mzdbcxb + 1 + 1 热心回复!
苏紫方璇 + 1 + 1 用心讨论,共获提升!
筱小熙儿 + 1 + 1 我很赞同!
22222 + 1 + 1 我很赞同!
懒虫asd + 1 + 1 用心讨论,共获提升!
巴洛克 + 1 + 1 谢谢@Thanks!
52Sql1920 + 1 用心讨论,共获提升!
九州重燃志 + 1 + 1 热心回复!
a5606495 + 1 + 1 谢谢@Thanks!
fengyuner + 1 + 1 谢谢@Thanks!
whklhh + 2 + 1 谢谢@Thanks!
WarWolf + 1 + 1 系列1,系列2...支持
梁萧 + 1 + 1 谢谢@Thanks!
gfjykldd + 1 + 1 用心讨论,共获提升!
wanttobeno + 2 + 1 好精彩的VMP分析
Ganlv + 4 + 4 感觉可以加精了
chenjingyes + 1 + 1 支持,期待蓝钻下一篇哈哈哈{:1_921:}
爱飞的猫 + 3 + 1 用心讨论,共获提升!
闲月疏云 + 2 感谢发布原创作品,吾爱破解论坛因你更精彩!
wuailaomao + 1 我很赞同!
yuishang + 1 + 1 谢谢@Thanks!
彷徨一世 + 1 + 1 我很赞同!
tlf + 1 我很赞同!
飞腾小子 + 1 + 1 用心讨论,共获提升!
Ravey + 1 + 1 谢谢@Thanks!
三楼有鬼 + 1 + 1 安心做一条会喊666的咸鱼了
yzy93 + 1 + 1 用心讨论,共获提升!
610100 + 3 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

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

这里提到的虚拟机实际是一种软件保护技术  是对二进制代码进行加密保护用的 典型工具是VMProtect  Themida 等 与VMWare 这种系统虚拟机是两回事儿
 楼主| Cr4ckm3 发表于 2018-4-4 14:13
poisonbcat 发表于 2018-4-3 16:50
还是想请教一下,如果用VMProtect去保护比较大的动态数据的软件(比如网游),是不是软件的运行效率会大大 ...

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

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

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

免费评分

参与人数 1吾爱币 +1 热心值 +1 收起 理由
poisonbcat + 1 + 1 谢谢@Thanks!

查看全部评分

绝恋人间 发表于 2018-3-19 16:19
Liture 发表于 2018-3-19 16:20
哇,分析虚拟机代码。可以的,mark。
绝恋人间 发表于 2018-3-19 16:27
请教楼主,左边那目录怎么弄的,求教哦。
头像被屏蔽
tlf 发表于 2018-3-19 16:29
这个有点深奥了
加强学习
无影 发表于 2018-3-19 16:36
绝恋人间 发表于 2018-3-19 16:27
请教楼主,左边那目录怎么弄的,求教哦。

MD(MarkDown)

QQ截图20180319163425.png

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

免费评分

参与人数 1吾爱币 +1 热心值 +1 收起 理由
绝恋人间 + 1 + 1 谢谢@Thanks!

查看全部评分

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
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-11-21 16:52

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表