VMP加壳(二):VMP的虚拟化原理

  介绍VMP虚拟化原理之前,先简单介绍一下计算机运行的原理。总所周知,现代计算机的核心部件是CPU、内存、磁盘、键盘、显示器等;最最最核心的就属CPU、内存和磁盘了。用户按开机键,CPU会把OS从磁盘加载到内存运行。由于CPU只能识别并执行二进制文件,所以代码、数据等都是以二进制存放在磁盘和内存的。

       1、为了在软件层面“虚拟化”出底层的硬件,让OS顺利在虚拟机运行,虚拟机也要提供CPU、内存和磁盘的“虚拟”环境,让二进制代码得以顺利执行。目前主流的虚拟化技术是intel/amd推出的VT技术,让VMware、virtualBox这种虚拟机可以“直接”让硬件CPU执行二进制代码,极大提升了虚拟机的效率和速度,减少了模拟的性能损耗。但这种虚拟机的功能相对较重,并且直接让硬件CPU执行虚拟机的二进制代码,达不到保护、混淆代码的目的,所以这种硬件虚拟化是不适合做代码保护的,只能考虑通过纯软件模拟虚拟机执行代码指令。

  为了在软件层面模拟CPU执行二进制的代码指令,需要有以下关键点:

  •   虚拟机的寄存器,用来存放各种临时数据
  •   虚拟机的堆栈,用来做各种数据交换
  •   虚拟机的指令。x86架构下,CPU一旦读取到0x55指令,就知道执行push ebp;一旦读取到0x8BEC,就知道执行mov ebp,esp; 同理,虚拟机也需要有自己的指令集,虚拟CPU才知道自己要干啥。一般虚拟机的指令要么是操作寄存器,要么是操作堆栈,要么做各种算数运算,虚拟机指令的handler都要模拟这些功能。那么问题来了,虚拟机的指令集能不能和物理CPU一样了? 显然是不行的! 两个原因:(1)如果一样,还要纯软件虚拟机干啥?  (2)如果一样,达不到混淆指令的目的
  •   虚拟机的EIP,用来指明虚拟CPU当前执行的代码

      为了满足以上关键点,VMP采取的方案:

  •   虚拟机的寄存器:在内存开辟一段连续的区域当成虚拟机的寄存器,业界称之为VM_CONTEXT,某些版本的VMP用EDI指向这个区域
  •   虚拟机的堆栈: 这个和物理机是一样的,直接在内存开辟就好。VMP还是用EBP指向栈顶
  •   虚拟机的指令:不同版本VMP的指令是不一样的,这样可以在一定程度上防止VMP本身被破解,业界俗称VM_DATA
  •   虚拟机的EIP:业界俗称vEIP,某些版本的VMP用ESI替代,指向VM_DATA,用以读取虚拟CPU需要执行的指令;

      2、VMP虚拟机的执行流程

  (1)想想启动VT时,是不是要先开辟一段内存空间,把当前guestOS部分寄存器的值保存好?VMP也一样,先保存物理寄存器的值,后续退出VM后才能还原

       (2)让vEIP从VM_DATA读取虚拟机的指令

       (3)由于虚拟机的指令和物理CPU完全不同,那么在指令读取后,该怎么去执行了?举个栗子:比如0x1表示入栈,0x2表示出栈,0x3表示寄存器之间互相传数据(当然实际的指令可能不会这么简单,VMP每个版本的指令集都不同),这些指令该怎么执行了?在VMP中,有个概念叫handler,专门根据不同的指令执行不同的操作(当然这些操作VMP事先都定义好了)。这个和VT中VMX的handler作用类似:根据不同的异常有不同的处理方法(我个人猜测VMP的作者肯定借鉴了VT的原理和思路);

          为了达到这种不同指令执行不同handler分支的效果,编码实现层面通常用switch+case实现,用于将不同的指令跳转到不同的分支执行,业界俗称dispatcher。具体到汇编代码,switch+case一般的汇编形式为:mov ecx,dword ptr ds:[eax*4+base] (注意寄存器可能会变成其他的,但这 xxx*4+基址的形式不会变), 这是比较明显的特征,用以用来定位VMP的dispatcher。

  (4)执行完一个handler,vEIP接着指向下VM_DATA的下一个指令,然后重复(2)-(4)这几个步骤;

       整个过程展示如下:

        

       (5)综上所述,要想全面了解、分析和掌控VMP,必须要找准这么几个点:

  •  VM_DATA:虚拟机的指令都集中在这了
  •    VM_CONTEXT:虚拟寄存器都保存在这里
  •    diapatcher:所有指令都从这里路由到对应的handler执行(可以简单理解为管理层派发活的,不过3.x版本的VMP貌似去掉了统一的dispatcher,由上个handler直接跳转到下个handler,有点P2P、区块链去中心化的感觉)
  •    handler:具体模拟执行虚拟指令的分支(可以简单理解为具体干活的工具人)。handler之间跳转通过jmp esi 或 push esi ,ret等指令实现(不同版本使用的寄存器可能不同,但跳转实现的方式就这些)
  •   vEIP:当前执行的指令,需要明确是由那个物理寄存器保存的
  •   vStack:存放了临时数据用于各种交换,目前版本还是由

  3、上面讲了大段各种理论,下面说说具体怎么找这些关键点(注意: 下面OD分析的截图不是一次调试截取的,事实上我测试时反复调试了十几次,每次用OD打开样本的地址都不同,但不影响代码执行的顺序和关键点的分析)。

      (1)demo代码如下:这里模拟一个密码、lisence之类的场景:正确的密码是123,输入后输出ok;输入错误的密码就输出fail;

#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>

char buf[1204];

void main()
{
    while (1)
    {
        scanf("%s", buf);
        if (!strcmp(buf, "123"))
            printf("ok\n");
        else
            printf("fail\n");
    }

}

  (2)这里只把main函数虚拟保护即可:

    

   (3)虚拟化后的结果:从40K增加到602K,增加了15倍;

   

    用OD打开:VMP0段564K,膨胀的部分都集中在这里了;

       

   (4)刚进入OD,看到的全是jmp。根据之前的理论分析,这些jmp构成了handler表:

  

   在刚才那个内存视图,选中vmp0段,右键选择内存访问断点,后续只要访问这个段就会立即断下来。然后开始运行,断到下面:push xxxx,call xxxx这是非常明显的VMP3.xx版本的壳特征

       

      不停F7单步进入后来到这里:看到了好多push 寄存器的指令,这里就是保存原始物理寄存器的值的,为后续进入VM加壳做准备;可以看到在push指令之间加载着大量没用的垃圾指令;

      

      这些push指令执行完后,栈如右图所示;注意这里一行关键代码已经标红:  esp+0x28处的值赋给了edi,edi指向了VM_DATA,也就是虚拟指令集(后面会进一步解密edi)

   

     经过下面add edi,edx后,得到真正的VM_DATA地址:

     

     继续往下:又有一条关键指令:sub esp,0xC0; 这里把esp往上开辟192byte的空间,作为VM_CONTEXT(也就是虚拟存器)。后续会用esp+edx来读取这些虚拟寄存器;此时堆栈图如右边:下面保存的是物理寄存器,上面是VM_CONTEXT,中间以ebp隔开

  

    继续F7(后续能看到大量这类似的代码):通过ebp从栈取原物理寄存器的值,然后通过edi取指令,接着根据指令对取出的数做各种运算:

     

    计算完毕后写回VM_CONTEXT:

  

  接着继续取指令和栈的值:

    

    计算完毕后继续写回VM_CONTEXT:

    

   期间还检查虚拟栈是否填满:

   

    栈顶和栈底比较,看看谁大:

    如果栈够用就通过jmp继续执行下个handler:

    

     如此往复好多次,都快把VM_CONTEXT填满为止,终于到了while循环。这时不知道密码是多少,先随便输入,看到程序输出了fail,突破点就在这了:  通过ASCII在内存中找fail,然后下个访问的断点,成功断下来:

    

    这里回溯栈:在栈中保存了函数的调用关系,这里肯定有while循环、判断对比的函数:这里把password在栈上所有的函数都轮询个遍(kernerl32、ucrtbase这些windows系统自带的API就没必要了),挨个下断点,一共下了十几个:

    

    这里用辨识度比较高的字符串"aaaaaaaaaaaaa"输入,然后挨个断点地检查,终于在一个断点处找到了密码字符:这个大概率是strcmp函数

    

 

总结:1、大段代码被人为切割成一小块一小块地执行,代码之间通过jmp、call、push+ret方式跳转

           2、统一的dispatcher是真的没有了,handler倒是有一个大表(整个程序的入口点也改到这里了),里面全是jmp语句,对应不同的handler分支

           3、esp指向VM_CONTEXT,edx是虚拟寄存器的偏移,通过esp+edx定位虚拟寄存器的位置;读取的虚拟寄存器保存在ecx,各种处理后通过ecx写回虚拟寄存器;

                 ebp指向虚拟栈顶,通过ebp+偏移挨个读写虚拟栈的数据;

           4、取指令、解密、执行、跳转到下一个handler:很多代码都是一样的,重复生成了好多次

 

参考:1、https://bbs.pediy.com/thread-225262.htm  新手篇VMProtect 1.81 Demo

           2、https://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458296943&idx=1&sn=8ba937d6216a37025d5f97d8a4989f4a    VMProtect 3.3.1虚拟机&代码混淆机制入门

           3、https://bbs.pediy.com/thread-225803.htm   如何分析虚拟机(2):进阶篇 VMProtect 2.13.8

           4、https://bbs.pediy.com/thread-224732.htm 谈谈VMP的爆破

posted @ 2021-01-09 17:33  第七子007  阅读(16432)  评论(0编辑  收藏  举报