简介:一份面向 Themida/WinLicense V1.8.X~V2.X 保护壳的脱壳工具合集,主要适合逆向工程学习者、安全分析人员以及需要分析该系列壳保护机制的开发者。资源描述虽简短,但强调其可以使用,包内提供多种语言工程与实例,覆盖从宏定义、配置脚本到可执行工具的相关内容,便于直接复用。压缩包共计301个文件、约32.77MB,核心内容包括 inc/vm/pas/cpp 等源码与工程文件,dll/exe 执行工具,chm/pdf 说明文档,以及 lng/rc 等界面或资源辅助项,目录结构兼顾代码示例与工具链,便于按需取用;另附少数历史配置与备份文件,便于对比不同版本间的调整差异。目前已有 1322 人学习下载。通过这份资料,读者可快速掌握针对该系列壳的脱壳思路,参考现成示例减少环境配置成本,也可将其中宏定义、汇编代码与工程配置迁移到自己的项目中,提升分析效率,并在实际调试中理解保护机制的具体表现。
1. 为什么 Themida 1.8.X-2.X 的壳这么难脱:先搞懂它到底在对抗什么
我做过不少带 Themida 和 WinLicense 保护的分析任务,对“最佳脱壳工具”这个说法最大的理解是:不存在一个万能按钮,但确实存在一套针对 V1.8.X-V2.X 最可靠的工具组合。你从调试器加载样本那一刻起,壳就开始多路试探——PEB 里的调试标志、调试端口、硬件断点余量、时间差,任何一路暴露,进程会直接结束或走进一条假流程。不少人在这一步连续翻车,不是不懂脱壳,而是没把“隐藏调试器—找 OEP—重建 IAT”整条链路当作一件事来对待。这篇讲的是怎么选工具、怎么设参数、哪些地方必须手动兜底、哪些坑反复出现,适合手里已有待分析样本、被反调试耗光耐心的安全测试和软件开发人员。
2. 选对工具链:三层配合才谈得上“最佳”
说句实在话,Themida 和 WinLicense 的脱壳从 1.8 到 2.X 变化不小,但通用的落地路径没变:先保证调试器不被发现,再找个能准确抓内存和重建导入表的工具,最后用脚本或手动判断把 OEP 从壳代码里摘出来。这三层少哪一层都会出问题——调试器暴露,进程秒退;dump 工具烂,抓一百遍都是废文件;OEP 判断错了,后面所有修复都是白做。
2.1 调试器与隐藏插件的搭配:为什么选 x64dbg + ScyllaHide
Themida 1.8.X-2.X 的反调试不是单一检测点,而是并发多路。常见的有 PEB 里的BeingDebugged、NtGlobalFlag、ProcessHeapFlags,也有通过NtQueryInformationProcess查询调试端口,还有把关键线程设为ThreadHideFromDebugger的骚操作。如果调试器不做隐藏,壳的第一段解压代码根本走不到。
我一般用 x64dbg 作主调试器,配 ScyllaHide。选这个组合而不是老牌 OllyDbg,主要三个理由:一是 64 位样本在 OllyDbg 上没法稳定跟;二是 x64dbg 的插件被 ScyllaHide 适配得更完整,后者的隐藏能力明显强于 OllyDbg 时代;三是 x64dbg 的条件断点记录和日志窗口在做大量 API 监控时更好用。
ScyllaHide 的核心参数不是全勾最优,这是很多人没意识到的。我常用的配置是下面这张表,目标是在“足以骗过早期探测”和“不留下新的行为痕迹”之间取平衡。
| 配置项 | 推荐值 | 作用 |
|---|---|---|
| Hide PEB: BeingDebugged | 勾选 | 清掉 PEB 里的进程被调试标志 |
| Hide PEB: NtGlobalFlag | 勾选 | 清掉堆调试相关全局标志 |
| Hide PEB: ProcessHeapFlags | 勾选 | 掩掉堆头部调试标志 |
| NtQueryInformationProcess | 勾选 | 截住调试端口类查询 |
| NtSetInformationThread | 勾选 | 防止线程被隐藏后断点失效 |
如果把 ScyllaHide 的全部功能都打开,反而容易被 Themida 的行为检测识破,因为有些系统调用在特定版本上不允许被改,强行挂钩会留下更明显的痕迹。我一般只开这五项,剩下保持默认,先过了第一轮特征检测再随机应变。走到第 3 章后你会发现,这个隐藏配置决定了后面 API 断点能不能用。
2.2 转储与 IAT 重建:Scylla 比 ImportREC 更适合这个版本段
确定 OEP 之后要解决两件事:把当前进程内存写成文件,再把被壳抹掉的导入表补回来。老牌的 ImportREC 对 Themida 1.8 的某些早期变种还有成功率,但在 2.X 上我几乎没见过它能一次修复干净,问题出在它把壳自己构造的跳板表当成了真实导入表,修出来的程序一运行就崩。
这个版本段我更信任 Scylla。它能识别真实导入表和 fake jmp 跳板的差异,也支持保存重定位表,正好堵住 x64 样本换基址就崩的常见缺口。操作顺序有讲究:先确认 OEP,再点 Auto Search 让 Scylla 扫 IAT 起始地址,然后 Get Imports 做一次预览,确认没有大量 OrdinalOnly 或 Invalid 标记后再 Dump,最后 Fix Dump。先 dump 再搜 IAT 是错误顺序,内存布局已经离开上下文,扫到的导入表会缺一截。
给一个最干净的启动命令:
# 直接把样本交给 x64dbg 加载,不要先开调试器再去 attach x64dbg.exe "C:\samples\legit_app.exe"这样做的原因是 Themida 对 attach 动作有专门的检测,直接启动可以少暴露一路特征。加载后先停到系统断点,确认 ScyllaHide 的注入日志正常,再按 3.1 的步骤走。
2.3 脚本引擎与“一键脱壳”的误区,顺便说下 VMProtect 对比
网上流传的脱壳脚本,大多基于 OllyDbg 的 ODbgScript 或 x64dbg 的脚本命令写。基本思路是:在VirtualAlloc、VirtualProtect、NtProtectVirtualMemory之类函数下断点,记录内存块的分配和属性变化,最后跳到壳认为的原始入口。这个思路在 1.8.X 上确实能跑通,但到 2.X 之后,脚本经常在某个普通的ret指令上停下来,往后单步全是虚拟化指令,继续走就进了死循环。
所以我把脚本定位成“缩小范围”的工具,而不是“一键脱壳”。配合日志来用:
# x64dbg 脚本,把核心内存操作函数断下来,再在断点属性里加参数日志 bp VirtualAlloc bp VirtualProtect bp NtProtectVirtualMemory设置完成后按 F9 运行,每命中一次就在日志窗口看地址和长度,最后被改成可执行属性的那块内存,就是壳完成解压的现场。脚本推的方向如果和日志记录矛盾,我会优先相信日志。
同样难脱的还有 VMProtect,很多人在搜 Themida 时会连带到“vmprotect 脱壳工具”。提醒一句,两者思路不同,工具链不能拿来直接换。Themida/WinLicense 主要靠频繁的 API 反调试和分阶段解密,而 VMProtect 会把代码搬进自带的虚拟机解释器,Scylla 对后者几乎无效。选工具之前先看清壳的种类,别在错误的壳上浪费一下午。
3. 用 x64dbg + Scylla 跑通一次 Themida 脱壳的最小流程
3.1 站在解压现场:用内存属性变化判断壳是否走完
跑通一次脱壳的关键不是从头单步,而是找到“解压完成”的信号。Themida 一般会在原始入口片段里先解密代码节,再转移控制权,解密过程中会反复调用VirtualProtect或NtProtectVirtualMemory,把内存区域从只读或读写改成可执行。
我通常在两个函数上下断点:
# x64dbg 断点命令:先断下来,再在断点属性里记录 p1 地址、p2 大小、p3 新属性 bp VirtualProtect bp NtProtectVirtualMemory每次断下后,日志窗口里会看到这次调用的参数。当某条记录显示目标模块名是样本本身、属性变成了0x40(PAGE_EXECUTE_READWRITE)或0x20(PAGE_EXECUTE_READ),并且地址落在代码节附近时,基本可以认定解压循环已经把关键代码写到位。在这块区域下断点,按 F9,命中时就是壳要交出控制权的临界点。
注意,日志里会混进大量 ntdll、kernelbase 自己的调用,那些不是壳的行为。判断标准只看两个维度:模块名是否为加载样本本身,属性是否朝着“可执行”变化。如果只有系统模块在刷日志,说明还没到壳主流程,继续跑。
3.2 用硬件执行断点确认 OEP,而不是普通断点
壳在进入原始入口之前,会做一次跳转,常见形式是jmp或push+ret。如果在这里下普通 INT3 断点,很容易被 Themida 的断点扫描发现,然后进程安静退出。我一般改用硬件执行断点,让断点信息存在 CPU 调试寄存器里,壳不容易扫描到。
在 x64dbg 里对候选地址用bph命令设置硬件执行断点:
# x64dbg 硬件断点命令,bph 后跟目标地址 bph 0x00401000设置后按 F9。命中时先不急着庆祝,检查当前指令上下文:如果看到push ebp / mov ebp, esp或sub rsp, 0x28这类常见编译器开头,并且附近有指向字符串的引用,这才是原始入口。判断不能只看一条指令,Themida 自己的解密桩也会伪造类似序言,要结合栈回溯和模块区段地址来确认。
3.3 Scylla 的 Dump 与修复顺序,以及 PE 头自检
确认 OEP 后切到 Scylla 插件,操作顺序固定为五步:
- 进程列表选当前进程,ImageBase 填模块加载基址。
- 点 Auto Search,Scylla 扫出 IAT 起始地址和大小。
- 点 Get Imports,看列表里是否出现大量 Invalid 或 Ordinal 占位。
- 没问题就点 Dump,保存为 unpacked.exe。
- 点 Fix Dump,把导入表写进文件。
有人喜欢在 Dump 之后直接用十六进制工具改文件,那是在给自己找麻烦。正确路径是让 Scylla 把导入表和新 PE 头一起处理好,改完再用脚本自检。
这里给一个 Python 校验脚本,读取 PE 区段信息,判断壳是不是真的从镜像里消失了:
import struct def dump_sections(path): with open(path, 'rb') as f: head = f.read(0x40) if head[:2] != b'MZ': print('not a valid MZ file') return pe_off = struct.unpack_from('<I', head, 0x3C)[0] f.seek(pe_off) pe_sig = f.read(4) if pe_sig != b'PE\x00\x00': print('no PE signature') return coff = f.read(20) num_sec = struct.unpack_from('<H', coff, 2)[0] opt_size = struct.unpack_from('<H', coff, 16)[0] f.seek(pe_off + 4 + 20 + opt_size) for i in range(num_sec): raw = f.read(40) name = raw[:8].rstrip(b'\x00').decode('latin1', 'replace') vsize = struct.unpack_from('<I', raw, 8)[0] rawsize = struct.unpack_from('<I', raw, 16)[0] flags = struct.unpack_from('<I', raw, 36)[0] print(f'{i+1:02d} {name:8s} vsize=0x{vsize:X} rawsize=0x{rawsize:X} flags=0x{flags:08X}') dump_sections('unpacked.exe')这段代码会遍历 PE 文件头里的区段表,打印每个区段的名字、虚拟大小、原始大小和属性标志。正常编译器生成的 PE 区段名通常是.text、.data、.rdata,壳处理过的文件则常常出现一组随机短名字节。修复前后各跑一次,对比区段数量就能量化壳残留。flags=0xE0000020表示“代码 + 可执行 + 可读 + 已初始化”,如果这个属性的区段数量比正常程序多出好几个,壳的代码段可能仍被保留在镜像里。
在修复时,Scylla 的参数里有一个New ImageBase,这个字段在 x64 样本上要格外小心。Themida 2.X 开了 ASLR,如果 dump 时把 ImageBase 改成 0,且没有保存重定位表,程序加载地址一变就会崩。一般来说保持原始 ImageBase,并勾选包含重定位表的修复选项。修复完程序仍然启动报错时,第一个要检查的就是这里。
4. 手动兜底:当 Scylla 失灵后的 OEP 精确定位与 IAT 手工修复
4.1 用条件记录日志缩小 OEP 候选区
Scylla 失灵大多不是因为工具不行,而是 OEP 根本没找对。壳跳进虚拟化片段时,你也会看到一些像正常代码的东西,但它可能是解释器的一块调度代码。这时我会上执行追踪,逐条记录跳转目标,看执行流有没有从壳的调度区真正落到样本的.text区段。
在 x64dbg 里对候选入口区块开启记录执行,跑几十上百条指令后看日志分布。如果日志里连续出现push、sub rsp、call这类常规函数序言,并且地址全部落在样本模块范围内,这就是原始代码开始执行的证据。如果日志里大量出现同一个跳板地址,或反复落在两个地址之间,说明还困在虚拟机分发循环里,没到 OEP。
这一步不要追求快,多跑几个来回。记录日志对性能有影响,但 Themida 脱壳本来就是慢工细活,宁可多花十分钟也不要抓一个带病的 dump 回去修。
4.2 手工补 IAT:从 GetProcAddress 调用点反向整理
Themida 1.8 早期版本经常在运行时通过LoadLibrary和GetProcAddress动态拿 API,Scylla 对这类动态 IAT 的静态扫描经常漏。遇到修复后列表里全是 Ordinal 或 Invalid 的情况,我改用动态跟踪:在 OEP 之后跟着执行流,遇到call eax或call [reg]时查看目标 API 地址和所在 DLL,记录下来。
把这些记录写回 dump 文件,最省事的办法是用 pefile 库先看当前导入状态:
import pefile pe = pefile.PE('unpacked.exe', fast_load=False) pe.parse_data_directories() for entry in pe.DIRECTORY_ENTRY_IMPORT: names = [] for imp in entry.imports: if imp.name: names.append(imp.name.decode('ascii', 'replace')) else: names.append(f'ordinal:{imp.ordinal}') print(entry.dll.decode('ascii', 'replace'), names[:8])这段代码会打印当前 PE 的导入表。如果打印出来的 DLL 是user32.dll、kernel32.dll或程序本身依赖的库,说明 Scylla 修复基本到位;如果全是系统路径下的杂项 DLL,或者导入项极度稀疏,说明 IAT 还没补齐,需要回到 4.1 重新找 OEP。pefile 的参数fast_load=False保证解析所有目录,比默认模式多花一点时间但信息完整。
注意,pefile 只能解析,不能自动补全动态 IAT。补全的工作是拿到上面打印的 DLL/API 列表之后,手动修改导入描述符,让新增 API 指向 Scylla 已留出的空白 IAT 位置。这块没有通用命令,因为每个样本的 IAT 布局不同,理解了结构之后再动手就不玄学了。
4.3 WinLicense 的授权校验和脱壳要分开看待
WinLicense 与 Themida 最大的区别,是它在壳之外还会加入注册码、试用期、内存补丁一致性校验之类的业务逻辑。经常出现的情况是:壳已经脱干净,程序也能启动,但几秒后弹窗提示授权无效。很多人误以为壳没脱干净,回头重抓 dump,重复几轮也没用。
我处理这类样本时会先确认提示代码的归属。如果弹窗来自业务模块而不是壳的解密段,那就说明脱壳已经完成,剩下的是授权管理逻辑,不在“脱壳工具”的解决范围内。分析时如果需要继续跟业务算法,带着授权提示操作即可,不要试图在脱壳阶段同时解决两个问题,那会浪费大量时间。
4.4 不要追求完整还原虚拟化代码
Themida 2.X 会对关键函数做虚拟化处理,让它变成壳解释器的字节码。Scylla、手工 IAT 都无法还原这段代码,因为它在执行时不会直接调用熟悉的 API,而是通过壳的指令解释循环。很多人在这一块死磕,试图把虚拟指令一一翻译回原始汇编,最后的产出往往只覆盖一小段,还容易引入新错误。
我的建议是:除非分析目标就是这段被虚拟化的算法本身,否则保留虚拟片段没有坏处。把 OEP 定位到能正常进入程序主流程的程度,虚拟化部分当作黑匣子处理,已经能满足大多数安全测试和软件维护需求。验证标准同样是程序能跑、关键 API 能被跟踪,而不是追求“完美还原”。
5. 避坑手册:Themida 1.8-2.X 脱壳中反复出现的 5 类翻车
在脱壳上吃过不少亏之后,我把最常见的翻车点整理成下面五类,每一条都是先讲现象,再说原因和解决。这些坑不是偶然发生的,基本每个样本都会踩中至少一个。
5.1 现象:样本还没跑到系统断点就崩溃
原因:ScyllaHide 没有随调试器完成注入,或者杀软拦截了注入用的驱动加载。Themida 在早期阶段就开始探测调试环境,插件没起来等于裸奔。
解决:以管理员身份启动 x64dbg,确认 ScyllaHide 的注入日志显示钩子已装;杀毒软件对调试目录做排除;某些场景下用 TITANHide 替换 ScyllaHide,能躲过更激进的特征识别。不要为了省事跳过这一步,否则后面所有断点都没有意义。
5.2 现象:下普通断点后进程安静退出
原因:INT3 断点会修改内存字节,Themida 的校验线程会周期性扫描代码段,发现被改过的字节就触发异常处理器,最终调用ExitProcess。普通断点在这个壳里非常不可靠。
解决:换成硬件断点,只存在 CPU 寄存器里,不改内存字节。如果硬件断点也无法命中,检查是否开启了 ScyllaHide 的上下文记录隐藏,必要时把目标线程的隐藏参数一起勾上。记住一个原则:能不用 INT3 就不用 INT3。
5.3 现象:Scylla 修复完启动即报“入口点错误”
原因:修复时写入的 OEP 地址实际上还在壳代码里,不是原始入口。Auto Search 找到的 IAT 地址可能是对的,但入口点落在了解密桩中间,程序跑了几条指令就失去上下文。
解决:重新用 3.2 的硬件断点确认 OEP,同时在目标地址单步一次,观察是否进入标准函数序言。也可以用 4.1 的日志法对比执行流。入口点错了的时候,导入表修得再漂亮也跑不起来。
5.4 现象:脱壳后程序提示缺少系统 DLL
原因:壳把一部分 DLL 留到运行时手动加载,静态扫描时这些 DLL 还没在导入表里出现;另外重定位表没保存的话,DLL 基址可能被覆盖,也会产生这类报错。
解决:保持 ImageBase 不变并勾选保存重定位;对运行时加载的 API 用手工 IAT 补丁;如果依赖链上有第三方 DLL,把对应文件放进分析目录,避免路径问题干扰验证结果。
5.5 现象:网上教程在 32 位能跑通,换 64 位样本全废
原因:32 位下壳依赖的 PEB 和调试端口检测路径在 64 位里有差异,Themida 2.X 对 x64 的虚拟化也更激进。老脚本大量基于 32 位 API 行为,直接套在 64 位进程上不生效。
解决:64 位样本优先用日志法和硬件断点,不依赖脚本;Scylla 里注意进程位数是否匹配,不要在一个 32 位调试器里加载 64 位 dump,修复结果会全部对不上。遇到网上教程先看样本位数,再看壳版本,最后才看工具参数。
6. 验证脱壳成果:用运行对比和静态扫描确认壳真的没了
6.1 三道验证:静态特征、模块视图和运行行为
脱壳只完成一半,另一半是验证。我至少跑三道确认,缺一不可。
第一道静态扫描,用 Detect It Easy 的命令行版在修复后的文件上扫特征:
# 在 Git Bash 或 Linux 分析机上执行,输出 JSON 后过滤壳特征 diec -j unpacked.exe | grep -i themida没有任何输出,说明 DIE 的特征库里没有发现 Themida 特征;有输出也不一定代表脱壳失败,可能只是区段名残留。继续第二道确认。
第二道运行验证:用 x64dbg 重新加载脱壳后的样本,停在入口后打开模块窗口,检查是否存在设备路径下的驱动模块。正常程序模块列表里只会出现系统 DLL 和程序自己的模块,如果出现壳相关的驱动名,说明注入还没清干净。
第三道运行行为验证:直接运行脱壳程序,观察窗口或控制台表现是否和原程序一致,再用进程监视器看 DLL 加载顺序。如果启动后大量访问非系统路径的临时文件,壳的代码可能仍在运作,需要回到第 4 章处理。
这三个验证过完,才敢把脱壳产物放进后续分析。我自己的习惯是把它当“后悔药”用:宁可验证多花十分钟,也不要等部署到分析环境里才发现 dump 带病。日志看得多了,OEP 的节奏自然就熟了,哪个地址可疑、哪条 API 是伪跳板,慢慢会有手感。希望帮到你。
本文还有配套的精品资源,点击获取