写在前面
为什么说是兼容脱壳分析呢,因为此壳涉及到IAT的操作。如果不注意的话,看着好像是脱壳完成了再脱壳的机器上也能正常运行,但是一旦放到另一个机器上就无法运行。我在一开始脱此壳时就是犯了这样的错误,下面我们就来一起探索这个壳。
脱壳工具
脱壳分析过程
寻找OEP
用OD加载被加壳软件后运行发现存在很多异常,那我们就用第一次异常法来寻找OEP。
配置好OD后我们运行程序来到最后一次异常处,我们发现这里是都是一些非法指令。
那我就来到此异常的异常处理处,从堆栈视图中我们可以得知异常处理函数的地址为0x46D7B4。我们再次地址处下断点然后Shif + F9运行程序,程序会停在此处。
然后我们打开内存窗口对代码段设置内存访问断点,运行程序后程序会停在OEP处。
我们观察OEP处代码发现和正常的入口点并不同,其应该存在Stolen bytes。
解决stolen bytes
我们重新加程序发现在EP时pushad后esp为0x12FFA4,那么在执行Stolen bytes前肯定会popad。
我们先运行到最后一次异常的异常处理程序后,对0x12FFA4下硬件访问断点。
然后我们运行程序后,程序会在执行完popad指令后暂停。我们向下单步跟踪发现Stolen Bytes代码,此代码存在花指令,所以我么需要单步跟踪并将Stolen bytes代码的机器码记录。
最后我们得到了Stolen的机器码
55 8B EC 6A FF 68 60 0E 45 00 68 C8 92 42 00 64 A1 00 00 00 00 50 64 89 25 00 00 00 00 83 C4 A8
53 56 57 89 65 E8 FF 15 EC F5 46 00 33 D2 8A D4 89 15 34 E6 45 00 8B C8 81 E1 FF 00 00 00 89 0D
30 E6 45 00 C1 E1 08
接着我们将此代码粘贴到假的OEP前,最后得到真正的OEP的RVA为0x271B0。
DUMP程序
正常的步骤我们就应该在寻找完OEP后dump程序,但此壳如果在此dump就无法脱去。我们可以先用LordPR工具dump程序,后面再分析为什么不能在此dump程序。
重建输入表
重建输入表最为麻烦,我们随便找到一处API调用。我们发现并没有看见直接的API调用提示,但是发现了类似于API调用的语句,call Xdword ptr ds:[0x46FA0C]。我们来到0x46FA0C地址处发现此处类似于一个函数的头部并且存在花指令。
我们F8单步向下执行发现其会jmp 到一个高地址处,此地址应该是某个dll的输出函数所在的空间。
我们来到此地址处发现此地址处为某个API的指令,而且我们发现此API此地址前的指令和刚刚在跳转到此地址前执行的指令相同。我们得知此壳应该是对程序调用的API头部进行了HOOK,将API头部的代码拷贝到自己申请的内存中自己执行后,又会跳到对应API地址处执行HOOK的API剩余的指令。
由 call Xdword ptr ds:[0x46FA0C] 指令可得0x46FA0C内存中保存的是壳代码动态申请内存的地址,我们需要将此地址处的内存的值修改为真正API的地址。我们重新加载程序然后来到最后一次异常处理程序地址处,我们对0x46FA0C地址下内存写入断点。运行程序后程序停止,我们发现此处其会将壳代码动态申请的内存地址存到此内存处。
如果把eax变为API真正的地址就可以使绕过壳对API的hook了。我们需要得到壳代码什么时候获得API真正的地址要想得到API真正的地址壳代码一定会调用GetProcAddress(),重新加载程序还是来到最后一次异常处理程序处,然后我们对GetProcAddress()API下断点(最好在API返回处下断点,因为测试发现壳代码会对API头部进行检测是否有断点)。运行程序发现程序断在GetProcAddress()函数处,我们Alt + F9返回到用户代码,可以看到eax为对应API的代码。
我们就可以用此关键点结合上一个可以绕过API hook的关键点进行打补丁。思路是:因为此关键处可以得到API真正的地址,我们在此处将API地址保存,另一处关键点如果eax为真正的API地址就可以将绕过API HOOK,那我们就在那打补丁让刚刚保存的API地址赋值给eax。
我们先在能获得eax地址的地方跳到一个空白地址处打补丁将API保存起来。
补丁代码为下,将API地址按双字保存到地址0x0045C700处。(0x477389内存处值为0x0045C700)
00477321 60 pushad
00477322 8B1D 89734700 mov ebx,dword ptr ds:[0x477389]
00477328 8903 mov dword ptr ds:[ebx],eax
0047732A 83C3 04 add ebx,0x4
0047732D 891D 89734700 mov dword ptr ds:[0x477389],ebx
00477333 61 popad
00477334 894424 1C mov dword ptr ss:[esp+0x1C],eax
00477338 61 popad
00477339 ^ E9 404BFFFF jmp 0046BE7E
0047733E 90 nop
然后我们在第一个关键处跳到一个空白地址处,打补丁将eax变为API真正的地址。
补丁代码为下,将刚刚保存的API地址取出来并赋给eax替换壳代码动态申请的地址,从而绕过API HOOK
00477349 60 pushad
0047734A 8B1D 89734700 mov ebx,dword ptr ds:[0x477389]
00477350 83EB 04 sub ebx,0x4
00477353 8B03 mov eax,dword ptr ds:[ebx]
00477355 8907 mov dword ptr ds:[edi],eax
00477357 61 popad
00477358 ^ E9 B94CFFFF jmp 0046C016
然后我们在代码块设置内存访问断点,运行程序后程序会停在假的OEP处。这时我们在看 call Xdword ptr ds:[0x46FA0C] API调用指令时其已经将API HOOK绕过直接调用API了。
按正常的规律0x46FA0C应该就是IAT所在处了,但是我么你在内存中查看发现实际API地址会 保存在一个以地址0x46F42A开头的表中,且API地址以一个字节0间隔,有的还以两个字节0间隔。正常的IAT是相同的dll函数地址在一起,不同的API地址以双字节0间隔。所以ImportRE是无法识别此表得,也就是无法重建输入表。我们需要形成一张规则的IAT表,我们刚刚打补丁将API地址都保存在了0x45C700处了。我们查看此地址发现此表基本符合IAT表的特征,但是其kernel32.dll的函数和ntdll.dll的函数混在了一起,而且各个不同的dll函数没有用双字节0间隔,
所以我们用OD脚本稍微修正一下,让其符合标准的IAT的特征。
var var_begin
var var_end
var var_address
var var_num
var var_value
var var_value1
mov var_begin,0045c704 //我们存放规则IAT表的地址为0x0045C700
mov var_end,0045c930 //kernel32.dll和ntdll.dll没修正前的结束地址
mov var_address,0045c704 //将修正后的kernel32.dll与ntdll.dll存在此地址后
mov var_num,0
Start: //Start将kernel32.dll与ntdll.dll分开
cmp [var_begin + var_num],7c920000
jb e0
mov var_value,[var_address]
mov [var_address],[var_begin + var_num]
mov [var_begin + var_num],var_value
add var_address,4
e0:
add var_num,4
mov var_value1,var_begin
add var_value1,var_num
cmp var_value1,var_end
ja Start0
jmp Start
Start0: //Start0将连续的IAT表不同的DLL函数用双字节0分开。
mov var_begin,0045cde0 //IAT表最后一个API地址所在的IAT地址
Start1: //判断是否到达不同DLL的交界处
mov var_num,C
cmp var_begin,0045cda4
je ee0
ja ee1
dec var_num
cmp var_begin,0045cd64
je ee0
ja ee1
dec var_num
cmp var_begin,0045cd60
je ee0
ja ee1
dec var_num
cmp var_begin,0045cd58
je ee0
ja ee1
dec var_num
cmp var_begin,0045cd4c
je ee0
ja ee1
dec var_num
cmp var_begin,0045cd38
je ee0
ja ee1
dec var_num
cmp var_begin,0045cd18
je ee0
ja ee1
dec var_num
cmp var_begin,0045ccf4
je ee0
ja ee1
dec var_num
cmp var_begin,0045cbc0
je ee0
ja ee1
dec var_num
cmp var_begin,0045c934
je ee0
ja ee1
dec var_num
cmp var_begin,0045c72c
je ee0
ja ee1
dec var_num
cmp var_begin,0045c704
ja ee1
dec var_num
ee0: //如果到达交界处就将交界上一个双字节写入0
mul var_num,4
mov [var_begin + var_num - 4],0
jmp ee2
ee1:
mul var_num,4
ee2:
mov [var_begin + var_num],[var_begin] //将IAT表项下移
sub var_begin,4
cmp var_begin,0045c700 //判断是否达到IAT表头,到达则结束脚本运行
jne Start1
mov [var_begin + 4],0
End:
ret
修正后的API表如下图,且符合IAT表的特征。
我们接下来需要将原来call API的指令即call不规则API地址表,变为call我们这个规则IAT表。借助下面脚本实现此功能。
var var_begin
var var_end
var var_IAT
var var_address1
var var_address2
var var_value
mov var_begin,401000 //代码段的起始地址
mov var_end,44B000 //代码段的结束地址
mov var_IAT,45C700 //我们修正后的IAT表所在的起始地址
Start:
findop var_begin,#FF15??# //查询CALL指令的机器码
mov var_address1,$RESULT //判断call指令所在地址是否超过代码段范围
cmp var_address1,var_end //超出就直接结束脚本运行
ja End
add var_address1,2 //获取call指令后面的[]中的值
mov var_value,[var_address1]
mov var_value,[var_value]
mov var_begin,var_address1
cmp var_value,50000000 //判断值是否大于0x50000000
ja e0 //大于说明就是API调用,否则就是正常的函数调用
jmp Start
e0:
mov var_address2,var_IAT
e1:
cmp [var_address2],var_value //查询并判断IAT表中与对应call调用的API地址相等的值
je e2
add var_address2,4
jmp e1
e2:
mov [var_address1],var_address2 //查询到后将call不规则API地址表换为 CALL我们修正后的IAT表
jmp Start
End:
ret
查看call API的指令发现,其API地址为我们修正的IAT表中的值。这样我们就可以用ImportRE工具来获取IAT并重建输入表了。
接着我们将入口点代码恢复,然后用LordPE工具dump程序。先修正镜像大小,然后在完整转存。
然后用ImportRE重建输入表,输入OEP的RVA为0x271b0, IAT的RVA为0x5c700。点击获取输入表信息,并截切掉无效的数据后得到完整的IAT,接着修复dump文件。
我们运行修复后的文件发现崩溃,用OD载入程序发现程序会调用0x400178。后面也会有一些call 401000前地址的指令,这是因为壳程序为了防止dump程序,将一部分代码写到了PE文件头的映射处,这样dump时就无法dump这段程序。
解决Anti dump
我们可以将未脱壳程序的0x400000PE文件头映射处的0x1000大小的数据拷贝到程序其他地方,然后在程序运行到达入口点前将这部分数据复制到PE文件头中,这样还需要将程序的入口点改为实现此功能的代码的开始处。
在0x44A1AF处打补丁,下面是补丁程序:
0044A191 . 56 69 72 74 75 61 6C 50 72 6F 74 65 63 74 00 ascii "VirtualProtect",0
0044A1A0 00 db 00
0044A1A1 . 6B 65 72 6E 65 6C 33 32 2E 64 6C 6C 00 ascii "kernel32.dll",0
0044A1AE 00 db 00
0044A1AF > $ 60 pushad
0044A1B0 . B8 A1A14400 mov eax,0x44A1A1 ; ASCII "kernel32.dll"
0044A1B5 . 50 push eax ; /FileName => "kernel32.dll"
0044A1B6 . FF15 C8C84500 call Xdword ptr ds:[0x45C8C8] ; \LoadLibraryA
0044A1BC . 68 91A14400 push 0x44A191 ; /ProcNameOrOrdinal = "VirtualProtect"
0044A1C1 . 50 push eax ; |hModule
0044A1C2 . FF15 CCC84500 call Xdword ptr ds:[0x45C8CC] ; \GetProcAddress
0044A1C8 . 68 81A14400 push 0x44A181
0044A1CD . 6A 40 push 0x40
0044A1CF . 68 00100000 push 0x1000
0044A1D4 . 68 00004000 push 0x400000
0044A1D9 . FFD0 call Xeax
0044A1DB . BB 00000000 mov ebx,0x0
0044A1E0 > 8A8B 00D04500 mov cl,byte ptr ds:[ebx+0x45D000]
0044A1E6 . 888B 00004000 mov byte ptr ds:[ebx+0x400000],cl
0044A1EC . 43 inc ebx
0044A1ED . 81FB 00100000 cmp ebx,0x1000
0044A1F3 .^ 72 EB jb X0044A1E0 ; dumped_3.0044A1E0
0044A1F5 . B8 A1A14400 mov eax,0x44A1A1 ; ASCII "kernel32.dll"
0044A1FA . 50 push eax ; /FileName => "kernel32.dll"
0044A1FB . FF15 C8C84500 call Xdword ptr ds:[0x45C8C8] ; \LoadLibraryA
0044A201 . 68 91A14400 push 0x44A191 ; /ProcNameOrOrdinal = "VirtualProtect"
0044A206 . 50 push eax ; |hModule
0044A207 . FF15 CCC84500 call Xdword ptr ds:[0x45C8CC] ; \GetProcAddress
0044A20D . 8B1D 81A14400 mov ebx,dword ptr ds:[0x44A181]
0044A213 . 68 81A14400 push 0x44A181
0044A218 . 53 push ebx
0044A219 . 68 00100000 push 0x1000
0044A21E . 68 00004000 push 0x400000
0044A223 . FFD0 call Xeax
0044A225 61 popad
0044A226 ^ E9 5CCFFDFF jmp 00427187 ; dumped_3.00427187
然后我们重新dump并重建输入表,并且需要将入口点的RVA改为0x4A1AF。得到最终的程序,我们运行发现运行成功。
如果你认为我们已经脱壳成功那么你就错了,我们刚刚是不兼容的脱壳方式,虽然在脱壳的机器上可以运行,但是放到另外一个机器上就无法运行。在另一个机器上我们用OD分析一下。我们发现其会从那个不规则的函数地址表里获取API的地址,并调用它。因为我们在dump程序时已经将此表写死,所以此API地址如果在脱壳机器上运行因其dll每次加载的基地址都相同,所以API的地址也相同因此能正常运行。但是放到另外一个机器上因为dll加载基地址不同了,所以其API的地址也不相同了。导致其call了一个无效的地址。
这是因为我们在脱壳的时候只考虑到了其程序会通过CALL [ 不规则地址表 ]的形式调用API,而忽略了其还会通过mov eax,[不规则地址表] call eax 。 jmp [不规则地址表]的形式调用API,导致会存在不兼容的问题。
解决兼容
为了解决兼容性的问题我们需要将不规则表的地址换成我们自己补丁程序的值,然后通过补丁程序来进一步调用我们自己修正的规则IAT表。
我们在将补丁程序放到0x45D000地址处,通过下面的脚本打补丁。
var var_begin
var var_address1_1
var var_address1_2
var var_address2_1
var var_num1
var var_num2
var var_num3
var var_value1
mov var_begin,0045d000 //补丁起始地址
mov var_address1_1,0046f42a //不规则API表起始地址
mov var_address1_2,0046fba8 //不规则API表结束地址
mov var_address2_1,0045c700 //我们修正的规则IAT表起始地址
mov var_num1,0
mov var_num3,0
Start:
mov var_value1,[var_address1_1 + var_num1],1 //因为是不规则API地址表,我们需要定位一个正确的API地址项
cmp var_value1,0
jne e
mov var_value1,[var_address1_1 + var_num1 + 1]
cmp var_value1,70000000
jb e
inc var_num1
e:
mov var_value1,[var_address1_1 + var_num1] //获得不规则API地址表的表项的API地址
mov var_num2,0
e0:
cmp [var_address2_1 + var_num2],var_value1 //通过我们修正的IAT中查找相等的API地址,然后得到其IAT表地址
je e1
add var_num2,4
jmp e0
e1:
mov [var_begin + var_num3],25FF //写入JMP的机器码25FF
mov [var_begin + var_num3 + 2],var_address2_1 + var_num2 //写入对应规则IAT表的对应项
mov [var_address1_1 + var_num1],var_begin + var_num3 //在不规则API表中写入我们补丁对应的地址
add var_num3,6 //定位到下一个补丁位置
add var_num1,5 //定位到下一个不规则API地址表项
cmp var_address1_1 + var_num1,var_address1_2 //判断是否达到不规则地址表最后一个表项
jne Start
mov var_address1_1,00460818
mov var_address1_2,00460f28
mov var_num1,0
Start1: //其除了存在一个不规则的API地址表,实际还存在一个规则的API地址项
mov var_value1,[var_address1_1 + var_num1] //下面逻辑和上面相似
cmp var_value1,01000000
ja ee
add var_num1,4
jmp Start1
ee:
mov var_num2,0
ee0:
cmp var_value1,[var_address2_1 + var_num2]
je ee1
add var_num2,4
jmp ee0
ee1:
mov [var_begin + var_num3],25FF
mov [var_begin + var_num3 + 2],var_address2_1 + var_num2
mov [var_address1_1 + var_num1],var_begin + var_num3
add var_num3,6
add var_num1,4
cmp var_address1_1 + var_num1,var_address1_2
jne Start1
END:
ret
打完补丁后我们可以看到0x45D000处都变为了jmp[ ]的API调用指令。这样无论是call [ ]形式的调用,还是mov eax , [ ] call eax的形式,还是jmp [ ]的形式都会来到此补丁处,然后通过此补丁调用对应的API。
因为我们把补丁程序放到了这,而我们前面将0x400000PE文件头防Anti Dump的数据也放到了这,所以我们需要寻找放到其他位置,我放到了0x45F000处。相应的我们前面在代码入口点打的补丁程序(为了将PE文件头映射地址数据恢复)也要稍微修改一下。
这样就可以实现兼容脱壳,现在我们将程序放到新机器上可以正常运行。
总结
这个壳难点在如何解决兼容,也就是容易忽略API的多种调用形式。有时候我们以为脱壳了,而程序放到其他机器无法运行很有可能就是因为这种情况。