ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Themida/WinLicense 脱壳实战:OEP 定位、IAT 重建与 custom_vms 调试辅助

Themida/WinLicense 脱壳实战:OEP 定位、IAT 重建与 custom_vms 调试辅助 简介这是一套面向逆向分析人员的Themida WinLicense脱壳与调试辅助工具集覆盖1.8.X至2.X版本保护程序适合具备一定Windows逆向基础、需要开展加壳识别、调试跟踪与脱壳流程验证的从业者。包内共292个文件约2.23MB以inc头文件、vm虚拟机皮肤、h头文件、lng多语言文件、pas与cpp源码、pb与go接口、lib库文件及sln/vcproj工程为主另含Themida64.exe主程序、SecureEngineSDK32.dll动态库、custom_vms自定义VM模板、ExamplesSDK示例工程、宏定义检查模块与Themida Help.chm帮助文档兼容COFF、OMF、PE等多种目标文件格式。目前已有86人学习。借助多语言开发接口与定制化消息DLL读者可完成保护状态检测、特征提取与脱壳流程复现并参考SDK示例快速搭建调试环境目录按模块划分清晰便于按需检索与二次开发。1. 拿到 Themida/WinLicense 工具集先别急着双击它到底能帮你解决什么如果你手头有个加了 Themida 或 WinLicense 壳的程序OD 载入直接卡死、x64dbg 断点全被吞、内存 dump 出来一堆乱码那你大概率已经体会过 SecureEngine 这套保护方案的恶心之处。这个工具集针对的就是 Themida / WinLicense 1.8 到 2.x 这一整段版本区间核心目标不是“一键脱壳”那种神话而是把脱壳和调试过程中最耗时间的几个环节——IAT 重建、OEP 定位、custom_vms 虚拟机代码识别、Themida64 下的反调试绕过——拆成可操作的辅助模块。它适合已经懂基本 PE 结构、能看懂汇编、但被 Themida 的虚拟机和反调试拖住进度的人。新手拿它当学习辅助可以但别指望不懂调试就能跑通。2. SecureEngine SDK 与 custom_vms先搞懂壳在保护什么2.1 SecureEngine 的三层保护结构Themida 和 WinLicense 底层共用 SecureEngine 这套保护引擎1.8 到 2.x 之间虽然版本跨度不小但保护骨架没变。第一层是代码变形mutation把原始指令替换成语义等价但形态完全不同的指令序列第二层是虚拟机VM把关键代码编译成 custom_vms 字节码运行时由内置解释器逐条执行第三层是反调试与反 dump包括时间戳检测、硬件断点寄存器清零、PE 头校验、内存页属性监控。你调试时遇到的“断点莫名其妙消失”多半是第二层和第三层联动VM 入口处会检查 DR0-DR7 是否非零一旦发现就改写执行流。custom_vms 的字节码没有公开指令集每个受保护程序的 VM 解释器还是随机化的所以你不能指望一套 pattern 通吃所有样本。工具集里针对 custom_vms 的部分思路是先在 VM 入口下硬件断点记录解释器 dispatch 循环的跳转表再根据跳转目标反推 handler 地址。这个过程需要你手动确认几个关键点工具只负责加速定位不负责替你判断。2.2 版本差异1.8 和 2.x 在调试辅助上的分水岭1.8 时代的 Themida 对 x64 支持还不完善Themida64 的 VM 结构相对粗糙dispatch 循环里跳转表是连续排列的用工具集里的vm_dispatch_scan脚本扫一遍就能拿到候选 handler 列表。到了 2.x跳转表被拆成多段中间插入垃圾字节而且 handler 地址经过一次异或混淆。我一般会先确认目标程序用的是哪个大版本看区段名1.8 常见.themida和.winlice2.x 多了.boot和.data1的变体。确认版本后再决定用哪套扫描参数。工具集里config/目录下按版本分了配置文件别拿 1.8 的配置去跑 2.x 样本否则扫描结果全是噪声。2.3 用 SDK 头文件反推保护标记SecureEngine SDK 的头文件SecureEngine.h、SecureEngineCustomVM.h虽然不直接出现在加壳后的程序里但你可以从导入表残留和字符串里找到 SDK 版本线索。比如SE_DEBUGGER_PRESENT这类标记字符串在 1.8 里是明文2.x 里被拆成两段分别存储。工具集提供了一个sdk_marker_scan.py用法如下# sdk_marker_scan.py 核心逻辑 import re from pefile import PE def scan_markers(pe_path): pe PE(pe_path) markers [] # 遍历所有区段的可读数据 for section in pe.sections: data section.get_data() # 1.8 明文标记 for m in re.finditer(rbSE_[A-Z_]{4,}, data): markers.append((plain, m.group().decode(), section.Name.strip(b\x00))) # 2.x 拆分标记两段各 4 字节中间隔 8 字节垃圾 for m in re.finditer(rbSE_([A-Z]{2,4}).{8}?([A-Z_]{2,6}), data): combined SE_ m.group(1).decode() m.group(2).decode() markers.append((split, combined, section.Name.strip(b\x00))) return markers if __name__ __main__: for kind, name, sec in scan_markers(target.exe): print(f[{kind}] {name} {sec})这段代码先按区段遍历可读数据1.8 的标记直接正则匹配SE_开头的大写串2.x 的拆分标记用非贪婪匹配跨过中间垃圾字节再拼接。跑完之后你会得到一份标记清单如果看到SE_DEBUGGER_PRESENT或SE_VM_START说明样本确实用了 SDK 的调试检测和 VM 入口标记后续下断点就有方向了。参数上pe_path传目标文件路径脚本不修改文件只读扫描。3. 脱壳实操从 OEP 定位到 IAT 重建的完整链路3.1 用内存断点找 OEP 的两种可靠姿势Themida 的 OEP 不在常规的.text段入口而是在 VM 解释器把控制权交还给原始代码的那一刻。常见做法有两种一是对.text段下内存写入断点等壳解压完原始代码后触发二是跟踪VirtualProtect调用看哪次调用把.text段属性改回可执行。工具集里oep_helper.dll注入后会自动在VirtualProtect和VirtualAlloc上下钩子记录每次内存属性变更的地址和大小。你只需要在日志里找“目标段从 RW 变 RX”的那一条对应的返回地址附近就是 OEP 候选。我一般会配合 x64dbg 的条件断点# x64dbg 条件断点命令在 VirtualProtect 返回处断下 bp VirtualProtect bp cond: [esp0x0C] 0x40 [esp0x08] 0x1000 # 0x40 PAGE_EXECUTE_READWRITE0x1000 一页以上第一条命令在VirtualProtect入口下断第二条是条件第三个参数新保护属性等于PAGE_EXECUTE_READWRITE且第二个参数大小大于一页。这样能过滤掉大量无关调用只留下壳在解压代码段时的关键操作。断下后看调用栈返回地址落在哪个模块那个模块的入口点就是 OEP 的强候选。3.2 IAT 重建为什么自动工具经常翻车Themida 对 IAT 的处理是“打散 加密 延迟填充”。1.8 里 IAT 项还是连续的只是每个指针被异或了一个固定 key2.x 把 IAT 拆成多块分别放在不同区段而且部分项在程序运行到特定代码路径时才填充。工具集里的iat_rebuild.py走的是“运行时抓取”路线在 OEP 断下后遍历原始 IAT 区域对每个非空指针尝试解析其指向的模块和函数名。如果指针指向的地址不在任何已知模块范围内就标记为“延迟填充”等程序继续运行后再二次抓取。# iat_rebuild.py 关键片段 def rebuild_iat(process_handle, iat_start, iat_size): iat_entries [] for offset in range(0, iat_size, 8): # 64 位每项 8 字节 addr read_process_memory(process_handle, iat_start offset, 8) if addr 0: iat_entries.append((offset, None, empty)) continue module find_module_by_address(process_handle, addr) if module: func_name resolve_export(module, addr) iat_entries.append((offset, func_name, resolved)) else: iat_entries.append((offset, addr, pending)) return iat_entriesread_process_memory是封装好的ReadProcessMemory调用find_module_by_address遍历进程模块列表比对地址范围resolve_export在模块导出表里反查函数名。跑完第一遍后pending项就是需要二次抓取的。参数上iat_start和iat_size从 PE 头的导入表目录里读注意 2.x 的导入表目录可能指向一个跳转桩而不是真实 IAT需要先跟一次跳转。3.3 修复重定位与区段对齐脱壳后的文件要能正常运行重定位表必须修复。Themida 在加壳时会抹掉原始重定位信息运行时由壳自己维护一份。工具集里的reloc_fix.py通过对比脱壳前后内存镜像和磁盘镜像的差异反推重定位项。具体做法在 OEP 断下后dump 一份完整内存镜像让程序继续运行到某个稳定状态再 dump 一份两份镜像做差差值非零且落在可重定位区域的地址就是重定位项候选。这个方法对 1.8 效果很好2.x 因为壳会动态改重定位需要多 dump 几次取交集。区段对齐方面dump 出来的区段VirtualAddress和PointerToRawData经常不一致用工具集里的section_align.py按 PE 规范重新对齐同时修正SizeOfImage和SizeOfHeaders。这一步不做脱壳文件在部分系统上会直接报“不是有效的 Win32 应用程序”。4. 避坑与排查Themida 调试中最容易翻车的五个点4.1 断点被吞现象是下断后程序照常运行现象在 x64dbg 里对某地址下 INT3 断点重新运行后断点消失程序没有任何停顿。原因Themida 的 VM 解释器在 dispatch 循环里会定期扫描代码段发现0xCC就恢复原字节。1.8 的扫描周期大约是每 200 条 VM 指令一次2.x 改成了随机周期。解决改用硬件断点DR0-DR3但注意 Themida 会检测 DR 寄存器。工具集里的dr_guard.dll在设置硬件断点前先备份 DR 值断下后立即恢复减少被检测窗口。或者用内存断点替代内存断点不修改代码字节。4.2 custom_vms 扫描结果全是垃圾现象用vm_dispatch_scan扫出来的 handler 地址反汇编后全是add [eax], al这类无效指令。原因扫描参数里的跳转表基址猜错了。2.x 的跳转表基址不是固定偏移而是由 VM 入口处的一条lea指令动态计算。解决先在 VM 入口下断单步跟到第一条lea reg, [base index*4]形式的指令把base记下来再把这个值传给扫描脚本。工具集配置文件里的vm_base字段就是干这个的别留空。4.3 dump 出来的文件运行报“入口点错误”现象脱壳后双击运行系统弹窗“无法定位程序输入点”或“入口点错误”。原因OEP 找错了dump 的起点不是真实入口或者 IAT 没重建完整。解决回到 OEP 定位步骤确认VirtualProtect断下时的返回地址确实落在原始代码段。一个验证方法是在候选 OEP 处下断看程序是否在正常启动流程中经过这里。如果跳过说明找错了。IAT 方面用iat_rebuild.py跑两遍确保没有pending项残留。4.4 Themida64 下 x64dbg 直接崩溃现象用 x64dbg 附加 Themida64 加壳的 64 位程序刚附加就崩溃或 x64dbg 本身无响应。原因Themida64 的反调试会检测调试器的窗口类和进程名x64dbg 的默认窗口类Qt5QWindowIcon在黑名单里。解决用工具集里的hide_debugger.py在附加前修改 x64dbg 的窗口类名和进程名或者改用 WinDbg 配合-p参数附加。WinDbg 的窗口类不在 Themida64 的检测列表里但 WinDbg 的命令行调试体验不如 x64dbg需要权衡。4.5 脱壳后程序功能正常但网络模块失效现象脱壳文件能启动、能操作界面但涉及网络请求的功能全部超时。原因Themida 对ws2_32.dll和wininet.dll的导入项做了单独加密iat_rebuild.py的通用解析逻辑对这两个模块的延迟导入处理不完整。解决手动在 OEP 后对ws2_32.dll的WSAStartup和socket下断记录实际调用地址再回填到 IAT 对应项。工具集里net_iat_patch.py就是针对这个场景的用法是在程序发起第一次网络请求时运行它会自动抓取当前 IAT 里所有指向网络模块的项并修正。5. 进阶技巧用差分调试法验证脱壳完整性脱壳做完不是终点你得验证脱壳文件的行为和原加壳文件一致。我常用的方法是差分调试同时运行原加壳程序和脱壳程序在关键 API 调用点记录参数和返回值逐项对比。工具集里的diff_debug.py实现了这个流程。它接受两个进程 ID在预设的 API 列表CreateFileW、RegOpenKeyExW、InternetConnectW等上同时下断把每次调用的参数序列化后写入两个日志文件最后做 diff。# diff_debug.py 核心对比逻辑 def diff_logs(log_a, log_b): entries_a parse_log(log_a) # [(api_name, args_tuple, retval), ...] entries_b parse_log(log_b) mismatches [] for i, (ea, eb) in enumerate(zip(entries_a, entries_b)): if ea[0] ! eb[0]: # API 名不同 mismatches.append((i, api_mismatch, ea, eb)) elif ea[1] ! eb[1]: # 参数不同 mismatches.append((i, arg_mismatch, ea, eb)) elif ea[2] ! eb[2]: # 返回值不同 mismatches.append((i, ret_mismatch, ea, eb)) return mismatchesparse_log按行解析日志每行格式是api_name|arg1,arg2,...|retval。对比时先看 API 名是否一致再看参数元组最后看返回值。如果某个 API 在一边出现另一边没出现zip会提前截断所以实际使用时要先对齐长度工具集里已经处理了。差分调试能发现很多隐蔽问题比如脱壳后某个注册表键的读取路径变了或者网络请求的超时参数被壳改过。我一般会跑三轮启动阶段、主要功能操作阶段、退出阶段每轮单独 diff。三轮下来没有 mismatch基本可以认为脱壳是完整的。从那以后我每次脱完 Themida 的壳都会强制走一遍差分调试哪怕看起来一切正常。因为壳对 API 参数的微调往往不影响启动但会在特定业务场景下暴露。希望帮到你。本文还有配套的精品资源点击获取
返回列表