VM
最后更新时间:
文章总字数:
预计阅读时间:
指令虚拟化
这篇文章解决什么问题
VM(Virtual Machine,虚拟机)类题目是逆向中常见的代码保护方式。出题人会把原本的逻辑转换成一套自定义字节码,再由解释器循环读取并执行。静态分析时,我们看到的往往不是直接的业务逻辑,而是“取 opcode → 分发 → 执行 handler”的解释器结构。
这篇文章先从一个最小 VM 示例入手,再说明如何定义指令集、实现解释器,以及逆向时应该如何还原 VM 的真实逻辑。
认识虚拟机
虚拟机本质上是用软件模拟一套执行环境。我们可以自定义一套指令,例如 0x01 表示把立即数放入虚拟寄存器,0x02 表示输出寄存器中的字符。程序运行时,解释器根据当前 eip 读取 opcode,再跳转到对应的 handler 执行。
在代码保护中,虚拟化会把原本直接执行的机器码转换成自定义字节码。这样一来,逆向者不能只看普通反编译结果,而需要先理解这套 VM 的指令集和解释器。

VM 的主程序通常是一个循环:不断读取 opcode,执行 opcode 对应的 handler,最后更新虚拟 eip。
最小示例
原始程序:
1 | |
可以用如下虚拟机思路重写:
1 | |
这个例子里:
0x01 imm:把立即数imm放入虚拟寄存器eax0x02:输出eax中保存的字符0xff:结束 VM 执行
所以逆向 VM 的第一步,不是急着找 flag,而是先把每个 opcode 的含义整理出来。
正向实现:
想要对抗指令虚拟化,首先要搞清楚用于保护的虚拟机是如何实现的
要想实现虚拟机,需要完成两个目标:
1.定义一套指令集
2.实现对应的解释器
结构体的定义:
真实赛题的VM通常会实现一个类似如下的结构体,用于保存虚拟机状态:
1 | |
书写机器码:
假定希望实现的语句如下:
1 | |
我们定义:
| opcode | 指令格式 | 指令长度 | 指令含义 |
|---|---|---|---|
| 0x10 | 0x10 操作数 | 2 | r1=操作数 |
| 0x11 | 0x11 操作数 | 2 | r2=操作数 |
| 0x20 | 0x20 操作数1 操作数2 | 3 | mem[操作数1]=操作数2 |
| 0x30 | 0x30 | 1 | mem[r1]^=r2 |
| 0x40 | 0x40 | 1 | 接收输入到&mem[0] |
| 0x50 | 0x50 操作数1 操作数2 | 3 | 返回memcmp(&mem[0],&mem[操作数1],操作数2) |
根据我们定义的指令对其进行拆分和重构,可以得到如下代码:
1 | |
初始化虚拟机:
在实际虚拟机运行之前,需要先对VM结构体进行初始化:
1 | |
解释器编写:
现在就可以来实现每条指令的handle以及dispatcher了:
1 | |
启动:
于是main函数可以这样写 :
1 | |
解题步骤:
遇到VM类的赛题,我们一般按照如下的步骤来解题:
1.分析VM结构:
结构体大小
有哪些字段
2.分析指令集:
指令长度是否可变
每种指令的构成
每种指令的含义
VM的退出条件
3.编写python版解释器,输出伪汇编代码
4.阅读伪代码,分析程序流程,写出去虚拟化的原始代码
5.书写解题脚本
本例的python解释器如下:
1 | |