VMProtect1.09分析

发布者:freakish
发布于:2017-09-25 21:42

前言

  • 有段时间没有写文章了,感觉快要断了线了,最近略有时间,闲暇又手痒抄起OllyDBG撸几发。这次把玩的对象是VMProtect的一个低级版本VMProtect1.09,只所以选择这个版本,还是因为想再回顾一遍VMProtect的核心脉络,减少高版本里夹带的各种垃圾指令干扰,也正好做一个记录,以前的分析都是随手分析完就算了,以后争取看过的东西都有个记录,方便自己和别人参考。

环境准备

  • VMProtect1.09
    • 之前从看雪下载的一个软件包里拉取的版本
  • 测试Demo
    • Visual Studio2013环境下,写下如下的测试代码:
    • 测试代码
    • 这里我没有SDK,于是编译完成直接拖入IDA,找到testVMP函数,记下地址,我这里的地址是 0x00432C40,打开VMProtect1.09,新建流程,切到保护选项这一栏,我们主要的目的是分析虚拟机,这里就不勾选其他选项,我设置的结果如下:
    • OK,直接点击绿色的编译按钮即可,至此我们的试验环境准备完毕了!

开始分析(主体流程)

  • VMProtect系列保护的分析,还是需要动静结合,就这个低版本来说,我们测试被保护的代码很少,基本可以单独跟踪一遍,那就废话不说,我们把test.vmp.exe载入OllyDBG,然后找到testVMP的位置(可以根据前面记录下来的地址,也可以根据VC程序的特征来寻找testVMP函数),于是进入了VMProtect的核心流程,我贴出虚拟机的核心代码块:

  • 这里是这个版本的虚拟机本身的代码了,总共30行左右的代码,VMProtect也是费心了,为了那一句 mov eax,1生生的加了这么多的代码,这段代码本身比较简单, 可以在OD里面单步跟踪下来,总体上就是:

    1. 压入两个常量(具体代码就是1、6两行), 使用这两个值计算出被虚拟的伪码的起始位置(esi)
    2. 从某一个内存地址开始(第8行)作为虚拟机运行过程中的私有堆栈和临时变量存放区域
    3. 引擎进入一个循环,这个循环从上面计算出来的esi的位置开始,不断的从esi中取出数据,执行虚拟逻辑
    4. 执行完所有的esi指向的伪码,退出虚拟机(退出到虚拟机之外,具体来说就是第一张图的第18行代码,return 0;)
  • 上面提到的结果,只要是有些汇编功底的人,都可以比较容易的跟踪出上面的结果,那么为什么这样的一段30行的虚拟机代码就可以达到虚拟的目的了呢,要彻底明白这样的问题,还需要看下被VMProtect加壳虚拟之后,程序变成了什么样了。于是我们继续前面载入OllyDBG之后的流程,Alt+M 打开程序的内存视图,我们发现程序在原本的基础上增加了2个段:

    1. 地址=011EF000 大小=00005000 (20480.) 属主=test_vmp 01140000 区段=.vmp0 初始访问=RWE
    2. 地址=011F4000 大小=00005000 (20480.) 属主=test_vmp 01140000 区段=.vmp1 初始访问=RWE

      • 先来看第2个段,0x11F4000 正是引擎代码的第8行,也就是说这个段的内存主要是用来给VMProtect运行时内存使用,如VM_Context
      • 第一个段,0x11EF000的地址,根据前面的代码跟踪,我们发现,28行代码 lea edx,dword ptr ds:[eax*4+11EF10F]中,这个0x11EF10F就是VMProtect Handler数组啊, 那我们直接Ctrl+G跳转到0x11EF000这个段的最开头,查看汇编代码,看汇编形式,很容易认出来这些就是各个Handler的实现,这些一个个的小函数的代码一直持续到 0x11EF10F的位置未知,接下来的内存,就是一个函数指针数组,也称为跳转表,总共256项,为什么是256项呢,从26 27 28行可以知道,28行中的函数索引eax是 al 这个单字节扩展来的,最大是FF=255,加上0这一项,那总共最大就是256了,再接着往下,就到了我们看到的虚拟机引擎代码的位置了,接着的内存就是大量的伪码存放的位置了,为了直观,还是上一张图:
  • 搞清楚了内存结构,再来理解一下虚拟机代码就简单一些了,通过跟踪流程,我们可以简单的归纳成一个上图色块部分之间的交互如下(大家俗称这个过程为dispatcher):

  • 这就是最著名的VMProtect解码循环了

分析小结

  • 所以,经过上面的一系列分析,我们可以做这样的总结,经过VMProtect的加壳,我们的程序变化了:
    1. 被保护部分的代码原来的位置变成了虚拟机引擎代码
    2. 加壳程序按照壳的设计,把被保护程序的程序结构进行了重新排布,这样加载进内存之后,按照虚拟机引擎的代码执行下来,就可以达到用虚拟码模拟被保护代码功能的目的(所以,这样看来,你的程序不是被隐藏了,而是被删掉了,然后加壳程序又按照你的逻辑给你用另一套复杂的指令模拟相同的功能来执行)

需要下回再续

  • 基本搞清楚了VMProtect的流程,那整个跟踪下来,有没有发现一头雾水,感觉自己完全不懂汇编了(这就是虚拟化的目的),其实这里才是VMProtect真正难搞的部分,那就是怎么理解虚拟码的内容,怎么爆破VMProtect保护的关键逻辑,给你一段被保护的代码,你能知道它原来的逻辑吗,有空再继续吧,毕竟夜深了…

二次分析(寄存器+堆栈使用)

  • VMProtect是基于堆栈的虚拟机,这是众所周知的了,那我们来详细的跟踪一遍,虚拟机是怎么样利用所谓的堆栈来完成虚拟指令模拟被保护逻辑的。
  • 在进入虚拟机之前,我们先记下当前的内存状态和寄存器状态(vESP = EBP, VM_Context=EDI),我以实际跟踪的结果为准,记录如下:
  • 虚拟机准备完毕,开始执行第一个Handler的时候(第一次执行到虚拟机的28行),我们再来观察内存:
  • 从这里观察可以知道,在VMProtect开始执行任何代码之前,先把进入VMProtect虚拟机之前的寄存器环境先保存起来(后面知道,在退出虚拟机之后,要恢复这些寄存器,以保证退出虚拟机之后的代码正常运行),然后我们继续开始单步,这个时候发现虚拟机开始进入ESI的解码循环了,一直跟踪,发现真实堆栈的内容在逐渐的移位,直到到达这个状态:
  • 这个状态,观察下来,我们把VM_Context这个内存看做VMProtect的堆栈,那么我们观察现在这个内存状态,可以发现,现在这个状态是经过虚拟机的操作之后,把真实的寄存器的值保存到了VM_Context中去,以备虚拟机退出的时候恢复现场使用
  • 接着我们F8经过2个Handler, 再来观察内存状态:
  • 现在观察,发现经过2个Handler,VM_Context+3*4的位置变成了1, 这个1有点熟悉,难道是我们mov eax, 1的这个常量吗??带着这个疑问,我们继续往下F8单步,直到真实寄存器到达这个状态,而虚拟机也执行到这个位置:
  • 这里就是退出虚拟机了,那我们再来观察各个位置的内存状态:
  • 代码停留在 __asm popad __asm popfd
  • 再观察真实堆栈中的值的分布,我们发现EAX的值已经是1了,这样,在退出虚拟机之前
    1. EAX= 1
    2. 除了EAX的值之外,其他的值都保持不变

二次分析小结

  • 进入虚拟机之前,虚拟机引擎会先保护现场,一边退出虚拟机之后恢复执行现场
  • 虚拟机引擎使用 VM_Context结构作为自己保存中间变量的地方或者其他信息的地方
  • 除了这个进入前现场保护的几个Handler, 退出前恢复现场的几个Handler, 中间的2个Handler就是模拟__asm mov eax, 1 这句话的具体实现了

三次分析(虚拟码加密)

  • 前面的分析中,VMProtect的加密选项只开启了指令虚拟化, 这次我们为了分析虚拟码加密之后的流程, 再次修改VMProtect的配置,打开虚拟码加密选项,配置如下:
  • 虚拟化保护的代码还是那块代码,于是拖入OllyDBG中,快速定位到虚拟机的主流程,通过对比我们发现,虚拟机主流程发生了一些变化,多了一些操作,请看图中标注的部分:
  • 通过和之前主流程的对比很容易发现, 每次从ESI指向的内存取出的结果,要经过中间红框部分的一系列的操作变换之后才能直接使用。其中一个值得注意的细节是,整个解密的过程中,引入了一个寄存器 bl, 每次解密的过程中都使用了这个寄存器中的值,并且一次解密之后还会更新这个寄存器的值,这就可以理解为 虚拟码的解密秘钥是一个动态的变化过程
  • 上面的过程是主流程解密Handler索引的时候的过程,那我们跟踪进去一个Handler中,会发现,Handler中使用到ESI中指向内存的数据的时候也是需要解密的过程的,类似的解密算法,动态变化的秘钥,顺便给出一个图做参考:

三次分析小结

  • 增加虚拟码加密之后:
    1. 静态虚拟码被加密
    2. 每次从ESI指向的虚拟码内存中获取一个字节之后,都要进行解密之后才使用
    3. 解密过程中使用 BL 寄存器中动态秘钥参与解密,并在解密之后更新秘钥

四次分析(API调用)

  • 这次我们来看下VMProtect 1.09这个版本,对于Windows API调用是如何处理的, 为了研究方便,我们需要修改我们的测试代码了,稍作改动,结果如下:
  • 接着我们对 testVMP 这个函数进行虚拟化(保持第一次分析的时候那个VMProtect加密选项配置),明确一下我们的目的是为了分析API调用的处理方式,所以testVMP函数中已经有6条x86指令了,为了快速定位关键点,所以我选择在VMProtect的主流程中跳转到Handler的时候,使用条件断点记录下Handler的值,然后等到程序弹出MessageBox的时候, 我通过查看OllyDBG的 log 可以知道经过了多少个Handler调用,就调用到了MessageBox函数,继续展示下我的操作:
  • 设置好了,直接F9, MessageBox弹出来了, 不要点确定,打开OllyDBG 的log窗口,看到了很多记录下来的Handler地址,复制到一个带行号的文本编辑器中,我得到的结果是:
  • 也就是经过了33个Handler的时候,程序跑到了MessageBox的调用,所以,如果我们再重新跑一次程序,然后继续使用条件断点,让程序在第30或者20次执行Handler的时候断下来,然后手动调试接下来的几个Handler就可以搞清楚API调用是怎么实现的了。
  • 接下来就是一些体力活了,一路F8跟着VMProtect的大循环转几圈就看到了如下结果:
    1. 在执行你的API调用之前,跟普通PE一样,输入表是Ready的,也就是MessageBox地址是准备好的
    2. 从字节码中拿出MessageBox在输入表中位置,然后经过一次重定位(VMProtect自己的重定位方式),获取到MessageBox的真实地址
    3. 真实堆栈压入一个进入VMP的函数地址
    4. 真实堆栈压入MessageBox地址
    5. popad popfd还原前面进入虚拟机的现场
    6. 退出虚拟机,ret方式 跳入MessageBox
    7. MessageBox执行完毕之后,MessageBox内部的Ret 直接就返回到了之前准备好的进入虚拟机函数的入口地址
    8. 进入虚拟机,继续执行虚拟代码

四次分析小结

  • 完成API调用,需要退出虚拟机代码,然后跳入计算出来的API地址, API执行完毕之后,ret方式重新进入虚拟机,继续虚拟代码执行

问题还在

  • 怎么进行逻辑还原呢? 还需要继续研究….(后续更新)

声明:该文观点仅代表作者本人,转载请注明来自看雪