16、代码混淆与加壳:什么是加壳、常见壳(UPX, ASPack, Themida)、手动脱壳思路
说到逆向工程,加壳是个绕不开的话题。我刚开始学逆向那会儿,拿到一个程序,用IDA一打开,发现全是乱码,函数列表就几个,当时就懵了。后来才知道,这玩意儿被加了壳。
说白了,加壳就是给原始程序穿上一件「外套」。这件外套负责在程序运行时,把真正的代码解压到内存里再执行。这样一来,你看到的文件体积可能变小了(压缩壳),也可能变大了(加密壳),但共同点是——静态分析基本没法看。
什么是加壳?
加壳的本质,是在原始可执行文件外面包裹一层额外的代码。这层代码我们叫它「壳代码」。壳代码先于原始程序执行,它的任务通常是:
- 解压缩:把压缩过的原始代码还原到内存
- 解密:把加密过的原始代码解密
- 修复导入表:重建程序的API调用关系
- 跳转到原始入口点(OEP):完成上述工作后,把控制权交给真正的程序
你想想看,如果原始代码被压缩或加密了,你拿静态分析工具看到的,就是一堆无意义的数据。只有等到程序运行起来,壳代码把原始代码还原到内存里,你才能看到真面目。这就是为什么脱壳通常需要动态调试。
核心概念:OEP(Original Entry Point)
OEP就是原始程序真正的入口点。脱壳的目标,就是找到这个OEP,然后把壳剥离掉,让程序直接从这个点开始执行。找到OEP,脱壳就成功了一大半。
常见壳的分类
壳的种类很多,我个人习惯把它们分成三类:压缩壳、加密壳、以及VM保护壳。下面这张图可以帮你快速理解它们的区别:
UPX:最经典的压缩壳
UPX(Ultimate Packer for eXecutables)是我接触的第一个壳。它免费、开源、跨平台,而且脱壳极其简单。很多软件用UPX压缩,纯粹是为了减小体积,而不是为了防逆向。
UPX的壳代码结构非常清晰。它会在程序入口处执行一段解压代码,然后直接跳转到OEP。用OD(OllyDbg)加载UPX加壳的程序,你会看到类似这样的入口代码:
00401000 > 60 pushad ; 保存所有寄存器
00401001 BE 00300000 mov esi, 0x3000 ; 源地址
00401006 8BFE mov edi, esi ; 目标地址
00401008 57 push edi
00401009 83CD FF or ebp, -1 ; 设置解压参数
0040100C EB 04 jmp short 0x401012
...
; 解压循环
00401050 8A06 mov al, [esi]
00401052 46 inc esi
00401053 8807 mov [edi], al
00401055 47 inc edi
00401056 E2 F8 loop 0x401050
...
00401080 61 popad ; 恢复所有寄存器
00401081 75 08 jne short 0x40108B
00401083 B8 00104000 mov eax, 0x401000 ; 跳转到OEP
00401088 FFE0 jmp eax
看到那个pushad和popad了吗?这是UPX的标志性特征。它先把所有寄存器压栈保存,解压完成后恢复,然后跳转到OEP。手动脱UPX的壳,最常用的方法就是ESP定律——在pushad执行后,对ESP下硬件断点,然后运行,程序会在popad处断下,再单步几步就能看到OEP。
ESP定律脱UPX壳的步骤:
- 用OD加载UPX加壳程序,停在入口处
- 在命令行输入
hr esp(对ESP下硬件访问断点) - 按F9运行,程序会在
popad处断下 - 单步几步,看到
jmp eax或jmp [xxx],跳过去就是OEP - 用OD的脱壳插件或LordPE dump内存,再修复导入表
ASPack:另一种常见的压缩壳
ASPack和UPX类似,也是压缩壳,但它的入口代码稍微复杂一点。ASPack不会直接用pushad/popad,而是用call指令来获取当前地址,然后进行相对寻址。
我记得有一次分析一个国产小工具,就是用的ASPack。它的入口长这样:
00401000 E8 00000000 call 0x401005 ; 获取当前地址
00401005 5D pop ebp ; ebp = 0x401005
00401006 8BC5 mov eax, ebp
00401008 81E8 00104000 sub eax, 0x401000
0040100E 8D8D 1C304000 lea ecx, [ebp+0x40301C]
...
脱ASPack的思路和UPX类似,也是找OEP。不过ASPack的OEP跳转方式更多样,有时候是retn,有时候是jmp。我个人习惯在壳代码执行到最后阶段,看到pop指令恢复寄存器时,多留意一下。
Themida:加密壳的代表
Themida是商业加密壳,和UPX、ASPack完全不是一个量级。它集成了反调试、反虚拟机、代码乱序、导入表保护等多种技术。我当年第一次遇到Themida,折腾了整整三天才脱掉。
Themida的入口代码是动态生成的,每次加壳都不一样。它会检测调试器、检测虚拟机、检测断点。如果你用OD直接加载,它可能会直接退出,或者进入死循环。
对付Themida,常用的手段包括:
- 使用隐藏OD:修改OD的窗口类名、进程名,绕过反调试检测
- 使用ScyllaHide插件:这个插件能隐藏调试器痕迹
- API断点法:在
CreateFileA、GetModuleHandleA等API下断点,跟踪壳代码的执行流程 - 内存断点法:对代码段下内存写入断点,等壳代码把原始代码解密到内存时断下
避坑指南:
我曾经在脱一个Themida加壳的软件时,用了整整两天没找到OEP。后来发现,它用了多层壳——外面是Themida,里面还套了一层VMP。这种情况,你得一层一层剥,先脱外层,再脱内层。千万别以为一次就能搞定。
手动脱壳的通用思路
不管是什么壳,手动脱壳的核心思路其实就三步:找OEP、dump内存、修复导入表。下面这张图展示了完整的流程:
具体来说:
- 找OEP:这是最关键的一步。常用的方法有ESP定律、内存断点法、API断点法。对于UPX这类压缩壳,ESP定律基本秒杀。对于Themida这类加密壳,可能需要组合多种方法。
- Dump内存:找到OEP后,不要急着运行。在OEP处暂停,然后用LordPE或OD的脱壳插件,把当前进程的内存镜像保存到文件。注意,要选择正确的镜像基址,通常是
0x400000。 - 修复导入表(IAT):Dump出来的文件,导入表可能是乱的。用ImportREC加载dump文件,让它自动扫描IAT,然后修复。如果扫描不到,可以手动指定OEP地址,让ImportREC重新分析。
一个小技巧:
如果你用ESP定律找不到OEP,试试「堆栈平衡法」。在壳代码执行过程中,观察堆栈的变化。当堆栈恢复到和入口处一致时,通常离OEP就不远了。这个方法对大多数压缩壳都有效。
脱壳后的验证
脱壳完成后,一定要验证一下。用IDA或OD打开脱壳后的文件,看看能不能正常分析。如果函数列表还是空的,或者导入表全是问号,说明脱壳失败了。
我一般会检查三点:
- OEP是否正确:脱壳后的程序应该从OEP开始执行,而不是从壳代码开始
- 导入表是否完整:所有API调用都应该能正确解析
- 程序能否正常运行:这是最终的检验标准。如果脱壳后程序跑不起来,那脱壳就是失败的
嗯,关于加壳和脱壳,今天就先聊这么多。这东西说白了就是一层窗户纸,捅破了其实没那么神秘。多动手练几次,你也能成为脱壳老手。
公众号:蓝海资料掘金营,微信deep3321