第八九课——深入浅出探讨脱壳细节

在做第五六课的打补丁作业时,遇到了在不脱壳的情况下修改程序。在复制到可执行文件时提示“无法定位数据”。这是因为程序加壳后,代码段显示全是空代码,我们修改代码时,系统执行将某代码1修改为某代码2,但系统在代码段找不到某代码1,所以提示“无法定位数据”。

但如果我们将程序脱壳了,就可在程序上随意修改,所以脱壳的重要性就体现出来了。脱壳毁一生,破解穷三代(bushi)。

脱壳细节

  1. 找OEP(只是万里长征的第一步)
  2. dump
  • 无法读取进程内存:换工具(Scylla不错)
  1. 修复IAT
  • 在此OEP入口没有找到任何有用信息:在你使用的工具选项中取消勾选使用来自磁盘的PE文件,还是不行换工具(Scylla)
  • 无效函数
    • 剪切指针
    • 跟踪级别1
    • 跟踪级别3
    • 插件跟踪
    • 手动查找输入表(在OD中找)
    • OD脚本

程序的加载过程:程序被加载进内存里->系统根据程序的导入表,填充程序所需的API函数到程序的内存里(IAT:导入地址表)。

壳:程序的导入表是静态可见的,包含了导入的DLL名称和其导入函数(用exeinfo可看),有些壳会在加壳的时候把导入表结构取出,然后自行加密,或改变结构。这时系统找不到导入表,也就无法给程序填充所需的API函数地址。加过壳的程序一定是可以运行的,所以很明显这个填充过程被取代了,这个过程会在壳段完成。如果脱壳时直接dump,程序的导入表也就不在了,IAT所填充的函数地址是当前系统的地址,所以这个程序可能只能在脱壳的机器上运行。为了解决这个问题,import REC导入IAT地址值生成一份导入表,然后让程序使用新的导入表,程序可跨系统,在各个机器上与运行。但有些加密壳会使import REC失效,导致导入表无法修复。

如何使import REC失效:

1
2
3
4
5
6
本来的函数调用
call addr_api

某些壳会修改代码
call packspace(packspace:junkcode/无效操作/vm)
jmp addr_api

也就是间接调用API函数,使工具无法修复导入表。

如何使import REC失效变为有效,将间接调用修改为直接调用,API有很多,所以不可能人工一个个API函数修改,而是用OD脚本。

1. 熟悉OD脚本工具的使用

在反汇编窗口右键->Script Functions->脚本运行窗口(或插件->ODbgScript->脚本运行窗口)打开脚本运行窗口。

(1)右键->载入脚本,将写好的脚本打开调试脚本。

(2)右键->运行脚本,将写好的脚本打开直接运行,不调试。

编写OD脚本的工具:

脚本的指令:

1
2
3
4
5
6
sti 				;F7
sto ;F8
bp 地址 ;F2
run ;F9
esto ;Shift+F9
gmi eip,CODEBASE ;获取代码段地址

简单写一个脚本尝试一下,脚本通常为.txt和.osc格式。

1
2
3
sti
sti
sti

载入脚本后,会自动运行脚本的第1行,按Tab键执行脚本的下一行,按空格键直接运行脚本。

2. UPX的大表哥

用普通的方法(单步跟踪、ESP定律、两次内存镜像等)就能找到OEP,关键在于修复IAT。

  1. 用importREC获取输入表,发现全都是无效函数,用跟踪级别1就可全部修复完。

  2. 编写OD脚本(这节课的重点),OD脚本是基于汇编语言编写的。

因为这个是UPX壳,有一个大跳转跳到OEP处。

1
00457765  - E9 4266FCFF     jmp upx的大?0041DDAC

编写到OEP的脚本

1
2
3
4
5
bp 00457765				;在jmp下断点
run ;F9运行至断点处
sti ;F7步入
MSG "OEP到了" ;弹窗提示OEP到了
ret ;退出脚本

直接运行脚本

但这个脚本仅限于当前程序且无重定位(如果当前程序有重定位,取消它的重定位即可)。相对来说,ESP定律的脚本更通用。

ESP定律是利用堆栈平衡找OEP的手段,首先要执行一条压栈指令来改变栈。等以后再次读取这个栈内容的时候,就说明有栈平衡的迹象,很有可能是栈的恢复。一般来说,压缩壳只有一次恢复,然后就到OEP了,利用这个特性来找OEP。

1
2
3
sti					;执行pushad
bphws esp,"r" ;给esp下硬件断点
run

执行完3行脚本后运行到00457758。还没到达OEP。

1
2
3
4
5
6
00457758    8D4424 80       lea eax,dword ptr ss:[esp-0x80]
0045775C 6A 00 push 0x0
0045775E 39C4 cmp esp,eax
00457760 ^ 75 FA jnz short upx的大?0045775C
00457762 83EC 80 sub esp,-0x80
00457765 - E9 4266FCFF jmp upx的大?0041DDAC

再改进一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sti					;执行pushad
bphws esp,"r" ;给esp下硬件断点
run
sti
sti
sti ;到了jnz
bp eip
@LOOP:
run
cmp esp,eax
jnz @LOOP
sti
sti
sti ;到OEP
ret

UPX的壳段代码是差不多的,这个脚本可以用来找这个UPX版本所有加壳程序的OEP,而且允许程序有重定位。

到OEP的下一步是找IAT,除了一些特别变态的加密壳比如:Themida,SE,VMProtect,都可以找指令为call dword...(FF 15),jmp dword...(FF 25)来找IAT。

发现注释是空的,在反汇编窗口的那条指令跟进去,push API函数地址

1
00401077    FF15 28204300   call dword ptr ds:[0x432028]
1
2
0096033C    68 88401877     push comctl32.InitCommonControlsEx
00960341 C3 retn

这两条指令相当于jmp InitCommonControlsEx。很明显,把API函数调用改成自己的函数,然后在自己的函数里跳到真实的API地址。

一般来说,间接调用API函数有两种方法:

  • 在指令二进制,直接带有地址(push)
  • 根据当前的eip加上指令中的偏移算出地址(jmp)

虽然它注释是空的,但可以知道IAT的起始位置前面全是空数据,IAT结束时是与用户函数分隔。可以推测IAT起始地址为432000,结束地址为432554

继续改进脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
mov iat_b,00432000
mov iat_e,00432554
sti ;执行pushad
bphws esp,"r" ;给esp下硬件断点
run
sti
sti
sti ;到了jnz
bp eip
@LOOP:
run
cmp esp,eax
jnz @LOOP
sti
sti
sti ;到OEP
@IAT_LOOP:
mov iat,[iat_b] ;比如iat=[432000]
cmp iat,0 ;修改不了空数据,所以要跳过
je @NEXT_LOOP
mov api,[iat+1] ;比如api=[[432000]+1]=[68 88401877+1]=88401877
mov [iat_b],api ;重建iat
@NEXT_LOOP:
add iat_b,4
cmp iat_b,iat_e
jne @IAT_LOOP
ret

运行完脚本再dump下来完成脱壳。

如果修改一个就不用脚本这么麻烦,双击下面这条指令复制API函数地址,去到对应的数据窗口地址修改其数值,右键->修改,粘贴API函数地址。

1
0096033C    68 88401877     push comctl32.InitCommonControlsEx
1
2
3
00432028  0096033C
修改为
00432028 77184088 comctl32.InitCommonControlsEx

此时就可看到API函数了。

3. 真假难辨

用单步跟踪、两次内存镜像等都能找到OEP

1
2
3
4
5
6
00446021  /.  55            push ebp                                 ;  OEP
00446022 |. 8BEC mov ebp,esp
00446024 |. 6A FF push -0x1
00446026 |. 68 70C04600 push 真假难辩.0046C070
0044602B |. 68 5CA84400 push 真假难辩.0044A85C ; SE 处理程序安装
00446030 |. 64:A1 0000000>mov eax,dword ptr fs:[0]

不会找IAT啊…不会写OD脚本…

4. 课后作业

查壳是telock的壳,用最后一次异常法到达OEP,不会去第一课找telock壳详解。也可以三次内存镜像.text->.rdata->.text直达OEP。

1
2
3
4
5
0045D4F6    55              push ebp
0045D4F7 8BEC mov ebp,esp
0045D4F9 6A FF push -0x1
0045D4FB 68 28704800 push 第八九课.00487028
0045D500 68 D4024600 push 第八九课.004602D4

方法一:重建IAT用插件跟踪,剩余4无效指针,直接剪切。程序正常运行。

方法二:用OD脚本修复IAT,起始位置为47D000,结束位置为47D6A0

下面这个脚本很容易看得懂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
;到达OEP才能用这个脚本
mov iat_s, 0047d024
mov iat_e, 0047d16c
@LOOP1:
mov iat,iat_s+663CFE
mov [iat_s],[iat]
add iat_s,4
cmp iat_s,iat_e
jne @LOOP1

mov iat_s,0047d16c
mov iat_e,0047D374
@LOOP2:
mov iat,iat_s+644310
mov [iat_s],[iat]
add iat_s,4
cmp iat_s,iat_e
jne @LOOP2

mov iat_s,0047D384
mov iat_e,0047D390
@LOOP3:
mov iat,iat_s+6A2CCE
mov [iat_s],[iat]
add iat_s,4
cmp iat_s,iat_e
jne @LOOP3

mov iat_s,0047D390
mov iat_e,0047D600
@LOOP4:
mov iat,iat_s+654543
mov [iat_s],[iat]
add iat_s,4
cmp iat_s,iat_e
jne @LOOP4

MSG "IAT修复完成!"
ret

拿@LOOP1来说,0047d024的数值为00AE0000,Alt+M进入找地址为00AE0000的数据段,开头是一大段杂乱的数据,后面是空数据,紧接着又一段有规律的数据,再接着空数据。这段有规律的数据都是以77开头的,猜测是dll领空的API函数地址。 而0047d02400AE0D22相差663CFE,所以要加上偏移663CFE即可得到API函数地址。@LOOP2到@LOOP4都同理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
00AE0D20   00 00 C0 6D F0 77 56 D2 EF 77 1D AF EF 77 FF 82  ..續饂V绎ww
00AE0D30 EF 77 17 6D F2 77 F1 7C EF 77 74 78 EF 77 D9 B4 飛m騱駖飛tx飛俅
00AE0D40 EF 77 71 5A EF 77 6D E4 EF 77 EF 61 EF 77 E0 5F 飛qZ飛m滹w颽飛郷
00AE0D50 EF 77 70 5B EF 77 79 6F EF 77 3D 99 EF 77 BE 99 飛p[飛yo飛=欙w緳
00AE0D60 EF 77 18 89 EF 77 1A E8 EF 77 A0 C7 F1 77 BB 9D 飛夛w栾w犌駑粷
00AE0D70 EF 77 86 77 EF 77 77 DE EF 77 0F 84 EF 77 78 ED 飛唚飛w揎w勶wx
00AE0D80 EF 77 A5 61 EF 77 C1 61 EF 77 2A EB EF 77 D1 86 飛飛羇飛*腼w褑
00AE0D90 EF 77 41 9D EF 77 EE BB EF 77 F3 D7 EF 77 83 9A 飛A濓w罨飛笞飛儦
00AE0DA0 EF 77 15 90 EF 77 B8 D9 F1 77 3A 71 F0 77 3D 8D 飛愶w纲駑:q饂=
00AE0DB0 EF 77 0A 70 EF 77 31 DB F0 77 6E F3 F0 77 D2 34 飛.p飛1垧wn箴w?
00AE0DC0 F0 77 5A 3F F2 77 5E EA EF 77 84 8E EF 77 65 34 饂Z?騱^觑w剮飛e4
00AE0DD0 F0 77 D8 8E EF 77 C1 DD F0 77 60 BE EF 77 F2 4E 饂貛飛凛饂`撅w騈
00AE0DE0 F2 77 38 67 F2 77 D8 D3 F0 77 58 D3 F0 77 5F 6E 騱8g騱赜饂X羽w_n
00AE0DF0 EF 77 60 83 EF 77 23 D3 EF 77 FA 6B EF 77 A0 7A 飛`冿w#语w鷎飛爖
00AE0E00 EF 77 D6 6A EF 77 D1 AB EF 77 D3 B8 EF 77 36 86 飛謏飛勋飛痈飛6
00AE0E10 EF 77 7A D8 EF 77 D7 D8 F1 77 E3 71 F0 77 81 BE 飛z仫w棕駑鉸饂伨
00AE0E20 EF 77 4C 7B EF 77 BE 95 EF 77 2C D7 EF 77 77 06 飛L{飛緯飛,罪ww
00AE0E30 F0 77 DB 5E EF 77 01 7C EF 77 79 7C EF 77 BA 92 饂踍飛|飛y|飛簰
00AE0E40 EF 77 B7 E8 EF 77 29 5E EF 77 EA C3 F0 77 89 63 飛疯飛)^飛昝饂塩
00AE0E50 F2 77 40 97 EF 77 06 98 EF 77 77 5D EF 77 A1 6A 騱@楋w橈ww]飛
00AE0E60 EF 77 A1 DD EF 77 2D A4 EF 77 00 00 00 00 00 00 飛≥飛-わw......
00AE0E70 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................

用最后一次异常法到OEP和修复IAT,来自论坛苏紫方璇的脚本:(高级,看不懂就是了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
var iat_b         ;iat起始位置
var iat_e ;iat结束位置
var tmp ;临时中转
var string ;储存代码段自动跟踪命令
var oldesp ;esp备份
var oldeip ;eip备份
var codebegin ;代码段起始地址
var codesize ;代码段大小

gmi eip,CODEBASE ;获取代码段地址
mov codebegin,$RESULT
gmi eip,CODESIZE ;获取代码段大小
mov codesize,$RESULT
add codebegin,codesize ;得到末尾地址
mov string,"eip < " ;构建自动跟踪指令
add string,codebegin
msg "请取消所有的忽略异常" ;StrongOD中的skip some exceptions要勾选
ESTO
ESTO
ESTO
ESTO
ESTO
ESTO
ESTO ;最后一次异常法
bphws [esp+4],"x" ;Seh地址下硬件断点
ESTO ;Shift+F9
bphwcall ;清除所有硬件断点
TICND string ;TICND "eip < 47d000" ;tc eip<47d000
cmt eip,"程序入口点"
msg "找到入口点"
;IAT修复
mov iat_b,0047D000
mov iat_e,0047D6A0
mov oldesp,esp ;备份esp,eip
mov oldeip,eip
@ILoop:
mov tmp,[iat_b] ;tmp=原始iat表值
cmp tmp,10000000
ja @Next ;若大于10000000就换下一个
cmp tmp,00400000
ja @Start ;若大于00400000就继续运行
cmp tmp,0
jz @Next ;等于0就换下一个
@Start:
mov eip,tmp ;设定eip
mov esp,oldesp ;恢复esp
rtr ;运行到返回
mov [iat_b],[esp] ;修复IAT
@Next:
add iat_b,4 ;下一个iat
cmp iat_b,iat_e ;判断是否结束
jnz @ILoop
mov eip,oldeip ;恢复eip,esp
mov esp,oldesp
ret