获取途径

http://www.drmsoft.cn/reseller/vmp.asp

技术剖析

虚拟机保护技术就是将基于x86汇编系统的可执行代码转换为字节码指令系统的代码,以达到保护原有指令不被轻易逆向和修改的目的,这种指令也可以叫伪指令,和VB的pcode有点类似。从本质上讲,虚拟指令系统就是对原本的x86汇编指令系统进行一次封装,将原本的汇编指令转换为另一种表现形式。

push uType
push lpCaption
push lpText
push hWnd,
call MessageBox这是一段x86的汇编指令,编译器在翻译的时候会产生一个固定模式的汇编代码(在同一个CPU指令集下)。
但如果我们对原本的C代码使用VMP SDK进行虚拟化,那么在编译这段代码的时候就会使用等效的VM虚拟指令来达到同样的功能。
vPushMem uType
vPushMem lpCaption
vPushMem lpText
vPushMem hWnd,
vCall vCode注意,虚拟指令的也有自己的机器码,但和原本的x86汇编机器码完全不一样,而且常常是一堆无意义的代码,它们只能由VM虚拟解释器(Dispatcher)来解释并执行(关于虚拟解释器接下来会详细解释),
所以,我们在用OD等工具进行反汇编分析的时候,看到的就是一大堆的无意义的代码,甚至还有大量的junk code,jmp code等,导致我们无法从反编译层面分析出原本的代码流向,自然也就无法轻易的进行
算法逆向分析了。我们在逆向虚拟机加壳后的程序中看到的汇编代码其实不是x86汇编代码,而是字节码(伪指令)。它是由指令执行系统定义的一套指令和数据组成的一串数据流。
java的JVM、.NET或其他动态语言的虚拟机都是靠解释字节码来执行的,但它们的字节码(中间语言IL)之间不通用,因为每个系统设计的字节码都是为自己使用的,并不兼容其他的系统。
所以虚拟机的脱壳很难写出一个通用的脱壳机,原则上只要虚拟指令集一变动,那原本的伪指令的解释就会发生变化。
我个人理解要逆向被VM SDK保护起来的原始代码,只有手工分析这段虚拟指令,找到虚拟指令和原始汇编的对应关系,然后重写出原始程序的代码,完成算法的逆向和分析。这张图是一个虚拟机执行时的整图概述,VStartVM部分初始化虚拟机,VMDispatcher负责调度这些Handler,Handler可以理解为一个个的子函数(功能代码),它是每一个伪指令
对应的执行功能代码,为什么要出现一条伪指令对应着一个Handler执行模块呢?这和虚拟机加壳的指令膨胀有关,被虚拟机加壳后,同样一条指令被翻译成了虚拟伪指令,
一条虚拟伪指令往往对应着好几倍的等效代码,当中可能还加入了花指令,整个Handler加起来可能就等效为原本的一条x86汇编指令。
Bytecode就是虚拟伪指令,在程序中,VMDispatcher往往是一个类while结构,不断的循环读取伪指令,然后执行。
Virtual Machine Loop Start
...
...
...
--> Decode VM Instruction's
...
--> Execute the Decoded VM Instruction's
...
...
--> Check Special Conditions
...
...
...
Virtual Machine Loop End在理解Handler(VM虚拟指令和x86汇编的映射)之前,有必要先理解一下VM的启动框架和调用约定,每种代码执行模式都需要有启动框架和调用约定。
在C语言中,在进入main函数之前,会有一些C库添加的启动函数,它们负责对栈区,变量进行初始化,在main函数执行完毕之后再收回控制权,这就叫做启动框架。而
C CALL,StdCall等约定就是调用约定,它们规定了传参方式和堆栈平衡方式。同样,对与VM虚拟机,我们也需要有一种启动框架和调用约定,来保证虚拟指令的正确执行以及虚拟和现实代码之间的切换。1. 调度器VStartVM
VStartVM过程将真实环境压入后有一个VMDispatcher标签,当handler执行完毕之后会跳回到这里形成了一个循环,所以VStartVM过程也可以叫做Dispatcher(调度器)
VStartVM首先将所有的寄存器的符号压入堆栈,然后esi指向字节码的起始地址,ebp指向真实的堆栈,edi指向VMContext,esp再减去40h(这个值可以变化)就是VM用的堆栈地址了。
换句话说,这里将VM的环境结构和堆栈都放在了当前堆栈之下200h处的位置上了。
因为堆栈是变化的,在执行完跟堆栈有关的指令时总应该检查一下真实的堆栈是否已经接近自己存放的数据了,如果是,那么再将自己的结构往更地下移动。
然后从 movzx eax, byte ptr [esi]这句开始,读字节码,然后在jump表中寻找相应的handler,并跳转过去继续执行。VStartVMpush eaxpush ebxpush ecxpush edxpush esipush edipush ebppushfdmov esi, [esp+0x20] ;字节码开始的地址(寄存器占了32byte,从32byte开始就是刚才push的字节码的位置)mov ebp, esp ;ebp就是堆栈了sub esp, 0x200mov edi, esp ;edi就是VMContextsub esp, 0x40 ;这时的esp就是VM用的堆栈了
VMDispatchermovzx eax, byte ptr [esi] ;获得bytecodelea esi, [esi+1] ;跳过这个字节jmp dword ptr [eax*4 + JUMPADDR] ;跳到handler执行处调用方法
push 指向字节码的起始地址
jmp VStartVM这里有几个约定
edi = VMContext
esi = 当前字节码地址
ebp = 真实堆栈在整个虚拟机代码执行过程中,必须要遵守一个事实。
1. 不能将edi,esi,ebp寄存器另做他用
2. edi指向的VMContext存放在栈中而没有存放在其他固定地址或者申请的堆空间中,是因为考虑到多线程程序的兼容2. 虚拟环境 VMContext
VMContext即虚拟环境结构,存放了一些需要用到的值
struct VMContext
{DWORD v_eax;DWORD v_ebx;DWORD v_ecx;DWORD v_edx;DWORD v_esi;DWORD v_edi;DWORD v_ebp;DWORD v_efl; 符号寄存器
}
平衡堆栈 VBegin和VCheckEsp
VMStartVM将所有的寄存器都压入了堆栈,所以,首先应该使堆栈平衡才能开始执行真正的代码(这是书上写的)。我的理解是这里要先将现实的代码中的寄存器复制切换到虚拟代码中Vebing:mov eax, dword ptr [ebp]  ;ebp指向真实的堆栈mov [edi+0x1C], eax       ;edi指向VMContextadd esp, 4mov eax, dword ptr [ebp]  ;ebp指向真实的堆栈mov [edi+0x18], eax       ;v_ebpadd esp, 4mov eax, dword ptr [ebp]  ;ebp指向真实的堆栈mov [edi+0x14], eax       ;v_ediadd esp, 4mov eax, dword ptr [ebp]  ;ebp指向真实的堆栈mov [edi+0x10], eax       ;v_esiadd esp, 4mov eax, dword ptr [ebp]  ;ebp指向真实的堆栈mov [edi+0x0C], eax       ;v_edxadd esp, 4mov eax, dword ptr [ebp]  ;ebp指向真实的堆栈mov [edi+0x08], eax       ;v_ecxadd esp, mov eax, dword ptr [ebp]  ;ebp指向真实的堆栈mov [edi+0x04], eax       ;v_ebxadd esp, 4mov eax, dword ptr [ebp]  ;ebp指向真实的堆栈mov [edi], eax       ;v_eaxadd esp, 4add esp, 4        ;释放参数jmp VMDispatcher执行这个"Handler"之后,堆栈就平衡了,就可以开始"继续"执行真正的伪代码了。还有一个问题,因为将VMContext结构存放在当前使用的堆栈(EBP)更低地址的区域(初始时VMContext距离栈顶EBP有0x200的空间),当堆栈被push压入数据时,
总会在某条指令之后改写VMContext的内容,为了避免这种情况,就需要对VMContext进行动态移位。VCheckEsp:lea eax, dword ptr [edi+0x100]  ;edi指向VMContextcmp eax, ebp     ;小于则继续正常运行jl VMDispatchermov edx, edi    ;否则,则进行移位mov ecx, espsub ecx, edx    ;计算一下需要搬移的字节空间,作为循环复制的次数push esi    ;保存ip指针mov esi, espsub esp, 0x60mov edi, esppush edi    ;保存新的edi VMContext基址sub esp, 0x40cldrep movsb    ;复制pop edipop esijmp VMDispatcher一些设计到堆栈的Handler在执行后跳到VCheckEsp判断esp是否接近VMContext所在的位置,如果是就将VMContext结构复制到更低的地方去2. VM虚拟指令和x86汇编的映射原理
这是VM虚拟机的核心部分,即把每条x86指令或每一类x86汇编指令转换为等效的伪指令Handler,可以说,不同的虚拟机都有自己的一套为指令集,不同的为指令集对原始的
x86汇编指令的翻译都是不同的handler分两大类:
1. 辅助handler,指一些更重要的、更基本的指令,如堆栈指令
2. 普通handler,指用来执行普通的x86指令的指令,如运算指令1. 辅助handler
辅助handler除了VBegin这些维护虚拟机不会导致崩溃的handler之外,就是专门用来处理堆栈的handler了。vPushReg32:mov eax, dword ptr [esi] ;从字节码中得到VMContext中的寄存器偏移add esi, 4mov eax, dword ptr [edi+eax] ;得到寄存器的值push eax ;压入寄存器jmp VMDispatchervPushImm32:mov eax, dword ptr [esi]add esi, 4push eaxjmp VMDispatchervPushMem32:mov eax, 0mov ecx, 0mov eax. dword ptr [esp] ;第一个寄存器偏移test eax, eaxcmovge edx, dword ptr [edi+eax] ;如果不是负数则赋值mov eax, dword ptr [esp+4] ;第二个寄存器偏移test eax, eaxcmovge ecx, dword ptr [edi+eax] ;如果不是负数则赋值imul ecx, dword ptr [esp+8] ;第二个寄存器的乘积add ecx, dword ptr [esp+0x0C] ;第三个为内存地址常量add edx, ecxadd esp, 0x10 ;释放参数push edx ;插入参数jmp VMDispatchervPopReg32:mov eax, dword ptr [esi] ;得到reg偏移add esi, 4pop dword ptr [edi+eax] ;弹回寄存器jmp VMDispatchervFree:add esp, 4jmp VMDispatcher2. 普通Handleradd指令的形式通常有
add reg,imm
add reg,reg
add reg,mem
add mem,reg
等写法....
如果将操作数都先交给堆栈handler处理,那么执行到vadd handler时,已经是一个立即数存在堆栈中了,vadd handler不必去管它从哪里来,只需要用这个立即数做加法操作即可。
也就是说,将辅助指令和普通指令配合起来来一起完成x86指令到伪指令的转换vadd:mov eax, [esp+4] ;取源操作数mov ebx, [esp] ;取目的操作数add ebx, eaxadd esp, 8 ;平衡堆栈push ebx ;压入堆栈下面的指令转换为伪代码:
add esi, eax
转换为
vPushReg32 eax_index    ;eax在VMContext下的偏移
vPushReg32 esi_index
vadd
vPopReg32 esi_indexadd esi, 1234
转换为
vPushImm32 1234
vPushReg32 esi_index
vadd
vPopReg32 esi_indexadd esi, dword ptr [401000]
转换为
vPushImm32 401000
vPushImm32 -1 ;scale
vPushImm32 -1 ;reg_index
vPushImm32 -1 ;reg2_index
vPushMem32 ;压入内存地址的值
vPushReg32 esi_index
vadd
vPopReg32 esi_index这就是add指令的多种实现,我们可以发现无论是哪一种形式,都可以使用vadd来执行,只是使用了不同的堆栈handler,这就是Stack_Based虚拟机的方便之处。标志位的问题:
在相关的handler执行前恢复标志位,执行后保存标志位。以stc命令来说,stc指令是将标志的CF位置为1
VStc:push [edi+0x1C]popfdstcpushfdpop [edi+0x1C]jmp VMDispatcher这样操作之后就能保证代码中的标志不会被虚拟机引擎所执行的代码所改变转移指令:
转移指令有条件转移、无条件转移、call和retn
实现时可以将esi指向当前字节码的地址,esi指针就好比真实CPU中的eip寄存器,可以通过改写esi寄存器的值来更改流程。无条件跳转jmp的handler比较简单vJmp:mov esi, dword ptr [esp] ;[esp]指向要跳转到的地址add esp, 4jmp VMDispatcher条件转移jcc指令稍微有一点麻烦,因为它要通过测试标志位来判断视为需要更改流程基本上所有条件跳转指令都有相应的CMOV指令来匹配。
vJne:cmovne esi, [esp]add esp, 4jmp VMDispatchervJa:cmova esi, [esp]add esp, 4jmp VMDispatchervJae:cmovae esi, [esp]add esp, 4jmp VMDispatchervJb:cmovb esi, [esp]add esp, 4jmp VMDispatchervJbe:cmovbe esi, [esp]add esp, 4jmp VMDispatcherje:cmove esi, [esp]add esp, 4jmp VMDispatcherjg:cmovg esi, [esp]add esp, 4jmp VMDispatchercall指令:
call和retn指令虽然也是转移指令,但是它们的功能不太一样。
首先,虚拟机设计为只在一个堆栈层次上运行mov eax, 1234push 1234call anotherfunc
theNext:add esp, 4其中第1、2、4条指令都是在当前堆栈层次上执行的,而call anotherfunc是调用子函数,会将控制权移交给另外的代码,这些代码是不受虚拟机控制的。所以碰到call指令,必须退出虚拟机,让子函数在真实CPU中执行完毕后再交回给虚拟机执行下一条指令。
vcall:push theNextjmp anotherfunc如果想在推出虚拟机后让anotherfunc这个函数返回后再次拿回控制权,可以更改返回地址,来达到继续接管代码的操作。在一个地址上写上这样的代码:
theNextVM:push theNextByteCodejmp VStartVM这是一个重新进入虚拟机的代码,theNextByteCode代表了theNext之后的代码字节码。只需将theNext的地址改为theNextVM的地址,即可完美地模拟call指令了。当虚拟机外部的代码执行完毕后ret回来的时候就会执行theNextVM的代码,从而使虚拟机继续接管控制权。vcall:push all vreg ;所有虚拟寄存器poo all reg ;弹出到真实寄存器中push 返回地址 ;push 要调用的函数的地址retnret指令:
retn指令和其他普通指令不太一样,retn在这里被虚拟机认为是一个推出函数。retn有两种写法:一种是不带操作数的,另一种是带操作数的。
retn
retn 4
第一种retn形式先得到当前esp中存放的返回地址,然后再释放返回地址的堆栈并跳转到返回地址
第二种在释放返回地址的堆栈时再释放操作数的空间vRetn:xor eax, eaxmov ax, word ptr [esi] ;retn的操作数是word型的,所以最大只有0xFFFFadd esi, 2mov ebx, dword ptr [ebp] ;得到要返回的地址add esp, 4 ;释放空间add esp, eax ;如果有操作数,同样释放push ebx ;压入返回地址push ebp ;压入堆栈指针push [edi+0x1C]push [edi+0x18]push [edi+0x14]push [edi+0x10]push [edi+0x0C]push [edi+0x08]push [edi+0x04] push [edi]pop eaxpop ebxpop ecxpop edxpop esipop edipop ebppopfdpop esp ;还原堆栈指针到esp中,而VM_CONTEXT也算是自动销毁了retn不可识别指令:
在这里任何不能识别的指令都可将其划分为不可模拟指令,碰到这类指令时,只能与vcall使用相同的方法,即先退出虚拟机,执行这个指令,
然后再压入下一字节码(虚拟指令)的地址,重新进入虚拟机。
 用VM虚拟指令构造一段小程序
上面阐述了一些关于虚拟机运行和虚拟指令的一些原理和理解,接下来从实践的调度来实现一个最简单的基于虚拟指令的小程序。
即我们的执行代码是我们自定义的一些伪指令,调度器在执行的时候不断的从伪指令字节码中取指令,然后到Handler地址表中寻址对应的执行体Handler,进行执行,执行完毕后
再跳回到调度器回收控制权,依次循环,知道执行完所有的虚拟指令。------------------------------
code:#include "stdafx.h"
#include "windows.h"/* 下面是虚拟指令 我们只模拟了2条指令 *///push 0x12345678  push一个4字节的数
#define vPushData    0x10//call 0x12345678  call一个4字节的地址
#define vCall        0x12//结束符
#define vEnd        0xff//一个字符串
char *str = "Hello World";/*这是我们构造的虚拟指令, 数据还不  在mian里面我们进行了修改push 0push offset strpush offset str  ;把字符串的地址入栈push 0call MessageBoxA ;
*/
BYTE bVmData[] = {    vPushData,    0x00, 0x00, 0x00,0x00, vPushData,    0x00, 0x00, 0x00,0x00,vPushData,    0x00, 0x00, 0x00, 0x00,vPushData,    0x00, 0x00, 0x00,0x00,vCall,    0x00, 0x00, 0x00, 0x00,vEnd};//这就是简单的虚拟引擎了
_declspec(naked) void  VM(PVOID pvmData)
{__asm{//取vCode地址放入ecxmov ecx, dword ptr ss:[esp+4]
__vstart://取第一个字节到al中mov al, byte ptr ds:[ecx]cmp al, vPushDataje    __vPushDatacmp al, vCallje    __vCallcmp al, vEndje __vEndint 3
__vPushData:inc ecxmov edx, dword ptr ds:[ecx]push edxadd ecx, 4jmp __vstart
__vCall:inc ecxmov edx, dword ptr ds:[ecx]call edxadd ecx, 4jmp __vstart
__vEnd:ret}
}int main(int argc, char* argv[])
{//修改虚拟指令的数据*(DWORD *)(bVmData+5 + 1) = (DWORD)str;*(DWORD *)(bVmData+10 + 1) = (DWORD)str;*(DWORD *)(bVmData+20 + 1) = (DWORD)MessageBoxA;//执行虚拟指令VM(bVmData);return 0;
}

这个程序中的__vstart就相当于前面说的VMDispatcher,对伪指令进行识别判断,采取调度策略使程序流跳转到相应的Handler里去。
每个Handler在执行完之后都有一句相同的代码:jmp __vstart 用于回收控制权到VMDispatcher中。以便下一次调度。
最后,当所有的伪指令都执行完之后,程序识别到vEnd,就ret退出程序。
 
基本上,这个虚拟机的原理就是这样了。

总结
这里有一点关键点其实没有弄清楚,就是x86汇编指令到虚拟机伪指令的转换问题,上面的小程序我们是直接自定义了一套伪指令和映射规则,实际情况中如VMProtect加壳软件要
解决的是根据原本的汇编指令动态的转换为伪指令再回写到原程序的二进制文件中。
 
还有一个问题没搞明白的就是,如果要对虚拟机的程序进行逆向分析或脱壳。我的理解就只能是想办法找到目标程序伪指令和x86汇编之间的映射关系,然后手工分析这段代码
(因为考虑到效率问题,一般的程序都是只对一些核心算法进行了虚拟化),将这段代码重写出来,以达到逆向分析或脱壳的目的。
 
不知道理解的对不对,逆向这块感觉算法分析和虚拟机是个难点,以后可以针对这个问题进行一些更深入的研究

科普四大猛壳:VMP,TMD,SE,ZP

VMP 虚拟机(加壳原理)相关推荐

  1. VMP虚拟机(加壳原理)

    虚拟机保护技术就是将基于x86汇编系统的可执行代码转换为字节码指令系统的代码,以达到保护原有指令不被轻易逆向和修改的目的,这种指令也可以叫伪指令,和VB的pcode有点类似. 从本质上讲,虚拟指令系统 ...

  2. 【胖虎的逆向之路】02——Android整体加壳原理详解实现

    [胖虎的逆向之路](02)--Android整体加壳原理详解&实现 Android Apk的加壳原理流程及详解 文章目录 [胖虎的逆向之路](02)--Android整体加壳原理详解& ...

  3. 加固加壳脱壳分析(1)_加固加壳原理和几代壳

    什么是加固加壳 对App资源代码进行保护,使其不容易被反编译工具解开. 加固的核心在于保证软件正常运行的同时又能保证源码的安全性. 为什么要加固加壳 若应用不做任何安全防护,极易被病毒植入.广告替换. ...

  4. android apk 防止反编译技术加壳技术(转)

    2019独角兽企业重金招聘Python工程师标准>>> 一.加壳技术原理 所谓apk的加壳技术和pc exe的加壳原理一样,就是在程序的外面再包裹上另外一段代码,保护里面的代码不被非 ...

  5. 安卓应用加固壳判断java厂商_Android APK加固(加壳)工具

    之前一篇文章Android proguard代码混淆,我们讲解了如何实现APK的代码混淆,让反编译者不那么容易阅读我们的源代码.虽然我们混淆,做到native层,但是这都是治标不治本的.反编译的技术在 ...

  6. Android APK加固(加壳)工具

    之前一篇文章Android proguard代码混淆,我们讲解了如何实现APK的代码混淆,让反编译者不那么容易阅读我们的源代码.虽然我们混淆,做到native层,但是这都是治标不治本的.反编译的技术在 ...

  7. Android DEX加壳

    1. APP加固 1). 原理 图1.png 加密过程的三个对象: 1.需要加密的Apk(源Apk) 2.壳程序Apk(负责解密Apk工作) 3.加密工具(将源Apk进行加密和壳Dex合并成新的Dex ...

  8. Android之Apk加壳

    基于ADT环境开发的的实现,请参考: Android中的Apk的加固(加壳)原理解析和实现  类加载和dex文件相关的内容,如:Android动态加载Dex机制解析 一.什么是加壳? 加壳是在二进制的 ...

  9. Android Apk加壳技术实战详解

    前言 前几天面试了一家信息加密相关的公司,经过两轮面试原以为坐等HR,结果还有一个实践测试ORZ-面试这么多家公司,真心觉得这家公司很特殊,尤其是那个逻辑测试-算了,不扯远了,走回正题. 面试官加我Q ...

  10. 基于linker实现so加壳技术上

    <基于linker实现so加壳技术基础>上篇 前言 本篇是一个追随技术的人照着网上为数不多的资料,在探索过程中发现了许多意想不到的问题,搜了好多文章发现对于这方面的记载很少,甚至连一个实现 ...

最新文章

  1. php mysql study_phpStudy 升级 MySQL5.7
  2. ACM题集以及各种总结大全(转)
  3. 医学科研中的作用_医学论文中参考文献的作用及常见类型
  4. Jmeter分布式部署如何操作
  5. mysql 卸载批处理_MYSQL 注册启动 及 停用卸载 批处理脚本 (补)
  6. viewgroup 渲染过程
  7. 每日一题(21)——malloc与free(二)
  8. 【离散数学】代数系统的同态(同构)
  9. exec和source的区别
  10. Oracle优化新常态 前半生
  11. VMware8序列号
  12. 女生应该读的30本书
  13. Python求解江苏小升初数学题与图像阴影绘制
  14. 房天下二手交易平台房源数据采集
  15. Java知识点总结【6】抽象类和接口
  16. 把思科端口速率改为不协商_端口汇聚—TRUNK技术介绍
  17. Android中使用CoAP协议
  18. Win10文件夹打开拒绝访问怎么解决
  19. [windows]-windows安装Mac OSX
  20. 为什么latex中的宋体和黑体与word中不一样 如何设置字体样式 renewcommand和newcommand的区别

热门文章

  1. matlab仿真的五个步骤,matlab simulink 仿真步骤
  2. 试试这个AI实验:把2D图像转换成3D
  3. 安装Eclipse的中文语言包
  4. jy-12-SPRINGMYBATIS02——学子商城-@成恒
  5. 实训-利用HTML+CSS制作某米官网首页
  6. 软考初级程序员真题资料(2009年上半年——2019年上半年)
  7. visio业务流程图教学_Visio流程图入门
  8. 软件开发技术文档编写规范
  9. 中职计算机考证的软件
  10. 家庭网络布线图与布线方案