介绍代码虚拟化[译]

介绍代码虚拟化[译]

标签(空格分隔): 代码虚拟化


介绍代码虚拟化

By Nooby

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

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

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

为什么称为虚拟机?

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

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

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

虚拟机是如何工作的呢?

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

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

最初的指令:
add eax, ebx
Retn

通过将其转换为虚拟代码:
push address_of_pcode                    ==> 将 p-code 的地址入栈
jmp VMEntry

VMEntry:
push all register values                 ==> 将所有的寄存器信息入栈
jmp VMLoop

VMLoop:
从 VMEIP 处 获取 p-code
调度处理程序 [Add_EAX_EBX_]
VMInit:
pop all register values into VMContext   ==> 将所有寄存器的值弹出到 VMContext  
pop address_of_pcode into VMEIP          ==> 将 p-code 弹出到 VMEIP
jmp VMLoop

Add_EAX_EBX_Hander:
do “add eax, ebx” on VMContext           ==> 在 VMContext 里面完成 操作
jmp VMLoop

VMRetn:
restore register values from VMContext   ==> 通过VMContext恢复寄存器信息  
do “retn”                                ==> 返回

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

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

Add_Hander:
pop REG                  ; REG = parameter2
add [STACK], REG         ; [STACK] points to parameter1
GetREG_Handler:
fetch P-Code for operand
push VMCONTEXT[operand]  ; push value of REG on stack
SetREG_Handler:
fetch P-Code for operand
pop VMCONTEXT[operand]   ; pop value of REG from stack
The P-Code of above function will be:
Init
GetREG EBX
GetREG EAX
Add
SetREG EAX
Retn

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

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

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

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

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

指令分解

逻辑指令

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

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

NOT(X) = NAND(X, X) = NOR(X, X)
AND(X, Y) = NOT(NAND(X, Y)) = NOR(NOT(X), NOT(Y)) 
OR(X, Y) = NAND(NOT(X), NOT(Y)) = NOT(NOR(X, Y))
XOR(X, Y) = NAND(NAND(NOT(X), Y), NAND(X, NOT(Y))) = NOR(AND(X, Y), NOR(X, Y))
算术指令

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

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的映射。看下面的例子:

原来的指令:

xchg ebx, ecx
add eax, ecx
不具备上下文轮循的P-Code:

当前寄存器映射

Real Registers  Virtual Registers
EAX     R0
EBX     R1
ECX     R2
GetREG R2 ; R2 = ECX
GetREG R1 ; R1 = EBX
SetREG R2 ; ECX = value of EBX
SetREG R1 ; EBX = value of ECX
GetREG R2
GetREG R0 ; R0 = EAX
Add
SetREG R0
具备上下文轮循的P-Code(在p-code生成完成时交换):
Before Exchange
Real Registers  Virtual Registers
EAX     R0
EBX     R1
ECX     R2
After Exchange
Real Registers  Virtual Registers
EAX     R0
EBX     R2
ECX     R1
[Map R1 = ECX, R2 = EBX] ; exchange
GetREG R1        ; R1 = ECX
GetREG R0        ; R0 = EAX
Add
SetREG R0        ; R0 = EAX

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

P-Code With Context Rotation 2:
[Map R1 = ECX, R2 = EBX]     ; exchange
GetREG R1   ; R1 = ECX
GetREG R0   ; R0 = EAX
Add
[Map R0 = Unused, R3 = EAX]     ; rotation
SetREG R3   ; R3 = EAX

寄存器别名:

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

采取以下的代码为例:

Original Instructions:
mov eax, ecx
add eax, ebx
mov ecx, eax
mov eax, ebx
P-Code:
Current Register Mappings
Real Registers  Virtual Registers
EAX     R0
EBX     R1
ECX     R2
[Make alias R0 = R2]
GetREG R1               ; R1 = EBX
GetREG R2               ; reading of R0 redirects to R2
Add
[R0(EAX) is being changed, since R0 is destination of an alias, just clear its alias]
[Map R0 = Unused, R3 = EAX]     ; rotation
SetREG R3               ; R3 = EAX
[Make alias R2 = R3]
GetREG R1
[R3(EAX) is being changed, since R3 is source of an alias, we need to do the assignment]
[Map R3 = ECX, R2 = EAX]    ; we can simplify the R2 = R3 assignment by rotation
[Map R0 = EAX, R3 = Unused] ; another rotation
SetREG R0               ; R0 = EAX

寄存器用法分析:

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

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

PUSH EBP
MOV EBP, ESP                          ; EAX|ECX|EBP|OF|SF|ZF|PF|CF
SUB ESP, 0x10                         ; EAX|ECX|OF|SF|ZF|PF|CF
MOV ECX, DWORD PTR [EBP+0x8]          ; EAX|ECX|OF|SF|ZF|PF|CF
MOV EAX, DWORD PTR [ECX+0x10]         ; EAX|OF|SF|ZF|PF|CF
PUSH ESI                              ; OF|SF|ZF|PF|CF
MOV ESI, DWORD PTR [EBP+0xC]          ; ESI|OF|SF|ZF|PF|CF
PUSH EDI                              ; OF|SF|ZF|PF|CF
MOV EDI, ESI                          ; EDI|OF|SF|ZF|PF|CF
SUB EDI, DWORD PTR [ECX+0xC]          ; OF|SF|ZF|PF|CF
ADD ESI, -0x4                         ; ECX|OF|SF|ZF|PF|CF
SHR EDI, 0xF                          ; ECX|OF|SF|ZF|PF|CF
MOV ECX, EDI                          ; ECX|OF|SF|ZF|PF|CF
IMUL ECX, ECX,0x204                   ; OF|SF|ZF|PF|CF
LEA ECX, DWORD PTR [ECX+EAX+0x144]    ; OF|SF|ZF|PF|CF
MOV DWORD PTR [EBP-0x10], ECX         ; OF|SF|ZF|PF|CF
MOV ECX, DWORD PTR [ESI]              ; ECX|OF|SF|ZF|PF|CF
DEC ECX                               ; OF|SF|ZF|PF|CF
TEST CL, 0x1                          ; OF|SF|ZF|PF|CF
MOV DWORD PTR [EBP-0x4],  ECX
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/

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值