一、工具及壳介绍

使用工具:Ollydbg、PEID、ImportREC、LoadPE、OllySubScript

1524637647121

1.1、信息总结

  • 链接器版本:6.0
  • 子系统:32位 GUI
  • 加壳方式:未知

二、脱壳

2.1、找到OEP

  将程序载入OD,先查看入口点。

1524638008945

  发现标准的pushad/pushfd。那么先使用ESP定律看看能不能找到OEP。

  使用ESP定律,单步到47A036,对[ESP]下写入断点,然后运行程序,程序断在了popfd下面。

1524638403970

  单步几下,到达OEP。可以看到VC6.0特征,同时CALL [XXXX]中的函数地址被加密了(IAT加密)。

1524638488205

2.2、解密IAT

  解密IAT一般方法是在IAT函数表上下硬件写入断点,我们可以观察在到达原始OEP处附近的函数调用,VC6.0第一个函数调用,根据VC6.0特性该函数应该是GetVersion,但现在看到的是一个随机的函数地址,像申请的内存地址。

1524640819051

  单步跟踪2E5039地址中的代码,可以发现原本IAT的函数地址被存放到了一个地址中,代码通过计算地址,获取到了GetVersion的函数地址,之后调用了GetVersion函数。

1524640921342

2.2.1、IAT加密分析

  1. 获取原始IAT函数地址,存放在一定位置(使用LoadlibraryA/W,GetProcAddress函数)
  2. 申请空间,构造新的IAT函数(使用VirtualAlloc申请空间,拷贝代码)
  3. 根据原始IAT函数地址计算加密值,隐藏真实地址
    计算类似GetVersion函数中的代码中的x值
    SUB EBX,X; ADD EBX,X;
  4. 最后就是将新IAT函数地址写入IAT,填充地址到IAT中。

  • 根据推测以及对壳Shell部分IAT的操作,我们可以总结一下,无论加密不加密IAT,壳其实都会填充IAT,只是加密IAT会填充加密之后的函数,现在只能找到加密前的IAT函数地址以及填充IAT的地方,并且能够在填充IAT时将加密前的函数地址写入,那么IAT就完成了解密,所以再这里我们分析的时候找到两个关键点进行破解和解密即可,一个就是写入IAT的地方,一个就是加密前IAT函数地址出现的地方。

2.2.2、重启程序

  根据以上分析,接下来我们先从写入IAT的地方开始分析,首先在原始OEP处,GetVersion函数的IAT处下写入断点,从新运行程序。

1524641216294

  当程序在断点上暂停时,我们可以找到写入IAT的地方。

1524641404072

  根据程序设计的经验,写入IAT的地方一般来说是一个循环,在这个循环中应该包括加载模块、获取函数地址等操作。所以我们可以在两个函数上下软件断点,LoadLibraryA/W和GetProcAddress函数,在写入IAT处下一行代码下硬件执行断点(壳中的代码一般都是解压、解密出来的,一般地址不可靠)。

  在写入IAT处断下之后,有两条分析路线,一个就是重新运行程序,等LoadlibraryA/W函数断下之后栈回溯分析代码,一个就是在写入IAT处继续单步分析,跟踪代码找到获取原始API函数地址的地方。我们先继续跟踪分析,发现代码计算出了一个地址,从这个地址获取了一个4字节的数,观察没有规律,一般这种只都是hash值。

1524643542105

  继续单步,发现获取了Kernel32模块基址,之后访问了数据目录表。

1524643735853

1524644069639

  单步跟踪,发现获取了导出函数字符串,根据上下文,推测代码实在获取导出函数字符串,求字符串的hash值,在于刚才获取的hash值进行对比。

1524644330802

  继续跟踪,发现程序加载了函数字符串的每个字节,并进行了计算,关键代码如下

001D1C81 LODS BYTE PTR DS:[ESI] ; 循环加载字符串
001D1C9E TEST AL,AL             ; 如果为0跳出循环
001D1CC3 ROL EDX,0x3
001D1CDB XOR DL,AL              ; 异或之后,重新加载字符串

  当计算完hash值之后,会进行比较,关键代码如下:

001D193D CMP ESI,EDI        ; 比较hash值
001D1A26 JNZ SHORT 001D1A64
001D1A28 JMP SHORT 001D1A40 ; hash相等时,函数名正确,在获取地址

  继续跟踪,可以跟踪到获取函数地址的地方。

001D1911 ADD EAX,DWORD PTR SS:[EBP+0x8] ; 获取函数地址

  继续跟踪,发现函数地址被处理,使用memcpy拷贝出了一段代码,函数地址被写入到了代码中。新的函数地址就是memcpy拷贝的首地址,这个地址被写入到IAT中。

001D0474 MOV EDX,EAX                         ; kernel32.GetVersionExA
001D14DC MOV DWORD PTR DS:[ECX+EAX-0x4],EDX  ; kernel32.GetVersionExA
001D0895 MOV DWORD PTR DS:[EDX],EAX          ; 写入函数地址

2.2.3、IAT加密流程

  1. 获取预先计算好的hash值
  2. 循环获取当前正在获取的模块中的导出函数名称,计算hash值,与预存的比较
    如果失败继续循环获取
  3. 如果正确,获取导出函数的地址
  4. 拷贝预存的代码到缓冲区,将导出函数地址写入到缓冲区中
  5. 将缓冲区首地址写入IAT处,完成填充IAT的操作

  • 如何才能解密IAT?答案其实不止一种,我们先从函数地址入手,如果当我们获取了原始函数地址,且在写入IAT时,寄存器中还保存的是原始函数地址,那解密IAT就会很容易完成。如果代码是线性执行,我们只需要改一下跳转应该就可以了,但是现在代码有点混乱,程序乱跳,很难找到规律,这个时候,如果足够耐心,其实改跳转是可以做到的,仔细跟踪代码,发现其实函数地址最初保存在EAX中,而后保存在EDX中,之后EDX被修改为IAT地址,EAX修改为加密的地址,在这个过程中,只要我们能做到EAX最后是函数地址即可,经过分析,修改两处代码即可。

  • 第一处
001D14DC MOV DWORD PTR DS:[ECX+EAX-0x4],EDX
改成
001D14DC MOV ECX,EDX

  这里的修改为了能将函数地址保存到ECX中,经过测试ECX中的值没有什么用处

  • 第二处
001D0EE2 MOV EAX,DWORD PTR SS:[EBP-0x58]
改成
001D0EE2 MOV EAX,ECX

  这里的修改是为了将函数地址保存到EAX中,因为最后填充IAT的代码使用的是EAX,如果代码地址都没有变化,我们可以重新运行程序,修改上面两处代码,解密IAT。解密之后的IAT,如下图:

1524705937860

  发现其中的函数函数地址并没有修复完全(除去第一个GetVersion是先执行后再修改的代码),猜测地址可能是随机的。

  当我们重新运行程序时,有时会发现写入IAT处的硬件断点无效(OD中查看硬件断点无法跟随)。说明代码地址发生变化,一般来说,地址随机有两种情况:

  • 随机基址
  • 代码所在处是在申请的内存空间中

      这种情况下解决方法就是找到代码基址,然后计算偏移,根据偏移在代码处下断点。很显然这个地方地址随机是因为申请了内存导致的,所以可以在VirtualAlloc处下断点。

      经过动态调试,发现在VirtualAlloc处断下的有很多处,最开始的一处栈回溯之后,代码地址是程序的模块中,推测这个地方申请的内存空间就是修复IAT代码的基址,将之前的代码偏移,减去基址之后加上偏移,代码与之前一样,所以这个地方就是获取代码基址的地方。

1524723657173

  加上上面的分析,只需合理下断点,修改代码,到原始OEP处即可解密所有IAT,剩下的转存内存文件,修复IAT即可完成脱壳。

三、脱壳脚本的编写

  • 总结一下脱壳的步骤
    • 0047148B 原始OEP处下断点
    • 0047A37F 地址下断点,获取代码基址(虚拟内存申请基址)
    • 在代码基址+14DC处下硬件执行断点
    • 等运行到代码基址+14DC处再进行下一步并取消此处硬件执行断点
    • 代码基址+14DC,修改代码为MOV ECX,EDX
    • 代码基址+0EE2,修改代码为MOV EAX,ECX
    • 在OEP出转存文件
    • 修复文件

  我们发现经过分析之后解密IAT就变成了机械式的步骤,在OD中有一个很不错的插件可以使用脚本完成自动化的操作OD,那就是OllySubScript插件,其语法非常简单易懂,按照上面的步骤我们可以编写脚本,如下:

 1 // 定义变量
 2 VAR dwOEP // 原始OEP
 3 VAR dwGetVirtualAllocBaseAddr // 获取申请内存的基地址
 4 VAR dwOffset1 // 要修改第1处代码的偏移
 5 VAR dwOffset2 // 要修改第2处代码的偏移
 6 VAR dwSaveVirtualAllocBase // 保存申请内存的地址
 7 VAR dwCode1 // 第一处代码地址
 8 VAR dwCode2 // 第二处代码地址
 9 
10 // 初始化变量
11 MOV dwOEP, 0047148B
12 MOV dwGetVirtualAllocBaseAddr, 0047A37F
13 MOV dwOffset1,14dc
14 MOV dwOffset2,0ee2
15 
16 // 清除所有断点
17 BPHWC  // 清除所有硬件断点
18 BC     // 清除所有软件断点
19 BPMC   // 清除所有内存断点
20 
21 // 设置断点
22 BPHWS dwOEP, "x"   // 设置OEP断点
23 BPHWS dwGetVirtualAllocBaseAddr, "x"  // 设置获取基址断点
24 
25 // 运行到基地址
26 loop1:
27 RUN
28 CMP dwGetVirtualAllocBaseAddr, eip   // 判断是否运行到VirtualAlloc下一行
29 JNE loop1
30 // 保存申请的内存基地址,保存两处修改代码地址
31 MOV dwSaveVirtualAllocBase, eax
32 MOV dwCode1, dwSaveVirtualAllocBase
33 ADD dwCode1, 14dc  // 取得第一处代码地址
34 MOV dwCode2, dwSaveVirtualAllocBase
35 ADD dwCode2, 0ee2  // 取得第二处代码地址
36 
37 // 在第一、二处下硬件执行断点
38 BPHWS dwCode1, "x"
39 BPHWS dwCode2, "x"
40 
41 // 运行到第一处修改代码处
42 loop2:
43 RUN
44 CMP dwCode1, eip
45 JNE loop2
46 // 修改第一处代码为MOV ECX, EDX
47 ASM dwCode1, "MOV ECX, EDX"
48 ADD dwCode1,2
49 FILL dwCode1,2,90 // 填充nop
50 
51 // 运行到第二处修改代码处
52 loop3:
53 RUN
54 CMP dwCode2, eip
55 JNE loop3
56 // 修改第二处代码为MOV EAX, ECX
57 ASM dwCode2, "MOV EAX, ECX"
58 ADD dwCode2,2
59 FILL dwCode2,1,90 // 填充nop
60 
61 
62 // 清除断点
63 BPHWC dwCode1
64 BPHWC dwCode2
65 
66 // 运行
67 loop4:
68 RUN
69 // 运行到OEP
70 CMP dwOEP,eip
71 JNZ loop4
72 MSG "到达OEP"
 

四、脱壳程序验证

  • 可以看到PEID以可以正确识别

 

五、个人总结

通过本次脱壳,了解到该壳使用了IAT加密、混淆、花指令,同时还有哈希加密,初步学习了OD脚本的编写。

六、 附件

未知加密壳



来自为知笔记(Wiz)



posted on 2018-07-19 10:11  PhantomW  阅读(421)  评论(0编辑  收藏  举报