|
|
 |
|
 |
| nspack3.5主程序脱壳分析(Aspr SKE 2.X) |
|
|
|
字体:小 大 |
|
|
中华网络安全联盟 作者:佚名 来源:网络 时间:2006-3-19
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.GetVersion64:A1 18000000 mov eax,dword ptr fs:[18] 7C8114B18B48 30mov ecx,dword ptr ds:[eax+30] 7C8114B48B81 B0000000mov eax,dword ptr ds:[ecx+B0] 7C8114BA0FB791 AC000000movzx edx,word ptr ds:[ecx+AC] 7C8114C183F0 FExor eax,FFFFFFFE 7C8114C4C1E0 0Eshl eax,0E 7C8114C70BC2 or eax,edx 7C8114C9C1E0 08shl eax,8 7C8114CC0B81 A8000000or eax,dword ptr ds:[ecx+A8] 7C8114D2C1E0 08shl eax,8 7C8114D50B81 A4000000or eax,dword ptr ds:[ecx+A4] 7C8114DBC3 retn//这里下断
F9运行,断下,F8返回,向上看看,看到oep了
00486C6855 push ebp 00486C698BEC mov ebp,esp 00486C6B6A FFpush -1 00486C6D68 38FB4A00push nSpack.004AFB38 00486C7268 50554800push nSpack.00485550 00486C7764:A1 00000000 mov eax,dword ptr fs:[0] 00486C7D50 push eax 00486C7E64:8925 00000000 mov dword ptr fs:[0],esp 00486C8583EC 58sub esp,58 00486C8853 push ebx 00486C8956 push esi 00486C8A57 push edi 00486C8B8965 E8mov dword ptr ss:[ebp-18],esp 00486C8EFF15 6C724A00call dword ptr ds:[4A726C]; kernel32.GetVersion 00486C9433D2 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 断了若干次后到这里 00C5764D8902mov dword ptr ds:[edx],eax ; // eax指向GetVersion的地址,写入ebx = 4A726C 00C5764FE9 20010000 jmp 00C57774
00C577748B45 0C mov eax,dword ptr ss:[ebp+C] 00C577778300 04 add dword ptr ds:[eax],4 00C5777A8D85 FAFEFFFF lea eax,dword ptr ss:[ebp-106] 00C577803BF8cmp edi,eax 00C5778274 07 je short 00C5778B 00C577848BC7mov eax,edi 00C57786E8 D9ADFDFF call 00C32564 00C5778B5Fpop edi 00C5778C5Epop esi 00C5778D5Bpop ebx 00C5778E8BE5mov esp,ebp 00C577905Dpop ebp 00C57791C2 1000 retn 10// F8下来后这里返回
返回后 00C5795AE8 59FCFFFF call 00C575B8//关键的call 进去看 00C5795F0FB707movzx eax,word ptr ds:[edi]//上面返回后是回到这里 00C5796283C0 02 add eax,2 00C5796503F8add edi,eax 00C579678A1Fmov bl,byte ptr ds:[edi] 00C5796947inc edi 00C5796A3A5E 34 cmp bl,byte ptr ds:[esi+34] 00C5796D^ 0F85 77FFFFFF jnz 00C578EA //继续当前dll的下一个导入函数 00C579738BDFmov ebx,edi 00C579758B03mov eax,dword ptr ds:[ebx] 00C5797785C0test eax,eax 00C57979^ 0F85 0AFFFFFF jnz 00C57889 //下一个dll
C575B8这个call就是对输入表的处理
00C575B855push ebp 00C575B98BECmov ebp,esp 00C575BB81C4 F8FEFFFF add esp,-108 00C575C153push ebx 00C575C256push esi 00C575C357push edi 00C575C48B55 14 mov edx,dword ptr ss:[ebp+14] 00C575C78B5D 08 mov ebx,dword ptr ss:[ebp+8] 00C575CA8DBD FAFEFFFF lea edi,dword ptr ss:[ebp-106] 00C575D08BC2mov eax,edx 00C575D248dec eax 00C575D383E8 02 sub eax,2 00C575D60FB630movzx esi,byte ptr ds:[eax] 00C575D98B45 10 mov eax,dword ptr ss:[ebp+10] 00C575DC83E8 02 sub eax,2 00C575DF0FB600movzx eax,byte ptr ds:[eax] 00C575E23B43 2C cmp eax,dword ptr ds:[ebx+2C] 00C575E576 06 jbe short 00C575ED //上面不去管它,这个跳转肯定满足
00C575ED33C0xor eax,eax 00C575EF8A43 3B mov al,byte ptr ds:[ebx+3B] 00C575F23BF0cmp esi,eax// 这里开始了4种情况的比较 00C575F475 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),从堆栈的 返回地址知道是这个让你挂了 00489AABE8 5065A100 call 00EA0000 00489AB01283 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个字节)
若干次后会断在这里 00C5BAD38945 00 mov dword ptr ss:[ebp],eax // 断在这儿:ebp指向489AAC,eax写入后,使那个地方变成call 00EA0000 00C5BAD66A 0A push 0A 00C5BAD8E8 7F9AFEFF call 00C4555C 00C5BADD8BC8mov ecx,eax 00C5BADF038B E4000000 add ecx,dword ptr ds:[ebx+E4] 00C5BAE58BD6mov edx,esi 00C5BAE78BC3mov eax,ebx 00C5BAE9E8 8EE5FFFF call 00C5A07C 00C5BAEEFF0C24dec dword ptr ss:[esp] 00C5BAF103B3 E4000000 add esi,dword ptr ds:[ebx+E4] 00C5BAF7833C24 00 cmp dword ptr ss:[esp],0 00C5BAFB^ 0F87 55FEFFFF ja 00C5B956//如果还有需要处理就跳上去 00C5BB0153push ebx 00C5BB02E8 5D000000 call 00C5BB64 00C5BB070183 EC000000 add dword ptr ds:[ebx+EC],eax 00C5BB0DB0 01 mov al,1 00C5BB0F83C4 24 add esp,24 00C5BB125Dpop ebp 00C5BB135Fpop edi 00C5BB145Epop esi 00C5BB155Bpop ebx 00C5BB16C3retn //这里结束
正如我所说,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时,上面的流程会经过这里 00C5B981FFD2call edx //call edx 结果在eax 00C5B983807B 20 00cmp byte ptr ds:[ebx+20],0 // eax 可能是1或0 00C5B9870F85 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即可 当你看到 00EA01662BDAsub ebx,edx 00EA0168FFD3call ebx//F7进入第二层 就知道要F7进入第二层了,当然别的aspr的壳可能是call eax或call esi等等 到了第二层,代码比较工整了,可以一路F8 最后 00EB00B95Cpop esp 00EB00BAFF6424 FC jmp dword ptr ss:[esp-4]//从第三层返回 是第三层回来,上面已提到,回来可能是回到原处call到一个新的地方进入导入函数,也可能就是完成回来
因此重点讲讲第二层 一路F8 可以看到这里 00C5B48F /75 63 jnz short 00C5B4F4 //比较call 00EA0000 返回地址的密文,不是就跳上去继续找 00C5B491 |807B 20 00cmp 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 |8D0440lea 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 |FFD2call edx //和第五章最后说的情况一下,再次比较是哪一种方式 00C5B4AA |3C 01 cmp al,1 //eax为1则是a情况,为0则是b情况 00C5B4AC |75 25 jnz short 00C5B4D3 00C5B4AE |56push esi 00C5B4AF |8D45 FC lea eax,dword ptr ss:[ebp-4] 00C5B4B2 |50push eax 00C5B4B3 |8B45 14 mov eax,dword ptr ss:[ebp+14] 00C5B4B6 |50push eax 00C5B4B7 |8B45 18 mov eax,dword ptr ss:[ebp+18] 00C5B4BA |50push eax 00C5B4BB |8B45 0C mov eax,dword ptr ss:[ebp+C] 00C5B4BE |50push eax 00C5B4BF |8B45 F0 mov eax,dword ptr ss:[ebp-10] 00C5B4C2 |50push eax 00C5B4C3 |8B4D 1C mov ecx,dword ptr ss:[ebp+1C] 00C5B4C6 |8B55 10 mov edx,dword ptr ss:[ebp+10] 00C5B4C9 |8BC3mov 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 |50push eax 00C5B4DB |8B45 18 mov eax,dword ptr ss:[ebp+18] 00C5B4DE |50push eax 00C5B4DF |8B45 0C mov eax,dword ptr ss:[ebp+C] 00C5B4E2 |50push eax 00C5B4E3 |8B45 F0 mov eax,dword ptr ss:[ebp-10] 00C5B4E6 |50push eax 00C5B4E7 |8B4D 1C mov ecx,dword ptr ss:[ebp+1C] 00C5B4EA |8B55 10 mov edx,dword ptr ss:[ebp+10] 00C5B4ED |8BC3mov eax,ebx 00C5B4EF |E8 64F1FFFF call 00C5A658 // b情况这里F7进去
先看a情况吧,进去后一路F8
很快到了这里 00C5B7DD8B45 F4 mov eax,dword ptr ss:[ebp-C] 00C5B7E08B80 E0000000 mov eax,dword ptr ds:[eax+E0] 00C5B7E60345 E4 add eax,dword ptr ss:[ebp-1C] 00C5B7E98945 FC mov dword ptr ss:[ebp-4],eax//到了这里eax的值就是导函数的地址了:) 不过我觉得这个点不太好,再往下F8 00C5B7EC33C0xor eax,eax 00C5B7EE8AC3mov al,bl 00C5B7F00145 10 add dword ptr ss:[ebp+10],eax 00C5B7F357push edi 00C5B7F46A 00 push 0 00C5B7F68D4D E0 lea ecx,dword ptr ss:[ebp-20] 00C5B7F98B45 F4 mov eax,dword ptr ss:[ebp-C] 00C5B7FC8B40 3C mov eax,dword ptr ds:[eax+3C] 00C5B7FF8B55 FC mov edx,dword ptr ss:[ebp-4] 00C5B802E8 6DB9FFFF call 00C57174 00C5B8078945 FC mov dword ptr ss:[ebp-4],eax 00C5B80A8B45 E0 mov eax,dword ptr ss:[ebp-20] 00C5B80D8B00mov eax,dword ptr ds:[eax] 00C5B80FE8 C0E6FFFF call 00C59ED4 00C5B8148BD0mov edx,eax 00C5B8160255 DF add dl,byte ptr ss:[ebp-21] 00C5B8198B4D FC mov ecx,dword ptr ss:[ebp-4]//这个点比较好
到了这里 [ebp-4C]是我们需要的导入函数的地址,dl中的值决定了是call(ff15)还是jmp(ff25) dl中的值不同的程序是随机,找几个call 00EA0000进去出来看一下就知道当前的程序中哪个对应ff15,哪个对应ff25了
再来看看b情况,进去后也是一路F8
00C5A7E73A45 EF cmp al,byte ptr ss:[ebp-11]//al和a情况中的dl一样,决定是ff15还是ff25 00C5A7EA0F85 9C000000 jnz 00C5A88C 00C5A7F0EB 01 jmp short 00C5A7F3
ff15和ff25产生的分支分别能到下面 00C5A7F38B45 F4 mov eax,dword ptr ss:[ebp-C] 00C5A7F68B80 E0000000 mov eax,dword ptr ds:[eax+E0] 00C5A7FC0145 FC add dword ptr ss:[ebp-4],eax
00C5A8A58B45 F4 mov eax,dword ptr ss:[ebp-C] 00C5A8A88B80 E0000000 mov eax,dword ptr ds:[eax+E0] 00C5A8AE0145 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并不需要从头跟到尾,只要重点的地方耐心分析就可以了,只要耐心,你能发现更多一些东西:) |
|
 |
|
 |
|