能力值:
( LV4,RANK:50 )
|
-
-
2 楼
当你需要分析一个文件中的代码的时候,第一步就是需要打开这个文件,加载其代码到内存里面吧。当然我们这里只讨论一下 Windows 下的PE文件,其他操作系统下的执行文件请参考各自的文件说明。
如果要加载一个PE文件,首先会想到的是是什么? LoadLibrary? 奥,不,在我们这里不能使用 LoadLibrary, 因为LoadLibrary函数会执行很多代码,如果是dll 文件,就会执行dll 文件 EntryPoint 很多dll 在EntryPoint 中做了些额外的事情,可能修改执行环境。所以我们不能选择 LoadLibrary。那么带参数的 LoadLibraryEx 怎么样? LoadLibraryEx 也会试图解析 dll/exe 的 IAT,加载一些连接的 dll,有时候会给我们的分析造成麻烦。
所以,我们自己手工来加载 exe/dll 文件吧, 自己来实现 LoadLibraryEx 吧,记得,请不要加载 IAT中的任何 dll, 反正我们就只需要把 pe 文件摊开在内存中,其他的甚么都不要做,哦,重定位需要做一下的。
如何手工加载 pe文件? 这是搞安全工作的任务。请参考以下文章
1) 微软的msdn Microsoft Portable Executable and Common Object File Format Specification
2) Iczelion's PE Tutorial
3) ReactOS 对 PE文件的加载代码在 $ReactOs/trunk/reactos/dll/win32/kernel32/client/loader.c 里面实现
4) 看不懂英文? 百度呀,输入PE文件格式,手动加载 PE 文件等等,会列出千百万个相同的或者类似的搜索结果,当然注意开启防火墙,**的网络环境很厉害!!!!!!
当你加载完代码后,请分析一下,哪段代码是可以执行的,哪段地址所指的数据是不可执行的。这为以后我们的分析提供了必要的环境。Dll EntryPoint 在哪儿? 如果我要从dll 映像中读取一个dword 需要做些什么工作? 导入表,导出表的详细信息,什么地方导出什么函数,自己加载的好处就是,自己对代码数据的所知非常清楚。等等……
到此为止,我们就把PE文件,干干净净的加载到内存里面了
|
能力值:
( LV4,RANK:50 )
|
-
-
3 楼
Vmp 编译后的代码,会编码一个或者多个 VmExecutor, 每个 VmExecutor 在程序中会存在 好几个入口,当需要执行 ByteCode 的时候,将ByteCode的地址(加密后的) 放入栈顶,然后然后跳转进入 VmExecutor
先看一个 1.7x 的进入 Vmp的方式
.vmp1:0046B1DE push 0B50EE113h <== 这就是 Byte Code 的地址(加密后)
.vmp1:0046B1E3 call sub_45298E<== 这就是入口地址
Vmprotect 2.x 的进入方式,从 call 变成了 jmp,但是总体情况和这类似
.vmp0:104321F2 push 24BC4F16h <== 这个就是 ByteCode 地址
.vmp0:104321F7 pusha
.vmp0:104321F8 mov [esp+24h+var_8], 7DCA24EDh
.vmp0:10432200 pushf
.vmp0:10432201 pusha
.vmp0:10432202 push [esp+48h+var_48]
.vmp0:10432205 lea esp, [esp+44h]
.vmp0:10432209 jmp loc_1043002A <== 跳入 Vmp 入口
好了,现在就是,怎么找到入口地址了。
首先,入口地址的明显标志就是 jmp 或者 call , 那么,从内存中暴力找 jmp/call指令,对应的机器码为 E8h/E9h, 然后计算跳转的目标地址在不在我们加载的 pe 地址范围…… 如果在,那么就当他是嫌疑地址,注意是嫌疑地址,这样对于一个PE映像来说,可以找到好几万个这样的jmp/call点,然后就是我们下一步确认这些点
第二步,确认 VmExecutor 的入口点。我们需要一个这样的筛子,从成千上完个疑似点中,识别出谁是真正的 VmExecutor入口,谁不是。类似于这样一个函数
bool IsVmExecutor(IntPtr address);
如何实现这个函数? 我们需要借助反汇编引擎,来对程序执行流进行分析。我选用的 beaengine,主要是他能反汇编出一些非法的执行,当然也可以选择其他的引擎,有些指令是反汇编不出非法指令的类似于 sal/shl 这类的指令。借助反汇编指令,我们从疑似入口处一条一条反汇编,碰到 call 就跳转,碰到 jmp/jcc 就跳转,这样就能一直走到 retn xx,或者走到非法地址。如果走到了非法地址,那么这个入口肯定就不是入口了,如果顺利的走到了retn xx,那么我们再看看路中间是碰到过 VmExecutor 的特征指令
1) [ESP], {&ESP:ESP} = pushfd EFLAGS
进入 VmExector 的时候, x86 的所有GPR + EFLAGS都需要入栈,其他的寄存器入展方式各式各样,只有Eflags 入栈方式只有这一种! pushfd.
2) ESI = mov [ESP+xxx]*
对于VmExector 其虚拟的 Instruction Pointer (类似于x86的 eip) 100%使用的是 esi, 为什么呢?后面我们会说。也就是说,最终我们放入栈中的 ByteCode Address 最终都要通过这种方式赋值给 esi。 如果不是这种方式的,我们需要借助数据流分析了。
3) EBX = mov ESI
ebx 在 VmExecutor 中担当解码密钥的作用,每次重新设置 InstructionPointer (ESI)后,都需要更新这个密钥,值就是 esi.
4) ESI, EFLAGS = add ESI, [EBP]*
这句话其实就是地址重定位, EBP 在VmExecutor 中扮演堆栈指针的角色,最后压入堆栈的是当前 Dll的 Relocation 地址,当一个pe加载进入内存的时候,其加载的实际地址,可能和想想的地址不一样,LoadLibrary 会根据编译器的重定位信息修改实际代码, VmExecutor 也需要做类似的事情,所以这条指令少不了
5) EXX = mov [EAX*4+xxxx]*
这是VmExecutor 的指令分发器,al 是取指令后,指令的 opcode, 指令执行就需要根据 opcode 进行分发,分发最简单的方法就是查表,这就是查表了,其中 Exx 可能是 EDX,也可能是 ECX, 右边 MemoryOperand 的 Offset 就是Handler表的绝对地址,
6) RET
最后程序转入 opcode Handler 的方式为 RET 这是 VmExecutor 的御用流程控制指令……
我们看看x86的 8个 GPR 在VmExecutor是转职干什么的……
EAX 指令的 Opcode
ECX 自由
EDX 自由
EBX 机密byte code 的
ESP 这个保存着非byte的的实际堆栈,VmExecutor 不能动,只能在栈顶小打小闹的 push/pop 一堆乱代码,最终还要还原。
EBP VMExecutor 的栈顶。
ESI ByteCode 指令指针
EDI VmExecutor 的内部存贮指针,类似于 x86的 几个 GPR 的地址。
从上面看,VmExecutor 能够自由使用的寄存器只有 ECX,EDX 两个,所以这 VmExecutor 的指令分发器会有两个版本
一个是
EDX = mov [EAX*4+xxx]*
另外一个是
ECX = mov [EAX*4+xxx]*
通过一堆的特征码,就可以基本确定谁不是 VmExecutor 的入口地址了,让然通过了上面的层层筛选,留下来的高度嫌疑犯,还有真不是的,
最后一招!就是校验
ESI = mov [ESP+xxx]* 这条代码处的 esp +xxx 和入口地址处的 esp差别!
如何计算? OMG 自己跟踪 ESP的变化
可能修改 ESP的指令如下
PUSH/POP/CALL/PUSHA/POPA/POPF/POPF/ADD/SUB/LEA 等等等等,每个修改ESP的量是多少,需要查表哦。
反正这样能够跟踪 ESP的值!
1.7 VMp 的堆栈是这样的
+-------------------------+
| 32bit ByteCode Address |
+-------------------------+
| 32bit nomeaning |
| 32bit nomeaning |
+-------------------------+
| 32 bit GPR/E***S 1 |
| 32 bit GPR/E***S 2 |
| 32 bit GPR/E***S 3 |
| 32 bit GPR/E***S 4 |
| 32 bit GPR/E***S 5 |
| 32 bit GPR/E***S 6 |
| 32 bit GPR/E***S 7 |
| 32 bit GPR/E***S 8 |
| 32 bit GPR/E***S 9 |
+-------------------------+
| 32bit relocation |
+-------------------------+
2.x 版本的布局有点变法
+-------------------------+
| 32bit ByteCode Address |
+-------------------------+
| 32bit nomeaning |
| 32bit nomeaning |
+-------------------------+
| 32 bit GPR/E***S 1 |
| 32 bit GPR/E***S 2 |
| 32 bit GPR/E***S 3 |
| 32 bit GPR/E***S 4 |
| 32 bit GPR/E***S 5 |
| 32 bit GPR/E***S 6 |
| 32 bit GPR/E***S 7 |
| 32 bit GPR/E***S 8 |
| 32 bit GPR/E***S 9 |
+-------------------------+
| 32bit unknown |
+-------------------------+
| 32bit relocation |
+-------------------------+
好了,这个布局可以确定esp了吧,在 ebp还未初始化之前,VmExecutor 用的是 ESP 定位堆栈,哈,算一下
1.7 版本中, esp应该比入口 -0xE8
2.x 版本中,esp 应该比入口 -0xEC
通过上面的指令特征,以及最后的 ESP 检验的……应该就是 VmExecutor的入口了,请列个表吧!!!
在找这些入口的时候,我们中间碰到了
EXX = mov [EAX*4+xxxx]*
请把这个立即数也记下,这就是 VmExecutor的 Handle表,每个VmExecutor 有不同的Handle 表,可以做个映射表表明哪个入口对应的是哪个VmExecutor的 Handler表,便于后面的分析
本文介绍的是如何暴力列举VmExecutor的入口清单,同时确认 VmExecutor 的个数,以后将要着重分析每个 VmExecutor 的详细信息。
|
能力值:
( LV6,RANK:90 )
|
-
-
4 楼
期待后续内容,比较关心VMP的IAT修复。
|
能力值:
( LV4,RANK:50 )
|
-
-
5 楼
当前内容没有计划 修复 IAT ,因为只注重vmp保护的代码的分析和还原,IAT的修复我一般靠暴力查表! 如果加壳的时候擦掉了 idata 部分,并且 IAT最终也不再这里的话,就不好办了
|
能力值:
( LV4,RANK:50 )
|
-
-
6 楼
每个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文件的关系了。
|
能力值:
( LV4,RANK:50 )
|
-
-
7 楼
简化后 VmExecutor 的流程图, 看雪不让上传 svg格式的图,只能用rar 将就一下了
|
能力值:
( LV7,RANK:110 )
|
-
-
8 楼
这么好的文章居然还是临时用户?
|
能力值:
( LV2,RANK:10 )
|
-
-
9 楼
楼主是发文章等转为正式用户吧?
|
能力值:
( LV2,RANK:10 )
|
-
-
10 楼
好文章,喜欢技术贴,楼主辛苦了,谢谢楼主分享
|
能力值:
( LV2,RANK:10 )
|
-
-
11 楼
楼主辛苦了,希望将来研究完事可以系统的做一个PDF文档,万分感谢
|
能力值:
( LV2,RANK:10 )
|
-
-
12 楼
太好了,楼主辛苦了,希望楼主继续贴技术贴。。。。
万分感谢。。
|
能力值:
( LV2,RANK:10 )
|
-
-
13 楼
很好的科普知识,谢谢
|
能力值:
( LV2,RANK:10 )
|
-
-
14 楼
好 写的不错 我等会就给你加精华
|
能力值:
( LV2,RANK:10 )
|
-
-
15 楼
顶起 但是没看懂 学习啦
|
能力值:
( LV2,RANK:10 )
|
-
-
16 楼
最重要一个事情就是没附代码
|
能力值:
( LV2,RANK:10 )
|
-
-
17 楼
太好了,楼主辛苦了,希望楼主继续贴技术贴。。。。
万分感谢。。
|
能力值:
( LV3,RANK:20 )
|
-
-
18 楼
目前是思路吗?还是实现?有很多细节文章里都没深入
|
能力值:
( LV2,RANK:10 )
|
-
-
19 楼
留个足迹,下次有机会详细学习!
|