VM:虚拟机保护技术
前言
VM逆向对于Re老手肯定是不陌生的,但是对于我一个Re废物来说,之前一直有听说过VM的逆向题目,但是因为时间原因一直没有去实践过一个题目,基本上是遇到就溜,感觉到十分棘手,因此在此次学习中将其进行记录,也希望对后来学习的同学有所帮助
基本介绍
VMP:也就是虚拟机保护技术,它是将基于x86汇编系统的可执行代码转换为字节码指令系统的代码,以达到保护原有指令不被轻易逆向和篡改的目的。这种指令执行系统和Intel的x86指令系统不在同一个层次中。
字节码(Bytecode):是由指令执行系统定义的一套指令和数据组成的一串数据流,由于每个系统设计的字节码都是供自己使用的,因此他们之间的字节码并不通用。
虚拟机执行时的情况:
VStartVM将真实环境压入栈后会生成一个VMDispather标签,当Handler执行完毕后会跳回这里,形成一个循环,所以VStratVM,也叫做dispather
做题思路
分析VM基本上是一个体力活,需要将程序的opcode提取出来进行翻译为高级的语言,之后再采用逆向的方式对其进行求解
分析VM题的一般套路:
- 提取出bytecode
- 根据op代入函数
- 转化成伪汇编代码
- 转化成高级语言代码(C/C++/Python)
- 逆向算法,写出解密脚本
VM小型虚拟机的实现
要想实现一个简单的虚拟机我们需要完成两个小目标
- 定义一套
opcode - 实现一个
opcode的解释器
初始化虚拟寄存器、opcode存放
1 | |
r1-r3用来传参或者是存放返回值,eip指向opcode的地址
定义opcode
opcode只是一个标识,可以随便定义
1 | |
关联opcode和对应handler函数
1 | |
虚拟机入口函数
1 | |
解释执行器编写
1 | |
具体执行函数实现
这里实现mov xor read三个简单的指令 其中read指令用于读取数据 在题目中用于读取flag。具体题目中根据题目要求实现不同的函数功能即可。所以说虚拟机类re题目很好的考察了参赛选手的代码能力
1 | |
定义opcode字符集
定义opcode字符集,每个字符对应一个函数功能模块
1 | |
至此,一个简化版的小型虚拟机就实现完了。该虚拟机实现了对输入字符串简单的异或加密,并将加密后的值存储到指定位置。
用gcc编译一下就可以在IDA上自己逆着玩(没想到意外的学会了VMRE怎么出题)
下面以一个VM逆向来进一步说明
[GWCTF 2019] babyvm
我们首先将程序查壳,发现是无壳64位程序,直接拖入到64位IDA中去
找到对应的主函数,可以发现函数十分少(下面图片中函数名都是后期修复的,前期需要自己进行识别修复)

我们主要分析的便是VM_init,进入该函数部分

可以看到这个程序对应的初始化过程和上文所写的关联opcode和对应handler函数过程十分相似,可以快速的帮我们进行识别出来eip
图片上标注错了,64位的程序对应寄存器该是
rip,但是不影响分析,后文仍然采用eip代替rip,方便进行描述
程序中明显的看到取地址符&,点进去便可以发现一大串的opcode,我们先将其进行提取
1 | |
提出opcode之后分析下方的函数,可以很容易进行识别,其中mov中还有一部分指令,作用是将输入的不同数据移动到对应的虚拟寄存器中,可以参考上文的具体执行函数实现

分析出来每个函数对应的作用我们就可以开始进行逆向了



需要注意的是在这个程序中对应的opcode中存在有两个输入,因此程序中可能会有两个判断过程,当我们输入完后后续还有一个过程。但是我们输入第一个发生错误后程序就直接结束了,因此我们判断第一个输入的check是假的,需要我们找到对应的函数,但是直接找函数却是无法找到,因此我们切换到汇编

切换到汇编后我们可以明显看到第一个判断的数据上方还有一个数据段长度和假的加密后的数据长度相同,因此我们合理猜测这个数据是我们真正加密后的数据,通过交叉引用可以发现真正的加密部分

真正加密部分:

对此我们的程序的大致分析就结束了,可以总结出来如下的一个函数段表,为我们后续的翻译opcode做准备:
| 机器码 | 备注 |
|---|---|
| 0xF1 | mov |
| 0xF2 | xor |
| 0xF4 | nop |
| 0xF5 | scanf |
| 0xF7 | mul |
| 0xF8 | swap |
| 0xF6 | 线性运算 |
| 0xE1 | 从内存单元input将数据移动到寄存器r1 |
| 0xE2 | 从内存单元input将数据移动到寄存器r2 |
| 0xE3 | 从内存单元input将数据移动到寄存器r3 |
| 0xE4 | 从寄存器r1将数据移动到内存单元input |
| 0xE5 | 从内存单元input将数据移动到寄存器r4 |
| 0xE7 | 从寄存器r2将数据移动到内存单元input |
至此我们就可以开始编写这个VM的翻译机器了将其转化为高级语言的语法格式,方便我们后续的逆向过程,翻译脚本如下
1 | |
翻译完成后我们可以得到这样的伪代码
1 | |
此时我们就可以对其进行求解,得到真正的输入部分,解题脚本如下
1 | |
得到 flag:flag{Y0u_hav3_r3v3rs3_1t!}
后记
至此一个简单的VM就结束了,对于后续的复杂VM题目也是类似的过程,将程序的opcode提取,翻译,解密。更多的还是花费时间进行调试与解密,希望这篇文章能对后者学习的同学有帮助吧
