VM:虚拟机保护技术

前言

VM逆向对于Re老手肯定是不陌生的,但是对于我一个Re废物来说,之前一直有听说过VM的逆向题目,但是因为时间原因一直没有去实践过一个题目,基本上是遇到就溜,感觉到十分棘手,因此在此次学习中将其进行记录,也希望对后来学习的同学有所帮助

基本介绍

VMP:也就是虚拟机保护技术,它是将基于x86汇编系统的可执行代码转换为字节码指令系统的代码,以达到保护原有指令不被轻易逆向和篡改的目的。这种指令执行系统和Intel的x86指令系统不在同一个层次中。

字节码(Bytecode):是由指令执行系统定义的一套指令和数据组成的一串数据流,由于每个系统设计的字节码都是供自己使用的,因此他们之间的字节码并不通用。

虚拟机执行时的情况:

img

VStartVM将真实环境压入栈后会生成一个VMDispather标签,当Handler执行完毕后会跳回这里,形成一个循环,所以VStratVM,也叫做dispather

做题思路

分析VM基本上是一个体力活,需要将程序的opcode提取出来进行翻译为高级的语言,之后再采用逆向的方式对其进行求解

分析VM题的一般套路:

  1. 提取出bytecode
  2. 根据op代入函数
  3. 转化成伪汇编代码
  4. 转化成高级语言代码(C/C++/Python)
  5. 逆向算法,写出解密脚本

VM小型虚拟机的实现

要想实现一个简单的虚拟机我们需要完成两个小目标

  1. 定义一套opcode
  2. 实现一个opcode的解释器

初始化虚拟寄存器、opcode存放

1
2
3
4
5
6
7
8
typedef struct
{
unsigned long r1; //虚拟寄存器r1
unsigned long r2; //虚拟寄存器r2
unsigned long r3; //虚拟寄存器r3
unsigned char *eip; //指向正在解释的opcode地址
vm_opcode op_list[OPCODE_N]; //opcode列表,存放了所有的opcode及其对应的处理函数
}vm_cpu;

r1-r3用来传参或者是存放返回值,eip指向opcode的地址

定义opcode

opcode只是一个标识,可以随便定义

1
2
3
4
5
typedef struct
{
unsigned char opcode;
void (*handle)(void*);
}vm_opcode;

关联opcode和对应handler函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void *vm_init()
{
vm_vpu *cpu;
cpu->r1 = 0;
cpu->r2 = 0;
cpu->r3 = 0;
cpu->eip = (unsigned char *)vm_code;//将eip指向opcode的地址

cpu->op_list[0].opcode = 0xf1;
cpu->op_list[0].handle = (void (*)(void *))mov;//将操作字节码与对应的handle函数关联在一起

cpu->op_list[1].opcode = 0xf2;
cpu->op_list[1].handle = (void (*)(void *))xor;

cpu->op_list[2].opcode = 0xf5;
cpu->op_list[2].handle = (void (*)(void *))read_;

vm_stack = malloc(0x512);
memset(vm_stack,0,0x512);//定义了一个新栈并在malloc上申请了0x512位的空间
}

虚拟机入口函数

1
2
3
4
5
6
7
8
void vm_start(vm_cpu *cpu)
{
cpu->eip = (unsigned char*)opcodes;//eip指向要被解释的opcode地址
while((*cpu->eip) != 0xf4)//如果opcode不为RET,就调用vm_dispatcher来解释执行
{
vm_dispatcher(*cpu->eip)
}
}

解释执行器编写

1
2
3
4
5
6
7
8
9
10
11
12
void vm_dispatcher(vm_cpu *cpu)
{
int j;
for(j=0;j<OPCODE_N;j++)
{
if(*cpu->eip ==cpu->op_list[i].opcode)
{
cpu->op_list[i].handle(cpu);
break;
}
}
}

具体执行函数实现

这里实现mov xor read三个简单的指令 其中read指令用于读取数据 在题目中用于读取flag。具体题目中根据题目要求实现不同的函数功能即可。所以说虚拟机类re题目很好的考察了参赛选手的代码能力

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void xor(vm_cpu *cpu)
{
int num;
num =cpu->r1 ^cpu->r2;
num ^=0x12;
cpu->r1= temp;
cpu->eip=eip+1;//这里一定要注意xor指令本身是占一个字节的。
}
void mov(vm_cpu *cpu)
{
/*mov指令的参数都隐藏在字节码中,指令表示后的一个字节是寄存器标识,第二到第五是要mov的数据在vm_stack上的偏移。这里只实现了从vm_stack栈上存取数据*/
unsigned char *res =cpu->eip+1;//寄存器标识
int *offset = (int *)(cpu->eip+2);
char *dest=0;
dest=vm_stack;
switch(*res){
case 0xe1:
cpu->r1=*(dest + *offset);
break;
case 0xe2:
cpu->r2=*(dest + *offset);
break;
case 0xe3:
cpu->r3=*(dest + *offset);
break;//数据寄存
case 0xe4:
{
int x=cpu->r1;
*(dest + *offset)=x;
break;
}//获取寄存器中数据
}
cpu->eip += 6;//mov指令占六个字节,所以eip要向后移6位
}

定义opcode字符集

定义opcode字符集,每个字符对应一个函数功能模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
unsigned char vm_code[] = {
0xf5,
0xf1,0xe1,0x0,0x00,0x00,0x00,0xf2,0xf1,0xe4,0x20,0x00,0x00,0x00,
0xf1,0xe1,0x1,0x00,0x00,0x00,0xf2,0xf1,0xe4,0x21,0x00,0x00,0x00,
0xf1,0xe1,0x2,0x00,0x00,0x00,0xf2,0xf1,0xe4,0x22,0x00,0x00,0x00,
0xf1,0xe1,0x3,0x00,0x00,0x00,0xf2,0xf1,0xe4,0x23,0x00,0x00,0x00,
0xf1,0xe1,0x4,0x00,0x00,0x00,0xf2,0xf1,0xe4,0x24,0x00,0x00,0x00,
0xf1,0xe1,0x5,0x00,0x00,0x00,0xf2,0xf1,0xe4,0x25,0x00,0x00,0x00,
0xf1,0xe1,0x6,0x00,0x00,0x00,0xf2,0xf1,0xe4,0x26,0x00,0x00,0x00,
0xf1,0xe1,0x7,0x00,0x00,0x00,0xf2,0xf1,0xe4,0x27,0x00,0x00,0x00,
0xf1,0xe1,0x8,0x00,0x00,0x00,0xf2,0xf1,0xe4,0x28,0x00,0x00,0x00,
0xf1,0xe1,0x9,0x00,0x00,0x00,0xf2,0xf1,0xe4,0x29,0x00,0x00,0x00,
0xf1,0xe1,0xa,0x00,0x00,0x00,0xf2,0xf1,0xe4,0x2a,0x00,0x00,0x00,
0xf1,0xe1,0xb,0x00,0x00,0x00,0xf2,0xf1,0xe4,0x2b,0x00,0x00,0x00,
0xf1,0xe1,0xc,0x00,0x00,0x00,0xf2,0xf1,0xe4,0x2c,0x00,0x00,0x00,
0xf4
};

至此,一个简化版的小型虚拟机就实现完了。该虚拟机实现了对输入字符串简单的异或加密,并将加密后的值存储到指定位置。

gcc编译一下就可以在IDA上自己逆着玩(没想到意外的学会了VMRE怎么出题)

下面以一个VM逆向来进一步说明

[GWCTF 2019] babyvm

我们首先将程序查壳,发现是无壳64位程序,直接拖入到64IDA中去

找到对应的主函数,可以发现函数十分少(下面图片中函数名都是后期修复的,前期需要自己进行识别修复)

1.png

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

2.png

可以看到这个程序对应的初始化过程和上文所写的关联opcode和对应handler函数过程十分相似,可以快速的帮我们进行识别出来eip

图片上标注错了,64位的程序对应寄存器该是rip,但是不影响分析,后文仍然采用eip代替rip,方便进行描述

程序中明显的看到取地址符&,点进去便可以发现一大串的opcode,我们先将其进行提取

1
opcode = [0xF5, 0xF1, 0xE1, 0x00, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x20, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x01, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x21, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x02, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x22, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x03, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x23, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x04, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x24, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x05, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x25, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x06, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x26, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x07, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x27, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x08, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x28, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x09, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x29, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x0A, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x2A, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x0B, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x2B, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x0C, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x2C, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x0D, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x2D, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x0E, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x2E, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x0F, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x2F, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x10, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x30, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x11, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x31, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x12, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x32, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x13, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x33, 0x00, 0x00, 0x00, 0xF4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF5, 0xF1, 0xE1, 0x00, 0x00, 0x00, 0x00, 0xF1, 0xE2, 0x01, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x00, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x01, 0x00, 0x00, 0x00, 0xF1, 0xE2, 0x02, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x01, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x02, 0x00, 0x00, 0x00, 0xF1, 0xE2, 0x03, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x02, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x03, 0x00, 0x00, 0x00, 0xF1, 0xE2, 0x04, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x03, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x04, 0x00, 0x00, 0x00, 0xF1, 0xE2, 0x05, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x04, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x05, 0x00, 0x00, 0x00, 0xF1, 0xE2, 0x06, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x05, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x06, 0x00, 0x00, 0x00, 0xF1, 0xE2, 0x07, 0x00, 0x00, 0x00, 0xF1, 0xE3, 0x08, 0x00, 0x00, 0x00, 0xF1, 0xE5, 0x0C, 0x00, 0x00, 0x00, 0xF6, 0xF7, 0xF1, 0xE4, 0x06, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x07, 0x00, 0x00, 0x00, 0xF1, 0xE2, 0x08, 0x00, 0x00, 0x00, 0xF1, 0xE3, 0x09, 0x00, 0x00, 0x00, 0xF1, 0xE5, 0x0C, 0x00, 0x00, 0x00, 0xF6, 0xF7, 0xF1, 0xE4, 0x07, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x08, 0x00, 0x00, 0x00, 0xF1, 0xE2, 0x09, 0x00, 0x00, 0x00, 0xF1, 0xE3, 0x0A, 0x00, 0x00, 0x00, 0xF1, 0xE5, 0x0C, 0x00, 0x00, 0x00, 0xF6, 0xF7, 0xF1, 0xE4, 0x08, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x0D, 0x00, 0x00, 0x00, 0xF1, 0xE2, 0x13, 0x00, 0x00, 0x00, 0xF8, 0xF1, 0xE4, 0x0D, 0x00, 0x00, 0x00, 0xF1, 0xE7, 0x13, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x0E, 0x00, 0x00, 0x00, 0xF1, 0xE2, 0x12, 0x00, 0x00, 0x00, 0xF8, 0xF1, 0xE4, 0x0E, 0x00, 0x00, 0x00, 0xF1, 0xE7, 0x12, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x0F, 0x00, 0x00, 0x00, 0xF1, 0xE2, 0x11, 0x00, 0x00, 0x00, 0xF8, 0xF1, 0xE4, 0x0F, 0x00, 0x00, 0x00, 0xF1, 0xE7, 0x11, 0x00, 0x00, 0x00, 0xF4]

提出opcode之后分析下方的函数,可以很容易进行识别,其中mov中还有一部分指令,作用是将输入的不同数据移动到对应的虚拟寄存器中,可以参考上文的具体执行函数实现

3.png

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

4.png

5.png

6.png

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

7.png

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

9.png

真正加密部分:

8.png

对此我们的程序的大致分析就结束了,可以总结出来如下的一个函数段表,为我们后续的翻译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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
opcode = [0xF5, 0xF1, 0xE1, 0x00, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x20, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x01, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x21, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x02, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x22, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x03, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x23, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x04, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x24, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x05, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x25, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x06, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x26, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x07, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x27, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x08, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x28, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x09, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x29, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x0A, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x2A, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x0B, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x2B, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x0C, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x2C, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x0D, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x2D, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x0E, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x2E, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x0F, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x2F, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x10, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x30, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x11, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x31, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x12, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x32, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x13, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x33, 0x00, 0x00, 0x00, 0xF4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF5, 0xF1, 0xE1, 0x00, 0x00, 0x00, 0x00, 0xF1, 0xE2, 0x01, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x00, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x01, 0x00, 0x00, 0x00, 0xF1, 0xE2, 0x02, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x01, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x02, 0x00, 0x00, 0x00, 0xF1, 0xE2, 0x03, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x02, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x03, 0x00, 0x00, 0x00, 0xF1, 0xE2, 0x04, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x03, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x04, 0x00, 0x00, 0x00, 0xF1, 0xE2, 0x05, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x04, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x05, 0x00, 0x00, 0x00, 0xF1, 0xE2, 0x06, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x05, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x06, 0x00, 0x00, 0x00, 0xF1, 0xE2, 0x07, 0x00, 0x00, 0x00, 0xF1, 0xE3, 0x08, 0x00, 0x00, 0x00, 0xF1, 0xE5, 0x0C, 0x00, 0x00, 0x00, 0xF6, 0xF7, 0xF1, 0xE4, 0x06, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x07, 0x00, 0x00, 0x00, 0xF1, 0xE2, 0x08, 0x00, 0x00, 0x00, 0xF1, 0xE3, 0x09, 0x00, 0x00, 0x00, 0xF1, 0xE5, 0x0C, 0x00, 0x00, 0x00, 0xF6, 0xF7, 0xF1, 0xE4, 0x07, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x08, 0x00, 0x00, 0x00, 0xF1, 0xE2, 0x09, 0x00, 0x00, 0x00, 0xF1, 0xE3, 0x0A, 0x00, 0x00, 0x00, 0xF1, 0xE5, 0x0C, 0x00, 0x00, 0x00, 0xF6, 0xF7, 0xF1, 0xE4, 0x08, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x0D, 0x00, 0x00, 0x00, 0xF1, 0xE2, 0x13, 0x00, 0x00, 0x00, 0xF8, 0xF1, 0xE4, 0x0D, 0x00, 0x00, 0x00, 0xF1, 0xE7, 0x13, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x0E, 0x00, 0x00, 0x00, 0xF1, 0xE2, 0x12, 0x00, 0x00, 0x00, 0xF8, 0xF1, 0xE4, 0x0E, 0x00, 0x00, 0x00, 0xF1, 0xE7, 0x12, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x0F, 0x00, 0x00, 0x00, 0xF1, 0xE2, 0x11, 0x00, 0x00, 0x00, 0xF8, 0xF1, 0xE4, 0x0F, 0x00, 0x00, 0x00, 0xF1, 0xE7, 0x11, 0x00, 0x00, 0x00, 0xF4]
r1 = 0
r2 = 0x12
r3 = 0
r4 = 0
for i in range(len(opcode)):
if opcode[i] == 0xF1:
if opcode[i + 1] == 0xE1:
print(f"r1 = input[{opcode[i+2]}]")
elif opcode[i + 1] == 0xE2:
print(f"r2 = input[{opcode[i+2]}]")
elif opcode[i + 1] == 0xE3:
print(f"r3 = input[{opcode[i+2]}]")
elif opcode[i + 1] == 0xE4:
print(f"input[{opcode[i+2]}] = r1")
elif opcode[i + 1] == 0xE5:
print(f"r4 = input[{opcode[i+2]}]")
elif opcode[i + 1] == 0xE7:
print(f"input[{opcode[i+2]}] = r2")
elif opcode[i] == 0xF2:
print("r1 ^= r2")
elif opcode[i] == 0xF5:
print("scanf")
elif opcode[i] == 0xF4:
print("nop")
elif opcode[i] == 0xF7:
print("r1 *= r4")
elif opcode[i] == 0xF8:
print("swap( r1, r2 ) ")
elif opcode[i] == 0xF6:
print("r1 = r3 + r2 * 2 + 3 * r1")

翻译完成后我们可以得到这样的伪代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
scanf
r1 = input[0]
r1 ^= r2
input[32] = r1
r1 = input[1]
r1 ^= r2
input[33] = r1
r1 = input[2]
r1 ^= r2
input[34] = r1
r1 = input[3]
r1 ^= r2
input[35] = r1
r1 = input[4]
r1 ^= r2
input[36] = r1
r1 = input[5]
r1 ^= r2
input[37] = r1
r1 = input[6]
r1 ^= r2
input[38] = r1
r1 = input[7]
r1 ^= r2
input[39] = r1
r1 = input[8]
r1 ^= r2
input[40] = r1
r1 = input[9]
r1 ^= r2
input[41] = r1
r1 = input[10]
r1 ^= r2
input[42] = r1
r1 = input[11]
r1 ^= r2
input[43] = r1
r1 = input[12]
r1 ^= r2
input[44] = r1
r1 = input[13]
r1 ^= r2
input[45] = r1
r1 = input[14]
r1 ^= r2
input[46] = r1
r1 = input[15]
r1 ^= r2
input[47] = r1
r1 = input[16]
r1 ^= r2
input[48] = r1
r1 = input[17]
r1 ^= r2
input[49] = r1
r1 = input[18]
r1 ^= r2
input[50] = r1
r1 = input[19]
r1 ^= r2
input[51] = r1
nop


scanf
r1 = input[0]
r2 = input[1]
r1 ^= r2
input[0] = r1
r1 = input[1]
r2 = input[2]
r1 ^= r2
input[1] = r1
r1 = input[2]
r2 = input[3]
r1 ^= r2
input[2] = r1
r1 = input[3]
r2 = input[4]
r1 ^= r2
input[3] = r1
r1 = input[4]
r2 = input[5]
r1 ^= r2
input[4] = r1
r1 = input[5]
r2 = input[6]
r1 ^= r2
input[5] = r1
r1 = input[6]
r2 = input[7]
r3 = input[8]
r4 = input[12]
r1 = r3 + r2 * 2 + 3 * r1
r1 *= r4
input[6] = r1
r1 = input[7]
r2 = input[8]
r3 = input[9]
r4 = input[12]
r1 = r3 + r2 * 2 + 3 * r1
r1 *= r4
input[7] = r1
r1 = input[8]
r2 = input[9]
r3 = input[10]
r4 = input[12]
r1 = r3 + r2 * 2 + 3 * r1
r1 *= r4
input[8] = r1
r1 = input[13]
r2 = input[19]
swap( r1, r2 )
input[13] = r1
input[19] = r2
r1 = input[14]
r2 = input[18]
swap( r1, r2 )
input[14] = r1
input[18] = r2
r1 = input[15]
r2 = input[17]
swap( r1, r2 )
input[15] = r1
input[17] = r2
nop

此时我们就可以对其进行求解,得到真正的输入部分,解题脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
check = [0x69, 0x45, 0x2A, 0x37, 0x09, 0x17, 0xC5, 0x0B, 0x5C, 0x72,
0x33, 0x76, 0x33, 0x21, 0x74, 0x31, 0x5F, 0x33, 0x73, 0x72]

check[13], check[19] = check[19], check[13]
check[14], check[18] = check[18], check[14]
check[15], check[17] = check[17], check[15]

for i in range(128):
if check[8] == ((i*3 + check[9]*2 + check[10])*check[12])&0xff:
check[8] = i
for j in range(128):
if check[7] == ((j * 3 + check[8] * 2 + check[9]) * check[12]) & 0xff:
check[7] = j
for k in range(128):
if check[6] == ((k * 3 + check[7] * 2 + check[8]) * check[12]) & 0xff:
check[6] = k

for i in range(6)[::-1]:
check[i] ^= check[i+1]

flag = ''
for i in range(len(check)):
flag += chr(check[i])
print(flag)

得到 flag:flag{Y0u_hav3_r3v3rs3_1t!}

后记

至此一个简单的VM就结束了,对于后续的复杂VM题目也是类似的过程,将程序的opcode提取,翻译,解密。更多的还是花费时间进行调试与解密,希望这篇文章能对后者学习的同学有帮助吧


VM:虚拟机保护技术
https://equinox-shame.github.io/2022/07/04/VM:虚拟机保护技术/
作者
梓曰
发布于
2022年7月4日
许可协议