16、代码混淆与加壳:什么是加壳、常见壳(UPX, ASPack, Themida)、手动脱壳思路

说到逆向工程,加壳是个绕不开的话题。我刚开始学逆向那会儿,拿到一个程序,用IDA一打开,发现全是乱码,函数列表就几个,当时就懵了。后来才知道,这玩意儿被加了壳。

说白了,加壳就是给原始程序穿上一件「外套」。这件外套负责在程序运行时,把真正的代码解压到内存里再执行。这样一来,你看到的文件体积可能变小了(压缩壳),也可能变大了(加密壳),但共同点是——静态分析基本没法看。

什么是加壳?

加壳的本质,是在原始可执行文件外面包裹一层额外的代码。这层代码我们叫它「壳代码」。壳代码先于原始程序执行,它的任务通常是:

  • 解压缩:把压缩过的原始代码还原到内存
  • 解密:把加密过的原始代码解密
  • 修复导入表:重建程序的API调用关系
  • 跳转到原始入口点(OEP):完成上述工作后,把控制权交给真正的程序

你想想看,如果原始代码被压缩或加密了,你拿静态分析工具看到的,就是一堆无意义的数据。只有等到程序运行起来,壳代码把原始代码还原到内存里,你才能看到真面目。这就是为什么脱壳通常需要动态调试。

核心概念:OEP(Original Entry Point)

OEP就是原始程序真正的入口点。脱壳的目标,就是找到这个OEP,然后把壳剥离掉,让程序直接从这个点开始执行。找到OEP,脱壳就成功了一大半。

常见壳的分类

壳的种类很多,我个人习惯把它们分成三类:压缩壳、加密壳、以及VM保护壳。下面这张图可以帮你快速理解它们的区别:

常见壳的分类与特点 压缩壳 加密壳 VM保护壳 UPX, ASPack Themida, VMP VMP, Code Virtualizer 特点 体积小、速度快 脱壳相对简单 特点 反调试、反分析 脱壳难度中等 特点 代码虚拟化 脱壳难度极高 脱壳难度:压缩壳 < 加密壳 < 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

看到那个pushadpopad了吗?这是UPX的标志性特征。它先把所有寄存器压栈保存,解压完成后恢复,然后跳转到OEP。手动脱UPX的壳,最常用的方法就是ESP定律——在pushad执行后,对ESP下硬件断点,然后运行,程序会在popad处断下,再单步几步就能看到OEP。

ESP定律脱UPX壳的步骤:

  1. 用OD加载UPX加壳程序,停在入口处
  2. 在命令行输入 hr esp(对ESP下硬件访问断点)
  3. 按F9运行,程序会在popad处断下
  4. 单步几步,看到jmp eaxjmp [xxx],跳过去就是OEP
  5. 用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断点法:在CreateFileAGetModuleHandleA等API下断点,跟踪壳代码的执行流程
  • 内存断点法:对代码段下内存写入断点,等壳代码把原始代码解密到内存时断下

避坑指南:

我曾经在脱一个Themida加壳的软件时,用了整整两天没找到OEP。后来发现,它用了多层壳——外面是Themida,里面还套了一层VMP。这种情况,你得一层一层剥,先脱外层,再脱内层。千万别以为一次就能搞定。

手动脱壳的通用思路

不管是什么壳,手动脱壳的核心思路其实就三步:找OEP、dump内存、修复导入表。下面这张图展示了完整的流程:

步骤1:找OEP ESP定律/内存断点 步骤2:Dump内存 LordPE / OllyDump 步骤3:修复IAT ImportREC 手动脱壳详细流程 1. 用OD加载加壳程序,停在入口处 2. 应用ESP定律或内存断点,找到OEP 3. 在OEP处,用LordPE或OD插件dump当前内存镜像 4. 用ImportREC扫描dump文件的IAT,修复无效的API调用

具体来说:

  1. 找OEP:这是最关键的一步。常用的方法有ESP定律、内存断点法、API断点法。对于UPX这类压缩壳,ESP定律基本秒杀。对于Themida这类加密壳,可能需要组合多种方法。
  2. Dump内存:找到OEP后,不要急着运行。在OEP处暂停,然后用LordPE或OD的脱壳插件,把当前进程的内存镜像保存到文件。注意,要选择正确的镜像基址,通常是0x400000
  3. 修复导入表(IAT):Dump出来的文件,导入表可能是乱的。用ImportREC加载dump文件,让它自动扫描IAT,然后修复。如果扫描不到,可以手动指定OEP地址,让ImportREC重新分析。

一个小技巧:

如果你用ESP定律找不到OEP,试试「堆栈平衡法」。在壳代码执行过程中,观察堆栈的变化。当堆栈恢复到和入口处一致时,通常离OEP就不远了。这个方法对大多数压缩壳都有效。

脱壳后的验证

脱壳完成后,一定要验证一下。用IDA或OD打开脱壳后的文件,看看能不能正常分析。如果函数列表还是空的,或者导入表全是问号,说明脱壳失败了。

我一般会检查三点:

  • OEP是否正确:脱壳后的程序应该从OEP开始执行,而不是从壳代码开始
  • 导入表是否完整:所有API调用都应该能正确解析
  • 程序能否正常运行:这是最终的检验标准。如果脱壳后程序跑不起来,那脱壳就是失败的

嗯,关于加壳和脱壳,今天就先聊这么多。这东西说白了就是一层窗户纸,捅破了其实没那么神秘。多动手练几次,你也能成为脱壳老手。


公众号:蓝海资料掘金营,微信deep3321