【文章标题】: 菜鸟啄硬壳(之四)——通用脱壳机习作
【文章作者】: Wulje
【下载地址】: 自己搜索下载
【使用工具】: OD
【作者声明】: 只是感兴趣,没有其他目的。失误之处敬请诸位大侠赐教!
--------------------------------------------------------------------------------
          2月13日上传的“通用脱壳机”在设计上有缺陷,致使入口地址大于4FFFFF的软件脱壳后不能运行。现已更改,重新上传。虽然它适用范围不太宽,但对UPX壳,ASPack壳好像还很有效。

【详细过程】
.......当网友读到这篇文章后,可能认为我并不很“菜”,装着“菜鸟”写文章来卖弄自己。其实我跟大家并不认识,没有这个必要,更没有什么本事可卖弄。我只想说一点,软件这个东西既有十分深奥的一面(深奥的是软件作者的思想),技术上却有一通百通的共性。无知者无畏,我从第一篇文章开始接触“壳”来,每解剖一个壳,就有不少收获,每篇文章中都明显地留下了我的足迹,只要能进行交流,菜不菜根本不是问题,我等菜鸟们更不可自卑。好了,言归正传!
.......我说过调用LoadLibrary和GetProcAddress函数是壳的共性,更是它的软肋,跟踪这两个函数并手动修改部分代码,脱壳基本上都是成功的。能不能写个软件,让它自动跟踪这两个函数,把装载API地址的工作改为写函数导入表(IAT)的工作,如果能成功,管它是什么壳,不是都给脱了吗?这就是我的“通用脱壳机”的构思。有了这个想法,但实施起来却困难重重。本着无知者无畏的精神,还居然搞出了一个适应范围很宽的“脱壳机”(见附件),我相信永远不会有脱壳机的终点。你有七算,壳作者的八算,加密与解密永远是“思想”的战争,绝不会有一劳永逸的软件。我的构思是这样的:
.......1.写一个替代LoadLibraryA和GetProcAddress的代码,当壳代码调用这两个API时,自动将它取代;
.......2.由于壳代码千变万化,为了“通用性”,只能以LoadLibraryA和GetProcAddress的调用地址和壳打交道,把壳看成一个“黑匣子”,壳的所有内部信息都从进出这两个地址的参数和寄存器值获取;
.......3.将自编代码嵌入壳中,壳解压时自动完成“导入表”的写入工作;
.......4.最困难的是“双重壳”中取代LoadLibraryA和GetProcAddress的时机掌握,因为它解压第一层壳时还必需使用真正的这两个API函数,只有当它开始解压第二层壳时才可以取代;
.......5.最没有规律的是OEP的搜索,目前还只能根据壳代码特征去设计搜索方案,一个可行的方案是搞一个“壳特征库”,根据不同的壳去查找不同的搜索方法。(对我这个爱好者来说没有这个必要和精力)
.......对具体代码感兴趣的网友,可继续看下面内容:
  
....... 一、替代LoadLibraryA和GetProcAddress的源代码
.......要壳自解压,必需运行壳。运行中,这两个函数地址是惟一和壳交道的通道(静态搜索OEP例外),所以在_GetProcAddress中处理的事件较多(动态处理)。
  ;--------------------------------------------------------
_LoadLibraryA.....uses    edi    _DLL_Addr  
..................xor     eax,eax
..................mov     dw_Flag,eax................;调用LoadLibraryA的标记
...................if     !dwCase....................;若是双重壳,第1次调用kernel32,则IID表不登记
..........................sub   dw_IID,4
...................endif
..................mov     edi,dw_IID.................;dw_IID是自定义的IID地址
..................mov     eax,_DLL_Addr
..................xor     eax,400000h      
..................mov     [edi],eax..................;写IID表(eax指向库函数地址)
..................add     dw_IID,4        
..................xor     eax,400000h
..................ret
_LoadLibraryA.....endp
  ;-------------------------------------------------
_GetProcAddress...proc   uses edi esi _DLL_Addr,_API_Name_Addr....;_API_Name_Addr是函数名地址或函数序列号
          
............if    _API_Name_Addr == 1.....................;某种壳的特殊结束标记
..................mov   esi,dw_IAT_Start..................;IAT表开始地址
..................xor   ecx,ecx
...................repeat.................................;清除某些IAT表中不规范的结束标记
.........................lods dword ptr [esi]
..........................if   eax >= 7fffffffh
...............................mov dword ptr [esi-4],0  
..........................endif  
.........................inc ecx
...................until ecx >= 80h
..................jmp    dw_Exit..........................;脱壳后入口地址(特殊壳使用)
.............elseif      dw_GetProcAddr < 2...............;开关,调用_GetProcAddress次数
..................push   esi
..................mov    esi,_DLL_Addr
..................mov    eax,[esi]
..................and    eax,0f0f0f0fh
...................if    eax == 0e02050bh.................;kernel32.dll
.........................mov    esi,_API_Name_Addr
.........................mov    eax,[esi]
..........................if eax == 74726956h..............;VirtualAlloc或VirtualFree函数
.............................push   _DLL_Addr
.............................call   A_LoadLibrary_1........;真实的windows地址
.............................push   _API_Name_Addr
.............................push   eax
.............................call   A_GetProcAddress_1.....;真实的windows地址
.............................pop    esi
.............................inc    dw_GetProcAddr    
.............................jmp    step3..................;返回的是Virtual等函数地址
......................... .endif      
...................endif
..................inc    dw_GetProcAddr
..................pop    esi
.............endif
............cmp   dwCase,0                                 
............jnz   step1....................................;获取各寄存器初值
............mov   dw_ecx,ecx...............................;寻找记录IAT表地址的寄存器
............mov   dw_ebx,ebx
............mov   dw_edx,edx
............mov   dw_esi,esi
............mov   dw_edi,edi
............inc   dwCase
............jmp   step2
step1:......cmp   dwCase,1
............jnz   step2
............pushad......................................... ;若找到需要的寄存器,填IID表
............sub   ecx,dw_ecx
............sub   ebx,dw_ebx
............sub   edx,dw_edx
............sub   esi,dw_esi
............sub   edi,dw_edi
.............if   ecx == 4..................................;两次差值为4的寄存器是写IAT表的寄存器
.....................mov  eax,1
.....................mov  dw_count,eax......................;将该寄存器打上标记
.....................mov  ecx,dw_ecx      
.....................mov  dw_IAT_Start,ecx..................;上次的寄存器值就是IAT表起始地址
.............elseif  ebx == 4    
.....................mov  eax,2
.....................mov  dw_count,eax
.....................mov  ebx,dw_ebx          
.....................mov  dw_IAT_Start,ebx  
.............elseif  edx == 4  
.....................mov  eax,3
.....................mov  dw_count,eax
.....................mov  edx,dw_edx
.....................mov  dw_IAT_Start,edx
.............elseif  esi == 4
.....................mov  eax,4
.....................mov  dw_count,eax
.....................mov  esi,dw_esi
.....................mov  dw_IAT_Start,esi
.............elseif  edi == 4
.....................mov  eax,5
.....................mov  dw_count,eax
.....................mov  edi,dw_edi
.....................mov  dw_IAT_ Start ,edi
.............endif
.....@@:....popad
............mov   edi,dw_IID                                
............mov   eax,dw_IAT_Start  
............xor   eax,400000h
............mov   [edi-10h],eax............................;填写IID数据表中的INT表地址
............inc   dwCase...................................;使用一次后关闭    
step2:                                                   
.............if   dw_Flag == 0.............................;调用一个新库函数后进入
..................inc  dw_Flag
...................if     dw_count == 1
..........................mov dw_IAT,ecx
...................elseif dw_count == 2
..........................mov dw_IAT,ebx
...................elseif dw_count == 3
..........................mov dw_IAT,edx
...................elseif dw_count == 4
..........................mov dw_IAT,esi
...................elseif dw_count == 5
..........................mov dw_IAT,edi
...................endif
..................mov   edi,dw_IAT                           
..................mov   eax,[edi-4]..........................;记录下一个IID表中的IAT开始位置
...................if   eax == -1
........................mov dword ptr [edi-4],0..............;把某些壳IAT结束标记FFFFFFFF清0
...................endif
..................mov   esi,dw_IID
..................xor   edi,400000h
..................mov   [esi],edi
..................add   dw_IID,10h
..................add   dw_IID_IAT,4                         
..................mov   eax,dw_IID_IAT
..................xor   eax,400000h
..................mov   [esi-10h],eax........................;填写IID表中的INT数据表地址
.............endif                                           
............mov   edi,dw_IID_IAT                             
............add   dw_IID_IAT,4
............mov   eax,_API_Name_Addr                         
.............if   eax > 400000h..............................;以函数名地址方式调用GetProcAddress
..................sub  eax,400002h                           
..................mov  [edi],eax.............................;写INT表(某些特殊壳需要)
.............else
..................xor  eax,80000000h.........................;以序列号方式调用GetProcAddress
..................mov  [edi],eax
..................xor  eax,80000000h
.............endif    
...step3:....ret
_GetProcAddress     endp
  
        二、嵌入代码和变量地址重定位(静态处理)
       嵌入前面的两段代码并不困难,困难的是嵌入代码不能使用壳程序的变量地址,因为它很可能被冲掉,所以变量地址必需自带。不同的壳长度是不同的,嵌入地址也是不同的,所以要重定位。嵌入代码总是在壳最后(加入的)1000字节中。
  ;--------------------------------------------------------------
............mov   ebx,PE_oldLench...................;壳原长度
............shr   ebx,08
............or    bx,4000h..........................;bx是重定位值
............mov   edi,lpMemory......................;内存壳映像文件开始地址
............add   edi,PE_oldLench...................;edi指向了添加的1000字节    
............mov   ecx,299h..........................;嵌入代码长度
............mov   esi,401000h.......................;代码起始位置
............add   edi,40h...........................;预留16个变量地址
............push  edi
..@@:.......lodsb...................................;嵌入代码
............stosb
............loop  @B
............pop   edi       
.............repeat
...................mov   ax,word ptr [edi+ecx]
....................if   ax == 4040h................;需要重定位的地址004040xxh
.........................mov   word ptr [edi+ecx],bx
....................endif
...................inc   ecx
.............until ecx >= 299h
  ;----------------------------------------------------------------------
  
.......三、入口地址设断的时机的掌握(进程调试)
.......怎样寻找LoadLibraryA和GetProcAddress地址并用代码地址替换是一个纯PE文件知识的问题,不是本文要讨论的问题,搜索OEP入口地址是静态分析后处理的技术问题,也不是本文讨论的问题。有兴趣的网友用OD打开附件中的程序就会一目了然。
.......进程调试(动态)中,如何在PE头中写字节(PE头是禁止写入的),至今我都没有解决。我第一篇壳文中提到的那个Keygen.exe壳(该壳叫什么名,我也不知道),整个解压代码都是写在PE头中的,要动态地在入口地址设断都不成功(有谁知道在进程中能对PE头写入,请交流一下)。所以我的设断都是在调用LoadLibraryA和GetProcAddress开始后。设断的目的是准确的把握抓取内存映像的时机,如果一开始就在OEP地址设断,那么壳在自解压的过程中会在该地址写入代码,冲掉你的断点。一般的壳在开始调用GetProcAddress时设置OEP断点地址是设有问题的,双重壳就不一定了,FantaMorph.exe壳(好象是个Aspack壳)它两次在OEP位置写入代码,过早地在OEP设断就会失败。我打开过用Aspack脱壳后的FantaMorph.exe,部分代码好象被重组过,OEP地址都变了,与我的脱壳机脱壳后有较大的不同。我的脱壳机除嵌入了替代LoadLibraryA和GetProcAddress函数代码、重写了IAT表、添加了INT表外(大多数程序不需要该表,但用VB6写的程序却需要),对原程序未作任何改动。
.......对付这种双重壳,我是采用分别在_LoadLibraryA和_GetProcAddress地址设断,两处“对倒”的方式(有点像机构抄股票),适当放行解压第一层壳需要的kernel32和VirtualAlloc和VitualFree函数,当开始解压第二层壳时在OEP设断,一旦进程到达该点并断下后,dump内存映像就一切OK了。 
.......FantaMorph.exe壳是不是一个“双重壳”,我并不清楚,有点乱说,因为我并不知道双重壳是什么概念。
.......如何在进程调试中设断点,也只是windows的API应用技术问题,不在此说明。
       
.......四、后记
.......我附件中的“通用脱壳机”让高手见笑了,到底它适用那些壳,我并不清楚,如果手动输入OEP地址(16进制)适应范围会宽很多。我只分析了我电脑中的三个壳文件(更多的我没有),还有一个UPX加壳机。本脱壳机对付UPX壳实在是一瞬间的事。任何脱壳机的生命都是有限的,这个也不例外。我对这个脱壳机并不报好大希望,更不需要报酬,完全免费提供给任何使用者。你要任何改造,如增加搜索OEP等的功能都请随便。它就当我从“看雪论坛”获得知识的一个回报。
.......我的全部源代码写得很烂,不好意思拿出来。原因是开始构思时认为处理的事件不会太多,就按过程顺序去处理,使用了太多的全局变量和转跳。后来插入的事件越来越多,源代码已经绞成了一团乱麻,把自己都看晕了头。想改成按功能模块独立设计,那就要前功尽弃了。算了,搞起玩的,将就!
.......(说明:贴子中怎样使代码按需要对齐,我始终没有解决,只好在前面添上“....”号,给阅读带来不便,甚至掩盖了代码中必需的“.”号。一些贴子对齐得那么好不知是怎样处理的?请指教!)
.......谢谢!
  
  
  
  
--------------------------------------------------------------------------------
【版权声明】: 本文原创于看雪技术论坛, 转载请注明作者并保持文章的完整, 谢谢!

                                                       2007年02月13日 12:52:52