VM逆向

软件防逆向工程与逆向工程相伴发展,早期的有花指令,反调试技术,代码混淆与加密,加压缩壳或者加密壳等等保护手段,这些技目前已经有了较好的解决方案,自动化的分析方法也比较成熟。目前比较前沿的软件保护技术是虚拟机保护(Virtual-Machine-Protect),当然这种虚拟化的思想也广泛用于软件开发等其他领域。

现在CTF比赛中的虚拟机保护也越来越多,再不学简单题也做不出来了。

基本原理

这里的虚拟机指的是一种解释执行系统或者模拟器(Emulator)。所以虚拟机保护技术,是将程序可执行代码转化为自定义的中间操作码(OperationCode,如果操作码是一个字节,一般可以称为Bytecode),用以保护源程序不被逆向和篡改。opcode通过emulator解释执行,实现程序原来的功能。在这种情况下,如果要逆向程序,就需要对整个emulator结构进行逆向,理解程序功能,还需要结合opcode进行分析,整个程序逆向工程将会十分繁琐。这是一个一般虚拟机结构:

img

做题方法

在比赛中,虚拟机题目常常有两种考法:

· 给可执行程序和opcode,逆向emulator,结合opcode文件,推出flag

· 只给可执行程序,逆向emulator,构造opcode,读取flag

拿到一个虚拟机之后,一般有以下几个逆向过程:

· 分析虚拟机入口,搞清虚拟机的输入,或者opcode位置

· 理清虚拟机结构,包括Dispatcher和各个Handler

· 逆向各个Handler,分析opcode的意义

调试过程中,在汇编层面调试当然是最基本最直接的方法,但是由于虚拟机Handler可能比较多,调试十分繁琐。
若虚拟机内部没有很复杂的代码混淆,可以考虑使用IDA进行源码级调试,这对于快速整理emulator意义很有帮助。
再进一步,可以结合IDA反编译伪代码,加上一些宏定义,加入输出,重新编译,可以十分快速的逆向整个emulator执行过程。

实现简单的虚拟机

想实现虚拟机的话需要完成两个目标:
1.定义一套opcode
2.实现opcode的解释器

定义opcode

enum opcodes
{
MOV = 0xf1,
XOR = 0xf2,
RET = 0xf4,
READ = 0xf5,
};

定义结构体

vm_cpu

typedef struct
{
unsigned long r1,r2,r3;
unsigined char *eip; // 指向正在解释的opcode地址
vm_opcode op_list[OPCODE_N]; // opcode列表
}vm_cpu;

vm_opcode

typedef struct
{
unsigned char opcode;
void (*handle)(void*);
}vm_opcode;

实现了虚拟环境之后就可以开始实现解释器了。解释器的功能就是对opcode解析,选择相应的handle函数,并且将相应的参数传递给handle函数,由handle函数来解释执行一条指令。

其中 r1-r3是定义的通用寄存器,用来传参或者是存放返回值,eip指向正在解释的opcode的地址,op_list则存放了所有opcode及其对应的handle函数。

关键函数

vm_init
void vm_init(vm_cpu *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);
}
vm_start
void vm_start(vm_cpu *cpu)
{
/*
进入虚拟机
eip指向要被解释的opcode地址
*/
cpu->eip = (unsigned char*)opcodes;
while((*cpu->eip)!= RET)//如果opcode不为RET,就调用vm_dispatcher来解释执行
{
vm_dispatcher(*cpu->eip);
}
}
vm_dispatcher
void vm_dispatcher(vm_cpu *cpu)
{
int i;
for(i=0 ; i < OPCODE_N ; i++)
{
if(*cpu->eip == cpu->op_list[i].opcode)
{
cpu->op_list[i].handle(cpu);
break;
}
}

}
handles
void mov(vm_cpu *cpu);      
void xor(vm_cpu *cpu); //xor flag
void read_(vm_cpu *cpu); //call read ,read the flag

void xor(vm_cpu *cpu)
{
int temp;
temp = cpu->r1 ^ cpu->r2;
temp ^= 0x12;
cpu->r1 = temp;
cpu->eip +=1; //xor指令占一个字节
}

void read_(vm_cpu *cpu)
{

char *dest = vm_stack;
read(0,dest,12); //用于往虚拟机的栈上读入数据
cpu->eip += 1; //read_指令占一个字节
}

void mov(vm_cpu *cpu)
{
//mov指令的参数都隐藏在字节码中,指令表示后的一个字节是寄存器标识,第二到第五是要mov的数据在vm_stack上的偏移
//我这里只是实现了从vm_stack上取数据和存数据到vm_stack上
unsigned char *res = cpu->eip + 1; //寄存器标识
int *offset = (int *) (cpu->eip + 2); //数据在vm_stack上的偏移
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,伪代码的功能是从标准输入中读取12个字节的字符串,然后将读入的字符串每个字符与0x0还有0x12进行异或,并且将结果存储在虚拟机的栈上。写出来大致就是下面这样子

/*
call read_
MOV R1,flag[0]
XOR
MOV R1,0x20; //这是将R1的值送到vm_stack+0x20的位置,后面的同上
MOV R1,flag[1]
XOR
MOV R1,0x21;
MOV R1,flag[2]
XOR
MOV R1,0x22
MOV R1,flag[3]
XOR
MOV R1,0x23;
MOV R1,flag[4]
XOR
MOV R1,0x24;
MOV R1,flag[5]
XOR
MOV R1,0x25;
MOV R1,flag[6]
XOR
MOV R1,0x26;
MOV R1,flag[7]
XOR
MOV R1,0x26
MOV R1,flag[7]
XOR
MOV R1,0X27
MOV R1,flag[7]
XOR
MOV R1,0x28
MOV R1,flag[7]
XOR
MOV R1,0X29
MOV R1,flag[7]
XOR
MOV R1,0x2A
MOV R1,flag[7]
XOR
MOV R1,0x2b
*/

将它转换成对应的字节码,然后用解释器去解释执行就可以实现伪代码的功能。

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
};

至此,一个简化版的小型虚拟机就实

参考链接

虚拟机保护逆向入门

逆向之虚拟机保护