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
2
3
4
5
6
7
typedef struct{
unsigned int r1;//寄存器r1
unsigned int r2;//寄存器r2
unsigned int eip;//寄存器eip
unsigned char mem[256];//虚拟内存数组
unsigned char code[1024];//指令数组
} VM;

定义指令集

  • 使用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
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
int vm_run(VM* vm) {
char opcode;//用来存放指令
char op1, op2;//用来存放操作数

while (1) {
opcode = vm->code[vm->eip];//读取eip指向的指令

switch (opcode) {
case 0x10:
op1 = vm->code[vm->eip + 1];//获取操作数
vm->r1 = vm->mem[op1];//r1=mem[op1]
vm->eip += 2;//跳转到下一条指令(指令+操作数占2个索引位)
break;

case 0x11:
op1 = vm->code[vm->eip + 1];//获取操作数
vm->r2 = vm->mem[op1];//r2=mem[op1]
vm->eip += 2;//跳转到下一条指令(指令+操作数占2个索引位)
break;

case 0x20:
vm->r1 += vm->r2;//r1+=r2
vm->eip += 1;//跳转到下一条指令(只有指令,占1个索引位)
break;

case 0x30:
op1 = vm->code[vm->eip + 1];//获取操作数1
op2 = vm->code[vm->eip + 2];//获取操作数2
scanf("%d %d", &vm->mem[op1], &vm->mem[op2]);//输入给mem[op1],mem[op2]
vm->eip += 3;//跳转到下一条指令(指令+操作数占3个索引位)
break;

case 0x40:
printf("%d", vm->r1);//输出r1的值
vm->eip += 1;//跳转到下一条指令(只有指令,占1个索引位)
break;

case 0x50:
return 1;//循环终止,函数返回,虚拟机关机
}
}
}

指令数组

1
2
3
4
5
6
7
8
unsigned char code[]={
0x30,0x0,0x1,//输入给mem[0],mem[1]
0x10,0x0,//r1=mem[0]
0x11,0x1,//r2=mem[1]
0x20,//r1+=r2
0x40,//输出r1的值
0x50//虚拟机关机
}

完整程序

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
#include <windows.h>
#include <stdio.h>

typedef struct{
unsigned int r1;//寄存器r1
unsigned int r2;//寄存器r2
unsigned int eip;//寄存器eip
unsigned char mem[256];//虚拟内存数组
unsigned char code[1024];//指令数组
} VM;

unsigned char code[]={
0x30,0x0,0x1,//输入给mem[0],mem[1]
0x10,0x0,//r1=mem[0]
0x11,0x1,//r2=mem[1]
0x20,//r1+=r2
0x40,//输出r1的值
0x50//虚拟机关机
};

VM* vm_new() {
VM* vm = (VM*)malloc(sizeof(VM));//为虚拟硬件结构体申请空间
memset(vm, 0, sizeof(VM));//初始化结构体
memcpy(vm->code, code, sizeof(code));//初始化code
return vm;
}

int vm_run(VM* vm) {
char opcode;//用来存放指令
char op1, op2;//用来存放操作数

while (1) {
opcode = vm->code[vm->eip];//读取eip指向的指令

switch (opcode) {
case 0x10:
op1 = vm->code[vm->eip + 1];//获取操作数
vm->r1 = vm->mem[op1];//r1=mem[op1]
vm->eip += 2;//跳转到下一条指令(指令+操作数占2个索引位)
break;

case 0x11:
op1 = vm->code[vm->eip + 1];//获取操作数
vm->r2 = vm->mem[op1];//r2=mem[op1]
vm->eip += 2;//跳转到下一条指令(指令+操作数占2个索引位)
break;

case 0x20:
vm->r1 += vm->r2;//r1+=r2
vm->eip += 1;//跳转到下一条指令(只有指令,占1个索引位)
break;

case 0x30:
op1 = vm->code[vm->eip + 1];//获取操作数1
op2 = vm->code[vm->eip + 2];//获取操作数2
scanf("%d %d", &vm->mem[op1], &vm->mem[op2]);//输入给mem[op1],mem[op2]
vm->eip += 3;//跳转到下一条指令(指令+操作数占3个索引位)
break;

case 0x40:
printf("%d", vm->r1);//输出r1的值
vm->eip += 1;//跳转到下一条指令(只有指令,占1个索引位)
break;

case 0x50:
return 1;//循环终止,函数返回,虚拟机关机
}
}
}

int main() {
VM* vm = vm_new();//生成一个虚拟机
if (vm_run(vm))//运行虚拟机
system("pause");
return 0;
}

简单示例的逆向

  • VM题最显著的特征应该就是虚拟硬件结构体的初始化和解释器了。解决这类问题,需要先复刻出一个解释器,通过在解释器中增加打印分析出的代码的操作,来得到去虚拟化的原程序,进而可以正常逆向分析

  • 先来看main函数:

    main函数

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

  • 看看v1=后面的函数

    v1=后面的函数

    目前没什么明显的VM特征

  • 再看看if括号里的函数

    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
    7
    typedef 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
    46
    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);
    }
    }
    }
  • 完整的脚本:

    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
    19
    mov 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”逐位异或的结果(不可见字符,尝试输入失败)

作者

SydzI

发布于

2025-08-06

更新于

2025-10-03

许可协议

评论