Vmp 静态分析: 第三步 VmExecutor特征提取
每个VmExecutor 之间大体之间是相同的,但是也不尽相同,这些不同点就构成了VmExecutor的特征。特征包括以下内容
1) 进入VmExecutor 的寄存器入栈顺序
2) ByteCodeAddress的编码解码算法
3) Opcode的解码算法
4) 指令指针的增减方向,有些VmExecutor指令指针是自增的,有些是递减的。
5) 以及DispatchTable的解码算法
6) 带立即数指令的立即数解码算法,包括以下指令
a) PUSH_I_B
b) PUSH_I_W
c) PUSH_I_D
d) PUSH_I_D_B
e) PUSH_I_D_W
f) PUSH_R_B
g) PUSH_R_W
h) POP_R_B
i) POP_R_W
7) 每个VmExecutor中Opcode编码和名称
8) VmHash指令的算法
本文就是说明如何将这些不同找出来。
3.1 x86代码优化
Vmprotect 在编译完代码后,产生的VmExecutor执行流程做了扰乱,或者叫做加花了,为了能够顺利地提取出VmExecutor 的特征信息,需要纠正这些代码流程,并且进行去花处理。这是提取VmExecutor特征的前提条件。如果不进行x86代码的优化,提取特征将要费更多的事情。
需要使用以下方法。
1) 剔除无条件跳转/call,这样可以将执行流变得平滑。
2) Switch分支表的分析,这个功能是帮助分析DispatchTable的。
3) 剔除无效指令,本文选用的是比较土的data-flow方法。在x86下记住几个原则,每条x86指令会读写一个或者多个寄存器,0个或者1个内存操作数,有的指令会修改eflags 有的指令需要读取eflags, 把这些关联起来,就能知道那些指令不应该存在,哪些指令是有效的。
4) 重新分配堆栈变量,因为VmExecutor会产生大量的无效PUSH/POP 之类的玩堆栈,让你眼花,所以变量有时候在堆栈中的位置也就是乱的,当这些无效的指令被剔除后,这些变量的位置也就可以变化了,我们重新分配这些变量在堆栈中的位置,以便后面更好的分析,尤其是合并相同分支的时候,很有好处。
5) 合并相同分支,剔除无效指令后, 可以发现,VmExecutor经常会产生两个或者两个以上完全同效果的分支,也就是说,这样程序留分支不分支其效果是一样的。这时候,需要将这些分支合并。
通过上面的一套组合算法下来,VmExecutor 终将能够清晰的战线在我们面前,我们对比一组数据:
优化前,一个VmExecutor 有 3735条指令,代码长度 11727 Byte,优化后,指令只有733条,代码长度2233 Byte。如果要你写一个VmExecutor 超过800行汇编代码就算不合格呀。
3.2 VmExecutor特征分析
经过x86指令化简后的VmExecutor现在完整而清晰的展现在我们面前,我们需要对其特征进行提取。
3.2.1 找到fetchPoint
首先,需要确认什么地方开始 fetch instruction, 也就是 fetch point, 这条指令很简单
AL = mov [ESI-X]*
3.2.2 找到进入VmExecutor时的两个特征
从VmExecutor 的入口,到fetchPoint 这条指令之前,算作VmExecutor的初始化部分,其中包含两个重要的内容需要提取:
1. ByteCode 的地址是如何解码的?
2. 进入VmExector时,x86寄存器是按照什么顺序存贮在堆栈中的
看看一个现存样本的经过优化后的代码流
[ESP-4]* = mov EBP
[ESP-8]* = mov ESI
ESP = lea &[ESP-8]*
[ESP], {&ESP:ESP} = pushfd EFLAGS
[ESP-4]* = mov EAX
ESP = lea &[ESP-68]*
[ESP], {&ESP:ESP} = pushfd EFLAGS
[ESP+64]* = mov ESI
[ESP+60]* = mov EDX
[ESP+56]* = mov EBX
[ESP+52]* = mov EDI
ESP = lea &[ESP+44]*
[ESP+4]* = mov ECX
ESI = mov [ESP+44]*
ESI, EFLAGS = rol ESI, 13(0x0d)
ESI = bswap ESI
ESI, EFLAGS = sub ESI, -1870272195(0x9085e93d)
ESI, EFLAGS = ror ESI, 22(0x16)
ESI, EFLAGS = neg ESI
ESI, EFLAGS = xor ESI, 966354638(0x399966ce)
EBP = lea &[ESP]*
ESP = lea &[ESP-36]*
[ESP], {&ESP:ESP} = pushfd EFLAGS
ESP, EFLAGS = sub ESP, 152(0x00000098)
EDI = mov ESP
从这段代码中,我们很容易就发现ByteCode Address 的解码代码
ESI, EFLAGS = rol ESI, 13(0x0d)
ESI = bswap ESI
ESI, EFLAGS = sub ESI, -1870272195(0x9085e93d)
ESI, EFLAGS = ror ESI, 22(0x16)
ESI, EFLAGS = neg ESI
ESI, EFLAGS = xor ESI, 966354638(0x399966ce)
也很容易(使用程序) 发现入栈的顺序如下
EBP
ESI
EFLAGS
EAX
ESP ? 这个其实不是ESP,ESP在进入VmExecutor 后靠ebp间接保存
EDX
EBX
EDI
ECX
3.2.3 确认VmExecutor的另外3个特征
确认进入V mExecutor这两个特征后,从FetchPoint 往后走,到遇见ret 代码之前,就能见到另外几个特征
1. 指令指针变化的方向, 是自增? 还是自减?
2. opcode 解码代码
3. handler 解码代码
3.2.4 确认VmExecutor中每个opcode hander地址
在ret之后,会有几十个 opcode handler, 怎么找到这些handler, 从dispatchTable中读取数据,使用刚才找到的算法,计算跳转目的地。
3.2.5 具体分析每个opcode handler
上一步,我们可以得到每个opcode handler 的地址,这时候可以单独分析每个opcode handler的代码,这个代码结束点在哪儿? 当然是前面找到的fetch point。
对于跨VmExecutor 的Branch 指令,代码结束点是对方VmExecutor的 ret 指令,这样可以找出每个 opcode handler 的指令流。
分析每个opcode handler 的指令流,我们要做的事情如下
1) 确认这个opcode handler 是做什么的, 给他命个名
2) 确认有没有立即数需要解码
3) 如果有立即数需要解码,这个解码代码是什么。
确认opcode handler的名称,比较好办,找出关键代码对比就可以了。
然后根据名称确认个handler是否有立即数解码, 直接提出立即数解码程序。
如果确认是 VM_HASH 指令,还需要提出 VM_HASH指令的算法,因为每个VmExecutor的 Hash 算法不一定一样。
3.3 根据VmExecutor特性产生新的代码
当我们提取完VmExecutor的特性后,这些特性需要在以后使用,这时候可以将这些算法,以及属性生成C 语言代码,然后调用build 工具编译成模块,以备以后使用。可以使用hook方式,也可以使用其它的方法,反正就是导出程序接口,让后来人可以使用程序分享 VmExecutor 的特性。
在这个新模块基础上,可以构建这个VmExecutor模拟器,这样就脱离和原始 PE文件的关系了。
