学脱ASProtect 2.1x SKE 壳必看文章
标 题: nspack3.5主程序脱壳分析(Aspr SKE 2.X)
发帖人:shoooo
时 间: 2005-12-11 13:26
原文链接:[url]http://bbs.pediy.com/showthread.php?threadid=19313[/url]
详细信息:
nspack 3.5 主程序脱壳介绍
xp sp2
flyodbg
Aspr SKE 2.X
零 需要哪里就重新来过重点分析哪里
come on let's go
一 PEiD可以不用, 但LordPE一定要先加载看看
.rsrc段上面有三个区段,没有名字,不过可以猜到是.text、.rdata和.data段,是VC的程序
二 看看能不能在OD下跑起来
OD载入nspack.exe,忽略所有异常,清除所有断点, 打上IsDebuggerPresent插件
F9运行 gogogo~
正常情况下能跑起来,alt+e看看加载的dll,看到有msvcrt.dll,没有发现mfc的dll
所以是普通VC或MFC静态
我猜我猜我猜猜猜
三 到oep看看
重来,OD截入,忽略所有...清除...打上..插件
到GetVersion的末尾retn下断
7C8114AB kernel32.GetVersion 64:A1 18000000 mov eax,dword ptr fs:[18]
7C8114B1 8B48 30 mov ecx,dword ptr ds:[eax+30]
7C8114B4 8B81 B0000000 mov eax,dword ptr ds:[ecx+B0]
7C8114BA 0FB791 AC000000 movzx edx,word ptr ds:[ecx+AC]
7C8114C1 83F0 FE xor eax,FFFFFFFE
7C8114C4 C1E0 0E shl eax,0E
7C8114C7 0BC2 or eax,edx
7C8114C9 C1E0 08 shl eax,8
7C8114CC 0B81 A8000000 or eax,dword ptr ds:[ecx+A8]
7C8114D2 C1E0 08 shl eax,8
7C8114D5 0B81 A4000000 or eax,dword ptr ds:[ecx+A4]
7C8114DB C3 retn //这里下断
F9运行,断下,F8返回,向上看看,看到oep了
00486C68 55 push ebp
00486C69 8BEC mov ebp,esp
00486C6B 6A FF push -1
00486C6D 68 38FB4A00 push nSpack.004AFB38
00486C72 68 50554800 push nSpack.00485550
00486C77 64:A1 00000000 mov eax,dword ptr fs:[0]
00486C7D 50 push eax
00486C7E 64:8925 00000000 mov dword ptr fs:[0],esp
00486C85 83EC 58 sub esp,58
00486C88 53 push ebx
00486C89 56 push esi
00486C8A 57 push edi
00486C8B 8965 E8 mov dword ptr ss:[ebp-18],esp
00486C8E FF15 6C724A00 call dword ptr ds:[4A726C] ; kernel32.GetVersion
00486C94 33D2 xor edx,edx // GetVersion返回到这里
VC6会GetVersion,VC7会GetVersionExA,可以都在末尾下断,到时候看哪个像oep附近就是了
四 输入表
GetVersion是在[4A726C],那么到那个地方向上看看,向下看看,找输入表的范围
结果
4A7000 到 4A7688
输入表没有加密 :)
有时候aspr SKE 2.X会把输入表加密,把一部分导入函数地址改的乱七八糟,可那些加密的地址是不存在的。
那它怎么用那里的导入函数呢? 其实它把代码中所有对加密导入函数的调用从原先的call [IAT]或jmp [IAT]
改成了call 00EA0000这种样子,从它自己的call 00EA0000进入导入函数,这样那些加密的导入函数就可以随便
写一个不存在的地址了。
如果输入表加密了,你可以这样完整修复:
OD截入,忽略所有...清除...打上..插件
随便对一个导入函数地址下内存写断点,比如这里GetVersion的4A726C
断了若干次后到这里
00C5764D 8902 mov dword ptr ds:[edx],eax ; // eax指向GetVersion的地址,写入ebx = 4A726C
00C5764F E9 20010000 jmp 00C57774
00C57774 8B45 0C mov eax,dword ptr ss:[ebp+C]
00C57777 8300 04 add dword ptr ds:[eax],4
00C5777A 8D85 FAFEFFFF lea eax,dword ptr ss:[ebp-106]
00C57780 3BF8 cmp edi,eax
00C57782 74 07 je short 00C5778B
00C57784 8BC7 mov eax,edi
00C57786 E8 D9ADFDFF call 00C32564
00C5778B 5F pop edi
00C5778C 5E pop esi
00C5778D 5B pop ebx
00C5778E 8BE5 mov esp,ebp
00C57790 5D pop ebp
00C57791 C2 1000 retn 10 // F8下来后这里返回
返回后
00C5795A E8 59FCFFFF call 00C575B8 //关键的call 进去看
00C5795F 0FB707 movzx eax,word ptr ds:[edi] //上面返回后是回到这里
00C57962 83C0 02 add eax,2
00C57965 03F8 add edi,eax
00C57967 8A1F mov bl,byte ptr ds:[edi]
00C57969 47 inc edi
00C5796A 3A5E 34 cmp bl,byte ptr ds:[esi+34]
00C5796D ^ 0F85 77FFFFFF jnz 00C578EA //继续当前dll的下一个导入函数
00C57973 8BDF mov ebx,edi
00C57975 8B03 mov eax,dword ptr ds:[ebx]
00C57977 85C0 test eax,eax
00C57979 ^ 0F85 0AFFFFFF jnz 00C57889 //下一个dll
C575B8这个call就是对输入表的处理
00C575B8 55 push ebp
00C575B9 8BEC mov ebp,esp
00C575BB 81C4 F8FEFFFF add esp,-108
00C575C1 53 push ebx
00C575C2 56 push esi
00C575C3 57 push edi
00C575C4 8B55 14 mov edx,dword ptr ss:[ebp+14]
00C575C7 8B5D 08 mov ebx,dword ptr ss:[ebp+8]
00C575CA 8DBD FAFEFFFF lea edi,dword ptr ss:[ebp-106]
00C575D0 8BC2 mov eax,edx
00C575D2 48 dec eax
00C575D3 83E8 02 sub eax,2
00C575D6 0FB630 movzx esi,byte ptr ds:[eax]
00C575D9 8B45 10 mov eax,dword ptr ss:[ebp+10]
00C575DC 83E8 02 sub eax,2
00C575DF 0FB600 movzx eax,byte ptr ds:[eax]
00C575E2 3B43 2C cmp eax,dword ptr ds:[ebx+2C]
00C575E5 76 06 jbe short 00C575ED //上面不去管它,这个跳转肯定满足
00C575ED 33C0 xor eax,eax
00C575EF 8A43 3B mov al,byte ptr ds:[ebx+3B]
00C575F2 3BF0 cmp esi,eax // 这里开始了4种情况的比较
00C575F4 75 5E jnz short 00C57654
C575F2的 cmp esi, eax开始了4种类型的比较
当前导入函数的类型是放在esi中,你可以在这里下个断点,然后一个一个看下来
第1种类型:用第1个密钥,还原真实导入函数地址,这里不防设esi值为1
第2种类型:用第2个密钥,还原真实导入函数地址,这里不防设esi值为2
第3种类型:用第2个密钥,不作任何处理,这里不防设esi值为3
第4种类型:GetProcAddress,这里不防设esi值为4
可见那些加密的导入函数地址,也就是第3种类型,与其说是加密,不如说是壳没有去处理
既然它和第2种类型处理方式一样,可以在cmp esi, eax这个点,修改esi中的值,把第3种
情况改成第2种情况就可以了
或者你也可以跑下去,把一些jnz或je改成magic jmp,让第3种情况跑到第2种情况也可以
需要说明的是esi的对每个aspr加壳的程序都是随机的,只要多看几个,就知道是哪个改哪个了
五 取得call 00EA0000的所有地址
按照上面所说的,可以在GetVerion返回后dump出来,然后用ImortREC修复输入表,把oep 86c68写回去
不妨叫做unpack1.exe,用od载它跑一下,它会告诉你call 00EA0000挂了,然后按F12(pause),从堆栈的
返回地址知道是这个让你挂了
00489AAB E8 5065A100 call 00EA0000
00489AB0 1283 4E04FF6A adc al,byte ptr ds:[ebx+6AFF044E]
EA0000是什么呢?
它是把导入函数调用的变形,原来的call [IAT] 和 jmp [IAT]的变形
EA0000是壳用VirtualAlloc的空间,不在区段中
在我的机机子上现在是call 00EA0000,在你的机子上就可能是call 1230000
也就是说,call 00EA0000是壳经过计算后写入的
于是我想看看,在它写入call 00EA0000前是什么样子
OD载入nspack.exe,忽略所有异常,清除所有断点, 打上IsDebuggerPresent插件
对489AAC下内存写入断点 (因为489AAB是'E8',我们要的是后4个字节)
若干次后会断在这里
00C5BAD3 8945 00 mov dword ptr ss:[ebp],eax // 断在这儿:ebp指向489AAC,eax写入后,使那个地方变成call 00EA0000
00C5BAD6 6A 0A push 0A
00C5BAD8 E8 7F9AFEFF call 00C4555C
00C5BADD 8BC8 mov ecx,eax
00C5BADF 038B E4000000 add ecx,dword ptr ds:[ebx+E4]
00C5BAE5 8BD6 mov edx,esi
00C5BAE7 8BC3 mov eax,ebx
00C5BAE9 E8 8EE5FFFF call 00C5A07C
00C5BAEE FF0C24 dec dword ptr ss:[esp]
00C5BAF1 03B3 E4000000 add esi,dword ptr ds:[ebx+E4]
00C5BAF7 833C24 00 cmp dword ptr ss:[esp],0
00C5BAFB ^ 0F87 55FEFFFF ja 00C5B956 //如果还有需要处理就跳上去
00C5BB01 53 push ebx
00C5BB02 E8 5D000000 call 00C5BB64
00C5BB07 0183 EC000000 add dword ptr ds:[ebx+EC],eax
00C5BB0D B0 01 mov al,1
00C5BB0F 83C4 24 add esp,24
00C5BB12 5D pop ebp
00C5BB13 5F pop edi
00C5BB14 5E pop esi
00C5BB15 5B pop ebx
00C5BB16 C3 retn //这里结束
正如我所说,call 00EA0000完全是在代码段解码后,申请空间,现在我申请到的是EA0000
那么它就将需要变形的地方计算后写成call 00EA0000,如果你申请到的是1230000,那么你
是call 1230000
断在这里,我当然想看一看在改写成call 00EA0000之前,那些地址是不是正常的
很可惜,那里在改写成call 00EA0000,本身就是乱掉的。
或者在某个时候能知道那些变形地址原先的真实情况,可惜我找不到。
也许只有作者知道在哪里
也许根本就找不到
因为根本就不需要
对于call 00EA0000,它加密前只要知道2件事,1.本身所在的地址 2.IAT中的位置
对于call 00EA0000,现在也只要知道2件事,1.本身所在的地址 2.最后要去的导入函数的地址
它没有理由记录IAT中的位置
我们要做的是找出最后到达的导入函数的地址,然后找出它在IAT中的位置
改成原先的call [IAT] 或 jmp [IAT]
回到正题,当我们断下时,前面可能已经处理若干个了
要想得到全部的表
你有好几种选择
1. 到oep后,写一段代码搜索出所有的call 00EA0000的地址
2. 想办法第一时间断在上面这个地方,即00C5BAD3,ebp-1就是变形的地址,保存所有的ebp-1
3. 也许内存中本身存在这张表,我没有去找,你可以找找
要找全他们并不难:)
啊,还有一个要说明的
在写入每一处的call 00EA0000时,上面的流程会经过这里
00C5B981 FFD2 call edx //call edx 结果在eax
00C5B983 807B 20 00 cmp byte ptr ds:[ebx+20],0 // eax 可能是1或0
00C5B987 0F85 3D010000 jnz 00C5BACA
如果是1,当前这个call 00EA0000处运行时,会重新回到调用地址,再进入导入函数
如果是0,当前这个call 00EA0000进入导入函数后出来(好像是废话),不过这种方式比较邪恶,它可能做更多的事情
下面我会讲到
六 call 00EA0000的修复
有没有想过一个有意思的问题,所有这样的调用都是进入EA0000一个地方,但是壳却知道最后
目的地址是哪一个导入函数,它是怎么判断的呢?
当到了EA0000,壳能看到什么?
1. 参数
2. 返回地址
第1种情况:鬼知道我会传什么参数,多少个参数,它不能作为评判标准的
第2种情况:只有你了,Aspr存着一张表,它记录了所有call 00EA0000的返回地址和最后导入函数的1对1关系
它是加密的
我们要做的是找出这张表,或者找到1个点能确定它们1对1的关系
简单说一下进入EA0000后发生了什么,一共三层
第一层:保存所有当前寄存器 (出来后还要继续运行的,不能影响后面,不过它不是明目张胆的pushad)
第二层:1. 决定是哪一种方式的导入函数调用
a. 第一种方式:将call 00EA0000 变成call F00004之类,出来后再次从原地进入F00004进入导入函数
b. 第二种方式:直接带着参数进入导入函数
2. 决定这个调用是call (ff15)还是jmp (ff25)
不要以为C的都是call,delphi的都是jmp
c. 如果是call (ff15),返回地址要+1 ,比如inc [esp],因为call 00EA0000 占5个字节,call (ff15)占6个字节
d. 如果是jmp (ff25),要esp+4,想一下就知道原因了:)
3. 如果是1.b的情况,可能有更邪恶的对下一行的偷代码,我一直没有找到好的方式解决它:(
第三层:恢复所有的寄存器返回
对于第一层的和第三层的操作,只要一路F7即可
当你看到
00EA0166 2BDA sub ebx,edx
00EA0168 FFD3 call ebx //F7进入第二层
就知道要F7进入第二层了,当然别的aspr的壳可能是call eax或call esi等等
到了第二层,代码比较工整了,可以一路F8
最后
00EB00B9 5C pop esp
00EB00BA FF6424 FC jmp dword ptr ss:[esp-4] //从第三层返回
是第三层回来,上面已提到,回来可能是回到原处call到一个新的地方进入导入函数,也可能就是完成回来
因此重点讲讲第二层
一路F8
可以看到这里
00C5B48F /75 63 jnz short 00C5B4F4 //比较call 00EA0000 返回地址的密文,不是就跳上去继续找
00C5B491 |807B 20 00 cmp byte ptr ds:[ebx+20],0 //找到了当前call 00EA0000的处理情况了
00C5B495 |74 3C je short 00C5B4D3
00C5B497 |8B45 E4 mov eax,dword ptr ss:[ebp-1C]
00C5B49A |0FB640 09 movzx eax,byte ptr ds:[eax+9]
00C5B49E |8D0440 lea eax,dword ptr ds:[eax+eax*2]
00C5B4A1 |8B5483 68 mov edx,dword ptr ds:[ebx+eax*4+68]
00C5B4A5 |8B45 FC mov eax,dword ptr ss:[ebp-4]
00C5B4A8 |FFD2 call edx //和第五章最后说的情况一下,再次比较是哪一种方式
00C5B4AA |3C 01 cmp al,1 //eax为1则是a情况,为0则是b情况
00C5B4AC |75 25 jnz short 00C5B4D3
00C5B4AE |56 push esi
00C5B4AF |8D45 FC lea eax,dword ptr ss:[ebp-4]
00C5B4B2 |50 push eax
00C5B4B3 |8B45 14 mov eax,dword ptr ss:[ebp+14]
00C5B4B6 |50 push eax
00C5B4B7 |8B45 18 mov eax,dword ptr ss:[ebp+18]
00C5B4BA |50 push eax
00C5B4BB |8B45 0C mov eax,dword ptr ss:[ebp+C]
00C5B4BE |50 push eax
00C5B4BF |8B45 F0 mov eax,dword ptr ss:[ebp-10]
00C5B4C2 |50 push eax
00C5B4C3 |8B4D 1C mov ecx,dword ptr ss:[ebp+1C]
00C5B4C6 |8B55 10 mov edx,dword ptr ss:[ebp+10]
00C5B4C9 |8BC3 mov eax,ebx
00C5B4CB |E8 C0010000 call 00C5B690 // a情况这里F7进去
00C5B4D0 |EB 01 jmp short 00C5B4D3
00C5B4D2 |E8 8D45FC50 call 51C1FA64
00C5B4D7 |8B45 14 mov eax,dword ptr ss:[ebp+14]
00C5B4DA |50 push eax
00C5B4DB |8B45 18 mov eax,dword ptr ss:[ebp+18]
00C5B4DE |50 push eax
00C5B4DF |8B45 0C mov eax,dword ptr ss:[ebp+C]
00C5B4E2 |50 push eax
00C5B4E3 |8B45 F0 mov eax,dword ptr ss:[ebp-10]
00C5B4E6 |50 push eax
00C5B4E7 |8B4D 1C mov ecx,dword ptr ss:[ebp+1C]
00C5B4EA |8B55 10 mov edx,dword ptr ss:[ebp+10]
00C5B4ED |8BC3 mov eax,ebx
00C5B4EF |E8 64F1FFFF call 00C5A658 // b情况这里F7进去
先看a情况吧,进去后一路F8
很快到了这里
00C5B7DD 8B45 F4 mov eax,dword ptr ss:[ebp-C]
00C5B7E0 8B80 E0000000 mov eax,dword ptr ds:[eax+E0]
00C5B7E6 0345 E4 add eax,dword ptr ss:[ebp-1C]
00C5B7E9 8945 FC mov dword ptr ss:[ebp-4],eax //到了这里eax的值就是导函数的地址了:)
不过我觉得这个点不太好,再往下F8
00C5B7EC 33C0 xor eax,eax
00C5B7EE 8AC3 mov al,bl
00C5B7F0 0145 10 add dword ptr ss:[ebp+10],eax
00C5B7F3 57 push edi
00C5B7F4 6A 00 push 0
00C5B7F6 8D4D E0 lea ecx,dword ptr ss:[ebp-20]
00C5B7F9 8B45 F4 mov eax,dword ptr ss:[ebp-C]
00C5B7FC 8B40 3C mov eax,dword ptr ds:[eax+3C]
00C5B7FF 8B55 FC mov edx,dword ptr ss:[ebp-4]
00C5B802 E8 6DB9FFFF call 00C57174
00C5B807 8945 FC mov dword ptr ss:[ebp-4],eax
00C5B80A 8B45 E0 mov eax,dword ptr ss:[ebp-20]
00C5B80D 8B00 mov eax,dword ptr ds:[eax]
00C5B80F E8 C0E6FFFF call 00C59ED4
00C5B814 8BD0 mov edx,eax
00C5B816 0255 DF add dl,byte ptr ss:[ebp-21]
00C5B819 8B4D FC mov ecx,dword ptr ss:[ebp-4] //这个点比较好
到了这里 [ebp-4C]是我们需要的导入函数的地址,dl中的值决定了是call(ff15)还是jmp(ff25)
dl中的值不同的程序是随机,找几个call 00EA0000进去出来看一下就知道当前的程序中哪个对应ff15,哪个对应ff25了
再来看看b情况,进去后也是一路F8
00C5A7E7 3A45 EF cmp al,byte ptr ss:[ebp-11] //al和a情况中的dl一样,决定是ff15还是ff25
00C5A7EA 0F85 9C000000 jnz 00C5A88C
00C5A7F0 EB 01 jmp short 00C5A7F3
ff15和ff25产生的分支分别能到下面
00C5A7F3 8B45 F4 mov eax,dword ptr ss:[ebp-C]
00C5A7F6 8B80 E0000000 mov eax,dword ptr ds:[eax+E0]
00C5A7FC 0145 FC add dword ptr ss:[ebp-4],eax
00C5A8A5 8B45 F4 mov eax,dword ptr ss:[ebp-C]
00C5A8A8 8B80 E0000000 mov eax,dword ptr ds:[eax+E0]
00C5A8AE 0145 FC add dword ptr ss:[ebp-4],eax
C5A7FC或C5A8AE做完后[ebp-4] 是我们需要的导入函数的地址
再看看[ebp-2c],如果它是FFFFFFFF,说明这个导入函数调用是干净的
如果它有值,表示它的下一行也偷了。具体处理可能对它下个硬件访问断点再跟踪
不过我比较没耐心
我喜欢把不干净的这些地方扣出来
然后跑过去猜
一般偷的都是
mov esi,eax 或 mov edi,eax等等
找到了这些点,写脚本也好,写代码恢复也好,修复就不难了
七 stolen oep
这个例子中没有stolen oep,所以没什么好讲,有兴趣的看看loveboom的文章
文章可能比较老,但是现在还是适用的
八 最后一些说明
到了这里差不多结束了,你可以像syscom那样,扫描所有有导入函数变形地址进行修复了
其实把原理搞清楚了,修复的时候就算碰到状况也就能知道怎么处理
脱aspr并不需要从头跟到尾,只要重点的地方耐心分析就可以了,只要耐心,你能发现更多一些东西:)