[摘要]   
        1.摸拟壳的装载过程,自动修复ITA表和IID表,脱壳变得如此轻松愉快。
        2.在OD中修改PE头,脱壳后程序可以立即运行。
[正文]
        现在市面上有很多脱壳工具,但大多功能有限,只能脱一些非常标准的已知壳。若壳稍加变化它们就无能为力了,正象杀毒软件一样,只能杀已知的病毒。壳是可以千变万化的,万能脱壳工具好象是没有的?有没有十分容易的脱壳方法呢?答案是看你有没有一个很好的脱壳思路。下面以脱Spy.exe 的壳为例谈点体会。
        我很喜欢窗口侦测的Spy.exe软件(见附件),搞软件破解的没有人不知道它的。它用UPX加壳,我说过LoadLibrary和GetProcAddress函数是壳的软肋,这正好来验证一下这个结论是否管用?
        
        1.对Spy.exe壳的认识
        用壳侦查软件测试Spy.exe,都报告说它是UPX壳。但用UPX脱壳工具(一大堆)上阵,要么没反映,要么windows要发“错误报告”,UpxShell更搞笑,说它是一个陈旧的版本不能解壳?没有一个能生成(那怕是不能运行的)脱壳文件,只有ProcDump稍好点,脱出了一个不能运行的Unpacked.exe。Spy.exe的壳肯定是一个老版本了,老版本尚且如此,新版本谈何容易?
        我在《菜鸟啄硬壳(之一)》中有一句话说过头了。我说:“无论是windows或者自己调用GetProcAddress装载API函数,都必须要有IAT表”。这句话错了(资历太浅)!凡事都有例外!这个Spy.exe壳装载API函数就没有现存的IAT表!抢先抓取IAT表的思路受阻了。
        
        2.使用对付壳的杀手锏——拦截“GetProcAddress”函数
        由于有了前面乱说的教训,我不敢说拦截LoadLibrary和GetProcAddress函数一定可以做到什么。拦截的目的是分析代码是如何装载API函数的?从分析中尽量做到下面四点。这四点是脱壳思路的源泉!
        1)获取API函数名(脱壳后)地址;
        2)抓取IAT表或获取IAT表地址(如果没有IAT表,那么装载第1个API的地址就是IAT表地址);
        3)抓取IID表或获取IID表的地址(可能在LoadLibrary访问的地址附近,但不一定有IID表);
        4)获取OEP地址。
        如果你能做到以上4点(无论你用什么方法),那么让程序自动修复ITA表和IID表且脱壳后可以立即运行的程序就离你不远了。下面说具体的对Spy.exe脱壳的操作方法:
        
        (1)用OD打开Spy,用右键打开弹出式菜单找“搜索—全部模块中的名称”找到LoadLibrary,记下地址:4B7154。在OD命令栏键入“HR 4B7154”回车,运行。(按ctrl+F9)来到下面代码:
004BA123      8B07................mov eax,dword ptr ds:[edi]
004BA125      09C0................or eax,eax
004BA127      743D................je short 004BA166                       
004BA129      8B5F04..............mov ebx,dword ptr ds:[edi+4]
004BA12C      8D8430 00600B00.....lea eax,dword ptr ds:[eax+esi+B6000]  ;获取了库函数名
004BA133      01F3................add ebx,esi                           ;取得了IAT表地址
004BA135      50..................push eax
004BA136      83C708..............add edi,8                             ;指向了库中第1个API
004BA139      FF96 54610B00.......call dword ptr ds:[esi+B6154]         ;调用LoadLibrary
004BA13F      92..................xchg eax,edx
004BA140      8A07................mov al,byte ptr ds:[edi]              ;获取API函数名
004BA142      47..................inc edi
004BA143      08C0................or al,al
004BA145      74DC................je short 004BA123                     ;是库函数返回,是API往下                    
004BA147      52..................push edx
004BA148      89F9................mov ecx,edi
004BA14A      7907................jns short 004BA153                    ;必跳,14C—152是花指令
004BA14C      0FB707..............movzx eax,word ptr ds:[edi]
004BA14F      47..................inc edi
004BA150      50..................push eax
004BA151      47..................inc edi
004BA152      B9 5748F2AE.........mov ecx,AEF24857                      ;花指令,153开始才是真指令
004BA157      52..................push edx
004BA158      FF96 58610B00.......call dword ptr ds:[esi+B6158]         ;调用GetProcAddress
004BA15E      5A..................pop edx
004BA15F      8903................mov dword ptr ds:[ebx],eax            ;装载IAT表
004BA161      83C304..............add ebx,4
004BA164      EBDA................jmp short 004BA140                      
         单步走几下,到4BA148,状态栏显示一个函数名,地址是4B1009。切换内存到4B1000一下就看到一连串的函数名,显然API函数名称表地址在4B1000。继续走,在4BA158调用GetProcAddress,eax中返回了API地址,在4BA15F将把该值存入[ebx],状态栏显示ebx=480140,内存切换到这,发现是全空白,没有装载前的IAT表,但该地址就是装载后的IAT表的位置。4BA139是调用LoadLibrary,当某个.dll中的API装完后,自然就会来访问它。当我们来到4BA12C时,显示user32.dll,地址是4B719A,内存切换过去就发现了一堆.dll函数,原来Spy.exe将库函数和普通API是分开放置的。稍往回走就发现了IID表,地址是4B7000。最后我们发现,程序开始的入口处就是一个pushad,显然出口就在popad(满足pushad、popad成对规律)。来到popad处立即发现jmp 47A6C0,该地址为OEP无疑。这样,前面四个问题就圆满解决了。
        
        (2)修复或创建IAT表,IID表的思路:
        ITA表的实质就是其值是指向API函数名字串地址的,当我们知道函数名表开始的地址,并在其中搜索到某函数时将其所在的“地址”依次填入IAT表,则该表就修复或创建好了。(IID表的实质就自己去看有关书籍吧)弄懂后思路就清晰了。现在关键是搞清楚函数名表中函数排列的规律:我们发现每个函数名是以00 01开始的,每个库函数是以00 00开始的。有了这个就够了(其实在每个00 00后面的数字中暗藏着装载API的地址)。这样就有下面的方法了:
        将4BA198的jmp 47A6C0(出口),改为jmp 4B6B30(跳到空白处),OD中键入下列代码:
004B6B30    BD 00004000...........mov ebp,400000                       
004B6B35    BF 3C010800...........mov edi,8013C                 ;80140是IAT表地址
004B6B3A    BE FE0F0B00...........mov esi,0B0FFE                ;B1000是函数名表
004B6B3F    BB 24704B00...........mov ebx,4B7024                ;4B7014是IID表地址
004B6B44    8A042E................mov al,byte ptr ds:[esi+ebp]  ;搜索函数名
004B6B47    46....................inc esi
004B6B48    81FE 90280B00.........cmp esi,0B2890
004B6B4E    7202..................jb short 004B6B52                      
004B6B50    C3....................retn                          ;此处设断
004B6B51    90....................nop
004B6B52    3C00..................cmp al,0
004B6B54    75EE..................jnz short 004B6B44                      
004B6B56    8A042E................mov al,byte ptr ds:[esi+ebp]
004B6B59    3C00..................cmp al,0
004B6B5B    740A..................je short 004B6B67                       
004B6B5D    4E....................dec esi
004B6B5E    89342F................mov dword ptr ds:[edi+ebp],esi   ;填写IAT表
004B6B61    83C704................add edi,4
004B6B64    46....................inc esi
004B6B65    EBDD..................jmp short 004B6B44                     
004B6B67    83C608................add esi,8
004B6B6A    83C704................add edi,4
004B6B6D    893B..................mov dword ptr ds:[ebx],edi       ;填写IID表
004B6B6F    83C314................add ebx,14
004B6B72    EBD0..................jmp short 004B6B44                      
        运行在4B6B50断下后,因为PE头在运行中是禁止写入的,输入表,导入函数表的“登记”只能手工写入了:OD中内存切换到400000,修改400180:为14700B00 40010000;修改4001D8:为40010800 64060000。最后把第2块节表名由:UPX1改名为:.rsrc(这样eXeScope才可以编辑资源)。一切就绪后,dump当前进程,并将“重建输入表”选项去掉,入口依然是7A6C0。怎么样,脱壳出来的程序是不是运行得很正常?
        
        (3)摸拟壳写IAT表的过程,直接写入函数名地址:
        照你这么说,何苦先让壳把IAT表写成了 API的调用地址,再自己来改成函数名地址,不如让壳直接写函数名地址?还省得我小心地去找函数名字串排列的规律,自编程序。这话有理,但可能难度较大,因为壳写IAT表的空间只有41h字节,且壳也没有写IID表。小心地设计程序,调整原可用代码的位置(因为空间太小了)也能实现,在OD中汇编如下代码(对照那些是原有的,那些是添加的):
004BA123      BA 24704B00.........mov edx,4B7024                ;B7014是IID表地址
004BA128      BD 00004000.........mov ebp,400000               
004BA12D      8B07................mov eax,dword ptr ds:[edi]    ;edi初值指向函数名表
004BA12F      09C0................or eax,eax
004BA131      7433................je short 004BA166              
004BA133      8B5F04..............mov ebx,dword ptr ds:[edi+4]
004BA136      03DE................add ebx,esi
004BA138      33DD................xor ebx,ebp
004BA13A      891A................mov dword ptr ds:[edx],ebx    ;填写IID表
004BA13C      33DD................xor ebx,ebp
004BA13E      83C214..............add edx,14
004BA141      83C708..............add edi,8
004BA144      8A07................mov al,byte ptr ds:[edi]      ;查找函数名
004BA146      47..................inc edi
004BA147      08C0................or al,al
004BA149      74E2................je short 004BA12D                      
004BA14B      8BCF................mov ecx,edi
004BA14D      57..................push edi
004BA14E      48..................dec eax
004BA14F      F2:AE...............repne scas byte ptr es:[edi]  ;指向下个函数名
004BA151      58..................pop eax
004BA152      48..................dec eax
004BA153      48..................dec eax
004BA154      33C5................xor eax,ebp
004BA156      8903................mov dword ptr ds:[ebx],eax    ;填写IAT表
004BA158      83C304..............add ebx,4
004BA15B      EBE7................jmp short 004BA144                     
004BA15D      90..................nop
        在47A6C0设断(壳已经修复好了两个表了),断下后最后的操作同(2),得到的也是一个可以立即运行的程序。
        
        后记:
        将脱壳后的程序用eXeScope打开,发现它是用Delhpi写成的,怪说不得装载了6000多个API函数(绝大多数是不参与运行的,也是垃圾代码,但不装载还不行!)若是用手工来修改IAT表,这几乎是不可能完成的!
        因为PE头的禁止写入,使脱壳还不能实现全自动化。若另写一程序来打开它,并摸拟它的解压并修改PE头,这不就是脱壳机吗?我没有学习过任何脱壳机的写法,不知是不是这个原理,即便如此,写个脱壳机后,它的适应范围会有多宽多久呢?
        谢谢你认真读完!请菜鸟共勉,请高手指教。

  • 标 题:答复
  • 作 者:kangaroo
  • 时 间:2007-02-05 13:03

嘿嘿,把壳解压IAT讲具体点!!!
004BA122   .  5F            pop     edi                          ;  edi指向函数名区块
004BA123   >  8B07          mov     eax, dword ptr [edi]         ;  共8个字节,前四个字节是库名的偏移,后四个字节为IAT表的偏移
004BA125   .  09C0          or      eax, eax
004BA127   .  74 3D         je      short 004BA166               ;  偏移为0,表示结束,调处加载IAT表
004BA129   .  8B5F 04       mov     ebx, dword ptr [edi+4]       ;  读后四个字节
004BA12C   .  8D8430 00600B>lea     eax, dword ptr [eax+esi+B600>;  esi为00401000加上B6000再加上偏移,得到函数名字符串地址
004BA133   .  01F3          add     ebx, esi                     ;  ebx为后四个字节的偏移,加上esi,即指向IAT表中的偏移!!!
004BA135   .  50            push    eax                          ;  库名入栈
004BA136   .  83C7 08       add     edi, 8                       ;  edi加8(跳过那8个字节)指向下一个地址
004BA139   .  FF96 54610B00 call    dword ptr [esi+B6154]        ;  调用LoadLibrary函数
004BA13F   .  92            xchg    eax, edx                     ;  将句柄保存入edx
004BA140   >  8A07          mov     al, byte ptr [edi]           ;  读取1个字节
004BA142   .  47            inc     edi                          ;  指向下一个字节
004BA143   .  08C0          or      al, al                       ;  判断是否为函数,如果为函数值为1,0就是库名
004BA145   .^ 74 DC         je      short 004BA123               ;  为0(库名)跳上去,
004BA147   .  52            push    edx                          ;  kernel32.7C800000
004BA148   .  89F9          mov     ecx, edi
004BA14A   .  79 07         jns     short 004BA153
004BA14C   .  0FB707        movzx   eax, word ptr [edi]
004BA14F   .  47            inc     edi
004BA150   .  50            push    eax
004BA151   .  47            inc     edi
004BA152      B9            db      B9
004BA153   .  57            push    edi
004BA154   .  48            dec     eax
004BA155   .  F2:AE         repne   scas byte ptr es:[edi]       ;  将edi指向下一个函数名
004BA157   .  52            push    edx
004BA158   .  FF96 58610B00 call    dword ptr [esi+B6158]
004BA15E   .  5A            pop     edx
004BA15F   .  8903          mov     dword ptr [ebx], eax
004BA161   .  83C3 04       add     ebx, 4
004BA164   .^ EB DA         jmp     short 004BA140

楼主方法不错值得敬仰,呵呵,基本上脱离了工具,全手工搞定!!!

  • 标 题:答复
  • 作 者:skylly
  • 时 间:2007-01-30 10:39

想仿照楼主的思路做个UPX的脱壳机
发现做出来的东西通用性不强,最后发现是有个地方弄错乐:
004BA14A      7907................jns short 004BA153                    ;必跳,14C—152是花指令
004BA14C      0FB707..............movzx eax,word ptr ds:[edi]
004BA14F      47..................inc edi
004BA150      50..................push eax
004BA151      47..................inc edi
004BA152      B9 5748F2AE.........mov ecx,AEF24857                      ;花指令,153开始才是真指令

14c-152那段不是花指令,当导入是by 序号的时候就会走这个分支,所以不能把它当作花指令(由于这个壳导入都是 by name,所以不走这个分支)