壳 (packer) 是一种程序压缩与保护手段,有时称为 “可执行程序资源压缩”。加壳过的程序可以直接运行,但是不能查看源代码,要经过脱壳才可以查看。
加壳是利用特殊的算法,对 EXE、DLL 文件里的资源进行压缩、加密。压缩之后的文件可以独立运行,解压过程完全隐蔽,都在内存中完成。
原始程序代码在磁盘文件中一般是以加密后的形式存在的,只在执行时在内存中还原,这样就可以比较有效地防止破解者对程序文件的非法修改,同时也可以防止程序被静态反编译。当然,很多恶意软件为了 “隐藏自己” 免受安全软件的查杀,也会使用加壳器。
壳的类型通常分为压缩壳和加密壳两类。压缩壳的特点是减小软件体积大小,加密保护不是重点。加密壳种类比较多,不同的壳侧重点不同,一些壳单纯保护程序,另一些壳提供额外的功能,如提供注册机制、使用次数、时间限制等。但如今这两类壳的界限并不清晰,有的壳兼具压缩和加密功能。
壳的加载
- 保存入口参数
- 加壳程序初始化时保存各寄存器的值
- 外壳执行完毕,恢复各寄存器值
- 最后再跳到原程序执行
通常用 pushad
/ popad
、pushfd
/ popfd
指令对来保存和恢复现场环境
- 获取所需函数 API
- 一般壳的输入表中只有
GetProcAddress
、GetModuleHandle
和LoadLibrary
这几个 API 函数 - 如果需要其他 API 函数,则通过
LoadLibraryA(W)
或LoadLibraryExA(W)
将 DLL 文件映射到调用进程的地址空间中 - 如果 DLL 文件已被映射到调用进程的地址空间里,就可以调用
GetModuleHandleA(W)
函数获得 DLL 模块句柄 - 一旦 DLL 模块被加载,就可以调用
GetProcAddress
函数获取输入函数的地址
- 解密各区块数据
- 处于保护源程序代码和数据的目的,一般会加密源程序文件的各个区块。在程序执行时外壳将这些区块数据解密,以让程序正常运行
- 外壳一般按区块加密,按区块解密,并将解密的数据放回在合适的内存位置
- 跳转回原程序入口点
- 在跳转回入口点之前,一般会恢复填写原 PE 文件输入表 (IAT),并处理好重定位项(主要是 DLL 文件)
- 因为加壳时外壳自己构造了一个输入表,因此在这里需要重新对每一个 DLL 引入的所有函数重新获取地址,并填写到 IAT 表中
- 做完上述工作后,会跳转到原始入口点 (OEP),将控制权移交原程序,并继续执行
如何脱壳
脱壳分为机脱和手脱。
如果加壳器有对应的脱壳脚本且原程序未对加壳结果进行修改,那么机脱是最优雅且快捷的,比如 UPX 壳。脱 UPX 壳最好的办法就是 upx -d
。[1] 机脱我们不多做介绍,这更多考验搜索能力和 PY。
当然,有的壳是没有(完整的)脱壳机的,而有些谨慎的程序员也会给壳添油加醋,这时就不得不进行麻烦的手脱了。
UPX 部分手脱
UPX 作为入门最常见的壳,官方程序自带脱壳功能。然而,仅仅是 upx -d
实在没有技术含量,因此进阶一点的 UPX 题目就会再次修改 UPX 加壳后的程序,使得 upx -d
失效。
upx -d
脱壳主要利用加壳时候写到加壳程序里的 PE 信息,直接抹掉原始加壳的留下的 PE 头信息就无法直接 upx -d
脱壳了。
- 修改 / 删除特殊区段名
UPX 压缩过的程序有特殊区段名UPX
,十六进制表示为55 50 58
。如果修改 / 删除区段名,那么upx -d
就无法识别了,而程序仍然能运行。UPX
区段保存着程序的压缩信息,在一定程度上可以验证程序的完整性。
UPX 压缩的程序还有一个区段.rsrc
,这是程序的头部和部分资源数据段,有时是UPX2
。
解决办法就是将修改的区段名恢复成UPX
或.rsrc
,一般来讲,程序中有 3 - 4 个 UPX 特征区段名,要看看它们是否全部修复。
另一个更强硬的手段就是直接删除 UPX 特定区段的字节,这样做甚至会损坏程序的运行。
总结起来,你需要的关注的地方有这些:1)UPX0
、UPX1
和.rsrc
/UPX2
;2)5.01 UPX! $
,数字代表程序使用的 UPX 版本,UPX! $
标志从此处开始解压原始程序。
手动脱壳常用方法
脱壳的主要目的是:找到 OEP,在 OEP 处设置硬件断点。如果想更近一步,就要输出脱壳后的程序到文件,并修复运行。
基本步骤:
graph LR A[查壳] --> B[寻找 OEP] B --> C[脱壳/Dump 原程序] C --> D[修复导入表]