1.1、信息总结
二、脱壳
2.1、找到OEP
2.2、解密IAT
2.2.1、IAT加密分析
2.2.2、重启程序
2.2.3、IAT加密流程
三、脱壳脚本的编写
四、脱壳程序验证
五、个人总结
六、 附件
一、工具及壳介绍
使用工具:Ollydbg、PEID、ImportREC、LoadPE、OllySubScript
1.1、信息总结
- 链接器版本:6.0
- 子系统:32位 GUI
- 加壳方式:未知
二、脱壳
2.1、找到OEP
将程序载入OD,先查看入口点。
发现标准的pushad
/pushfd
。那么先使用ESP定律看看能不能找到OEP。
使用ESP定律,单步到47A036,对[ESP]下写入断点,然后运行程序,程序断在了popfd
下面。
单步几下,到达OEP。可以看到VC6.0特征,同时CALL [XXXX]中的函数地址被加密了(IAT加密)。
2.2、解密IAT
解密IAT一般方法是在IAT函数表上下硬件写入断点,我们可以观察在到达原始OEP处附近的函数调用,VC6.0第一个函数调用,根据VC6.0特性该函数应该是GetVersion,但现在看到的是一个随机的函数地址,像申请的内存地址。
单步跟踪2E5039地址中的代码,可以发现原本IAT的函数地址被存放到了一个地址中,代码通过计算地址,获取到了GetVersion的函数地址,之后调用了GetVersion函数。
2.2.1、IAT加密分析
- 获取原始IAT函数地址,存放在一定位置(使用LoadlibraryA/W,GetProcAddress函数)
- 申请空间,构造新的IAT函数(使用VirtualAlloc申请空间,拷贝代码)
- 根据原始IAT函数地址计算加密值,隐藏真实地址
计算类似GetVersion函数中的代码中的x值
SUB EBX,X; ADD EBX,X; -
最后就是将新IAT函数地址写入IAT,填充地址到IAT中。
-
根据推测以及对壳Shell部分IAT的操作,我们可以总结一下,无论加密不加密IAT,壳其实都会填充IAT,只是加密IAT会填充加密之后的函数,现在只能找到加密前的IAT函数地址以及填充IAT的地方,并且能够在填充IAT时将加密前的函数地址写入,那么IAT就完成了解密,所以再这里我们分析的时候找到两个关键点进行破解和解密即可,一个就是写入IAT的地方,一个就是加密前IAT函数地址出现的地方。
2.2.2、重启程序
根据以上分析,接下来我们先从写入IAT的地方开始分析,首先在原始OEP处,GetVersion函数的IAT处下写入断点,从新运行程序。
当程序在断点上暂停时,我们可以找到写入IAT的地方。
根据程序设计的经验,写入IAT的地方一般来说是一个循环,在这个循环中应该包括加载模块、获取函数地址等操作。所以我们可以在两个函数上下软件断点,LoadLibraryA/W和GetProcAddress函数,在写入IAT处下一行代码下硬件执行断点(壳中的代码一般都是解压、解密出来的,一般地址不可靠)。
在写入IAT处断下之后,有两条分析路线,一个就是重新运行程序,等LoadlibraryA/W函数断下之后栈回溯分析代码,一个就是在写入IAT处继续单步分析,跟踪代码找到获取原始API函数地址的地方。我们先继续跟踪分析,发现代码计算出了一个地址,从这个地址获取了一个4字节的数,观察没有规律,一般这种只都是hash值。
继续单步,发现获取了Kernel32模块基址,之后访问了数据目录表。
单步跟踪,发现获取了导出函数字符串,根据上下文,推测代码实在获取导出函数字符串,求字符串的hash值,在于刚才获取的hash值进行对比。
继续跟踪,发现程序加载了函数字符串的每个字节,并进行了计算,关键代码如下
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加密流程
- 获取预先计算好的hash值
- 循环获取当前正在获取的模块中的导出函数名称,计算hash值,与预存的比较
如果失败继续循环获取 - 如果正确,获取导出函数的地址
- 拷贝预存的代码到缓冲区,将导出函数地址写入到缓冲区中
-
将缓冲区首地址写入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,如下图:
发现其中的函数函数地址并没有修复完全(除去第一个GetVersion是先执行后再修改的代码),猜测地址可能是随机的。
当我们重新运行程序时,有时会发现写入IAT处的硬件断点无效(OD中查看硬件断点无法跟随)。说明代码地址发生变化,一般来说,地址随机有两种情况:
- 随机基址
-
代码所在处是在申请的内存空间中
这种情况下解决方法就是找到代码基址,然后计算偏移,根据偏移在代码处下断点。很显然这个地方地址随机是因为申请了内存导致的,所以可以在VirtualAlloc处下断点。
经过动态调试,发现在VirtualAlloc处断下的有很多处,最开始的一处栈回溯之后,代码地址是程序的模块中,推测这个地方申请的内存空间就是修复IAT代码的基址,将之前的代码偏移,减去基址之后加上偏移,代码与之前一样,所以这个地方就是获取代码基址的地方。
加上上面的分析,只需合理下断点,修改代码,到原始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脚本的编写。