0%

代码保护技术 一

0x00 代码虚拟化简介

代码虚拟化通过对原生的native指令集代码进行自定义字节码替换,在执行的时候由虚拟机中的解释器来执行,由于是用户自定义的字节码,基于本地native指令集的反汇编器无法进行识别,所以虚拟机保护下的代码相对来说更能延缓攻击者的分析与破解。目前虚拟机技术常用于代码虚拟化、加密壳、沙盒、解释器如JVM等等。

0x01 代码代码虚拟化的混淆

代码虚拟化也可以算作是混淆的一种,能够有效延缓攻击者的分析时间,但是由于解释器是基于native指令,所以通过动态调试可以得到native指令和字节码之间的映射关系,所以说到底代码虚拟化的混淆并不是无解的,常见的代码虚拟化保护有两种,一种是通过给壳进行虚拟化,让程序的解密过程变得复杂,从而让攻击者分析起来有难度,但是这种保护方式对动态调试来说效果不大,因为程序脱下解密壳以后能被dump下来,程序的逻辑也就一清二楚了;第二种是将程序的源代码转化为字节码通过解释器解释,这种保护无论静态还是动态都是有效的。

0x02 如何实现

虚拟机主要有三个部分,字节码、CPU、解析器。
1、首先需要实现一套字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
enum OPCODES
{
MOV = 0XA0,
XOR = 0XA1,
CMP = 0XA2,
RET = 0XA3,
SYS_READ = 0XA4,
SYS_WRITE = 0XA5,
JNZ = 0XA6
};
enum REGISTERS :unsigned char
{
R1 = 0X10,
R2 = 0X11,
R3 = 0X12,
R4 = 0X13,
EIP = 0X14,
FLAG = 0X15
};

这边由于只是加深理解所以只是定义了几个简单的字节码指令。
2、其次需要一个CPU,负责指令的执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//单个字节码结构体,包含字节码内容和与native指令的映射关系
typedef struct opcode_t //包含字节码和native处理函数
{
unsigned char opcode;
void(*func)(void *); //函数指针
}vm_opcode;
/*

VIRTUAL PROCESSOR
*/
typedef struct process_t
{
int r1; //虚拟寄存器r1
int r2; //虚拟寄存器r2
int r3; //虚拟寄存器r3
int r4; //虚拟寄存器r4
int flag; //虚拟机flag寄存器,类似于主机eflags标志位寄存器
unsigned char *eip; //虚拟机的eip寄存器,指向正在执行的指令地址
vm_opcode op_table[OPCODE_NUM];//定义了所有,注意是所有虚拟机字节码和主机native指令的对应关系,一个字节码对应一个函数,一个函数中可能包含多个操作
}vm_processor;

这边定义了一个CPU处理器的结构体,结构体中包含四个自定义寄存器、一个标志寄存器、一个指令指针寄存器、一个handler数组,handler数组中包含了所有虚拟机字节码和主机native指令之间的映射关系,这些handler是对字节码进行native解释的关键。
3、最后需要定义一个解析器,负责对字节码进行解析并交给CPU进行处理,起一个调度作用,这边的解析器比较简单,比较复杂的有AST抽象语法树解析,这边比较简单,只是起一个调度作用。

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
void exec_opcode(vm_processor *proc)
{
int flag = 0;
int i = 0;
while (!flag&&i < OPCODE_NUM)
{
if (*proc->eip == proc->op_table[i].opcode)
{
flag = 1;
proc->op_table[i].func(proc);
}
else
{
i++;
}
}
}
void vm_interp(vm_processor *proc)
{
proc->eip = target_func;
while (*proc->eip != RET)
{
exec_opcode(proc);
}
}

以上三个部分结合起来就是一个简单的虚拟机了。

0x04 完整代码

xvm.h

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
#pragma once
#define OPCODE_NUM 7
#define HEAP_SIZE_MAX 1024
char *heap_buf;//vm_heap
/*
opcode enum
*/
enum OPCODES
{
MOV = 0XA0,
XOR = 0XA1,
CMP = 0XA2,
RET = 0XA3,
SYS_READ = 0XA4,
SYS_WRITE = 0XA5,
JNZ = 0XA6
};
enum REGISTERS :unsigned char
{
R1 = 0X10,
R2 = 0X11,
R3 = 0X12,
R4 = 0X13,
EIP = 0X14,
FLAG = 0X15
};
//单个字节码结构体,包含字节码内容和与native指令的映射关系
typedef struct opcode_t //包含字节码和native处理函数
{
unsigned char opcode;
void(*func)(void *); //函数指针
}vm_opcode;
/*

VIRTUAL PROCESSOR
*/
typedef struct process_t
{
int r1; //虚拟寄存器r1
int r2; //虚拟寄存器r2
int r3; //虚拟寄存器r3
int r4; //虚拟寄存器r4
int flag; //虚拟机flag寄存器,类似于主机eflags标志位寄存器
unsigned char *eip; //虚拟机的eip寄存器,指向正在执行的指令地址
vm_opcode op_table[OPCODE_NUM];//定义了所有,注意是所有虚拟机字节码和主机native指令的对应关系,一个字节码对应一个函数,一个函数中可能包含多个操作
}vm_processor;

VM_Easy.cpp

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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
#include "pch.h"
#include <iostream>
#include "xvm.h"
#include <stdlib.h>
#include <string.h>
using namespace std;
unsigned char target_func[] =
{0xa0,0x10,0x00,0x00,0x00,0x00,0xa0,0x11,0x12,0x00,0x00,0x00,0xa4, 0xa0, 0x14, 0x00,
0x00, 0x00, 0x00, 0xa0, 0x11, 0x29, 0x00,0x00, 0x00, 0xa1, 0xa2, 0x20, 0xa6, 0x5d, 0xa0,
0x14, 0x01, 0x00, 0x00, 0x00, 0xa1,0xa2, 0x21, 0xa6, 0x50, 0xa0, 0x14, 0x02, 0x00, 0x00,
0x00, 0xa1, 0xa2, 0x22, 0xa6,0x47, 0xa0, 0x14, 0x03, 0x00, 0x00, 0x00, 0xa1, 0xa2, 0x23,
0xa6, 0x3a, 0xa0, 0x14,0x04, 0x00, 0x00, 0x00, 0xa1, 0xa2, 0x24, 0xa6, 0x31, 0xa0, 0x14,
0x05, 0x00, 0x00,0x00, 0xa1, 0xa2, 0x25, 0xa6, 0x26, 0xa0, 0x14, 0x06, 0x00, 0x00, 0x00,
0xa1, 0xa2,0x26, 0xa6, 0x1b, 0xa0, 0x14, 0x07, 0x00, 0x00, 0x00, 0xa1, 0xa2, 0x27, 0xa6,
0x10,0xa0, 0x10, 0x30, 0x00, 0x00, 0x00, 0xa0, 0x11, 0x09, 0x00, 0x00, 0x00, 0xa5, 0xa3,
0xa0, 0x10, 0x40, 0x00, 0x00, 0x00, 0xa0, 0x11, 0x07, 0x00, 0x00, 0x00, 0xa5, 0xa3 };
void vm_xor(vm_processor *proc)
{
int arg1 = proc->r1;
int arg2 = proc->r2;
proc->r1 = arg1 ^ arg2;
proc->eip += 1;//xor只占一个字节
}
void vm_cmp(vm_processor *proc)
{
int arg1 = proc->r1;
char *arg2 = *(proc->eip + 1) + heap_buf;
if (arg1 == *arg2)
{
proc->flag = 1;
}
else
{
proc->flag = 0;
}
proc->eip += 2;//cmp占两个字节
}
void vm_jnz(vm_processor *proc)
{
char arg1 = *(proc->eip + 1);
if (proc->flag == 0)
{
proc->eip += arg1;
}
else
proc->eip += 2;//jnz占两个字节
}
void vm_ret(vm_processor *proc)
{
std::cout << "Finish";
}
void vm_read(vm_processor *proc)
{
char *arg1 = heap_buf + proc->r1;
int arg2 = proc->r2;
cin.read(arg1, arg2);
proc->eip += 1;
}
void vm_write(vm_processor *proc)
{
char *arg1 = heap_buf + proc->r1;
int arg2 = proc->r2;
cout.write(arg1, arg2);
proc->eip += 1;
}
void vm_mov(vm_processor *proc)
{
unsigned char *dest = proc->eip + 1;
int *src = (int*)(proc->eip + 2);
switch (*dest)
{
case 0x10:
proc->r1 = *src;
break;
case 0x11:
proc->r2 = *src;
break;
case 0x12:
proc->r3 = *src;
break;
case 0x13:
proc->r4 = *src;
break;
case 0x14:
proc->r1 = *(heap_buf + *src);
default:
break;
}
proc->eip += 6;
}
void exec_opcode(vm_processor *proc)
{
int flag = 0;
int i = 0;
while (!flag&&i < OPCODE_NUM)
{
if (*proc->eip == proc->op_table[i].opcode)
{
flag = 1;
proc->op_table[i].func(proc);
}
else
{
i++;
}
}
}
void vm_interp(vm_processor *proc)
{
proc->eip = target_func;
while (*proc->eip != RET)
{
exec_opcode(proc);
}
}
//初始化处理器
void init_vm_processor(vm_processor *proc)
{
proc->r1 = 0;
proc->r2 = 0;
proc->r3 = 0;
proc->r4 = 0;
proc->flag = 0;
proc->op_table[0].opcode = MOV;
proc->op_table[1].opcode = XOR;
proc->op_table[2].opcode = CMP;
proc->op_table[3].opcode = RET;
proc->op_table[4].opcode = SYS_READ;
proc->op_table[5].opcode = SYS_WRITE;
proc->op_table[6].opcode = JNZ;
proc->op_table[0].func = (void(*)(void *))vm_mov;
proc->op_table[1].func = (void(*)(void *))vm_xor;
proc->op_table[2].func = (void(*)(void *))vm_cmp;
proc->op_table[3].func = (void(*)(void *))vm_ret;
proc->op_table[4].func = (void(*)(void *))vm_read;
proc->op_table[5].func = (void(*)(void *))vm_write;
proc->op_table[6].func = (void(*)(void *))vm_jnz;
heap_buf = (char*)malloc(HEAP_SIZE_MAX);
memcpy(heap_buf + 0x20, "syclover", 8);
memcpy(heap_buf + 0x30, "success!\n", 9);
memcpy(heap_buf + 0x40, "error!\n", 7);
}
int main()
{
vm_processor proc = { 0 };
//init vm processor
init_vm_processor(&proc);
vm_interp(&proc);
return 0;
}

主要就是CPU和字节码结构体的定义、Handler函数的处理以及Eip的调度,其余的还是相对简单的。

0x05 保护效果

ida中的反汇编显示:
image
可以看到程序执行流那里的字节码ida并没有识别,但是解释器的逻辑是可以看到的,但是多个handler在一起代码量相对较大,分析起来并不方便。

0x06 对抗

CTF和现实中都有一些基于代码虚拟化的程序,该如何对抗进行逆向分析呢,这边推荐两种方式

  • 动态调试&静态分析得到解释器的字节码&native指令之间的映射关系,手动理清逻辑
  • 自动化工具,类似Pin等的二进制插桩、控制流分析工具(侧信道分析指令执行条数得到执行路径)

0x07 总结

以上就是对代码虚拟化保护的原理介绍,主要就是介绍了一下实现原理,手动实现了一个简单虚拟机,真正投入使用的成品/商业化虚拟机指令集是多很多的,解析过程也更复杂,之后打算去看一下OLLVM的源码,分析一下更成熟完整的代码虚拟机实现方式。