导语:虚拟机保护技术已经出现了10多年,目前已有较多、较为成熟的商业化虚拟机保护产品,如VMProtect、Themida等。这些产品实现的虚拟机过于复杂,而本文仅仅是逆向分析和编程实现小型虚拟机保护。
1.引言
虚拟机保护技术已经出现了10多年,目前已有较多、较为成熟的商业化虚拟机保护产品,如VMProtect、Themida等。这些产品实现的虚拟机过于复杂,而本文仅仅是逆向分析和编程实现小型虚拟机保护,这种小型虚拟机仅用于说明虚拟机保护这种技术,可以用于开发CrackMe,但与真正的虚拟机保护还有非常远的路要走。
2017年看雪CTF大赛正在火热征题中,防守组的各位参赛者随手写一个适用于自己CrackMe的虚拟机,相信可以如虎添翼。
GitHub确实是个好地方,写代码之前可以先去逛一逛。笔者原本打算自行写一个小型虚拟机,不过在GitHub找到了非常理想的代码,所以干脆把这份代码拿出来说一说。这份代码出自NWMonster大神的CrackMe仓库。
本文的后续按照如下方式组织:首先逆向分析一下这个CrackMe,进而分析一下源码并实现自己的小型虚拟机,最后是一个小小的总结。
2.逆向分析
在逆向分析之前,了解一点关于小型虚拟机的程序结构还是很有帮助的。一般来讲,一个小型的虚拟机保护结构图如下:
上图中,VM_code代表虚拟机可以解读的字节码,VM_register为虚拟机使用的寄存器,在一次VM_loop中,虚拟机读取VM_code,并根据读取的内容选择并执行相应的VM_handler。为了明确被虚拟机保护的代码所实现的功能,我们需要了解每个VM_handler的含义,从而理解被隐藏的代码。
需要注意的是,该流程图并不适用于商业虚拟机保护软件,商业的虚拟机保护往往更复杂一些。
通常情况下,可以使用OllyDbg (OD)的trace功能,找到虚拟机的VM_loop。但本次我们使用IDA完成分析,这主要因为IDA的图形界面更加清晰明了。为了方便调试,笔者将源码重新编译为32位windows的debug版本。后文会把所有的材料都上传到Github,需要的读者可自行下载。
用IDA打开CrackMe,不难发现其207字节的VM_code:
上图中的VM_code被赋值给[eax + 20h],如下图:
根据这些代码,可以推测VM_register的结构如下:
Structure VM_register{ +0x00 ?? +0x04 ?? +0x08 ?? +0x0C ?? +0x10 ?? +0x14 ?? +0x18 ?? +0x1C ?? +0x20 IP register }
其中,eax指向VM_register,[eax + 20h]指向虚拟机的指令寄存器。紧接着就可以找到完整的VM_loop,如下图:
数一下上图中的方块数量,可以推测这里有大约23个VM_handler。下面开始根据VM_code的顺序逐一分析这些VM_handler。VM_code的第一个字节为0xDE,该字节会使虚拟机执行VM_handler_defaultcase,如下图所示:
而VM_handler_defaultcase的详细内容如下:
上图中,[this + 0x24]即为指令寄存器,因为VM_register是VM类中的第一个元素,其伪代码如下:
class VM{ +0x00 point to virtual table +0x04 VM_register + 0x20 IP register … }
所以,在0x20的基础上又增加了0x4字节的偏移。这段代码并无实际功能,只增加了指令寄存器,故可理解为NOP指令。VM_code中的第2,3,4字节同样会令虚拟机执行VM_handler_defaultcase分支,故可以如下代码重写VM_code:
1 0xDE NOP 2 0xAD NOP 3 0xC0 NOP 4 0xDE NOP
VM_code的第5个字节为0x7C,该字节会使虚拟机执行VM_handler_22分支,该分支的具体内容如下:
该段代码的含义是:将后续0xF字节的VM_code与0x66异或。暂不清楚其异或的目的为何。可以用如下方式表示该指令:
5 0x7C 0x2E 0x27 … 0x23 0x32 XOR the following 0xF bytes with 0x66
VM_code的第21字节为0x70,与之相应的VM_handler_10的内容如下:
这个VM_handler的作用是明显是入栈操作,即将后续4字节的VM_code压入虚拟机的栈,其[this + 20h]为栈指针。由此,可以进一步推断出VM_register结构如下:
Structure VM_register{ +0x00 ?? +0x04 ?? +0x08 ?? +0x0C ?? +0x10 ?? +0x14 ?? +0x18 ?? +0x1C SP register +0x20 IP register }
该虚拟机指令可表示如下:
21 0x70 0x00 0x00 0x00 0x2F push 0x2F
到此为止,笔者已经分析了25字节的VM_code和3个VM_handler以及一部分VM_register,后续内容还是留给感兴趣的读者。此外,其被虚拟机保护的代码被还原为等价的C代码也上传到了Github,供读者参考。
3.编程实现小型虚拟机
根据已有的源代码,可以很方便的构造小型虚拟机。首先来看一下该源代码,虚拟机使用的VM_register结构如下图:
其中,cf为标志位,*db指向用户输入数据。虚拟机中的VM_loop是通过一个while-switch结构实现的:
上图中的r.ip指向虚拟机的VM_code,该字节码被定义为一个字节数组,如图:
而完整的虚拟机类,即VM类如下图:
其中,结构体REG即为VM_register,为VM类的第一个成员。虚拟机中的各个VM_handler也可以在该类中找到。在每次VM_loop中,虚拟机读取 VM_code,并选择执行响应的VM_handler。
基于这些已有代码,构造一个定制的虚拟机变得非常简单。可以使用已有的VM_handler和VM_register,也可以增加更多的VM_handler,并适当地增加一些混淆。增加混淆的VM_handler会使虚拟机保护更加难以分析。
在已有的框架之上,定制虚拟机的问题是变为设计一段VM_code用来实现所需目的即可。比如说,如果我们需要检查用户输入是否为0x27字节的16进制数字,则可以使用如下VM_code:
PUSHD, 0x00, 0x00, 0x00, 0x2f, POP, 0x30, MOVD, 0x00, //loop begin XOR, 0x22, CMP, 0x02, JZ, 0x33, INCD, PUSHD, 0x00, 0x00, 0x00, 0x46, POP, 0x10, CMP, 0x01, JG, 0x27, //error user-input < ‘F’ ascII:0x46 PUSHD, 0x00, 0x00, 0x00, 0x30, POP, 0x10, CMP, 0x01, JL, 0x16, //error user-input > ‘0’ ascII:0x30 PUSHD, 0x00, 0x00, 0x00, 0x39, POP, 0x10, CMP, 0x01, JL, 0x0b, //correct user-input < ’9’ ascII:0x39 PUSHD, 0x00, 0x00, 0x00, 0x41, POP, 0x01, CMP, 0x01, JL, 0x06, //error user-input < ‘A’ ascII:0x41 XOR, 0x00, CMP, 0x00, JZ, 0x03, XOR, 0x00, //error out R0 = 0 END, LOOP, 0x3E, //loop end XOR, 0x00, //correct out R0 = 1 INC, 0x00, END,
这部分代码是取自源文件中的VM_code,并加以相应的修改。在以上列表中,令用户输入的每一位与‘F’, ‘0’, ’9’, ’A’进行比较,从而判断出是否为16进制数字。当全部满足条件时,虚拟机运行结束后,R0寄存机为1,否则为0。
最后,用这段VM_code替换原先的代码,调试运行确认没有bug之后,就可以正常使用了。
4.小结
本文主要讨论了小型虚拟机保护的相关内容,前一部分逆向分析了一个使用虚拟机保护的CrackMe,后一部分讨论了如果定制一些被虚拟机保护的代码。在分析虚拟机保护过程中,最主要的步骤是分析每一个VM_handler的作用,从而理解被保护的代码含义;而在定制小型虚拟机保护是,关键是设计一段VM_code使之能够顺利完成其目的。文章中提到的所有材料可以在此链接下载到:https://github.com/zzz66686/simple-virtual-machine