简介:这是一套面向逆向分析人员的Themida WinLicense脱壳与调试辅助工具集,覆盖1.8.X至2.X版本保护程序,适合具备一定Windows逆向基础、需要开展加壳识别、调试跟踪与脱壳流程验证的从业者。包内共292个文件,约2.23MB,以inc头文件、vm虚拟机皮肤、h头文件、lng多语言文件、pas与cpp源码、lib库文件及各类工程配置为主,涵盖32/64位主程序、C/Delphi/Go/PureBasic等多语言开发接口、SecureEngineSDK动态库、SDK示例工程、宏定义检查模块与帮助文档。资源同时提供dolphin、shark、puma、fish系列虚拟机皮肤与custom_vms自定义VM模板,兼容COFF、OMF、PE等多种目标文件格式,并支持定制化消息DLL与保护状态检测。已有86人学习关注,读者可据此搭建完整的Themida分析环境,对照示例工程理解SDK调用方式,提取加壳特征并验证脱壳思路,适合作为逆向学习与工具二次开发的参考素材。
1. Themida/WinLicense 1.8–2.x 脱壳与调试辅助:先搞清楚你在对抗什么
拿到一个加了 Themida 或 WinLicense 1.8–2.x 壳的目标,很多人第一反应是找“一键脱壳工具”,结果脚本跑完要么进程直接崩,要么 dump 出来的 PE 一运行就报错。问题不在工具,在于没搞清这一代保护到底做了什么。Themida 和 WinLicense 是同一套保护体系的两个产品线,核心机制一致:代码虚拟化、反调试、内存校验、IAT 加密、区段混淆层层叠加。1.8 到 2.x 这个区间横跨了多个内部版本,每个小版本的 VM handler 结构和反调试触发点都有差异,所以“专用工具集”不是指某一个万能脚本,而是一组针对不同环节的辅助手段——反反调试补丁、内存 dump、IAT 重建、VM 入口定位。这套东西适合谁?适合已经能用调试器跟到 OEP 附近、但卡在反调试或 IAT 修复上的逆向工程师。如果你还没搞懂 PE 结构和基本调试流程,先补基础,否则后面每一步都是玄学。
2. 反调试对抗:Themida 1.8–2.x 的检测点与绕过思路
2.1 先看清它用了哪些反调试手段
Themida 1.8–2.x 的反调试不是单一 API 调用,而是多层组合。常见的有:IsDebuggerPresent和CheckRemoteDebuggerPresent这类基础检测;NtQueryInformationProcess查ProcessDebugPort、ProcessDebugFlags、ProcessDebugObjectHandle;NtSetInformationThread把ThreadHideFromDebugger设上让调试器收不到异常;时间戳检测(rdtsc)判断单步执行;NtQuerySystemInformation查内核调试器;还有硬件断点寄存器 DR0–DR3 的清零检测。1.8 版本偏重 API 层检测,2.x 开始大量用直接系统调用(syscall)绕过用户层 hook,同时增加了对调试器窗口标题和进程名的扫描。
我一般会先用一个干净环境跑一遍,在调试器里下NtQueryInformationProcess断点,看它查了哪些 class。但 2.x 可能不走导入表,直接内联 syscall,这时候断点要下在ntdll的 stub 上或者用内核调试。常见做法是配合 WinDbg 双机调试,在目标机内核里下nt!NtQueryInformationProcess断点,这样不管用户层怎么绕都能拦到。
2.2 用补丁方式绕过用户层检测
对于 1.8 和早期 2.x,用户层补丁仍然有效。核心思路是把关键检测函数的返回值改成“未检测到调试器”。下面是一个典型的补丁脚本框架,用 Python 配合pefile和capstone定位并修改:
import pefile from capstone import Cs, CS_ARCH_X86, CS_MODE_32 def find_and_patch_antidebug(filepath, output_path): pe = pefile.PE(filepath) # 定位 .text 段 for section in pe.sections: if b'.text' in section.Name: code = section.get_data() base = section.VirtualAddress break md = Cs(CS_ARCH_X86, CS_MODE_32) # 扫描 IsDebuggerPresent 调用模式:FF 15 xx xx xx xx patches = [] for insn in md.disasm(code, base): if insn.mnemonic == 'call' and 'dword ptr [0x' in insn.op_str: # 检查目标是否为 IsDebuggerPresent 的 IAT 项 # 实际使用时需解析导入表拿到 IAT 地址 pass # 更直接的方式:把 IsDebuggerPresent 的 IAT 项指向一个返回 0 的 stub # 这里省略 IAT 解析细节,重点在思路 pe.write(output_path) print(f"[+] patched file written to {output_path}") # 参数说明: # filepath: 原始加壳文件路径 # output_path: 补丁后输出路径 # 实际补丁时需先解析导入表,找到 IsDebuggerPresent 的 IAT 槽位 # 将其替换为指向自定义 stub 的地址,stub 内容为 xor eax,eax; ret这段代码展示的是定位思路,实际落地时更稳的做法是直接在 IAT 里把IsDebuggerPresent、CheckRemoteDebuggerPresent的地址替换成自己写的返回 0 的函数。参数上注意:32 位和 64 位 PE 的 IAT 结构不同,Themida 2.x 的 64 位版本还会对 IAT 做加密,直接改 IAT 可能触发校验。这时候要配合内存断点,在 IAT 解密完成后、校验之前修改。
2.3 处理时间戳和硬件断点检测
rdtsc检测单步的绕过方式是在调试器里禁用单步,改用硬件断点或内存断点。硬件断点检测的对抗更麻烦:Themida 会读 DR 寄存器,如果发现非零就认为被调试。绕过方法是在调试器里把硬件断点设成“一次性”的,触发后立即清除,或者用NtSetContextThread在异常处理里临时清零 DR 寄存器再恢复。WinDbg 里可以用.thread切换后手动改上下文,但操作繁琐。我一般会写一个调试器插件,在每次异常分发前自动清零 DR0–DR3,异常处理完再恢复,这样对目标透明。
提示:2.x 版本对
rdtsc的检测频率很高,单纯跳过一两次没用,需要在关键循环里持续 patch 或者用虚拟机时间戳固定。
3. 内存 dump 与 OEP 定位:从运行态到可执行文件
3.1 找到 OEP 的三种实用方法
Themida 的 OEP 通常藏在 VM 入口之后,代码经过虚拟化还原后才跳回原始入口。1.8–2.x 的 OEP 定位常用三种方法。第一种是内存断点法:在.text段或栈上设访问断点,等壳的解密循环跑完,最后一次跳转往往就是 OEP。第二种是栈回溯法:壳在跳转前会把原始入口地址压栈,在pushad/popad附近下断,观察栈上出现的地址。第三种是区段特征法:Themida 解密后的原始代码段通常有特定的区段名或节区特征,用Scylla或PE-bear扫描内存中的 PE 头,找到与磁盘文件不一致的区段。
我一般先用内存断点粗定位,再用栈回溯确认。具体操作:在 x64dbg 里对.text段设“内存访问”断点,运行后会在壳的解密代码处断下,此时单步跟到jmp或call到原始代码的位置,那个目标就是 OEP。注意 2.x 可能会多次跳转,中间穿插 VM 代码,需要耐心跟。
3.2 用 Scylla 做 dump 和 IAT 修复
找到 OEP 后,dump 和 IAT 修复是下一步。Scylla 是目前最顺手的工具,支持 32/64 位,能自动扫描 IAT 并重建。操作步骤:
- 在 OEP 处暂停调试器,打开 Scylla,选择目标进程。
- 填入 OEP 地址(Scylla 通常能自动识别,识别不到就手动填)。
- 点击 “IAT Autosearch”,让 Scylla 扫描导入表。
- 如果自动扫描结果不全,点 “Get Imports” 手动补充,或者用 “Trace” 功能动态跟踪。
- 确认无误后点 “Dump”,保存内存镜像。
- 再点 “Fix Dump”,选择刚才的 dump 文件,Scylla 会重建 IAT 并输出修复后的 PE。
参数上注意:Scylla 的 “Advanced” 里可以设置 IAT 搜索的起始和结束地址,如果自动搜索漏了,手动指定.text段范围通常能补全。2.x 的 IAT 可能被拆成多段,需要多次 “Get Imports” 合并。
3.3 处理 dump 后的区段对齐和校验
dump 出来的 PE 经常遇到区段对齐问题:内存中的区段按SectionAlignment对齐,磁盘文件按FileAlignment对齐,直接 dump 会导致文件大小异常。修复方法是用PE-bear或LordPE调整区段属性,把RawSize和VirtualSize对齐。另外 Themida 会在区段里留校验和,修改后可能触发自校验崩溃。绕过方式是在 dump 前先 patch 掉校验函数,或者用Scylla的 “Fix Dump” 时勾选 “Remove Section Checksum”。
注意:2.x 版本对
.themida区段有完整性校验,dump 后如果保留该区段,运行时会重新校验并崩溃。建议在修复时直接删除或重命名该区段。
4. 避坑与排查:Themida 脱壳中最容易翻车的五个点
4.1 现象:调试器一附加就退出,连断点都来不及下
原因:Themida 2.x 在进程启动早期就调用了NtSetInformationThread设置ThreadHideFromDebugger,同时用NtQueryInformationProcess检测调试端口。如果调试器附加时机太晚,壳已经完成检测并触发退出。
解决:用调试器的“启动时暂停”功能,在进程创建后立即断下。WinDbg 用-o参数配合sxe ld在加载 ntdll 时断下;x64dbg 在“选项→事件”里勾选“系统断点”。然后在ntdll的NtSetInformationThread入口下断,拦截ThreadHideFromDebugger调用并跳过。
4.2 现象:OEP 找到了,dump 出来运行报“不是有效的 Win32 应用程序”
原因:dump 时只保存了内存镜像,没有修复 PE 头。内存中的 PE 头可能被壳修改过,SizeOfImage、AddressOfEntryPoint等字段与原始文件不一致。
解决:用Scylla的 “Fix Dump” 自动修复 PE 头,或者手动用PE-bear对比内存和磁盘的 PE 头,把关键字段改回正确值。重点检查AddressOfEntryPoint是否指向 OEP,SizeOfImage是否覆盖所有区段。
4.3 现象:IAT 修复后部分函数仍然无效,调用就崩
原因:Themida 对部分 IAT 项做了加密或重定向,Scylla 自动扫描时可能把加密后的地址当成有效导入,或者漏掉了被 VM 保护的项。
解决:用 “Trace” 功能动态跟踪 IAT 调用,在运行时记录每个 API 的实际地址。具体操作:在 Scylla 里点 “Trace”,然后让目标程序运行一段时间,Scylla 会记录所有经过 IAT 的调用并生成更完整的导入表。对于被 VM 保护的项,需要手动在调试器里跟到调用点,记录真实地址后手动添加到 IAT。
4.4 现象:脱壳后的程序功能正常,但过几分钟自动崩溃
原因:Themida 在后台线程里做周期性自校验,检查代码段和 IAT 是否被修改。脱壳后校验线程仍然在跑,发现不一致就触发崩溃。
解决:在脱壳前先定位校验线程的入口,patch 掉校验逻辑。常见做法是在CreateThread或NtCreateThreadEx下断,找到壳创建的监控线程,然后在其入口处直接ret。或者用Scylla的 “Advanced” 选项里的 “Kill Anti-Debug Threads” 功能自动清理。
4.5 现象:64 位版本用 32 位工具脱壳,dump 出来完全不能用
原因:Themida 2.x 的 64 位版本和 32 位版本在 VM handler、IAT 结构、反调试实现上完全不同。用 32 位调试器附加 64 位进程,或者用 32 位 Scylla 处理 64 位 dump,都会导致地址截断和结构错乱。
解决:确认目标进程位数,用对应的 64 位调试器(x64dbg 的 64 位版本、WinDbg x64)和 64 位 Scylla。检查 PE 头的Machine字段,0x8664是 64 位,0x014c是 32 位。64 位版本还要注意RIP相对寻址和IMAGE_THUNK_DATA的 64 位结构。
5. 进阶技巧:用脚本自动化重复性脱壳流程
5.1 把反调试补丁和 dump 串成一条流水线
手动脱壳做多了会发现,大部分步骤是重复的:附加、下断、patch 反调试、找 OEP、dump、修 IAT。我习惯用 x64dbg 的脚本功能把这些串起来。x64dbg 支持类似汇编的脚本语法,可以在特定断点触发时自动执行操作。下面是一个脚本片段,展示如何在NtQueryInformationProcess断点处自动修改返回值:
// x64dbg 脚本:拦截 NtQueryInformationProcess 并返回未检测到调试器 // 在命令窗口用 scriptload 加载,或保存为 .txt 后用 scriptcmd 执行 // 在 ntdll.NtQueryInformationProcess 入口下断 bp ntdll.NtQueryInformationProcess // 断点触发时的处理 // 读取第二个参数 ProcessInformationClass // 如果是 ProcessDebugPort (7) 或 ProcessDebugFlags (0x1f) // 直接设置返回值为 STATUS_SUCCESS 并跳过原函数 // 实际脚本中需要用寄存器读取和条件判断 // 这里展示的是逻辑框架,具体寄存器操作依位数而定脚本的关键在于条件判断:不是所有NtQueryInformationProcess调用都要拦截,只拦ProcessDebugPort、ProcessDebugFlags、ProcessDebugObjectHandle这几个 class。参数上注意 32 位用[esp+8]读第二个参数,64 位用rdx。拦截后把eax或rax设为 0,然后ret适当字节数跳过原函数。
5.2 用 Python 批量处理多个样本
如果手头有多个同版本壳的样本,可以写一个 Python 脚本批量跑 Scylla 的命令行版本。Scylla 有 CLI 模式,支持-dump和-fix参数。配合pefile做 dump 后的区段清理,能省不少时间。我一般会先手动跑通一个样本,记录下 OEP 偏移和 IAT 范围,然后把这些参数写进脚本,批量处理时直接套用。注意不同样本的 OEP 可能不同,脚本里要留手动调整的入口。
5.3 验证脱壳结果的三个检查点
脱壳完成后别急着收工,先做三个检查。第一,用PE-bear打开 dump 文件,确认区段对齐、入口点、导入表都正常。第二,在干净虚拟机里运行脱壳后的程序,观察是否有异常退出或功能缺失。第三,用IDA加载脱壳文件,看代码段是否可反编译,如果大量代码显示为db字节,说明 dump 不完整或 VM 代码没还原。这三个检查点能过滤掉大部分“看起来成功实际不能用”的情况。
我自己的习惯是:每次脱壳前先拍一个虚拟机快照,脱壳过程中所有 patch 操作都在快照里做,一旦崩溃直接回滚重来。这个习惯帮我省了无数次重装系统的时间。Themida 1.8–2.x 的脱壳没有银弹,每个版本都有细微差异,但把反调试、OEP、IAT、校验这四块拆开逐个击破,大部分样本都能拿下。希望帮到你。
本文还有配套的精品资源,点击获取