公众号

VMP虚拟化保护技术原理

注意看,这是练习两年半的实习生写的一段密码校验程序:

bool check_password() {
    char input[100] = {0};
    scanf_s("%s", input);
    if (strcmp(input), "") {
        printf("success!");
        return true;
    } else {
        printf("failed!");
        return false;
    }
}

这样编译出来的程序,用IDA一打开就露馅儿了。

如果你不想让别人知道你是怎么在校验密码的,怎么办呢?

我们可以选择对编译出来的程序进行加壳,加壳软件通过压缩、加密等措施,把可执行文件中的内容进行处理,然后在运行的时候动态解密还原,这样IDA里面看到的就是乱码了,起到了保护的目的。

但加壳总有在内存中解压缩解密的时刻,如果能找到那个时刻,把内存中的文件dump出来,就能对程序脱壳了。还是不够安全,还有没有其他办法呢?

还有一种更强大的保护手段就是虚拟化保护技术,可能很多同学听说过VMP的大名,那它的工作原理是什么,它是怎么保护程序的,看完这个视频,相信你一定会有收获,今天的内容有点干,一定要看到最后哦。

什么是虚拟机?

虚拟机,顾名思义,虚拟的计算机。

我们真实的计算机执行程序,是CPU从内存取指令,然后译码,交给CPU内部的电路完成这条指令背后的操作。

具体到x86平台上,CPU运行的时候大致过程是这样的:

程序指令放在内存中,CPU设置了一个指令寄存器EIP指向下一条要执行的指令,CPU通过这个EIP不断从内存中读取指令,然后译码,交给内部的功能单元完成这条指令。

在这个过程中,有时候需要临时存储数据,CPU设置了一些寄存器可以派上用场,同时还在内存中划一块区域用来记录程序的调用关系以及一些中间数据,这就是栈。

以上就是x86CPU工作的一个简单描述。

而虚拟机,则是通过软件的方式虚拟出一个CPU来,模拟真实的CPU来执行指令。

想要用软件来模拟出上面描述的过程,这个虚拟的VM也需要具备几个基本的元素:

好了,截止到现在,我们指令集有了,指令集中指令具体的软件实现也有了,调度器也有了,指令寄存器也有了,编译器也有了,理论上,我们的虚拟机就可以工作了。

在这个虚拟机中,调度器不断从虚拟的指令寄存器中读取将要执行的指令,读取后,还要将虚拟指令寄存器的指向往后移动,然后根据读取到的指令,调用对应的Handler进行执行。

这就是一个虚拟机最简单的模型。其实除了我们软件安全领域用到的虚拟机保护技术,在编程语言的设计领域,虚拟机也是非常常见的,比如Java、Python这类非本地语言,都是通过虚拟机在解释执行的。都是把高级语言编译成它们的虚拟指令集,然后通过内建的虚拟机来解释执行。

上面介绍了虚拟机技术,那什么又是虚拟机保护技术呢?

虚拟机保护技术就是分析我们要保护的程序,把它里面的CPU指令,替换成虚拟机指令集中的指令,然后再在这个程序中塞入一个虚拟机,通过这个虚拟机来模拟执行这些替换后的虚拟指令。

这样一来,目标程序中不再有真实的物理CPU指令,全是虚拟的指令,在传统的反汇编工具看来,全都是乱码,无法分析。而且它还不同于传统的加壳,会在内存中有解密的过程,虚拟机是没有这个过程的,所以逆向分析的难度陡增。

知名的虚拟机保护软件有VMProtect、Themida、Winlicense等等,这里面尤其以VMP名气最大,逆向工程师们遇到VMP保护的程序很多都只能含泪拖进回收站。

虚拟化执行思想

接下来,我们通过代码来了解一下虚拟机的工作原理。

来看这样一段汇编指令:

mov eax, dword ptr [esp+4]
mov ebx, dword ptr [esp+8]
add eax, ebx

这几条汇编指令,实现了从堆栈中取出两个整数,然后相加,存入eax寄存器返回。实际上就是一个加法函数。用C语言的形式来写,就是这样:

int add(int a, int b) {
    return a + b;
}

这几条指令的CPU机器码是这样的:

8B 44 24 04          mov         eax,dword ptr [esp+4]
8B 5C 24 08          mov         ebx,dword ptr [esp+8]
03 C3                add         eax,ebx

假设我现在定义3个字节码:

01:代表第一条指令,从ESP+4的地方读取一个4字节整数到EAX寄存器

02:代表第二条指令,从ESP+8的地方读取一个4字节整数到EBX寄存器

03:代表第三条指令,把EBX寄存器的值和EAX的值相加,放到EAX中

我再定义一个解释执行的函数:

typedef unsigned char byte;
int interpreter() {
    while(true) {
        byte byte_code = get_byte_code();
        switch(byte_code) {
            case 0x01:
                handler_01();
                break;
            case 0x02:
                handler_02();
                break;
            case 0x03:
                handler_03();
                break;
            default:
                printf("error\n");
        }
    }
}

__declspec(naked)
    void handler_01() {
    __asm {
        mov eax,dword ptr [esp+4]
        }
}

__declspec(naked)
    void handler_02() {
    __asm {
        mov ebx, dword ptr [esp+8]
        }
}

__declspec(naked)
    void handler_03() {
    __asm {
        add eax, ebx
        }
}

现在我把可执行文件中原来的这一段CPU机器码给去掉,换成我的01 02 03。再把我上面的这个解释器代码嵌入进去。

解释器率先得到执行,然后依次解析01 02 03,分别调用对应的handler完成相应的动作。

这样,原始可执行文件里面的CPU指令全被拆乱打散了,你直接逆向分析是很难看出原来这是在做加法。

上面的这个例子只是一个非常简单的示意,实际上这样做会有很多问题,比如这样只是把指令打散了而已。还是执行的原始CPU指令,花点时间还是容易被分析出来,另外就是我们的解释器程序本身编译后的指令也需要用到这些CPU的寄存器,非常容易跟拆分后的原始程序指令用到的寄存器冲突。

实际上的虚拟机远比这复杂。但思想是一致的。

VMP的雏形

正如前面所说,首先需要有一套指令集,然后为指令集中的这些指令,指定实现的Handler,然后编写一个解释器,或者调度器来执行虚拟的指令。

那为了解决虚拟机的指令和原始程序的指令涉及到寄存器的冲突问题,虚拟机还要虚拟出一套寄存器来供虚拟指令来使用。

接下来我们来看一个更真实点的虚拟机例子。这个例子是加密与解密这本书里的虚拟机示例,非常经典,学习这个例子对大家学习了解VMProtect也非常有帮助。

先来理解一下虚拟机工作的整体框架:

首先通过VStartVM启动虚拟机,完成一些初始化准备工作,然后通过VMDispatcher不断读取字节码,接着调度各个Handler的执行。

整个逻辑就是这么简单。

虚拟机初始化

来看一下这个启动虚拟机的VStartVM函数,注意这都是用汇编语言编写的代码:

代码很短,我们来挨个分析一下,并注意指令执行过程中堆栈的变化。

前面的7条指令是在把通用寄存器压栈,第8条指令pushfd是在把标志寄存器压栈。

接下来这条mov指令,把esp+0x20这个地址的内容,赋值给esi寄存器。

0x20也就是十进制的32,除以4就是8,那esp+32,刚好跳过刚刚压入堆栈的这8个寄存器的内容,指向的就是VStartVM函数在最开始压栈之前栈顶的位置,注意堆栈的增长方向是从高地址到低地址方向进行的。这里面现在是啥呢?是不是调用这个函数的时候,压入的上一级调用者函数的返回地址呢?

要回答这个问题,我们得来看一下这个VStartVM是如何被调用的。

注意,它是直接通过jmp来得到执行的,而不是通过call指令。并且在jmp之前,还向堆栈中压入了字节码的起始地址。

所以前面那个问题的答案就有了,这里的这条mov指令,是把虚拟的字节码的起始地址放入了ESI寄存器。

继续往下看,把ESP的值赋值给EBP,现在EBP就指向了刚刚push的eflags寄存器的地方了。

继续下一条指令,把ESP寄存器的值减去0xC8,换算成十进制就是200个字节。现在栈顶一下被抬高到这上面来了。

接下来这一条指令,把最新的ESP寄存器的值赋值给EDI寄存器,现在EDI也指向了这里。

这一连串的动作是在做什么呢?别着急,答案即将揭晓。继续往下看。

虚拟机核心调度器

继续往下执行,就来到了虚拟机的调度器VMDispatcher的地盘了。我们来看看这个指令调度器是怎么运行的。

第一条指令,一条movzx指令。movzx 指令用于将一个较小的数据单位移动到一个较大的寄存器,并在移动时将高位填充为0。这里后面的byte ptr表示读取的数据长度为1个字节。整个这条指令的意思是把ESI寄存器指向的地方,读取1个字节放到eax寄存器中,并把eax寄存器的高几个字节填充为0。

而刚才我们分析了,ESI现在指向的是字节码的地址。所以这里就是在从内存中加载读取1个字节的字节码。

接下来这条lea指令,把ESI的地址加1,然后又保存到ESI寄存器中。其实相当于add esi, 1,或者inc esi指令,都是把ESI寄存器的值+1。只不过LEA指令执行不会影响到一些标志位。

这个加1的动作就是让ESI指向下一个字节码。这里实际上就是在模拟CPU执行指令过程中,不断更新EIP寄存器,让它指向下一条指令的行为。这里的ESI就是虚拟机里的指令寄存器。

接下来是一条跳转指令,跳转的地址存储在以JUMPADDR为基地址,EAX乘以4为偏移处的地方。

这里的JUMPADDR实际上就是一个字节码的Handler数组的起始地址,以字节码为索引,找到各自Handler的入口地址。

这条跳转指令就跳到具体的Handler去执行了。

但要注意,Dispatcher需要不断读取字节码来调度,这样跳出去以后,怎么才能把执行权限收回来呢?

所以这样设计的话,每个Handler的结尾,必须重新跳回到Dispatcher来。这样,Dispatcher就能不断重复这个过程了。

虚拟机寄存器VMContext

我们刚才遗留了一个问题没有解决,就是在初始化的VStartVM函数中,进行了一堆的压栈和抬高栈顶的操作,这是在干嘛呢?

再往前我提到过,如果仅仅是把指令打散拆解到各个Handler中,解释器程序本身编译后的指令跟拆分后的原始程序指令用到的寄存器非常容易冲突。

这句话什么意思呢?比如上面这里,解释器用到了ESI寄存器作为虚拟的指令寄存器,但如果原来程序中指令也用到ESI寄存器,那不是乱套了吗?

所以,虚拟指令用到的寄存器和真正的物理CPU寄存器必须隔离开来。

为此,虚拟机需要准备一套新的寄存器,以供虚拟后的指令来使用,这就是VMContext,虚拟机上下文。

既然是软件虚拟出来的寄存器,那放在哪里呢?自然是内存中。那放内存那里呢?你可以在堆区malloc或者new动态分配一块内存来作为虚拟寄存器的存放地址,也可以把这个东西放在栈上面。但考虑到多线程的情况,一般选择放在栈上面。

这里这个例子就是放在栈上面的。

虚拟机保护可以只保护一段代码,甚至只保护一小段指令。所以被虚拟机保护的指令和程序原来未被保护的指令它们是混在一起的,执行到被保护的指令时,就进入虚拟机,执行完以后又退出虚拟机,接着执行未被保护的指令,这就涉及到进入虚拟机和退出虚拟机的操作。

前面的VStartVM就是进入的操作,那在进入的时候,需要把当前状态寄存器的值先复制一份到虚拟的寄存器中,然后虚拟机在运行的过程中,一直操作的都是虚拟寄存器,等到虚拟机退出的时候,再把虚拟寄存器的值装载到真实的CPU寄存器中,一切就大功告成。

回头来看前面的VStartVM,这一段push指令,就是在把进入虚拟机之前的执行上下文寄存器的值通通入栈保存起来。

那保存的这个区域就是VMContext结构吗?

答案是否定的,真正的VMContext在另外一个地方,接下来要把刚刚保存的这些寄存器的值,填充到真正的VMContext里去。

这就是Dispatcher调度的第一个字节码对应的Handler要干的事,这个Handler就是vBegin。

大家可以仿照前面分析VStartVM那样,逐行分析这段代码,它实际上干的一件事,就是把刚刚压入到栈里的寄存器值,再Copy一份到EDI寄存器指向的地方。也就是说,EDI指向的就是VMContext结构!

注意看里面每复制一个,ebp就+4移动一次,等全部复制完,ebp就来到了最开始跳转到VStartVM之前也就是进入虚拟机之前的状态了。在这个虚拟机例子中,EBP寄存器用来记录原来的真实的栈顶的位置。

现在,所有的准备工作全部就绪,接下来就是虚拟机正式来模拟执行要保护的指令的时候了。

大家通过这个图可以看到。VMContext实际上是放在栈上面的,之前特意把栈顶位置拉高,中间留了这一大段空白,就是为了给虚拟机中的虚拟指令来使用的。

从这一刻开始,对于虚拟机中的各个指令(也就是Handler)来说,接下来它们的工作,就是在这一段空白的空间里进行表演。

虚拟机堆栈检查机制

聪明的你可能会想到一个问题,万一这一段空间不够用了,会淹没这上面的VMContext结构怎么办?

确实会有这个问题,所以还得要引入一个检查机制。这就是VCheckESP这个Handler要干的事情。

这个Handler就是检查当前的栈顶EBP和EDI指向VMContext之间的安全距离是否足够,如果小于安全距离,就继续抬高栈顶,把VMContext结构复制到更高的位置去。

对于虚拟机里面所有可能涉及修改栈操作的Handler,在执行后都要跳转到这个Check Handler。

虚拟机原理总结

好,我们来总结一下,在这个虚拟机例子里面,ESI是指令寄存器,指向字节码地址,EBP是栈指针寄存器,EDI指向VMContext结构,这个结构里面包含了虚拟机虚拟出来的一套寄存器。

这之后,正式开始执行字节码,所有的Handler中,涉及到压栈操作,都是通过EBP来操作,所有对寄存器的操作,都是对VMContext中的变量来操作。

总的来说,虚拟机保护的过程,就是先保存真实的现场到VMContext,然后模拟执行被保护的指令。在退出虚拟机的时候,再把虚拟的寄存器又装载回真实的寄存器。这样,被保护的代码通过这样模拟执行,起到了被保护的目的。

VMProtect逆向

以上就是虚拟机保护技术的原理讲解,也是早期版本的VMP简化后的核心流程。现在的VMProtect比这还要更加复杂。VMP历经二十多年的发展,在虚拟化架构、寄存器的使用变换、垃圾指令填充、代码混淆等多个方面不断调整变化,越来越难。

回顾我当初的学习过程,就像是在迷雾中寻找出路一般,网络上的资料零散不成体系,这方面的课程培训班又贵的吓人,动辄都是几千上万。所以我只能一点点死磕,多次想要放弃,走了不少弯路。好在后来我还是坚持下来了,在这个过程中还积累了不少心得,非常清楚新手在学习的时候,困惑的点在哪里。

我之前在知识星球里创作了《从零开始学逆向》这门课程,从那时起,很多小伙伴都公开或者私下询问我,什么时候带他们学习一下VMP,工作中经常遇到,每次都只能干着急。

为了响应大家想要学习VMP的需求,我花了几个月时间,终于肝出了一套《VMP逆向分析:从入门到进阶》的视频课程,把我掌握的VMP分析技能倾囊相授。

考虑到我的粉丝很多都是学生党,也考虑到我当初学习时候面临的价格昂贵问题,我决定只用市面上十分之一的价格就让大家能够学到VMP这项高深的逆向分析技术。只需要几百块,就能少走很多弯路,毕竟时间才是最宝贵的。

在这套课程里,我将带大家从最基础的虚拟机核心流程学习到字节码Handler分析,还包含如何用程序自动化的完成VMP的分析,尤其是高版本的VMP混淆和垃圾指令填充非常恐怖,人肉分析根本不现实,必须借助程序把很多工作自动化。

目前课程已经更新到了第四课,收到了很多小伙伴的好评:

如果大家对于虚拟化分析和VMP感兴趣,想深入学习这方面的内容,欢迎扫码添加我的微信,加入我们一起学习。