【文章标题】: 菜鸟啄硬壳(之四)――通用脱壳机习作
【文章作者】: 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
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)