虚拟机虚拟环境与代码动态变形技术_第1页
虚拟机虚拟环境与代码动态变形技术_第2页
虚拟机虚拟环境与代码动态变形技术_第3页
虚拟机虚拟环境与代码动态变形技术_第4页
虚拟机虚拟环境与代码动态变形技术_第5页
已阅读5页,还剩10页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

1、虚拟机、虚拟环境与代码动态变形技术1. 简介虚拟机保护是这两年颇为流行的软件保护技术。这个词源于俄罗斯的著名软件保护软件"VmProtect",以此软件为开端引起了软件保护壳领域的革命。各大软件保护壳开发团队都将虚拟机保护这一新颖的技术加入到自己的产品中。如今针对虚拟机保护领域的文章大多是对"VmProtect"的逆向和分析。没有过多的资料与文章来谈谈它的设计与构建。此篇文章源自我自己的软件保护壳对于虚拟机保护这一部分的解决方案。其中借鉴了一部分"VmProtect"的技术,这方面的主要资料来源于"看雪论坛"以及与

2、朋友们的讨论。其中又加入了我的一些个人对于虚拟机保护的理解。设计中我采取了三种不同的虚拟机来构成一套完整虚拟机保护系统。文中基本描述了我的虚拟机保护所采用的技术与原理,但具体算法与数据结构并不涉及。文中的第二个重要的议点是当今流行的保护技术-乱序与替换。有人也喜欢把替换称为抽取,这里只是觉的替换更加直观。并把这两部分合称为代码混淆,在文章的最后讨论了对于花指令生产器的设计。单纯的乱序与替换是不能够起到迷惑Cracker的作用,唯有与随机生成的花指令联合起来才可以达到延长破解者逆向时间的作用。2. 虚拟机保护的构建2.1. 整体的设计在我解决方案中设计了三种不同的虚拟机来达到保护的目的。从保护壳

3、大体来讲我构建了一台现实生活中不存在的计算机与它的汇编器和调试器,并用它编写了保护壳的大部分重要代码。第三方虚拟机保护在一些crackme中可以找寻到它的影子,主要是为了保护注册算法不被逆向。在局部方面我定义了两种虚拟机保护,一种是使用x86体系结构衍生出的虚拟机保护。因为x86的编码是可识别的,所以防御的重点就是对于x86编码的转换与加解密。第二种保护是来源与SSCON2008 著名程序员刘涛涛的扭曲加密变换和"VmProtect"的一些设计思路。其中一些设计又不同于这两者。2.2. 保护对象-函数在进行保护之前,首先有一个重要的问题就是保护对象,虚拟机保护并不是壳。在稳

4、定性的前提下很难满足使用虚拟机进行全程序保护又能保持高稳定性。主要影响稳定性的原因是以下三种情况:1. 重定位问题2. 模拟程度的问题3. 各种猥琐编码技巧引起不可预知的错误对于重定位的引起的不稳定,似乎是一个不可修复的硬伤。由于本文讨论的技术都有一个共同的BUG就是重定位。所以对它的讨论,另外有一节专门讲述。对于第二种情况是显而易见的,我们设计的毕竟是虚拟机。在有限的开发周期与资源的情况下并不太可能开发出可以模拟当时完整运行情况的虚拟机,那样的虚拟机并不是用来做虚拟机保护的。例如大名鼎鼎的bochs。内存访问,异常处理,未模拟的指令,都是造成不稳定的因素。最后一种情况是这样的。当一个程序被一

5、个壳保护,或者在编写这个软件的过程中应用了反逆向的编程手段,或者是我们要保护的目标程序是一个已经经过某个壳保护了的程序。这个壳应用了大量非常规的编码手段。还有可能是目标程序使用的编译器生成或者优化的代码比较特殊。最后一种情况也属于壳稳定性讨论范围,毕竟没有一款壳可以号称兼容所有的程序。即便是简单的添加新节后XOR。所以,为了把兼容性减小到最低,我们虚拟机保护的对象定位在函数级别上。这里要注意的是使用虚拟机保护的人必须了解要保护软件的内部构造,知道要保护哪一部分,哪些部分是可以稳定的进行保护。而不是盲目的使用壳随意对任意函数进行保护,这样会造成兼容性降低。当我们确定了我们的保护对象,现在首先面临

6、的问题就是如何找出我们的保护对象。从一个PE文件的代码节中找出特定函数帧并不是非常困难的问题。但要做到100%的识别出所有函数还是比较困难的,这里就涉及了很多问题。先让我们看一个标准的函数框架55 push ebp8BEC mov ebp, esp. 任意代码.8BE5 mov esp, ebp5D pop ebpC3 retn对于识别这种标准框架的流程很简单。通过反汇编引擎从代码开头开始遍历。先找函数开头的标记后,接下来找函数结尾,找到后与前一个开头标记进行闭合并记录。遇到其余指令则直接略过。依次循环下去。但是事实并非想象的那样美好,优化过后的代码往往不够那么标准,加上编译器的不同对于函数的

7、建立也有所不同。还有就是加入反逆向代码的程序。所以即便是著名的反汇编软件IDA也不能达到完全正确的识别。这里采用的解决方案是,1.建立一套大多数函数框架的特征码库。2.让用户指定要保护代码的起始地址与长度。2.3. x86虚拟机保护2.3.1. 为什么要使用x86编码指令x86的编码毕竟是公开的,任何反汇编器或者调试器都可以对它进行分析。我为什么还要使用x86虚拟机作保护呢?原因有三点:1. 开发周期短2. 稳定性高3. 避免的重定位修复基于开发周期和成本的考虑,x86虚拟机保护是所有虚拟机保护中成本开销最小的虚拟机了。基于编码复杂度的考虑,由于本身就是用x86虚拟机所以代码长度并不用进行修改

8、。这样就避免了函数中跳转偏移的修复。由于x86编码是公开的,而我只是要借用x86虚拟机的便利性,所以就提出了以下四种解决方案对x86虚拟机解释进行保护。2.3.2. 随机OPCODE映射表x86编码在大多数情况下是由两张表来进行指定,每张表256个字节,第二张表由第一张表的0F字节引出。换句话说就是当遇到一个x86指令如果OPCODE的第一个字节是0F那么这条指令的OPCODE段就有第二个字节。否则指令的OPCODE段为一个字节。具体x86的编码方式不在本文的讨论范围之内,请参照IA-32 Architecture Instruction Set。随机OPCODE映射表的原理就是将这两张表进行

9、替换。简单的说如果原来50机器码对应的是push eax指令,那么在映射之后65机器码才是对应的push eax。并且保证保护要保护的每个函数都有自己的一套对应规则。随机映射表的生成与下一个节的字节码加解密有着很大的联系。随机映射表映射的过程实际上也是对每条指令进行加密的过程。而映射表的每个字节表示了解码所需要的算法。2.3.3. 字节码的加解密如何保护好字节码也是重要的问题。虽然OPCODE(包括了前缀段)表进行了随机映射。但是在x86编码中还存在ModRM/SIB段和常数段。对ModRM/SIB解析规则进行重新规则应该是最好的解决手段。对常数段的保护可以直接对其进行加密。在ModRM/SI

10、B中进行解密(ModRM/SIB段是什么参见IA-32 Architecture Instruction Set)我在这里的解决方案是采用加密的原则。加密的原则不采用整体加密原则,而是采用局部加密原则,把解密算法放到每条指令做解析的过程当中,当然每条指令的加解密算法不相同,并且加解密算法跟随OPCODE表的映射进行变换随之变换。这里需要提到的是多态加解密算法生成器,因为不在本文讨论范围之内,所以不做解释。当然关于ModRM/SIB也做了同样的处理。每个被保护的函数的ModRM/SIB解析函数内的解密函数都不相同。而密钥的选择也与VMProtect类似采用可以防止外部篡改字节码的原则。我采用了取

11、当前指令HASH值作为随后指令解密的密钥的方式,详细说明一下:加密后指令1 -> decoder(随机的密钥) -> 指令1 -> HASH(指令1) = key1加密后指令2 -> decoder(key1) -> 指令2 -> HASH(指令2) = key2加密后指令3 -> decoder(key2)-> 指令3 -> HASH(指令3) = key3. 以此类推直到解密完成这里需要解释的是,加密是加密哪一部分。如果是单字节指令不需要进行解密,OPCODE随机的生成就是对它的一种加密。当一条指令有多个字节时就需要对指令进行加密。加密

12、是这个部分。当遇到要有处理ModRM/SIB段的指令时,此加密结果是在已经加密过后的ModRM/SIB上进行再一次的加密。当进入ModRM/SIB处理程序后,它会对自身的字节再一次进行解密。而对于带有常数的ModRM/SIB也是经过了两次加解密。2.3.4. 变形后的指令处理句柄在我阅读逆向VMProtect的文章时,发现大多数逆向虚拟机的过程都是找OPCODE与处理Handler之间的对应。如果每次的对应都不相同,那么会给逆向带来很大的麻烦。在上一节中我把OPCODE的对应进行随机。每个被保护函数都有一套自己的OPCODE表。现在就是把自己的处理句柄进行随机化。每个被保护的函数都有一套自己的

13、Handler。我们为每个被保护的函数都编写一套指令执行规则显然是不现实的。那么我们只能寻求变形的方法。在每次要保护一个函数时,将最原始的Handler函数进行添加花指令或者采用下一个章节提到的指令扩展的方式。由于原始Handler是自己编写。所以在做指令扩展时可以保证完全的兼容性。(由指令扩展引起的不兼容在指令扩展章节讨论)。2.3.5. 虚拟机上下文结构随机上下文结构随机的思路也取自于VMProtect。这个设计是这样的当每次要执行虚拟机时必定有一个结构的某一个字段保存了模拟寄存器的数组。例如如下结构:typedef struct _VM_CONTENT / 寄存器数组 DWORD Reg

14、isters8; / 其他字段定义. VM_CONTENT, *PVM_CONTENT#define R_EAX0#define R_ECX1#define R_EDX2/ 其他寄存器索引定义.在代码执行的过程中我们可以通过索引分析出此时虚拟机操作的是哪个寄存器。所以在我的设计中采用了VMProtect寄存器随机这一原则。并将它扩大到随机上下文结构。他的目的是在于不让逆向者分析出VM_CONTENT这个结构。让每条指令都有一个自己上下文结构并不困难,难点在于如何判别随机化后的上下文结构。所以就又衍生出了另一个结构用于描述如何还原随机化后的结构。这样当执行一条指令后将当前上下文结构进行传递并且与

15、之一同传递的还有一个虚拟机执行上下文重定位结构此结构用于描述前一个上下文结构的修改,此结构的目的是告诉下一条指令的处理句柄,如何将上一条指令的上下文结构还原为原始的上下文结构,便于按照原始的结构构造自己的上下文结构。2.3.6. 加载流程在进入虚拟机保护之前,我们的虚拟机保护加载器需要模拟出一段内存用作虚拟机本身的栈段,所以就要有一个切栈操作,使得原始的栈指针(esp, ebp)归虚拟机字节码程序拥有以便达到最大的稳定性。而虚拟机本身做解释执行的栈段要重新进行模拟。这里只需要在进入虚拟机之前保存原始ebp, esp 并将ebp, esp重新指向一段事先分配好的内存。在虚拟机推出之前再做一此切栈

16、操作。以恢复程序的正常运行。当刚进入被保护的函数中(被保护的函数处的代码被修改为虚拟机的加载代码)流程如下:1.将当前运行的上线文设定到虚拟机的VM_CONTENT结构中2.保存esp, ebp 并将esp, ebp 指向全新的内存3.跳入虚拟机中执行这里存在两种设计,一种就是将此处要保护的代码的字节地址一同传递给虚拟机保护解释器初始化函数,另外一种设计就是每段要保护的代码都拥有一个唯一的初始化函数这里我的选择是后者。2.3.7. 异常的模拟关于在虚拟机保护中需要注意的异常有以下两种1. 内存访问权限不足2. 未知指令其中最平凡是内存访问权限的问题,第二种的多数是因为虚拟机本身没有模拟完足够的

17、指令。第二种情况可以通过增加新的指令进行修复。如果本身就是x86的不可解析的指令,那么把控制权交还给SEH链就好了。而这种控制权的移交是被动的。这里最频繁的异常主要是第一种情况。由于被保护程序运行环境,用户输入,程序本身等等会造成这样那么的错误。如果发生了内存访问错误。同样也不需要管它,WINDWS的SHE异常处理会自动接管它。2.4. 指令扩展保护2.4.1. 源自扭曲变形加密在去年(SSCON 2008)上著名程序员刘涛涛提出了扭曲加密变换的理论(详细解释参考扭曲加密变换技术一文)简单介绍一下此项技术的核心例如一条指令 add eax, 1我们可以将它转化为call ADD_EAX_1Jm

18、p ADD_EAX_1_END; 这里为垃圾代码ADD_EAX_1:push ebpmov ebp, espsub esp, 0x08push ebxmov dword ptr ebp-0x04, 5mov ebx, 4sub dword ptr ebp-0x04, ebxadd eax, dword ptr ebp-0x04mov esp, ebppop ebpRetADD_EAX_1_END:1条 简单的指令可以扩展为一个函数,这样的扩展只局限与想象力。如果整篇代码进行这样的扩展是逆向的难度是不可想象的。其中他的做法是采用OBJ文件作为中间文件,当然这样做用途是避免重定位的问题,而我要保护

19、的对象是已经生成出来的PE文件,其中大多数都是不带重定位表的(PE文件结构与OBJ文件结构这里不做解释,请参考相关文档)。这就需要我们像上一节x86虚拟机保护那样让用户有选择的进行保护并且在保护之后对代码进行重定位修复。具体做法下文中有讨论。2.4.2. 指令模板化因为我们要扩展我们的代码例如像上文提到的一样将add eax, 1变成一个函数的调用,所以我们要有一整套模板。记录了常用指令要变化的映射,并在生成代码时进行随机筛选。在生成时还可以随机插入一些花指令让变形的力度更大。这里值得一提的是VMProtect关于and or not xor这些逻辑运算的替换。在电路门中有种成为万用逻辑闸的东

20、西,具体原理就是使用not not and或者not not or 实现对以上的逻辑运算的模拟。以下公式以及图参见(破解vmp程序的关键点(海风月影)一贴)这样我们可以把在要保护代码中的逻辑运算用此来替代。设:P(a, b) = a & bNot (a) = P(a, a)And (a, b) = P(P(a, a), P(b, b)Or(a, b) = P(P(a, b), P(a, b)Xor(a, b) = P(P(P(a, a), P(b, b), P(a, b)这5条算是基础指令,如果把这些指令进行展开,在通过随机算法控制这个展开的过程,那么几条简单的逻辑运算指令就可以变的非

21、常复杂。2.4.3. 重定位修复重定位问题是以上所有问题需要涉及的为什么会产生重定位的问题。让我们看一个实际的问题。原始代码插入代码后并修复的情况在这两幅图的对比之后会发现由于中间插入了代码,所以像JMP,CALL,JCC这样的指令都需要进行修复。修复的原则为1.重定位模块的设计重定位模块是整个代码集成的关键模块。它依靠反汇编引擎来识别代码。并且也负责修订病毒体偏移的任务。先读取代码节,以代码节的各个跳转指令为节点进行分段。记录它的偏移长度以及跳转方向。分析跳转指令之间产生的空隙。按空隙的大小对要插入的代码进行分段。在跳转中插入代码后,修订跳转的偏移量2.重定位模块何时插入在将病毒代码插入到宿

22、主代码中最理想的位置是两段跳转指令之间,这样可以不用修复任何跳转偏移。修复偏移只有在插入到跳转指令的集合内,如果有跳转指令有交集则修复所有跳转指令的偏移。3.修复内存跳转在遇到如call dword ptr 404340,jmp dword ptr 404340等代码,需要拆解ModRM,SIB两个位得到。由于call,jmp由内存指定值跳转的编码同为FF,扩展编码为call:02,jmp:04。由于我们编写的病毒在32位下运行,所以16位的编码不进行考虑。查INTEL手册编码得到两组可以跳转的情况。单ModRm情况:当为call,ModRM取15h,为jmp,ModRM取25h.具有SIB的

23、情况:Mod为00h.查看R/M为04h RO为2或者4,所以得下两种情况ModRM为14h,24h。检查SIB,SIB的情况仅与Index,Base字段有关。当Index = 04h,Base = 05h下的情况全部可以重定位。从指向的内存中取出要跳转的地址,进行偏移修复。4.指令的变换某些跳转指令的立即数是单字节,这些指令无论向上还是向跳转的空间只有127个字节,所以当插入代码长度与偏移的和大于这个数字时就要利用相同作用但偏移量更大的指令替换。在扩展指令后,指令的长度进行了增大,这时如果此指令在其他跳转集合内需要再进行一次重定位修复。5.重定位模块的缺陷重定位模块只能修订以偏移量作为跳转情

24、况 如果是动态跳转例如:Jmp dword ptr ebxCall eax 等类似的代码重定位模块则不能修复。 以上描述引用自我在看雪论坛关于重定位模块一贴。具体连接为:2.5. 第三方虚拟机2.5.1. 壳的虚拟化第三方虚拟机的在软件保护中的应用,可以在某些crackme中见到。在我的壳的设计当中有这样一台虚拟机,它控制着整个壳的运行流程,例如重建引入表。加解密运算等。整个壳重要的部分都由此虚拟机进行编码,与x86虚拟机保护相同的是OPCODE表的随机化和对指令的加解密。让壳本身就是处于虚拟机保护的状态。2.5.2. 用户的自定义功能扩展引入第三方虚拟机的另一个好处是可以让用户扩展壳的功能。

25、只要虚拟机本身提供足够强大的SDK。引入用户扩展的另一项功能是为了弥补壳在添加功能上的不便理性。例如反调试模块。这里展示一副在我的壳中已经进行工作的第三方虚拟机工作图。目前还只是汇编语言阶段。没有在此基础上开发出基于此汇编语言的高级语言编译器。壳本身带有一个此虚拟机的调试器用来让用户调试编写的代码。2.6. 外部函数动态跳转引发的状况除第三方虚拟机外,无论是x86虚拟机保护还是指令扩展式保护都有一个严重的硬伤。如果有一段被保护的函数A,以及一段函数B,B中有一跳转指令形如:Mov eax, A函数地址Add eax, 某个偏移Jmp eax此时eax的结果为A函数中的某段地址。类似的情况我们是

26、无法进行重定位已经处理的。为了避免有其他函数跳入到被保护函数体内而不是头地址的情况,在被保护后,我修复了这段代码节除被保护函数代码区域的所有偏移性跳转。以达到最大修复的目的。这种修复可用于代码扩展。但是对于x86虚拟机保护以及其他解析性质的虚拟机保护来说,此方法无效。3. 代码混淆3.1. 代码乱序乱序这种技术已经在大量壳中得以应用。实现技术简单,稳定性高。并且在此基础上可以构建出很多有趣的花样。代码乱序的目的有两个。1.是可以增加逆向工程的难度。2.可以让壳重新获得对程序的控制权。乱序的原理十分的简单,具体的流程分为以下几个步骤:1. 分析要保护文件的代码节,并遍历找出所有的CALL或者JM

27、P的代码2. 修改CALL或者JMP其后的偏移将它指向到自己所指定的地址3. 在新的地址处填充花指令或者自行定义的代码,在代码结尾处跳入到原始的目标地址这里给出两段代码予以说明,下面的代码是正在对32位偏移跳转的指令进行记录把这条指令当前的地址与要跳转的目标地址进行记录。这样做的目的是流程的第一步找出所有的CALL或者JMP的代码。ud_obj是u86diasm反汇编引擎的结构体。 if (ud_obj.inp_sess0 = 0x0F) / 32位 (*pCodeFlowNode)->dwBits = Jmp_Bit_32; (*pCodeFlowNode)->dwInsLen

28、= ud_obj.inp_ctr; DWORD dwOffset = ud_obj.operand0.lval.udword; (*pCodeFlowNode)->dwOffset = dwOffset; if (dwOffset >= 0x80000000) (*pCodeFlowNode)->bGoDown = FALSE; dwOffset = dwOffset; dwOffset+; dwOffset -= ud_obj.inp_ctr; (*pCodeFlowNode)->dwGotoMemoryAddress = (*pCodeFlowNode)->d

29、wMemoryAddress - dwOffset; DWORD dwRaw = PeDiy.Rva2Raw(pMem, (DWORD)(*pCodeFlowNode)->dwGotoMemoryAddress - dwImageBase); (*pCodeFlowNode)->pGotoFileAddress = pMem + dwRaw; else (*pCodeFlowNode)->bGoDown = TRUE; dwOffset += ud_obj.inp_ctr; (*pCodeFlowNode)->dwGotoMemoryAddress = (*pCodeF

30、lowNode)->dwMemoryAddress + dwOffset; DWORD dwRaw = PeDiy.Rva2Raw(pMem, (DWORD)(*pCodeFlowNode)->dwGotoMemoryAddress - dwImageBase); (*pCodeFlowNode)->pGotoFileAddress = pMem + dwRaw; /* end else */接下来的事情就是遍历这个记录好的结构。给目标程序填充新节,将要乱序的偏移进行修复并指向新的空间。乱序的流程图3.2. 代码替换这里的替换是指的自动的替换。用户指定一段要保护的内存后,我们

31、的壳程序将这段内存处CALL/JMP/JCC之间的代码自动的进行抽取。并将其移动到另一篇区域,然后将进入这片代码的地方使用JMP语句跳入到新的区域内。在新的区域内执行完毕后跳入到原来执行完毕的地址。以下是流程描述: 1.找到两个JMP/CALL/JCC之间的代码段,这个代码段符合以下要求,长度要大于等于5个字节(以便放置 跳转指令)这段指令没有其他跳转跳入(如果能排除动态跳转最好,但是不太可能,这也是产生不能修复的 BUG的原因) 1-找到的这个代码段可以不忽略有其他跳转跳入 2.添加一个新节以便储存这些被替换的代码。 3.遍历这个结构,把要替换的代码段搬运到新的空间,在新的空间末尾添加一个跳入原先代码段结束的 地址。 4.将原先的代码块的首5个字节添加一个JMP跳入到搬运后的地址。并填充同样长度的花指令。 5.将这个代码块除了首5个字节其余字节以花指令填充。 5-x.将这个代码

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论