Cool-Y.github.io/source/_posts/pack-and-unpack.md

160 lines
16 KiB
Markdown
Raw Permalink Normal View History

2019-05-17 08:23:48 +00:00
---
2019-07-09 06:49:24 +00:00
title: 加壳与脱壳
2019-05-17 08:23:48 +00:00
date: 2019-05-14 11:20:59
tags:
---
壳是最早出现的一种专用加密软件技术。一些软件会采取加壳保护的方式。
壳附加在原始程序上通过Windows加载器载入内存后先于原始程序执行以得到控制权在执行的过程中对原始程序进行解密还原然后把控制权还给原始程序执行原来的代码。
加上外壳后,原始程序在磁盘文件中一般是以加密后的形式存在的,只在执行时在内存中还原。这样可以有效防止破解者对程序文件进行非法修改,也可以防止程序被静态反编译。
# 壳的加载过程
壳和病毒在某些地方类似,都需要获得比原程序更早的控制权。壳修改了原程序执行文件的组织结构,从而获得控制权,但不会影响原程序正常运行。
1. 保存入口参数
加壳程序在初始化时会保存各寄存器的值待外壳执行完毕再恢复各寄存器的内容最后跳回原程序执行。通常用pushad/popad等指令保存和恢复现场环境。
2. 获取壳本身需要使用的API地址
一般情况下外壳的输入表中只有GetProcAddress、GetModuleHandle和LoadLibrary这三个API函数甚至只有Kernel32.dll及GetProcAddress。如果需要其他API函数可以通过LoadlibraryA或LoadlibraryExA将DLL文件映像映射到调用进程的地址空间中函数返回的HINSTANCE值用于标识文件映像所映射的虚拟内存地址。
3. 解密原程序各个区块的数据
在程序执行时,外壳将解密区块数据,使程序正常运行。
4. IAT的初始化
IAT的填写本来由PE装载器实现但由于在加壳时构造了一个自建输入表并让PE文件头数据目录表中的输入表指针指向自建输入表PE装载器转而填写自建输入表程序的原始输入表被外壳变形后存储IAT的填写会由外壳程序实现。外壳从头到尾扫描变形后的输入表结构重新获得引入函数地址填写在IAT中。
5. 重定位项的处理
针对加壳的DLL
6. Hook API
壳在修改原程序文件的输入表后自己模仿操作系统的流程向输入表填充相关数据。在填充过程中外壳可以填充Hook API代码的地址从而获得程序控制权。
7. 跳转到OEP
# 通用脱壳方法
通常脱壳的基本步骤如下:
1:寻找OEP
2:转储(PS:传说中的dump)
3:修复IAT(修复导入表)
4:检查目标程序是否存在AntiDump等阻止程序被转储的保护措施,并尝试修复这些问题。
以上是脱壳的经典步骤,可能具体到不同的壳的话会有细微的差别。
## 寻找OEP
2019-07-01 09:27:20 +00:00
1. 搜索JMP或者CALL指令的机器码(即一步直达法,只适用于少数壳,包括UPX,ASPACK壳)
2019-05-17 08:23:48 +00:00
对于一些简单的壳可以用这种方式来定位OEP,但是对于像AsProtect这类强壳(PS:AsProtect在04年算是强壳了,嘿嘿)就不适用了,我们可以直接搜索长跳转JMP(0E9)或者CALL(0E8)这类长转移的机器码,一般情况下(理想情况)壳在解密完原程序各个区段以后,需要一个长JMP或者CALL跳转到原程序代码段中的OEP处开始执行原程序代码。按CTRL+B组合键搜索一下JMP的机器码E9(CTRL+L查看下一个,看看有没有这样一个JMP跳转到原程序的代码段。
2019-07-01 09:27:20 +00:00
2. 使用OllyDbg自带的功能定位OEP(SFX法)
2019-05-17 08:23:48 +00:00
演示这种方法目标程序我们还是选择CRACKME UPX.EXE,用OD加载该程序,然后选择菜单项Options-Debugging options-SFX。该选项只有当OllyDbg发现壳的入口点位于代码段之外的时候才会起作用,壳的入口点位于代码段中的情况还是比较少见的。
2019-07-01 09:27:20 +00:00
3. 使用Patch过的OD来定位OEP(即内存映像法)
2019-05-17 08:23:48 +00:00
正常的内存访问断点读取,写入,执行的时候都会断下来,该Patch过的OD内存访问断点仅当执行的时候才会断下来,我们可以利用这一点来定位OEP。
UPX壳的解密例程会解密原程序的各个区段并将各个区段原始字节写回到原处,我们最好不要在解密区段的过程中断下来,说不定要断成千上万次才能到达OEP,这里有了这个Patch过的OD就方便多了,其内存访问断点仅当执行的时候才会断下来,当其在执行第一个区段中的代码时,基本上就可以断定是OEP了。
2019-07-01 09:27:20 +00:00
4. 堆栈平衡法(即ESP定律法)
2019-05-17 08:23:48 +00:00
这种方法适用于一些古老的壳。这些壳首先会使用PUSHAD指令保存寄存器环境,在解密各个区段完毕,跳往OEP之前,会使用POPAD指令恢复寄存器环境。
有的情况下保存寄存器环境可能不是第一条指令,但也在附近了,还有些情况下,有些壳不使用PUSHAD,而是逐一PUSH各个寄存器(例如:PUSH EAX,PUSH EBX等等),总而言之,在解密完区段,跳往OEP之前会恢复寄存器环境。
按F7键执行PUSHAD:可以看到各个寄存器的初始值被压入到堆栈中了,这里我们可以对这些初始值设置内存或者硬件访问断点,当解密例程读取这些初始值的时候就会断下来,断下来处基本上就在OEP附近了。
这里我们可以通过在ESP寄存器值上面单击鼠标右键选择-Follow in dump在数据窗口中定位到这些寄存器的初始值。对这些初始值的第一个字节或者前4个字节设置硬件访问断点。当壳的解密例程读取该值的时候断了下来停在popad的下一行,紧接着下面就是跳往OEP处,说明这个方法起作用了。
2019-07-01 09:27:20 +00:00
5. VB应用程序定位OEP法(Native 或者 P-CODE)
2019-05-17 08:23:48 +00:00
定位VB程序的OEP比较容易,因为VB应用程序都有一个特点-开始都是一个PUSH指令,紧接着一个CALL指令调用一个VB API函数。我们可以使用Patch过的OD,首先定位到VB的动态库,接着给该动态库的代码段设置内存访问断点,
当壳的解密例程解密完原程序各个区段,接着就会断在VB DLL的第一条指令处,接着我们可以在堆栈中定位到返回地址,就可以来到OEP的下一条指令处。这里我们也可以使用前面介绍的方法-跟逐一给各个区段设置内存访问断点(使用Patch过的OD),但是很多壳会检测这种方法,所以大家可能根据需要不同的情况来尝试这不同的方法。这种方法很容易理解,我就不举例子了,以后大家如果遇到了VB程序可以试试这种方法。
2019-07-01 09:27:20 +00:00
6. 最后一次异常法
2019-05-17 08:23:48 +00:00
如果我们在脱壳的过程中发现目标程序产生大量异常的话,就可以使用最后一次异常法,将EXCEPTIONS菜单项中的忽略各个异常的选项都勾选上,运行起来。这里我们可以看到产生了好几处异常,但是都不是位于第一个区段,说明这些异常不是在原程序运行期间发生的,是在壳的解密例程执行期间产生的异常。重新启动OD,将EXCEPTIONS菜单项中忽略的异常选项的对勾都去掉,仅保留Ignore memory access violations in KERNEL32这个选项的对勾。按SHIFT + F9忽略异常继续运行,我们直到最后一次异常。
接着我们可以 ***对代码段设置内存访问断点*** ,可能有人会问,为什么不在一开始设置内存访问断点呢?原因是很多壳会检测程序在开始时是否自身被设置内存访问断点,如果执行到了最后一次异常处的话,很可能已经绕过了壳的检测时机!
2019-07-01 09:27:20 +00:00
7. 用壳最常用的API函数来定位OEP
2019-05-17 08:23:48 +00:00
将忽略的异常选项都勾选上,我们来定位一下壳最常用的API函数,比如GetProcAddress,LoadLibrary。ExitThread有些壳会用。
使用bp GetProcAddress命令给该API函数设置一个断点。我们只需要知道壳在哪些地方调用GetProcAddress,所以我们在断下来的这一行上面单击鼠标右键选择-Breakpoint-Conditional log,来设置条件记录。将Pause program这一项勾选上Never,记录的表达式设置为[ESP],也就是记录返回地址,这样我们就能知道哪些地方调用GetProcAddress。接着在日志窗口中单击鼠标右键选择-Clear Log(清空日志)。运行起来,我们可以看到程序的主窗口弹了出来,打开日志窗口,看看最后一次GetProcAddress(排除掉第一个区段中调用的位置)是在哪里被调用的。
我们可以在 ***对代码段设置内存访问断点*** 之前尝试一下这种方法,这样就可以绕过很多壳对内存断点的检测,但是有一些壳也会对API函数断点进行检测,所以说我们需要各种方式都尝试一下,找到最合适的。
2019-07-01 09:27:20 +00:00
8. 利用应用程序调用的第一个API函数来定位OEP
## IAT表修复
为了确保操作系统将正确的API函数地址填充到IAT中,应该满足一下几点要求:
1:可执行文件各IAT项所在的文件偏移处必须是一个指针,指向一个字符串。
2:该字符串为API函数的名称。
如果这两项满足,就可以确保程序在启动时,操作系统会将正确的API函数地址填充到IAT中。
假如,我们当前位于被加壳程序的OEP处,我们接下来可以将程序dump出来,但是在dump之前我们必须修复IAT,为什么要修复IAT呢?难道壳将IAT破坏了吗?对,的确是这样,壳压根不需要原程序的IAT,因为被加壳程序首先会执行解密例程,读取IAT中所需要的API的名称指针,然后定位到API函数地址,将其填入到IAT中,这个时候,IAT中已经被填充了正确的API函数地址,对应的API函数名称的字符串已经不需要了,可以清除掉。
大部分的壳会将API函数名称对应的字符串以密文的形式保存到某个地址处,让Cracker们不能那么容易找到它们。
2019-05-17 08:23:48 +00:00
# 压缩壳
压缩壳的特点就是减小软件的体积加密保护不是重点。目前兼容性和稳定性较好的压缩壳有UPX、ASPack、PECompact等。
## [UPX](https://upx.github.io/)
UPX-the Ultimate Packer for eXecutables是以命令行方式操作的可执行文件压缩程序。
UPX早期的压缩引擎是有UPX自己实现的其3.x版本也支持LZMA第三方压缩引擎。UPX除了对目标程序进行压缩也可以解压缩。它不包含任何反调试或保护策略。另外UPX保护工具UPXPR、UPX-sCRAMBLER等可修改UPX加壳标志使其自解压功能失效。
```
Usage: upx [-123456789dlthVL] [-qvfk] [-o file] file
```
### 识别UPX加壳
被加壳程序:点击按钮之后弹框
1. 导入函数很少
未加壳程序的导入函数:
![](https://res.cloudinary.com/dozyfkbg3/image/upload/v1557817831/%E5%8A%A0%E5%A3%B3/1.png)
加壳后的导入函数:
![](https://res.cloudinary.com/dozyfkbg3/image/upload/v1557817867/%E5%8A%A0%E5%A3%B3/2.png)
2. 使用IDA识别代码段
只有少量的代码被识别
3. 使用OllyDbg打开程序警告被加壳
![](https://res.cloudinary.com/dozyfkbg3/image/upload/v1557819108/%E5%8A%A0%E5%A3%B3/3.png)
4. 程序的节名包含加壳器的标识
加壳后的程序节名为UPX0、UPX1、rsrc
5. 程序拥有不正常的节大小。例如.text节的原始数据大小为0但虚拟大小非0
![](https://res.cloudinary.com/dozyfkbg3/image/upload/v1557819290/%E5%8A%A0%E5%A3%B3/4.png)
6. 使用加壳探测工具如PEiD
![](https://res.cloudinary.com/dozyfkbg3/image/upload/v1557820111/%E5%8A%A0%E5%A3%B3/5.png)
7. 熵值计算
压缩或加密数据更接近于随机数据熵值更高。如使用PEiD计算熵值
![](https://res.cloudinary.com/dozyfkbg3/image/upload/v1557821441/%E5%8A%A0%E5%A3%B3/6.png)
PEiD计算熵值的方法
1.重新组织需要计算的数据
i以下数据不列入计算熵的范围导出表数据、导入表数据、资源数据、重定向数据。
ii. 尾部全0的数据不列入计算熵的范围。
iii. PE头不列入计算熵的范围。
2.分别计算每一部分数据的熵E和该部分数据大小S。
3.以下列公式得到整个PE文件的熵 Entropy = ∑Ei * Si / ∑Si (i = 1,2…n)。
### UPX手动脱壳
根据 ***栈平衡原理*** 寻找OEP
在编写加壳软件时必须保证外壳初始化的现场环境各寄存器值与原程序的现场环境相同。因此加壳程序在初始化时保存各寄存器的值待外壳执行完毕后恢复寄存器的内容最后跳转到原程序执行。通常用pushadpush eax/ecx/edx/ebx/esp/ebp/esi/edi、popad来保存和恢复现场环境。
首先用Ollydbg加载已加壳的程序起始代码如下
![](https://res.cloudinary.com/dozyfkbg3/image/upload/v1557836854/%E5%8A%A0%E5%A3%B3/7.png)
此时现场环境(寄存器值)如下:
![](https://res.cloudinary.com/dozyfkbg3/image/upload/v1557837144/%E5%8A%A0%E5%A3%B3/8.png)
在执行pushad指令后寄存器的值被压入栈中如下所示
![](https://res.cloudinary.com/dozyfkbg3/image/upload/v1557837250/%E5%8A%A0%E5%A3%B3/9.png)
此时esp指向12FFA4h对这个地址设置硬件访问断点然后运行程序在调用popad恢复现场环境时会访问12FFA4h造成中断此时离OEP已经不远了
![](https://res.cloudinary.com/dozyfkbg3/image/upload/v1557837519/%E5%8A%A0%E5%A3%B3/10.png)
```
005B5155 .- E9 9506F4FF jmp carckUPX.004F57EF
```
2019-07-01 09:27:20 +00:00
即为跳转到OEP的指令设置断点跟进到004F57EF此时我们就来到了OEP。
dump和修复IAT表的工具很多。
1. 使用Ollydump进行程序脱壳和IAT表修复。
2019-05-17 08:23:48 +00:00
![](https://res.cloudinary.com/dozyfkbg3/image/upload/v1557837859/%E5%8A%A0%E5%A3%B3/11.png)
使用PEiD检查果然壳已经脱掉
2019-07-01 09:27:20 +00:00
2. 使用PETools dump和Import Reconstruct修复IAT
使用PETools出来的程序不能运行提示win32无法识别这是因为IAT表没有重建。
使用Import Reconstruct需要知道IAT表的起始位置。
我们知道API函数的调用通常是通过间接跳转或者间接CALL来实现的。
即JMP [XXXXXXX] or CALL [XXXXXX]这样是直接调用IAT中保存的API函数地址。
首先定位到获取IAT中函数地址的跳转表,这里就是该程序将要调用到的一些API函数,我们可以看到这些跳转指令的都是以机器码FF 25开头的,有些教程里面说直接搜索二进制FF 25就可以快速的定位该跳表。
看到整个IAT后,我们直接下拉到IAT的尾部,我们知道属于同一个动态库的API函数地址都是连续存放的,不同的动态库函数地址列表是用零隔开的。
Import REConstructor重建IAT需要三项指标:
1)IAT的起始地址,这里是403184,减去映像基址400000就得到了3184(RVA:相对虚拟地址)。
2)IAT的大小
IAT的大小 = 40328C - 403184 = 108(十六进制)
3)OEP = 401000(虚拟地址)- 映像基址400000 = 1000(OEP的RVA)。
2019-05-17 08:23:48 +00:00
## [ASPack](http://www.aspack.com/)
ASPack是一款Win32可执行文件压缩软件可压缩Win32可执行文件EXE、DLL、OCX具有很高的兼容性和稳定性。
# 加密壳
加密壳种类较多,一些壳只保护程序,另一些壳提供额外的功能如注册、使用次数、时间限制。越有名的加密壳,其破解可能性越大。
## ASProtect
这个壳在pack界当选老大是毫无异议的当然这里的老大不仅指它的加密强度而是在于它开创了壳的新时代SEH,BPM断点的清除都出自这里更为有名的当属RSA的使用使得Demo版无法被crack成完整版本,code_dips也源于这里。IAT的处理即使到到现在看来也是很强的。他的特长在于各种加密算法的运用这也是各种壳要学习的地方。
它可以压缩、加密、反跟踪代码、CRC校验和花指令等保护措施。
使用Blowfish、Twofish、TEA等加密算法以RSA1024为注册密钥生成器通过API钩子与加壳程序通信。
ASProtect为软件开发人员提供了SDK从而实现了加密程序的内外结合。
ASProtect 1.x系列低版本用Stripper工具可自动脱壳。ASProtect的SKE系列主要在protect OEP和SDK上采用了虚拟机技术。
### 加密后的特征
1. 导入函数很少
2. 程序的节名不再是典型的.text
3. 使用IDA打开几乎没有可识别的代码
4. 使用OllyDbg打开程序被警告该程序已加密查找参考文本字符串都是乱码
5. 使用PEiD检查出是ASProtect加壳