系统学习vm虚拟机逆向

最近做了两个vm逆向的题,为理解的更加透彻并且动手实践,所以写下这篇博:
(非全部原创,是整理下其他师傅的博客并做了一些总结,具体文末会加上链接)

首先,来说一下什么是vm逆向?

vm逆向

首先这里的虚拟机往往不是商业的vmp之类的保护软件,而是出题人实现的小型虚拟机,题目本身一般也没有实现某些复杂的功能。

基本原理

这里的虚拟机当然并不是指VMWare或者VirtualBox之类的虚拟机,而是指的意思是一种解释执行系统或者模拟器(Emulator)。
逆向中的虚拟机保护是一种基于虚拟机的代码保护技术。它将基于x86汇编系统中的可执行代码转换为字节码指令系统的代码,来达到不被轻易逆向和篡改的目的。简单点说就是将程序的代码转换自定义的操作码(opcode),然后在程序执行时再通过解释这些操作码,选择对应的函数执行,从而实现程序原有的功能。

vm_start:

虚拟机的入口函数,对虚拟机环境进行初始化

vm_dispatcher:

调度器,解释opcode,并选择对应的handle函数执行,当handle执行完后会跳回这里,形成一个循环。

opcode :

程序可执行代码转换成的操作码

在这种情况下,如果要逆向程序,就需要对整个emulator结构进行逆向,理解程序功能,还需要结合opcode进行分析,整个程序逆向工程将会十分繁琐。这是一个一般虚拟机结构:

系统学习vm虚拟机逆向_第1张图片

常见的结构有:while-switch / if型

分析方法

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

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

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

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

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

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

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

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

实现一个小型虚拟机

我们通过实现一个简化版的小型虚拟机来加深对虚拟机的认识,语言用的是C语言。要想实现虚拟机的话需要完成两个目标:
  1.定义一套opcode
  2.实现opcode的解释器

一些关键的结构体

vm_cpu

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的地址。

vm_opcode

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

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

关键函数

vm_init

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

vm_start

void vm_start(vm_cpu *cpu)
{
    /*
    进入虚拟机
    eip指向要被解释的opcode地址
    */
    cpu->eip = (unsigned char*)opcodes;
    while((*cpu->eip) != 0xf4)//如果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指令的参数都因曾在字节码也就是vm_code中,指令表示后的一个字节是寄存器表示,第二到
//第五是要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位
}
    

解释器到这里就实现结束了。接下来是要将想要实现功能的伪代码转成自定义的vm_code,伪代码的功能是从标准输入中读取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[8]
    XOR
    MOV R1,0X27
    MOV R1,flag[9]
    XOR
    MOV R1,0x28
    MOV R1,flag[10]
    XOR
    MOV R1,0X29
    MOV R1,flag[11]
    XOR
    MOV R1,0x2A
    RET
*/

vm_code用来存放操作码,例如:

​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,
    0xf4
};

​
 

其实就是在vm_start()函数中对opcode进行解析,把*cpu->eip做if/switch,在其嵌套中做一些操作并改变指针。

至此,一个简化版的小型虚拟机就实现完了。我在虚拟机中实现了对输入字符串简单的异或加密,并将加密后的值存储到指定位置。自己动手做这个过程是非常重要的,这样可以让大家更细致的明白它整个的原理。

用gcc编译一下就可以在ida上自己逆,看一下整个逻辑结构。

※ 该类型的一些例题写在下一篇吧~


参考链接:
逆向之虚拟机保护
虚拟机保护逆向入门

你可能感兴趣的:(#,RE)