[关闭]
@FadeTrack 2015-10-21T10:17:37.000000Z 字数 5649 阅读 1806

介绍代码虚拟化[译]

代码虚拟化


介绍代码虚拟化
By Nooby

翻译:FadeTrack

本文描述了如何通过“虚拟机”保护代码 并且 将这种技术运用在流行的虚拟机上。

带着你从入门到精通。 :)

翻译不当之处多多见谅,遇到疑惑或者有问题的地方,请以原文为准。

为什么称为虚拟机?

早期的软件保护,比较成熟的办法都是基于 模糊 和 变化(混淆、膨胀),这种方法将垃圾代码插入到原始代码流,或是改变原来的指令为意义相似的指令,又或是替换一些常量计算。插入有条件的和无条件的分支 以及 在关键的执行代码上随机的插入一些字节(使得这个流程不可逆)。
例子 example_obfuscated.exe 就是这样处理的。

随着时间的推移,程序员的水平上升和调试器的增强。一些成熟的逆向工程工具和手段使得原本被保护的代码变得 可读/可逆。使用膨胀的方法确实能有效阻止逆向,但是会增大软件的体积。于是,人们开始追求一个新的保护代码的方法,这种方法不用增长大小。

这样的循环代码就像一个原始代码的“模拟器”(或者称为解释器)。
接收的数据流 ( 又称为Pesudo-code or P-code),做微操作(处理程序),就像一个“虚拟机”执行指令集那样。最终这个过程演变为了: code virtialization。(指令虚拟化)

虚拟机是如何工作的呢?

我们知道真实的处理器 拥有 寄存器,翻译解码器 以及 逻辑处理器。虚拟级也是一样的。
虚拟机的入口代码实际上是在收集实体处理器的上下文信息,执行循环将读取 P-Code 并派发到相应的处理程序(handler),当虚拟机退出时,它将通过之前保存的上下文信息来更新真实的处理器的寄存器信息。

这里做一个简单例子,来假设一个函数将通过虚拟机执行。

  1. 最初的指令:
  2. add eax, ebx
  3. Retn
  4. 通过将其转换为虚拟代码:
  5. push address_of_pcode ==> p-code 的地址入栈
  6. jmp VMEntry
  7. VMEntry:
  8. push all register values ==> 将所有的寄存器信息入栈
  9. jmp VMLoop
  10. VMLoop:
  11. VMEIP 获取 p-code
  12. 调度处理程序 [Add_EAX_EBX_]
  13. VMInit:
  14. pop all register values into VMContext ==> 将所有寄存器的值弹出到 VMContext
  15. pop address_of_pcode into VMEIP ==> p-code 弹出到 VMEIP
  16. jmp VMLoop
  17. Add_EAX_EBX_Hander:
  18. do add eax, ebx on VMContext ==> VMContext 里面完成 操作
  19. jmp VMLoop
  20. VMRetn:
  21. restore register values from VMContext ==> 通过VMContext恢复寄存器信息
  22. do retn ==> 返回

注意,虚拟机最好不要效仿x86指令,因为如果指令也能被真正的处理器执行的话,可能会在某些特定的地方导致虚拟机退出,之后就会卡死在这里或则出现一些不可预料的情况( Ps: 这一句不太好翻译)。
实际的虚拟机的处理程序通常用更通用的设计思想,而不是上面的示例中的处理程序。
通常 p - code也决定了操作数。
“Add_EAX_EBX_Hander”可以被定义为“Add_Handler”,它需要两个参数,产生一个结果。
也会 加载/存储 寄存器,处理过程/保存参数和结果。
这样会提高处理程序的可重用性,以便跟踪处理程序而不需要去理解虚拟机的原始构造代码。

现在我们来看看 一个基于堆栈(stack-based)的虚拟机是如何工作的:

  1. Add_Hander:
  2. pop REG ; REG = parameter2
  3. add [STACK], REG ; [STACK] points to parameter1
  4. GetREG_Handler:
  5. fetch P-Code for operand
  6. push VMCONTEXT[operand] ; push value of REG on stack
  7. SetREG_Handler:
  8. fetch P-Code for operand
  9. pop VMCONTEXT[operand] ; pop value of REG from stack
  10. The P-Code of above function will be:
  11. Init
  12. GetREG EBX
  13. GetREG EAX
  14. Add
  15. SetREG EAX
  16. Retn

现代虚拟机针对逆向工程做了些什么?

对于虚拟机来说,代码混淆和变形是非常重要的,因为直接将虚拟机的解释器暴露在外的话,逆向者就可以通过一些自动化的工具对虚拟机的底层构架进行分析。
由于处理器上的一些寄存器并没有被使用(VMContext和 虚拟机解释器可能使用的少量寄存器 是 分开存储),所以它们可以被用作额外的混淆。
虚拟机的处理程序可以设计成尽可能少的操作数/上下文依赖性。
此外,真正的处于 VMContext 的 堆栈指针可能被追踪,堆栈可以被抛弃在解释器循环内。

通过以上说明,不难看出代码混淆和变形可以是非常有效的。

混淆虚拟机的例子可以在example_virtualized.exe中找到。

现在我们知道如何保护虚拟机的执行部分,让我们继续看看将指令转换为p-Code 的方法,这是虚拟化的精彩的一部分代码。

指令分解

逻辑指令

这里有一个增加可用性和复杂性的方法。

逻辑处理可以根据下面的公式分解成类似 NAND/NOR 的操作:

  1. NOT(X) = NAND(X, X) = NOR(X, X)
  2. AND(X, Y) = NOT(NAND(X, Y)) = NOR(NOT(X), NOT(Y))
  3. OR(X, Y) = NAND(NOT(X), NOT(Y)) = NOT(NOR(X, Y))
  4. XOR(X, Y) = NAND(NAND(NOT(X), Y), NAND(X, NOT(Y))) = NOR(AND(X, Y), NOR(X, Y))

算术指令

减法可被转换成带EFlags进位计算的加法。

  1. SUB(X, Y) = NOT(ADD(NOT(X), Y))

将 EFLAGES 之前的最后一个 NOT 作为 A, EFLAGES 之后的最后一个 NOT 最为B,那么计算如下:
EFLAGS = OR(AND(A, 0x815), AND(B, NOT(0x815))) ; 0x815 masks OF, AF, PF and CF

寄存器抽象

由于虚拟机可以比一个实际的x86处理器有更多的寄存器,真正的处理器寄存器可以动态地映射到虚拟机寄存器,可以使用额外的寄存器来存储中间值或用来做混淆。使得指令通过下文所述的内容得到进一步的模糊和优化。

上下文轮循

由于寄存器的抽象,不同的 p-code可以有不同的寄存器映射,这样就可以不时的去改变设计,使得逆向更加困难。

当下一块 P-Code 具有不同的寄存器映射时,虚拟机仅仅交换处于上下文的值。

当像XCHG这一类的转换指令时,它可以简单地改变寄存器而不产生任何P- code的映射。看下面的例子:

原来的指令:

  1. xchg ebx, ecx
  2. add eax, ecx

不具备上下文轮循的P-Code:

当前寄存器映射

  1. Real Registers Virtual Registers
  2. EAX R0
  3. EBX R1
  4. ECX R2
  1. GetREG R2 ; R2 = ECX
  2. GetREG R1 ; R1 = EBX
  3. SetREG R2 ; ECX = value of EBX
  4. SetREG R1 ; EBX = value of ECX
  5. GetREG R2
  6. GetREG R0 ; R0 = EAX
  7. Add
  8. SetREG R0

具备上下文轮循的P-Code(在p-code生成完成时交换):

  1. Before Exchange
  2. Real Registers Virtual Registers
  3. EAX R0
  4. EBX R1
  5. ECX R2
  6. After Exchange
  7. Real Registers Virtual Registers
  8. EAX R0
  9. EBX R2
  10. ECX R1
  1. [Map R1 = ECX, R2 = EBX] ; exchange
  2. GetREG R1 ; R1 = ECX
  3. GetREG R0 ; R0 = EAX
  4. Add
  5. SetREG R0 ; R0 = EAX

这样的轮循也可以应用到最后SetREG操作, 这样的结果还将写入另一个未使用的虚拟机寄存器(即 R3), 舍弃拥有无效数据的R0。这一块 P-Code 的操作将在3个寄存器上,所以它很难被还原。

  1. P-Code With Context Rotation 2:
  2. [Map R1 = ECX, R2 = EBX] ; exchange
  3. GetREG R1 ; R1 = ECX
  4. GetREG R0 ; R0 = EAX
  5. Add
  6. [Map R0 = Unused, R3 = EAX] ; rotation
  7. SetREG R3 ; R3 = EAX

寄存器别名:

当处理一条指令时,尤其是在寄存器之间赋值时,可能会出现 源寄存器 和 目标寄存器 之间的映射。除非源寄存器将被改变(强制重新映射或GetREG & SetREG操作)。
这种映射可以读访问权目标寄存器,将其重定向到它没有实际执行任务的来源。

采取以下的代码为例:

  1. Original Instructions:
  2. mov eax, ecx
  3. add eax, ebx
  4. mov ecx, eax
  5. mov eax, ebx
  1. P-Code:
  2. Current Register Mappings
  3. Real Registers Virtual Registers
  4. EAX R0
  5. EBX R1
  6. ECX R2
  7. [Make alias R0 = R2]
  8. GetREG R1 ; R1 = EBX
  9. GetREG R2 ; reading of R0 redirects to R2
  10. Add
  11. [R0(EAX) is being changed, since R0 is destination of an alias, just clear its alias]
  12. [Map R0 = Unused, R3 = EAX] ; rotation
  13. SetREG R3 ; R3 = EAX
  14. [Make alias R2 = R3]
  15. GetREG R1
  16. [R3(EAX) is being changed, since R3 is source of an alias, we need to do the assignment]
  17. [Map R3 = ECX, R2 = EAX] ; we can simplify the R2 = R3 assignment by rotation
  18. [Map R0 = EAX, R3 = Unused] ; another rotation
  19. SetREG R0 ; R0 = EAX

寄存器用法分析:

给定上下文的一组指令,它可以确定,在某些时候某些寄存器的值改变的而不影响程序逻辑,以及一些EFLAGS计算的开销可以忽略。

例如,在0 x4069A8 example.exe 的一段代码:

  1. PUSH EBP
  2. MOV EBP, ESP ; EAX|ECX|EBP|OF|SF|ZF|PF|CF
  3. SUB ESP, 0x10 ; EAX|ECX|OF|SF|ZF|PF|CF
  4. MOV ECX, DWORD PTR [EBP+0x8] ; EAX|ECX|OF|SF|ZF|PF|CF
  5. MOV EAX, DWORD PTR [ECX+0x10] ; EAX|OF|SF|ZF|PF|CF
  6. PUSH ESI ; OF|SF|ZF|PF|CF
  7. MOV ESI, DWORD PTR [EBP+0xC] ; ESI|OF|SF|ZF|PF|CF
  8. PUSH EDI ; OF|SF|ZF|PF|CF
  9. MOV EDI, ESI ; EDI|OF|SF|ZF|PF|CF
  10. SUB EDI, DWORD PTR [ECX+0xC] ; OF|SF|ZF|PF|CF
  11. ADD ESI, -0x4 ; ECX|OF|SF|ZF|PF|CF
  12. SHR EDI, 0xF ; ECX|OF|SF|ZF|PF|CF
  13. MOV ECX, EDI ; ECX|OF|SF|ZF|PF|CF
  14. IMUL ECX, ECX,0x204 ; OF|SF|ZF|PF|CF
  15. LEA ECX, DWORD PTR [ECX+EAX+0x144] ; OF|SF|ZF|PF|CF
  16. MOV DWORD PTR [EBP-0x10], ECX ; OF|SF|ZF|PF|CF
  17. MOV ECX, DWORD PTR [ESI] ; ECX|OF|SF|ZF|PF|CF
  18. DEC ECX ; OF|SF|ZF|PF|CF
  19. TEST CL, 0x1 ; OF|SF|ZF|PF|CF
  20. MOV DWORD PTR [EBP-0x4], ECX
  21. JNZ 0x406CB8

注释展示了在指令执行之前的违背使用的 寄存器/flag 的状态。 这些被用作生成寄存器轮循。
EFLAGS计算冗长并且夹杂垃圾指令,这使得逆向更加复杂。

其他的 P- code陷阱和优化

常量加密

将原本指令内的常量给转换成算式计算,这样常量出现在运行时,可以避免直接暴露。

堆栈混淆

虚拟机可以通过pushing/writing随机值来混淆的堆栈,而真正的ESP可以从VMContext 计算/跟踪得到。

用多个虚拟机解释

It is possible to use multiple virtual machines to execute one series of P-Code. On certain points, a
special handler leading to another interpreter loop is executed. The P-Code data after such points
are processed in a different virtual machine. These virtual machines need only to share the
intermediate run-time information such as register mappings on switch points. Tracing such P-Code
will need to analyze all virtual machine instances, which is considerably much more work.

References

VMProtect
http://vmpsoft.com/
Code Virtualizer
http://www.oreans.com/
Safengine
http://www.safengine.com/
ReWolf's x86 Virtualizer
http://rewolf.pl/stuff/x86.virt.pdf
OllyDBG
http://www.ollydbg.de/
VMSweeper
http://forum.tuts4you.com/topic/25077-vmsweeper/

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注