吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

搜索
查看: 3131|回复: 24
上一主题 下一主题

[原创] PESpin 0.3x兼容脱壳分析

  [复制链接]
跳转到指定楼层
楼主
镇北看雪 发表于 2020-9-12 23:55 回帖奖励
本帖最后由 镇北看雪 于 2020-9-13 10:08 编辑

写在前面


为什么说是兼容脱壳分析呢,因为此壳涉及到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的多种调用形式。有时候我们以为脱壳了,而程序放到其他机器无法运行很有可能就是因为这种情况。

加壳程序与脱壳程序.zip

453.66 KB, 下载次数: 20, 下载积分: 吾爱币 -1 CB

免费评分

参与人数 16威望 +2 吾爱币 +122 热心值 +14 收起 理由
tocabd + 1 + 1 热心回复!
小哥9527 + 1 热心回复!
晚安说给自己听 + 1 + 1 我很赞同!
gaosld + 1 + 1 热心回复!
小朋友呢 + 2 + 1 热心回复!
fengbolee + 1 + 1 用心讨论,共获提升!
Hmily + 2 + 100 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
二娃 + 2 + 1 谢谢@Thanks!
CrazyNut + 3 + 1 用心讨论,共获提升!
antiol + 3 + 1 我很赞同!
wyb1109_2008 + 1 谢谢@Thanks!
笙若 + 1 + 1 谢谢@Thanks!
pack39 + 1 + 1 虽然win壳已日落西山,但你这水平真不是一般的高
FleTime + 3 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
朱朱你堕落了 + 1 + 1 人菜看不懂,纯支持一个。
董督秀 + 1 用心讨论,共获提升!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

推荐
二娃 发表于 2020-9-14 01:10
首先感谢楼主的分享,在这里提一个小小的疑问,
为啥在修复API调用的时候是正着存GetProcAddress得到的地址然后反着写回到调用表中?是因为这个壳就是这样干的吗?
推荐
 楼主| 镇北看雪 发表于 2020-9-14 07:03 |楼主
二娃 发表于 2020-9-14 01:10
首先感谢楼主的分享,在这里提一个小小的疑问,
为啥在修复API调用的时候是正着存GetProcAddress得到的地 ...

对,一般都是这么干的
沙发
chen4321 发表于 2020-9-13 06:53
3#
pack39 发表于 2020-9-13 07:13
你这水平,比之前的脱神有过之而无不及。
4#
ekanshao 发表于 2020-9-13 10:00
谢谢楼主分享!好货!
5#
wyb1109_2008 发表于 2020-9-13 12:34
楼主辛苦了。写这么多。学习观摩中
6#
singleboy990624 发表于 2020-9-13 15:56

谢谢楼主分享!好货
7#
liltn 发表于 2020-9-13 17:42
感谢楼主分享!!
8#
我乃常山赵子龙 发表于 2020-9-13 20:37

楼主辛苦了。写这么多
9#
InMenory 发表于 2020-9-13 23:34
谢谢楼主分享!好货
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则 警告:本版块禁止灌水或回复与主题无关内容,违者重罚!

快速回复 收藏帖子 返回列表 搜索

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2020-10-21 12:46

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表