在学习《加密与解密》脱壳部分时,做了一次关于ASProtect的脱壳实验,但按照书上的步骤会报保护错误(Protect error)。为了明白原因,我继续往下深入。除了分析结果,以下还记录了关于脱壳学习的小结,方便以后自己和坛友查看。
寻找OEP
Dump
解除
“Emulate standard system functions”功能
通过脚本解除
“Emulate standard system functions”功能
1.寻找OEP
关于ASProtect壳,找到原始入口点OEP(original entry point)有几种方法,坛论上也有很多相关的帖子,比如安于此生翻译的 “使用OllyDbg从零开始Cracking 第五十一章-ASProtect v2.3.04.26脱壳-Part1
” ,其中介绍了两种方法,一是Ollybone 插件;二是最后一次异常法。Ollybone这款插件很方便,在 401000 代码段使用break-on-execute断点,即可到达OEP。不过 ASProtect的外壳的起始地址也是401000,所以需要按F7,单步跟踪来到外壳的另一个段,再使用执行断点。注意Ollybone很久没 更新了,因此不支持现在的Windows高版本,比如Win8。在《加密与解密》一书中,也介绍了两种方法,一是使用VolX脚本,二是 分析外壳代码。在第二方法中,作者分析了外壳代码,来到了处理“Emulate standard system functions”处(之后会说到), 当外壳执行完“模拟系统函数
”
的功能后,只需对401000代码段设内存断点,再点击F9,即可来到OEP。
注:书中所述的方法需要在“Options->Debugging options->exceptions”中勾选“Memory access violation”,即忽略内存访问异常断点。
2.Dump
在ASProtect这个例子中,加壳选项只设置了“Emulate standard system functions”(以下简称模拟系统函数)。如果直接到达OEP,那么Dump下来的程序是无法运行的。因为Dump的数据只包括IMAGE_SECTION_HEADER所指定的,即只有代码段、数据段等,没有外壳代码部分。(这部分外壳代码的地址是通过VirtualAlloc分配的,因此不会被Dump下来,所以当Dump下来的程序执行时,会因找不到外壳代码而出错。)接下来到正题了。
3.解除 “Emulate standard system functions”功能
为能够让大家也尝试一次,在讲述正题前,先说一下快速来到“模拟系统函数”代码的方法。首先用OD打开附件中的“TraceMe_Emulate standard system functions.exe”,点击一次F9,来到401000代码段。
对401000设内存写断点,点击F9,来到4074C3处。
连续点击F8数次,跳过rep指令,再点击F9,来到2BBCDE处。(这里不取消内存写断点,一直按住F8跳过rep要快点,也方便之后按F9再次断在401000段。)
按Ctrl+G,输入2DBA97,并对2DBA97设内存访问断点,点击F9,来到“模拟系统函数
” 代码处。(由于2B0000这片空间是动态分配的,所以大家的OD可能不是2B0000,根据自己的地址来,为定位模拟系统函数的代码,计算地址:address = 0x2B0000 + 0x20000 + 0x0BA97 = 0x2DBA97。按Alt+M,查看内存窗口,可看到从2B0000开始的空间大小是0x00043000,这片地址会执行外壳的很多操作,包括模拟系统函数。)
好了,定位到目标地址处,现在来说正题。“模拟系统函数”的功能是复制系统函数的一小段代码到外壳代码中,并修改原程序 call 调用的目的地址,使代码重定位到外壳代码处。在执行几句代码后,便跳回到系统函数的内部代码继续执行(通过push retn)。书中介绍说为了解除模拟系统函数的功能,可执行一段脚本,并在分配的空间中写入一段代码,修改call所调用的地址。当在2DBA97设置硬件断点,脚本就会执行,直到修复完所有原程序 call 调用的目的地址
。(分配空间使用了HideOD插件的Alloc Memory,脚本和写入的代码在附件raw.zip的code.txt中)
外壳修改原程序的调用地址:
002DBAB5 2BC5 sub eax, ebp
002DBAB7 83E8 05 sub eax, 5
002DBABA 45 inc ebp //外壳修改后的代码是call 0x????????,因此ebp是加1(第一个字节为 EB ),而在新写入的代码中是加2(前两个字节是 FF15)
002DBABB 8945 00 mov dword ptr [ebp], eax //此处的eax为外壳代码的地址,如0x1F0004,0x1F0000是VirtualAlloc给外壳分配的。
脚本部分:
LABEL:
cmp eip,2DBA97 //0x2DBA97设硬件断点,使脚本在 jmp LABEL 后能循序执行
jne END
mov eip,470000 //用HideOD新分配的空间
run
jmp LABEL
END:
pause
在新分配空间写入的代码部分:
00470000 60 pushad
00470001 66:C745 00 FF15 mov word ptr [ebp], 15FF //call dword ptr [0x????????] 机器码的前两个字节
00470007 BE 00404000 mov esi, 404000 //IAT起始地址
0047000C 3906 cmp dword ptr [esi], eax //在 0x2DBA97 处eax的值为即将修改的系统函数地址
0047000E 74 13 je short 00470023
00470010 83C6 04 add esi, 4
00470013 81FE D0404000 cmp esi, 4040D0
00470019 ^ 72 F1 jb short 0047000C
0047001B FF05 50004700 inc dword ptr [470050] //检查是否在IAT中,根据实验,这行代码不会被执行,只是方便之后错误的排查
00470021 EB 03 jmp short 00470026
00470023 8975 02 mov dword ptr [ebp+2], esi //call dword ptr [0x????????] 机器码的后四字节
00470026 61 popad
00470027 - E9 8DBAE8FF jmp 002DBABE
从以上代码可看到,此过程跳过了2DBA97到2DBABE的代码。当eip为 0x2DBA97,执行脚本。运行中却弹出了“Protect Error”对话框。
经过分析,发现因为没有执行这两个函数,导致在2DADEC函数中检验时会出错。
002DBAA3 E8 CCB6FFFF call 002D7174 //这两个函数实现“模拟系统函数”
002DBAB0 E8 EF010000 call 002DBCA4 //
002DBA81 E8 66F3FFFF call 002DADEC //这个函数负责检查在执行“模拟系统函数”期间,是否有什么异常发生,比如INT3断点等,如果发现则报错
可推测函数 0x002D7174 和 0x002DBCA4 在内部会动态更新某些参数,而 0x002DADEC 会检查这些参数,每当执行一次,这些参数就会改变一次,这些参数很可能保存在栈中。往回看 0x2DBA97 位置处,可发现外壳在每修改一个函数地址时,[esp+4]和[esp+8]都会动态更新,并作为参数传递给 0x2DADEC 函数以供检查,这也增加了以上推测的可能性。
这里我还是使用书中所述的方法,不过要修改脚本和写入的代码。基本思路是让外壳正常执行函数 0x002D7174 和 0x002DBCA4 ,只是不让它修改函数调用的地址。在 0x2DBA97 设置一个硬件断点,当执行脚本时跳到写入的代码,保存将被修改的函数的真正地址(该地址事先保存在eax中)。然后在 0x2DBABA设一个硬件断点,运行脚本修正原程序的调用地址。这和书上说的方法只有一点不同,即让外壳执行了函数 002D7174 和 002DBCA4。
修改后的脚本部分:
LABEL:
cmp eip,2DBA97
jne NEXT
mov eip,470060
run
jmp LABEL
NEXT:
cmp eip,2DBABA
jne END
mov eip,470000
run
jmp LABEL
END:
pause
修改后的代码部分:
00470000 60 pushad
00470001 66:C745 00 FF15 mov word ptr [ebp], 15FF
00470007 A1 70004700 mov eax, dword ptr [470070] //执行 0x470060 时保存的系统函数地址
0047000C BE 00404000 mov esi, 404000
00470011 3906 cmp dword ptr [esi], eax
00470013 74 13 je short 00470028
00470015 83C6 04 add esi, 4
00470018 81FE D0404000 cmp esi, 4040D0
0047001E ^ 72 F1 jb short 00470011
00470020 FF05 50004700 inc dword ptr [470050]
00470026 EB 03 jmp short 0047002B
00470028 8975 02 mov dword ptr [ebp+2], esi
0047002B 61 popad
0047002C - E9 8DBAE8FF jmp 002DBABE
00470060 60 pushad //新添加的代码
00470061 A3 70004700 mov dword ptr [470070], eax
00470066 61 popad
00470067 52 push edx
00470068 - E9 2BBAE8FF jmp 002DBA98
结果本应美好,但又出现了问题。第一次来到2DBA97后,设置两个硬件断点,通过HideOD分配空间,并在分配空间中写入代码,然后使用脚本让其运行。当一气呵成地点击“Plugins->ODbgScript->运行脚本”后,结果弹出了如下对话框。
之前分析一个加了ASProtect壳(ASProtect 1.2x - 1.3x [Registered] -> Alexey Solodovnikov)的软件时,还没分析出OEP就会报这个错,无论想从什么地方跳转到OEP,都会弹出这个对话框。似乎把加壳的软件调成兼容模式“Windows XP(Service Pack 2)”,就不会弹出以上对话框,但我尝试之后还是不行。如果有知道的坛友,希望能指点指点。
回到刚才报错的对话框。为了知道为什么会报这个错,我在新分配的空间中将写入代码的第一行(0x470060)设内存断点。当eip为 0x2DBA97 时,执行脚本,单步跟踪,当跳回 0x2DBA98 时,没有报错。那很可能是在第二个硬件断点处执行脚本时报错的,于是我重复之前的操作,在 0x470000 处设内存断点,当eip为 0x2DBABA 时,执行脚本,单步跟踪。当跳回 0x2DBABE 时,也没有报错。之后我试了几次,结果也是一样。因此总结当外壳修改第一个地址时,如果像刚才一样单步跟踪,就不会报错。好的,那么第一次就单步跟踪来修改原程序函数所调用的地址,之后当回到 0x2DBA97,准备修改原程序
第二个
函数所调用的地址时,执行脚本,让程序运行起来。
看着OD的代码界面一闪一闪的,从外壳跳到写入代码,再跳回外壳部分,一次一次顺利地进行着,然而当 dword ptr [esi]为2时,即还剩两个地址需要修改,这时又弹出了一个错误,无法从 0x40?????? 读取数据,eip也跑飞了。不知道问题出在何处,打算继续单步跟踪。查看在新分配空间写入的代码,在 0x470070 处保存了系统函数的地址,该地址为 0x????6BB0 = k + 0x10000, k为Kernel32.dll的代码段起始地址,函数名为GetStringTypeExA。这说明是在修改GetStringTypeExA的地址时报“读错”的。因此跟之前一样,单步跟踪完第一次地址修改,可在 0x2DBA90 处加上条件断点 eax == 0x????6BB0,其中eax保存了即将修改的系统函数地址。这样当要修改该函数地址时,能断在 0x2DBA90 处。当设好条件断点后,执行脚本,程序运行起来,最后断在了 0x2DBA90 处。之后分别在 0x2DBA97 和 0x2DBABA 处执行脚本,单步跟踪。结果是什么也没发生,一切正常。和“Workstation, No servicePack”相似,手动单步跟踪就没问题了。最后,再次执行脚本修改完所有系统函数的地址后,对401000代码段设内存访问断点,点击F9,便可跳到OEP处,这时可看到函数都被修改为真正的系统函数地址。此处把文件Dump下来,使用ImportREC修复导入表后,脱壳就结束了。
PS:关于“Workstation, No servicePack”错误 和 0x40?????? 读取错误,在我之后整理笔记,进行实验时,发现有些时候并不会报这两个错误,这可能和测试环境有关吧。所以如果没有遇到这两个错误,自然是好。如果遇到了,就手动单步跟踪,跳过这些节点即可。
4.
通过脚本解除“Emulate standard system functions”功能
以上是分析原代码来修正“模拟系统函数”,当然也可以直接来到OEP,然后Dump下来,之后通过写脚本的方式来修正这些函数调用,这里说下我的思路,如果以后有时间,可能会把脚本写出来。回到加壳的软件,重新加载,直接来到OEP,在代码区域右键“Search for->All intermodular calls”,可看到很多类似call 2010004,call 2030004这样的调用。
可以发现,每个被外壳修改后的函数调用都是 call ??????04 这样的形式,因此只要修复这些函数即可。随便跳入一个函数中,比如 0x2010004,代码如下。
02010004 FF0424 inc dword ptr [esp] //将返回地址加1
02010007 68 00000002 push 2000000 //这里有可能直接是jmp指令,如 0x1EB0004
0201000C C3 retn //跳到 0x2000000处
01EB0004 FF0424 inc dword ptr [esp]
01EB0007 - E9 F4FFFEFF jmp 01EA0000
通过以上代码可知,call 2010004 后的一个字节是垃圾数据,这也是之前修改函数为 call dword ptr []的原因,因为这种函数调用占6个字节,刚好把一字节的垃圾数据覆盖了。我们再来到 0x2000000 处。
02000000 8BFF mov edi, edi
02000002 55 push ebp
02000003 8BEC mov ebp, esp
02000005 64:A1 30000000 mov eax, dword ptr fs:[30]
0200000B 83EC 18 sub esp, 18
0200000E 53 push ebx
0200000F 8B58 10 mov ebx, dword ptr [eax+10]
02000012 56 push esi
02000013 8B35 9C07E275 mov esi, dword ptr [75E2079C]
02000019 85F6 test esi, esi //以上的代码是系统函数的开头部分,即外壳复制的一小段代码
0200001B 68 FB8FD375 push 75D38FFB
02000020 C3 retn //此处返回系统函数内部
类似 0x2010004、0x2000000组合,其他外壳修改的部分都一样。接下来是脚本的思路。
1.检查每个函数调用,如果跳转的地址 A 最后一个字节是 0x04,则执行第二步,否则继续检查下一个函数。
2.读取[A],若前三字节为0xFF0424(inc dword ptr [esp]),则A很可能是需要修正的地址,执行第三步,否则回到第一步。
3.判断 byte ptr [A+3],如果是68(push),则读取后四字节放入地址B;如果是E9(jmp),则需要将jmp后一条指令地址加上E9的后四字节,结果放入地址B。
4.循环判断 ([B]==0x68 && [B+4]==0xC3),如果不成立,B自加1,如果成立,则 dword ptr [B+1]是将要跳转的地址,将该地址放入地址C;如果找不到,则回到第一步,继续寻找。(因为有一些特例不需要修改 。)
2.Dump
在ASProtect这个例子中,加壳选项只设置了“Emulate standard system functions”(以下简称模拟系统函数)。如果直接到达OEP,那么Dump下来的程序是无法运行的。因为Dump的数据只包括IMAGE_SECTION_HEADER所指定的,即只有代码段、数据段等,没有外壳代码部分。(这部分外壳代码的地址是通过VirtualAlloc分配的,因此不会被Dump下来,所以当Dump下来的程序执行时,会因找不到外壳代码而出错。)接下来到正题了。
3.解除 “Emulate standard system functions”功能
为能够让大家也尝试一次,在讲述正题前,先说一下快速来到“模拟系统函数”代码的方法。首先用OD打开附件中的“TraceMe_Emulate standard system functions.exe”,点击一次F9,来到401000代码段。
对401000设内存写断点,点击F9,来到4074C3处。
连续点击F8数次,跳过rep指令,再点击F9,来到2BBCDE处。(这里不取消内存写断点,一直按住F8跳过rep要快点,也方便之后按F9再次断在401000段。)
按Ctrl+G,输入2DBA97,并对2DBA97设内存访问断点,点击F9,来到“模拟系统函数
” 代码处。(由于2B0000这片空间是动态分配的,所以大家的OD可能不是2B0000,根据自己的地址来,为定位模拟系统函数的代码,计算地址:address = 0x2B0000 + 0x20000 + 0x0BA97 = 0x2DBA97。按Alt+M,查看内存窗口,可看到从2B0000开始的空间大小是0x00043000,这片地址会执行外壳的很多操作,包括模拟系统函数。)
好了,定位到目标地址处,现在来说正题。“模拟系统函数”的功能是复制系统函数的一小段代码到外壳代码中,并修改原程序 call 调用的目的地址,使代码重定位到外壳代码处。在执行几句代码后,便跳回到系统函数的内部代码继续执行(通过push retn)。书中介绍说为了解除模拟系统函数的功能,可执行一段脚本,并在分配的空间中写入一段代码,修改call所调用的地址。当在2DBA97设置硬件断点,脚本就会执行,直到修复完所有原程序 call 调用的目的地址
。(分配空间使用了HideOD插件的Alloc Memory,脚本和写入的代码在附件raw.zip的code.txt中)
外壳修改原程序的调用地址:
002DBAB5 2BC5 sub eax, ebp
002DBAB7 83E8 05 sub eax, 5
002DBABA 45 inc ebp //外壳修改后的代码是call 0x????????,因此ebp是加1(第一个字节为 EB ),而在新写入的代码中是加2(前两个字节是 FF15)
002DBABB 8945 00 mov dword ptr [ebp], eax //此处的eax为外壳代码的地址,如0x1F0004,0x1F0000是VirtualAlloc给外壳分配的。
外壳修改原程序的调用地址:
002DBAB5 2BC5 sub eax, ebp
002DBAB7 83E8 05 sub eax, 5
002DBABA 45 inc ebp //外壳修改后的代码是call 0x????????,因此ebp是加1(第一个字节为 EB ),而在新写入的代码中是加2(前两个字节是 FF15)
002DBABB 8945 00 mov dword ptr [ebp], eax //此处的eax为外壳代码的地址,如0x1F0004,0x1F0000是VirtualAlloc给外壳分配的。
脚本部分:
LABEL:
cmp eip,2DBA97 //0x2DBA97设硬件断点,使脚本在 jmp LABEL 后能循序执行
jne END
mov eip,470000 //用HideOD新分配的空间
run
jmp LABEL
END:
pause
脚本部分:
LABEL:
cmp eip,2DBA97 //0x2DBA97设硬件断点,使脚本在 jmp LABEL 后能循序执行
jne END
mov eip,470000 //用HideOD新分配的空间
run
jmp LABEL
END:
pause 在新分配空间写入的代码部分:
00470000 60 pushad
00470001 66:C745 00 FF15 mov word ptr [ebp], 15FF //call dword ptr [0x????????] 机器码的前两个字节
00470007 BE 00404000 mov esi, 404000 //IAT起始地址
0047000C 3906 cmp dword ptr [esi], eax //在 0x2DBA97 处eax的值为即将修改的系统函数地址
0047000E 74 13 je short 00470023
00470010 83C6 04 add esi, 4
00470013 81FE D0404000 cmp esi, 4040D0
00470019 ^ 72 F1 jb short 0047000C
0047001B FF05 50004700 inc dword ptr [470050] //检查是否在IAT中,根据实验,这行代码不会被执行,只是方便之后错误的排查
00470021 EB 03 jmp short 00470026
00470023 8975 02 mov dword ptr [ebp+2], esi //call dword ptr [0x????????] 机器码的后四字节
00470026 61 popad
00470027 - E9 8DBAE8FF jmp 002DBABE
在新分配空间写入的代码部分:
00470000 60 pushad
00470001 66:C745 00 FF15 mov word ptr [ebp], 15FF //call dword ptr [0x????????] 机器码的前两个字节
00470007 BE 00404000 mov esi, 404000 //IAT起始地址
0047000C 3906 cmp dword ptr [esi], eax //在 0x2DBA97 处eax的值为即将修改的系统函数地址
0047000E 74 13 je short 00470023
00470010 83C6 04 add esi, 4
00470013 81FE D0404000 cmp esi, 4040D0
00470019 ^ 72 F1 jb short 0047000C
0047001B FF05 50004700 inc dword ptr [470050] //检查是否在IAT中,根据实验,这行代码不会被执行,只是方便之后错误的排查
00470021 EB 03 jmp short 00470026
00470023 8975 02 mov dword ptr [ebp+2], esi //call dword ptr [0x????????] 机器码的后四字节
00470026 61 popad
00470027 - E9 8DBAE8FF jmp 002DBABE
从以上代码可看到,此过程跳过了2DBA97到2DBABE的代码。当eip为 0x2DBA97,执行脚本。运行中却弹出了“Protect Error”对话框。
经过分析,发现因为没有执行这两个函数,导致在2DADEC函数中检验时会出错。
002DBAA3 E8 CCB6FFFF call 002D7174 //这两个函数实现“模拟系统函数”
002DBAB0 E8 EF010000 call 002DBCA4 //
002DBA81 E8 66F3FFFF call 002DADEC //这个函数负责检查在执行“模拟系统函数”期间,是否有什么异常发生,比如INT3断点等,如果发现则报错
002DBAA3 E8 CCB6FFFF call 002D7174 //这两个函数实现“模拟系统函数”
002DBAB0 E8 EF010000 call 002DBCA4 //
002DBA81 E8 66F3FFFF call 002DADEC //这个函数负责检查在执行“模拟系统函数”期间,是否有什么异常发生,比如INT3断点等,如果发现则报错
可推测函数 0x002D7174 和 0x002DBCA4 在内部会动态更新某些参数,而 0x002DADEC 会检查这些参数,每当执行一次,这些参数就会改变一次,这些参数很可能保存在栈中。往回看 0x2DBA97 位置处,可发现外壳在每修改一个函数地址时,[esp+4]和[esp+8]都会动态更新,并作为参数传递给 0x2DADEC 函数以供检查,这也增加了以上推测的可能性。
这里我还是使用书中所述的方法,不过要修改脚本和写入的代码。基本思路是让外壳正常执行函数 0x002D7174 和 0x002DBCA4 ,只是不让它修改函数调用的地址。在 0x2DBA97 设置一个硬件断点,当执行脚本时跳到写入的代码,保存将被修改的函数的真正地址(该地址事先保存在eax中)。然后在 0x2DBABA设一个硬件断点,运行脚本修正原程序的调用地址。这和书上说的方法只有一点不同,即让外壳执行了函数 002D7174 和 002DBCA4。
修改后的脚本部分:
LABEL:
cmp eip,2DBA97
jne NEXT
mov eip,470060
run
jmp LABEL
NEXT:
cmp eip,2DBABA
jne END
mov eip,470000
run
jmp LABEL
END:
pause
修改后的代码部分:
00470000 60 pushad
00470001 66:C745 00 FF15 mov word ptr [ebp], 15FF
00470007 A1 70004700 mov eax, dword ptr [470070] //执行 0x470060 时保存的系统函数地址
0047000C BE 00404000 mov esi, 404000
00470011 3906 cmp dword ptr [esi], eax
00470013 74 13 je short 00470028
00470015 83C6 04 add esi, 4
00470018 81FE D0404000 cmp esi, 4040D0
0047001E ^ 72 F1 jb short 00470011
00470020 FF05 50004700 inc dword ptr [470050]
00470026 EB 03 jmp short 0047002B
00470028 8975 02 mov dword ptr [ebp+2], esi
0047002B 61 popad
0047002C - E9 8DBAE8FF jmp 002DBABE
00470060 60 pushad //新添加的代码
00470061 A3 70004700 mov dword ptr [470070], eax
00470066 61 popad
00470067 52 push edx
00470068 - E9 2BBAE8FF jmp 002DBA98
修改后的脚本部分:
LABEL:
cmp eip,2DBA97
jne NEXT
mov eip,470060
run
jmp LABEL
NEXT:
cmp eip,2DBABA
jne END
mov eip,470000
run
jmp LABEL
END:
pause
修改后的代码部分:
00470000 60 pushad
00470001 66:C745 00 FF15 mov word ptr [ebp], 15FF
00470007 A1 70004700 mov eax, dword ptr [470070] //执行 0x470060 时保存的系统函数地址
0047000C BE 00404000 mov esi, 404000
00470011 3906 cmp dword ptr [esi], eax
00470013 74 13 je short 00470028
00470015 83C6 04 add esi, 4
00470018 81FE D0404000 cmp esi, 4040D0
0047001E ^ 72 F1 jb short 00470011
00470020 FF05 50004700 inc dword ptr [470050]
00470026 EB 03 jmp short 0047002B
00470028 8975 02 mov dword ptr [ebp+2], esi
0047002B 61 popad
0047002C - E9 8DBAE8FF jmp 002DBABE
00470060 60 pushad //新添加的代码
00470061 A3 70004700 mov dword ptr [470070], eax
00470066 61 popad
00470067 52 push edx
00470068 - E9 2BBAE8FF jmp 002DBA98
结果本应美好,但又出现了问题。第一次来到2DBA97后,设置两个硬件断点,通过HideOD分配空间,并在分配空间中写入代码,然后使用脚本让其运行。当一气呵成地点击“Plugins->ODbgScript->运行脚本”后,结果弹出了如下对话框。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
上传的附件: