• 标 题:ACProtect Professional 1.3C 主程序脱壳
  • 译 者:softworm
  • 时 间:2004-11-26
  • 链 接:http://bbs.pediy.com

脱壳过程感觉与Unpacking Saga的那个UnpackMe没有太大的不同。最明显的一点是,其中的异常多了很多。大部分是固定模式的int 3解码。因为一开始打算全程跟,所以一边跟一边修改去除junk codeIDC script,OllyScript脚本,并用WinHex把解出的代码贴到ACProtect_Fixed.exe,IDA中用Load File|Reload the input file加载后对照。

 

如果在OllyDbg110C下忽略所有异常,运行时报:

 

 

1.     关于int 3解码

 

壳中有大量的int 3解码,一边执行一边解。具有相同的调用模式:

 

 

int 3SHE(下面的div 0异常也是同一个SEH),exception code0x80000003,Dr0-Dr3中设置了新的值,即设置了4个硬件执行断点,就在紧临int 3下面的位置。这样执行到这4,会触发4次异常。如果在这里F7,SHE不会执行。

 

 

当由上面的4个断点引发异常时:


每次异常,会设置某个寄存器。4次异常处理必须执行,否则寄存器中没有正确值,下面的解码会失败。这样又顺便清除cracker的断点,是个不错的主意J

 

 

结束异常处理后,开始解码。格式是一样的,用的register不同。这里可以得到起始地址(<-edi)

 

 

jnz下面的地址F4,然后查看前面用于寻址的寄存器的值,1就是解码结束地址。

 

解完后跟着一个div 0异常。只有anti-debug作用,直接跳过即可。

 

 

2. 避开IAT加密

 

在第16int 3的解码过程中,隐藏了IAT加密。其实在OllyDbg的状态栏可以看到这附近有不少dll被加载了。

4处检查,决定是否特殊处理。在用GetProcAddress得到API的地址后,开始检查。是否为特殊的API?

 

是否为MessageBoxARegisterHotKey(这个比较少见J)? 是则替换api地址。

 

下面检查dll的映射地址是否为kernel32.dlluser32.dll

 

这里通不过检查,就不做处理,直接把API的地址保存到IAT了。否则:

IAT:

 

用脚本执行到这一段,patch4处的跳转,在处理完全部dll的地方下断,ImportRec得到完整的IAT,全部有效。

 

 

size = 6E3828 – 6E3140 = 6E8

 

注意此时OEP不对。但IAT已经拿到了。先把tree保存起来。

 

 

3. 寻找OEP

 

得到IAT,试试忽略所有异常,到这里已经可以在OllyDbg下执行了。试了一下,关闭其它异常,只留下int 3,还要shift-F9 60次才能运行L。这样一步步跟不把人累死。算了,先换省力的办法吧。

 

干脆,在执行目前的script停下后,忽略所有异常。对Code section下内存访问断点。

 

 

1) 406EDC,返回到壳

 

返回到壳代码,所以这是stolen code发出的call

 

 

2) 46261C,返回到壳

 

 

3) 读操作断下

 

 

4) 462634,返回到壳

 

这里的代码也会引发异常。先remove内存访问断点,73CEDD设断。断下后恢复内存访问断点。

5) 读操作断下

 

 

6) 又在462634,是循环吗?按第4步的办法处理

 

 

7)到站了J

 

 

这里是false OEP。下面的空间有限。可以看到,stolen codes中含有的call:
406EDC,46261C,462634(
执行2)

 

dump窗口中看:

 

OEP应该在4D9DE4

4. dump

 

根据脱US UnpackMe的经验,不能在false OEPdump,此时BSS section中许多数据已经初始化了。最好在第一句(push ebp)dump。可是那个push ebpfalse OEP很远L

 

Packer EP的初始化环境:

 


 

 

先看看发出第1call的壳代码:

 

可以看到(跟一下可以证实),call入原程序空间前,最后的异常是div 0。用现在的OllyDbg脚本(跟到737B63),停下后修改异常拦截选项:

 

试试能否拦截异常找到合适的dump位置。当前OllyScript脚本停下时的环境:

 

 

栈中为pushad的结果。

 

7div 0异常时:

 

 

注意ebp的值已经入栈了。Pushad的结果,对应寄存器值为:
esp = 12FFC0         (
原来为12FFC4)

ebp = 12FFC0 = esp (原来为12FFF0)

 

所以,在这里已经执行过了:

Push ebp

Mov ebp,esp

 
 

 

 

 


看看BSS section:

 

 

全为0,就在这里dump如果需要仔细跟stolen code,也可以从这里入手(可以从脚本停下的第6div 0异常处理后开始)

 

先用4D9DE4OEP

 

 

LoadPE查看节表:

 

先把CODEVsizeRsize加大,以避免修复stolen codes时空间不够。

 

 

重新跟到false OEP(4D9E37)。重建输入表。

 

 

用了Add new section,会不会有问题(好象有篇脱文里提到过这个)? 先这样处理。

IDA编译dumped_.exe,结果不错。

 

现在剩下stolen codereplaced code

 

 

5. 修复stolen code

 

既然不打算仔细跟,就只有猜了。对执行第1call以前的stolen code进行猜测,应该是安全的。后面的4call需要仔细看看。

 

1) call 406EDC

 

IDA中看dumped_.exe的结果:

 

先执行脚本,停下后只勾选div 0异常。直到73A9AF。经过一番遮遮掩掩的代码:

 

call前的寄存器:

 

堆栈:

 

call完后,下面的pushad为分界线,标识stolen code的结束。所以到这里的stolen code(对于不确定的,可以在Packer EP修改寄存器值,到这里核对):

Push ebp

Mov ebp,esp

Push edx

Push edi

Push ebx

Push eax

Mov eax,4D9BCC
call 406EDC

mov eax,dword ptr ds:[503B94]

 

 
 

 

 

 

 

 

 

 

 

 

 


Call完后,pushad前的环境:

 

 

2) call 46261C

 

 

 

到这里的stolen code:

Push ebp

Mov ebp,esp

Push edx

Push edi

Push ebx

Push eax

Mov eax,4D9BCC
call 406EDC

mov eax,dword ptr ds:[503B94]

 

mov eax,dword ptr ds:[eax]

call 46261C

push edi

mov ecx,dword ptr ds:[503938]

 

 

 
 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


从这里开始,不能图省事了。要追出全部的stolen code,必须跟(必须确保上一次pushad和下一次popad时的环境一致,才不会丢代码)。先试直接拦截div 0异常,注意后面是否有popad。如果两次的环境不一致,则必须单步跟L

 

另外,stolen code不是一次完成的,所以有些图中寄存器和堆栈中的数据对不上,不必管它。

 

3) 0073BC47

 

 

 

 

 

 

 

 

 

 

Push ebp

Mov ebp,esp

Push edx

Push edi

Push ebx

Push eax

Mov eax,4D9BCC
call 406EDC

mov eax,dword ptr ds:[503B94]

 

mov eax,dword ptr ds:[eax]

call 46261C       

//这里去掉前面的push edi

mov ecx,dword ptr ds:[503938]

 

mov eax,dword ptr ds:[503B94]

mov eax,dword ptr ds:[eax]

push eax

push ecx  

mov ecx,47FCD8    //多余,前面push ecx,后面还会pop的吧?

mov eax,47FCD8

 
 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


4) 0073C558

 

Push ebp

Mov ebp,esp

Push edx

Push edi

Push ebx

Push eax

Mov eax,4D9BCC
call 406EDC

mov eax,dword ptr ds:[503B94]

 

mov eax,dword ptr ds:[eax]

call 46261C       

mov ecx,dword ptr ds:[503938]

 

mov eax,dword ptr ds:[503B94]

mov eax,dword ptr ds:[eax]

 

mov edx,dword ptr ds:[47FCD8]

push edx

mov edx, 462634      //注意这是下一次call的地址

 

 
 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


5) 1call 462634

 

这个函数有2个参数L

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Push ebp

Mov ebp,esp

Push edx

Push edi

Push ebx

Push eax

Mov eax,4D9BCC
call 406EDC

mov eax,dword ptr ds:[503B94]

 

mov eax,dword ptr ds:[eax]

call 46261C       

mov ecx,dword ptr ds:[503938]

 

mov eax,dword ptr ds:[503B94]

mov eax,dword ptr ds:[eax]

 

mov edx,dword ptr ds:[47FCD8]

 

call 462634

push eax

push esi

mov eax, 50385C

 
 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


6) 2call 462634

Push ebp

Mov ebp,esp

Push edx

Push edi

Push ebx

Push eax

Mov eax,4D9BCC
call 406EDC

mov eax,dword ptr ds:[503B94]

 

mov eax,dword ptr ds:[eax]

call 46261C       

mov ecx,dword ptr ds:[503938]

 

mov eax,dword ptr ds:[503B94]

mov eax,dword ptr ds:[eax]

 

mov edx,dword ptr ds:[47FCD8]

 

call 462634

 

mov ecx,dword ptr ds:[50385C]

mov eax,dword ptr ds:[503B94]

mov eax,dword ptr ds:[eax]

mov edx,dword ptr ds:[47F948]

call 462634

mov eax,dword ptr ds:[503B94]

mov eax,dword ptr ds:[eax]

 

 
 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


                                                            

 

 

这是最后一次后面存在popaddiv 0异常。看看是如何回到原程序的。

73D872忽略所有异常,code section下内存访问断点。中间在kernel32中有一次内存访问异常。

 

 

最后一次div ebx73DBE1。单步跟到这里:

 

73DE38破坏前面代码。

 

 

至此修复所有stolen codes,将前面的6部分和fasle OEP2call合在一起。

 

Push ebp

Mov ebp,esp

Push edx

Push edi

Push ebx

Push eax

Mov eax,4D9BCC
call 406EDC

mov eax,dword ptr ds:[503B94]

mov eax,dword ptr ds:[eax]

call 46261C       

mov ecx,dword ptr ds:[503938]

mov eax,dword ptr ds:[503B94]

mov eax,dword ptr ds:[eax]

mov edx,dword ptr ds:[47FCD8]

call 462634

mov ecx,dword ptr ds:[50385C]

mov eax,dword ptr ds:[503B94]

mov eax,dword ptr ds:[eax]

mov edx,dword ptr ds:[47F948]

call 462634

mov eax,dword ptr ds:[503B94]

mov eax,dword ptr ds:[eax]

call 4626B4

call 4049D8

lea eax, [eax+0]

 
 

 

 

 

 

 

 

 

 

 

 

 

6. 修复IAT

 

运行程序,crashedL。直接用修复完stolen codedumped_.exe看看。从EP的第1call进去就有问题。

 

 

 

 

 

OllyDbg中可以看到:

 

有部分IAT在壳中。这部分代码前面是跟到了的(在第6int 3以后),原来认为这只是loader自己需要的API实际上正常的程序代码也使用了这个IAT

 

如果用ImportRec把这部分也取出来,属于不同dllapi混排了,ImportRec不能处理。

这些地址是在736643处理的。

 

而且有一个不能识别(这个实际上是Hooked MessageBoxA)

 

 

估计这是加壳时做的手脚。当发现主程序使用了与壳代码同样的API,修改了对应的调用代码,使其调用到壳中的IAT。这样可以加强与壳的联系。反过来看,这里大部分API(除了只有壳使用的),在前面避开IAT加密处理后,都已经import了。

 

ACProtect_Fixed中已经有解好的api名。

 

紧临的函数指针:

 

 

注意函数名及指针与7339A9附近的代码并不完全对应。有的函数名或指针在别处。

 

可以在程序入口自己调用LoadLibrary,GetProcAddress进行处理,填入相应的地址。或者把Loader的代码搬过来。也可以修改主程序的代码,使其调用前面得到的干净的IAT而不是壳中的IAT(这样要处理的地方太多,很麻烦)

 

注意ACProtected_Fixed,import了几个基本的API:

在加载API时壳代码会使用这些函数。这几个地址直接填入007300D8开始的IAT中了。修改dumped_.exeEP,从壳代码开始(可以看到dump出来的代码,这部分已经解开了),

 

 

 

注意Loader代码中用的几个API(GetModuleHandleA,GetProcAddress)改为使用避开IAT加密后得到的主程序的IAT

 

原来在壳代码import4API,从主程序的IAT直接取。结束处理后跳回前面的OEP 409DE4。用pushad,pushad保存寄存器值。

 

 

注意最后一项7301CC实际是MessageBoxA,ACProtect用做SDK的接口 不要填入MessageBox的真正地址。

 

下面的73013C在跟原程序的过程中始终为0,可能是注册版才有?

 

处理完后,位于壳空间的IAT已经赋上了正确值。

 

 

7. 修复Replaced code (1)

 

用前面的OllyScript脚本,停下后对Code section设内存访问断点。忽略所有异常。断下后,在抽取代码的共同入口722416设断,运行。第1次调用在004047E5

 

里面还有SMC,解出后贴到ACProtect_Fixed中对照跟踪。这部分的处理与低版本基本相同。

 

1)    查返回地址表

 

 

7225CCebx指向返回地址表,里面是RVA:

 

找到匹配地址后,ecx等于表项的offset,1项为0x2A

 

2)    opcode

1中的到的offsetopcode数据表(007262ED):

密文opcode:

 

counter(记录每个地址的调用次数,超出0x20次将使用新的地址解码):

 

1次解出的变形码:

 

 

3)    调整stack以便正确调用变形码及返回

 

 

 

4)    破坏解出的代码,ret到变形码

 

 

 

5)    执行变形码,返回到原程序

 

 

 

6) Patch

 

先把解出的代码binary copydumped_.exe(直接copy 722416 - 7226B0的代码即可)

 

关闭722579的解码(解出7225C1 - 7226B0):

 

关闭7226A8处对前面代码的破坏:

 

patch 72260C: jnz->jmp,无论执行多少次都使用同一解码地址

 

 

copy正确的ImageBase,这里为0,72FC1F400000

 

 

 

copy 正确的10 bytes key(1 byte0)

 

 

 

执行,仍然crashedL

 

8. 修复Replaced code (2)

 

还有replaced code,在这里出异常:

 

->

 

将解出的代码贴到ACProtect_Fixed.exe。可以看到,这些实际上是变形的call代码。第1次执行到这里,buffer中解出的变形码为:

 

 

XOR的结果:

 

 

目前的dumped_.exe,地址72ED83的值为0(这些值是loader写入的)

 

406DF8的变形码原来是call GetKeyboardType。原程序的call API 全部被抽掉了。壳代码的动作与前面相似,用返回地址查表,获取相应的指针,生成jmp dword ptr ds:[xxxxxxxx]指令,该地址则指向类似72124C的变形码,调用正确的API

 

变形call API的返回RVA地址表:

 

开始:

 

结束:

 

寻址变形码数组下标表(每项1字节),用于查变形码指针数组:


 

变形码地址表(指向变形码的指针):


 

开始打算写个inline-patch:

 

用同样的查表动作,把对应的变形码copy出来,得到对应的API地址,与跳过加密而得到的干净IAT对比。查到匹配值后,修改对应的opcode,使其直接callIAT中的地址。

 

OllyScript脚本跳过IAT加密,得不到变形码(此时从变形码地址表中得到的就是API的真正地址,46项指针无效,0xCCCCCCCC)。

 

另一个问题却是难以解决的,replaced code只有5字节:

这里的call0xE8,调用壳中的绝对地址。InlinePatch写到一定程度才发现,如果要修复代码,使其调用到IAT,需要相对地址调用6 bytesL。真是个低级错误。

 

现在patch的结果:

 

真正需要的是:

 

 

这样只有保留变形码。把壳中对应的代码copy过来,OEP前生成正确的变形码。而且脱壳后的程序不能直接看到API名字,很不舒服。

 

只好把壳的相应代码搬过来。再次修改dumped_.exe入口处代码,在把loader空间中的IAT填好后,跳到处理变形码的位置:


 

 

loader在处理IAT时需要调用几个API,及判断dll的映射地址、API地址等,先保存需要的数据(我们有干净的IATJ):

 

 

由于在前面避开了IAT加密,生成变形码需要的数据已经被正确的API地址覆盖了。用LoadPEACProtectidata section存到文件,然后加到dumped_.exe

 

 

把这个section的密文数据copydumped_.exeidata section,覆盖掉干净的IAT,我们已经不需要它了。现在只要伪造好现场J

 

 

往下执行loaderIAT处理代码,做几处小小的修改,使其使用刚才保存的API地址等数据。

 

IAT及变形码处理结束后回到OEP

 

 

执行。又挂了L。这次是内存访问异常。跟一跟可以知道,是在Hooked MessageBoxA中。这里面的代码还没有仔细看,有几个switch-case分枝。第1eax5

 

 

进去后有几个查表动作:

 

 

用调用Hooked MessageBoxA的返回地址查表。这张表在721F25,dumped_.exe中有,21项。

注意查表时不是找相等值,而是找大于返回地址值且最接近的值。

 

 

继续->

 

 

这里出现了另外2张表。7220B5的表中数据为sizeDumped_.exe中有:

 

 

问题出在第3张表:

 

 

dump出的数据为0。这段代码要把主程序中的一段数据copy到这张表中数据所指的地址。在loader中执行时,这里填入了指向动态分配内存的指针。

 

 

显然不能直接复制这些值。有个简单的办法可以骗过loader。从那张size表中可以看到,最大的数据FD5D。用LoadPE再次增加1section,sizeFFFF即可。

 

 

修改dumped_.exe,设置21项数据,使其全部指向该地址。

 

 

W2K下运行,显示窗口,但不能响应输入。在WinXP下运行什么也不显示。

 

下面该与主程序交手了,这需要把板凳坐穿的耐心L