壳 (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 壳,我记得 52 上有哪位大佬说脱 UPX 壳最好的办法就是 upx -d
。机脱我们不做介绍,这更多考验搜索能力和 PY。
当然,有的壳是没有(完整的)脱壳机的,而有些谨慎的程序员也会给壳添油加醋,这时就不得不进行麻烦的手脱了。
脱壳常用方法
脱壳的主要目的是:找到 OEP,在 OEP 处设置硬件断点。如果想更近一步,就要输出脱壳后的程序到文件,并修复运行。
基本步骤:
graph LR A[查壳] --> B[寻找 OEP] B --> C[脱壳/Dump 原程序] C --> D[修复导入表]
UPX 手脱方法
再说一遍,UPX 最完美的仍是用 upx -d
机脱,或者修复 UPX 自解压信息后再机脱。
机脱已经在如何解决魔改后的加密算法处有所说明