当前位置:首页 > 逆向工程 > 正文

加壳识别入门:为什么脱壳比找偏移难得多

约 5255 字 · 预计阅读 14 分钟
一句话总结:拿到一个程序,导入表只有两个函数、节名看不出来、IDA 打开一片乱码——这就是加壳。这篇给三个判断依据(节表特征、熵值、导入表)、一段算熵的代码,再讲清压缩壳和虚拟化壳的区别,以及为什么「脱壳」这件事远比找一个偏移复杂。

上一篇讲 PE 结构时提过一句:.text 节如果同时可写可执行,基本可以确定被加壳或自修改代码。当时只是一笔带过,这篇专门说这件事。

先明确概念。壳分两类,处理难度差一个数量级。

压缩壳:把原始代码压起来,运行时在内存里解压再跳回去执行。代表是 UPX。这类壳的目的是减小体积,不是为了对抗分析。

保护壳:加密代码、插入反调试、虚拟化关键指令。代表是 VMProtect、Themida。这类壳的目的就是让你看不懂。

三个判断依据

依据一:节表长得不对劲

正常编译出来的程序,节名是 .text、.rdata、.data、.rsrc。加壳之后常见的异常:

  • 节名变成 UPX0、UPX1、.aspack、nsp0,或者干脆是一串随机字符
  • 节数量很少但体积很大(一两个节占了 90% 的体积)
  • 某个节的 VirtualSize 远大于 SizeOfRawData(内存里要展开,磁盘上没多少内容)
  • 出现 RWX 节(可读可写可执行)

我用 PE 脚本 dump 节表时,最先看的就是 RWX 这条。正常的编译器产物里,同时可写又可执行的节极少见。

依据二:熵值接近 8

熵是这段数据「有多乱」的度量。取值范围 0 到 8(按字节计算):全是一样的字节熵为 0,完全随机则接近 8。

普通代码段的熵通常在 6 到 6.5 之间,压缩或加密过的数据会逼近 7.5 以上。

import math, pefile

def entropy(data: bytes) -> float:
    if not data:
        return 0.0
    freq = [0] * 256
    for b in data:
        freq[b] += 1
    total = len(data)
    return -sum((c / total) * math.log2(c / total) for c in freq if c)

pe = pefile.PE("target.exe")

for s in pe.sections:
    name = s.Name.rstrip(b"\x00").decode(errors="ignore")
    data = s.get_data()
    e = entropy(data)
    flag = "  <-- 可疑" if e > 7.2 else ""
    print(f"{name:<8} 大小 {len(data):>9}  熵 {e:.3f}{flag}")

跑一遍,如果某个节熵值 7.8 以上且占了大半体积,基本可以确定被压过。

有一点要注意:熵高不等于加壳。资源节里如果是压缩过的图片、加密的配置,熵也会很高。判断要看组合:高熵 + 节名异常 + 导入表稀少,三条都中才稳。

依据三:导入表只剩下两个函数

这是最直观的一条。加壳程序在磁盘上的入口是壳的解压代码,真正的业务 API 都是运行时动态解析的,所以在静态导入表里只看得到:

  • LoadLibraryA / LoadLibraryW
  • GetProcAddress
  • 偶尔加个 VirtualProtect

如果你打开一个几百 KB 的程序,导入表只有这三样,不用再看了。

用工具确认

手动算熵适合写脚本批量筛,单个样本直接用 Detect It Easy(DIE)。它内置了几百种壳的特征签名,能识别 UPX、ASPack、Themida、VMProtect 等,还会告诉你「可能是 XX 的哪个版本」。

DIE 的输出里有个字段值得留意:如果它识别出壳名但加了「?」,说明特征匹配不完整,可能是魔改版。

压缩壳与保护壳的分水岭

UPX 这类压缩壳,如果能直接脱,说明没被魔改:

upx -d packed.exe -o unpacked.exe

如果报 CantUnpackException,说明壳头被改过(常见的手法就是改几个字节让 upx -d 失效),只能手动脱。

手动脱壳的标准流程是三步:找 OEP、dump 内存镜像、修复 IAT。

找 OEP 最常用的是「ESP 定律」:加壳程序的入口通常先 pushad 保存所有寄存器,然后在解压完成后 popad 恢复。在 pushad 之后对 ESP 指向的地址下硬件断点(写入),壳解压完执行 popad 时会命中,此时单步跟几次就能到原始入口点。

dump 用 Scylla 或 PE-bear 都行。难点在第三步:IAT 修复。

原始导入表的地址在加壳时被抹掉了,运行时的 IAT 是壳填进去的。你要把内存里的 IAT 抓出来,反推每个函数属于哪个 DLL、叫什么名字,再重建一份导入表写回 dump 出来的文件。Scylla 能自动做大部分,但遇到间接调用、混淆过的 IAT 就需要手工补。

这就是「脱壳比找偏移难」的原因:找偏移是在已知结构里找一个数字,脱壳是要重建一个被破坏的结构。

保护壳是另一个量级

VMProtect、Themida 这类壳做了三件压缩壳不做的事:

  1. 代码虚拟化:把 x86 指令翻译成自定义字节码,由一个内置虚拟机解释执行。你反汇编看到的不是原始逻辑,是虚拟机的解释器
  2. 反调试:检测调试器、检测断点、检测内存改动(手法见反调试那篇)
  3. 反 dump:检测内存镜像是否被读取,检测代码段校验和

对虚拟化的代码段,「脱壳」这个思路直接失效——因为它没有「解压后的原始代码」可以还原,逻辑只存在于虚拟机的字节码里。这种情况下分析重点是虚拟机的解释器本身,通过找 handler 分发表去还原语义,工作量是另一个层级。

我的判断很直接:遇到 VMProtect 保护的商业软件,评估一下投入产出比再决定要不要碰。很多情况下找厂商 API、读文档,比逆虚拟化代码划算得多。

图 1:三条判断依据指向「压缩壳」还是「保护壳」,决定后面完全不同的路线

坑清单

坑一:只看节名就下结论。 改节名是加壳工具的基本操作,UPX0 也可能是伪装。要看熵和导入表。

坑二:熵高就当加壳。 资源节、加密配置、压缩过的字体文件熵都高。看它占的体积比例,以及和导入表是否矛盾。

坑三:upx -d 失败就放弃。 魔改过的 UPX 很常见,改两三个字节就能让官方的解压器失效。手动脱壳仍然可行。

坑四:dump 完直接运行。 没修 IAT 的 dump 一跑就崩,因为所有外部函数地址都是错的。必须先修再跑。

坑五:在保护壳上用 ESP 定律。 虚拟化的代码不走正常的函数序言,pushad 可能压根不存在。硬套会对着一堆伪指令调一天。

坑六:忽略 32/64 位差异。 x64 下 pushad 不存在(被拆成多条 push),ESP 定律要改成对 RSP 操作,且寄存器保存方式不同。

边界

这篇的内容限于分析自己有权分析的程序:自己写的、靶场里的、明确授权的样本。加壳识别本身是恶意样本分析的常规步骤,也是软件兼容性排查的手段(比如判断某个 DLL 为什么加载失败)。

需要提醒的是:脱壳在多数司法辖区涉及「规避技术保护措施」,对商业软件做这件事的法律风险和技术难度同样高。评估之前先想清楚目的。

FAQ

怎么快速判断有没有加壳?

三步:DIE 扫一遍、看导入表是不是只有 LoadLibrary 和 GetProcAddress、算一下最大节的熵。三步两分钟。

UPX 脱壳成功率多高?

标准编译的 UPX 基本 100%。魔改版要看改了哪里,改壳头的话官方工具失效但手动仍可行。

脱壳之后一定能看懂吗?

不一定。脱壳只是恢复可分析状态,代码本身可能还经过混淆、控制流平坦化。脱壳是起点不是终点。

.NET 程序需要脱壳吗?

.NET 的壳(如 .NET ReActor)保护的是 IL 而不是机器码,脱壳思路完全不同,一般用 DNSpy 配合专用脱壳工具。

虚拟机保护的代码有办法还原吗?

有,但成本很高。常规做法是定位 handler 表、逐条还原语义,或者做动态 trace 记录实际执行路径。不是「点一下就好」的活。

结语

识别加壳不难,三个特征两分钟就能判断。难的是判断之后:压缩壳值得脱,虚拟化壳要评估投入。

真正省时间的做法是先花十分钟判断「值不值得」,而不是埋头开始脱。

参考资料

目录导航
  • 三个判断依据
    • 依据一:节表长得不对劲
    • 依据二:熵值接近 8
    • 依据三:导入表只剩下两个函数
  • 用工具确认
  • 压缩壳与保护壳的分水岭
  • 保护壳是另一个量级
  • 坑清单
  • 边界
  • FAQ
  • 结语
    • 参考资料
  • 相关文章:

  • Ghidra 静态分析入门:把地址对过去,看伪代码2026-10-28 08:41:33
  • Frida 动态注入入门:hook 一个函数、改一个返回值2026-10-28 08:41:33
  • PE 文件结构入门:导入表、节表,和「这个 EXE 到底链接了什么」2026-10-28 08:41:33
  • 反调试手段识别:调试点运行就退出,到底是谁在拦你2026-10-28 08:41:33
  • x64dbg 入门:断点、调用栈,以及「谁在改这个地址」2026-10-28 08:41:33
  • 用 ReClass 还原游戏结构体:从内存碎片到可直接用的偏移表2026-10-28 08:41:33
  • Cheat Engine 入门实战:从扫描到指针链,自己动手定位游戏数据2026-10-28 08:41:33
  • 发表评论

    ◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。