Day15:指令虚拟化
指令虚拟化、实现一个小型虚拟机程序、VM逆向实践
什么是指令虚拟化
CPU厂商在开发CPU前会制定一个规范,建立起数据和操作的对应关系(如x86-64规定0x30对应异或操作),而这个数据又可以根据操作命名出汇编指令(如0x30命名为xor),因此有了数据与操作的对应关系和汇编指令与操作的对应关系,称之为指令集。CPU可以视作一个解释器,负责读取数据或者汇编指令然后带动机器产生操作。
通俗的讲,指令虚拟化实际上就是自定义指令集并为自己的指令集构建解释器,用自定义的指令实现程序的过程。这个过程利用高级语言代码实现,也就是用软件来模拟硬件,类似于虚拟机,因此称之为指令虚拟化。
实现指令虚拟化,需要定义寄存器变量(至少需要一个EIP来指向运行的指令)、内存空间数组、解释器和指令数组。
实现自定义指令集的解释器,可以采用C语言的switch(其中code是自定义指令,通过循环输入):
1
2
3
4
5
6
7
8
9
10
11//解释器:1输出11111,2输出22222,3输出33333
switch(code):
case 1:
printf("11111");
break;
case 2:
print("22222");
break:
case 3:
print("33333");
break;
实现一个小型虚拟机程序
- 尝试编写一个输入两个数,输出和的程序
声明虚拟硬件结构体
1 | |
定义指令集
- 使用op1、op2表示指令后面跟的操作数,寄存器作为隐式参数不会出现在code中
| 指令 | 使用格式 | 对应汇编指令/伪代码 | 操作解读 |
|---|---|---|---|
| 0x10 | 0x10,op1 | mov r1,mem[op1] | r1=mem[op1] |
| 0x11 | 0x11,op1 | mov r2,mem[op1] | r2=mem[op1] |
| 0x20 | 0x20 | add r1,r2 | r1+=r2 |
| 0x30 | 0x30,op1,op2 | scanf->mem[op1],mem[op2] | 输入给mem[op1],mem[op2] |
| 0x40 | 0x40 | printf r1 | 输出r1的值 |
| 0x50 | 0x50 | ret | return 1 |
编写解释器
1 | |
指令数组
1 | |
完整程序
1 | |
简单示例的逆向
VM题最显著的特征应该就是虚拟硬件结构体的初始化和解释器了。解决这类问题,需要先复刻出一个解释器,通过在解释器中增加打印分析出的代码的操作,来得到去虚拟化的原程序,进而可以正常逆向分析
先来看main函数:

下面三个带字符串的函数可以看出是printf和system
看看v1=后面的函数

目前没什么明显的VM特征
再看看if括号里的函数

看到出现switch了,开始推测是VM的解释器
回到上一个函数,发现确实和VM的初始化函数很像,最后面一个函数的第二个参数对应出来的是一堆数字,和初始化code对应上了

OK,那就对伪代码进行一点点优化,然后开始分析解释器
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//推测虚拟硬件结构体
__int64 __fastcall vm_run(unsigned int *vm)
{
sub_140001940(&unk_1401150A2);
while ( 1 )
{
switch ( *((_BYTE *)vm + vm[2] + 268) )//vm+268是code所在,结合下文分析,加上vm[2]就构成了code[eip]
{
case 0x10:
*vm = *((char *)vm + vm[2] + 269);//*vm=code[eip+1],vm应该是一个寄存器,这句是mov r1,op1
vm[2] += 2;//每个case都有,推测是eip。+=2,说明有一个操作数
break;
case 0x11:
vm[1] = *((char *)vm + vm[2] + 269);//vm[1]=code[eip+1],vm[1]应该也是一个寄存器,这句是mov r2,op1
vm[2] += 2;//有一个操作数
break;
case 0x20:
*((_BYTE *)vm + *((char *)vm + vm[2] + 269) + 12) = *((_BYTE *)vm + vm[2] + 270);
//*((char *)vm + vm[2] + 269)是code[eip+1],即op1,所以左边是vm+12+op1,推测vm+12是虚拟内存,刚好跨过三个连续的int型寄存器
//右边即vm+vm[2]+268+2即code[eip+2],即op2
vm[2] += 3;//有两个操作数
break;
case 0x30:
*((_BYTE *)vm + *vm + 12) ^= *((_BYTE *)vm + 4);//mem[r1]^=r2
++vm[2];//无操作数
break;
case 0x40:
sub_140001610(&unk_1400D74B0, vm + 3);//unk这个是%5s,所以这是scanf。这里的vm是int,加三跳过了单个寄存器,是mem[0]
++vm[2];//无操作数
break;
case 0x50:
return sub_1400D4430(vm + 3, (char *)vm + *((char *)vm + vm[2] + 269) + 12, *((char *)vm + vm[2] + 270));
//三个参数分别为mem[0],mem[op1],op2,推测是memcmp(怎么没暗示操作数个数
default:
continue;
}
}
}推测虚拟硬件结构体如下:
1
2
3
4
5
6
7typedef struct{
unsigned int r1;
unsigned int r2;
unsigned int eip;
unsigned char mem[268-3*4=256];
unsigned char code[36];
} 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
33
34
35
36
37
38
39
40
41
42
43
44
45
46int vm_run(VM* vm) {
char opcode;
char op1, op2;
while (1) {
opcode = vm->code[vm->eip];
switch (opcode) {
case 0x10:
vm->r1 = code[vm->eip + 1];
vm->eip += 2;
cout << "mov r1," << vm->r1 << endl;
break;
case 0x11:
vm->r2 = code[vm->eip + 1];
vm->eip += 2;
cout << "mov r2," << vm->r2 << endl;
break;
case 0x20:
op1 = code[vm->eip + 1];
op2 = code[vm->eip + 2];
vm->mem[op1] = op2;
vm->eip += 3;
cout << "mov mem[" << (int)op1 << "]," << op2 << endl;
break;
case 0x30:
vm->mem[vm->r1] ^= vm->r2;
++vm->eip;
cout << "xor mem[" << vm->r1 << "]," << vm->r2 << endl;
break;
case 0x40:
scanf("%5s", &vm->mem[0]);
++vm->eip;
printf("scanf mem\n");
break;
case 0x50:
op1 = code[vm->eip + 1];
op2 = code[vm->eip + 2];
return memcmp(&vm->mem[0], &vm->mem[op1], op2);
}
}
}完整的脚本:
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#include <windows.h>
#include <stdio.h>
#include<stdlib.h>
#include<iostream>
using namespace std;
typedef struct {
unsigned int r1;
unsigned int r2;
unsigned int eip;
unsigned char mem[256];
unsigned char code[36];
} VM ;
unsigned char code[] = {
0x20, 0x10, 0x48, 0x20, 0x11, 0x65, 0x20, 0x12, 0x6C, 0x20, 0x13, 0x6C, 0x20, 0x14, 0x6F, 0x40,
0x11, 0x21, 0x10, 0x00, 0x30, 0x10, 0x01, 0x30, 0x10, 0x02, 0x30, 0x10, 0x03, 0x30, 0x10, 0x04,
0x30, 0x50, 0x10, 0x05
};
VM* vm_new() {
VM* vm = (VM*)malloc(sizeof(VM));
memset(vm, 0, sizeof(VM));
memcpy(vm->code, code, sizeof(code));
return vm;
}
int vm_run(VM* vm) {
char opcode;
char op1, op2;
while (1) {
opcode = vm->code[vm->eip];
switch (opcode) {
case 0x10:
vm->r1 = code[vm->eip + 1];
vm->eip += 2;
cout << "mov r1," << vm->r1 << endl;
break;
case 0x11:
vm->r2 = code[vm->eip + 1];
vm->eip += 2;
cout << "mov r2," << vm->r2 << endl;
break;
case 0x20:
op1 = code[vm->eip + 1];
op2 = code[vm->eip + 2];
vm->mem[op1] = op2;
vm->eip += 3;
cout << "mov mem[" << (int)op1 << "]," << op2 << endl;
break;
case 0x30:
vm->mem[vm->r1] ^= vm->r2;
++vm->eip;
cout << "xor mem[" << vm->r1 << "]," << vm->r2 << endl;
break;
case 0x40:
scanf("%5s", &vm->mem[0]);
++vm->eip;
printf("scanf mem\n");
break;
case 0x50:
op1 = code[vm->eip + 1];
op2 = code[vm->eip + 2];
return memcmp(&vm->mem[0], &vm->mem[op1], op2);
}
}
}
int main() {
VM* vm = vm_new();
if (vm_run(vm))
printf("failed\n");
else
printf("good\n");
system("pause");
return 0;
}这样我们得到了原程序的汇编代码(参杂着奇怪的代码)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19mov mem[16],H
mov mem[17],e
mov mem[18],l
mov mem[19],l
mov mem[20],o
12345
scanf mem
mov r2,33
mov r1,0
xor mem[0],33
mov r1,1
xor mem[1],33
mov r1,2
xor mem[2],33
mov r1,3
xor mem[3],33
mov r1,4
xor mem[4],33
failed可以看到,程序先把”Hello”传给了mem的一片区域,然后获取输入,把输入的数与33异或,最后执行返回处的判断。返回处是对比mem[0]和mem[op1],对比op2位,结合code最后几位,知道是对比mem[0]和mem[16],对比5位。所以要求的输入是”Hello”逐位异或的结果(不可见字符,尝试输入失败)