ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Themida与WinLicense脱壳实战:OEP定位、IAT重建与避坑指南

Themida与WinLicense脱壳实战:OEP定位、IAT重建与避坑指南 简介面向 Themida 与 WinLicense V1.8.XV2.X 的脱壳工具集适合有一定逆向基础的安全分析人员、病毒分析工程师和软件保护研究者用于解析商业壳的虚拟机指令、导入表还原、代码抽取与反调试对抗等常见脱壳难题。压缩包共301个文件大小约32.77MB并非单一可执行程序而是以源码与工程为主的完整工具包除inc、h、pas、cpp等核心实现文件外还包含vm、asm等虚拟机与汇编相关文件lng语言文件、rc资源脚本、dpr/vcxproj/sln/vcproj等工程与编译配置以及少量exe/dll可直接使用的组件和示例。透过这些文件可了解脱壳框架的模块划分、编译流程与依赖关系chm与pdf提供查阅手册txt与log等记录使用过程或更新要点。整体结构偏向可扩展的源码级方案既能直接运行验证也方便对照阅读、裁剪或二次开发。目前已有1322人学习下载对深入理解Themida/WinLicense保护机制、提升手动脱壳与调试能力有较大帮助。1. Themida WinLicense 脱壳到底难在哪先说结论再动手没有哪个加壳工具能像闹钟一样准时准点地给出 OEPThemida 和 WinLicense 这套 Oreans 的商业保护方案尤其如此。V1.8.X-V2.X 这一代在恶意样本、老商业软件和 CTF 训练题里出现频率极高它用内核驱动反调试叠加代码虚拟化再配合导入表重定向让最常见的“附加-下断-单步”调试方式在第一回合就出局。先说结论不存在一键完成的“Themida 脱壳工具”能做到稳定复现的方案是调试器脚本 内存 dump PE 重建的组合全程两到四小时能跑通一个样本。这篇文章写给三类人被加壳样本卡住的恶意样本分析师、要做兼容性调试的程序员、以及想验证自身保护强度的开发者。下面不聊玄学只讲能落地的东西。2. 分清 Themida 与 WinLicense 的版本差异决定你用哪种脱壳路线拿到样本后第一件事不是急着开调试器而是确认壳的具体特征。这直接决定后面的断点策略和工具链。常见的错误认知是把 Themida 和 WinLicense 当成两个不相关的产品其实 WinLicense 是前者加了许可证验证功能的商业版本底层的 VM 引擎和反调试驱动是同一套。对我们做脱壳的人来说更关心的是 V1.8.X 到 V2.X 的演进老版本积攒的公开脚本能不能用拿到了新版本样本是继续硬调还是换模拟执行都从这里开始判断。2.1 V1.8.X 与 V2.X 的行为差异先确认你手上是哪一代用 Detect It EasyDIE扫描文件识别结果一般会直接显示 TheMida 或 WinLicense并在详细信息里给出一个大致的版本区间。PEiD 的签名库也能认老版本但 V2.X 时代很多样本的签名被清理得很干净PEiD 会误报成“什么都没找到”。这时候的做法是启动样本在 Process Hacker 里看内核模块列表如果出现了一个随机文件名的 .sys 驱动并且在短暂加载后消失基本就是 Oreans 的驱动可以判定为 V2.X 系列。两个世代在行为上有几个稳定差异这些差异会落实成具体的脱壳手段观察项V1.8.XV2.X反调试驱动单个驱动检测逻辑集中部分旧隐藏插件有效驱动加多线程心跳检测隐藏插件经常失效虚拟化粒度主要保护入口点和少数 SDK 标记函数可对任意函数做 VM指令碎片化更严重IAT 表现部分 API 可以在 dump 后直接识别几乎全部经跳板重定向必须运行追踪脚本生态网上流传的 OllyDbg 脚本大多停留在这一代脚本很少基本靠手动断点加 Scylla差异带来的直接选择是V1.8.X 可以优先考虑在 OllyDbg 里跑老脚本如果脚本半路失效再切手动V2.X 不要浪费时间在老脚本上直接把 x64dbg 加 Scylla 这套组合摆出来。另外注意一个反直觉的事实WinLicense 加壳的样本不一定比 Themida 难脱许可证功能集中在壳外层核心虚拟化引擎一样处理流程完全通用。2.2 代码虚拟化与重定向OEP 为什么飘忽不定Themida 会把原始入口指令转换成一段专有的 VM 字节码由解释器循环执行。这意味着在调试器里单步跟踪时你看到的是成万条无意义的指令在 switch 循环里打转永远找不到“原始入口”在哪。正确的目标不是去逆向 VM 字节码而是找到 VM 解释器退出、跳到原始代码那个瞬间。退出点在行为上有明显信号。壳在切换到原始代码前必须先对目标内存页申请写权限写入还原后的指令再把页属性改成可执行最后才跳转。所以断点策略是监控内存属性变化而不是盲目单步。我一般会对这几个 API 下断VirtualAllocVirtualProtectNtProtectVirtualMemoryZwProtectVirtualMemory当断下后看调用栈和参数如果返回地址落在目标模块范围内且目标地址接近模块基址这就是 OEP 候选点。实际判断还要配合寄存器形态VM 退出时栈顶通常会还原出原始程序入口时的现场比如push ebp; mov ebp, esp这种 VC 入口序列。表里归纳了三个关键判断点判断点说明栈顶附近是连续调用形成的返回地址已经从壳代码回到用户代码当前指令与 ImageBase 附近原始代码相似常见编译器入口特征目标页属性是 RX 而不是 RWX防止 dump 出大量冗余数据记住不要尝试在 VM 主循环里单步穿越每经过一轮解释器循环都会触发一次反调试自检跑不完 100 条就会被踢出进程。2.3 搭建分析环境虚拟机、驱动签名与工具清单脱壳操作必须在虚拟机里做一是安全二是方便回滚。Themida 的驱动在检测到调试环境时会触发系统重启或蓝屏真机上翻一次车就得重装系统虚拟机里一个快照就回到从前。配置上不用太豪华双核 CPU、4GB 内存、SSD 硬盘就够系统建议 Windows 7 SP1 x86 配老样本V2.X 的 x64 样本用 Windows 10 LTSC。运行样本时断开虚拟网络避免其它意外。Windows 10 上跑 V2.X 会遇到驱动签名问题Oreans 的自定义驱动没有微软签名默认内核会拒绝加载。需要在管理员命令行里开启测试签名模式# 在调试虚拟机中开启测试签名模式管理员执行 bcdedit /set testsigning on # 关闭强制签名检查部分 Win10 版本还需要这一步 bcdedit /set nointegritychecks on # 重启后生效 shutdown /r /t 0这里的参数说明一下testsigning让内核允许加载测试签名的驱动nointegritychecks关闭完整性校验。两个都开能覆盖绝大多数 Oreans 驱动的加载要求。但要注意不要在这个环境里启用内核调试模式Windbg 双机联调在这种壳面前等于给自己挂了个“我是调试器”的牌子驱动检测到内核调试钩子会直接卡死系统。工具链我固定用这一套x64dbg开发版做主调试器Scylla 做 dump 和 IAT 重建DIE 做版本识别Process Hacker 做运行行为观察PE-bear 做脱壳后的 PE 结构检查再加一个 ScyllaHide 或 TitanHide 隐藏调试器。后面所有流程都以这套组合为例展开。3. 通用脱壳流程从 dump 到干净 PE 的三步重建这套流程不挑版本V1.8.X 和 V2.X 都走得通。核心逻辑是三段式先通过内存属性监控定位 OEP再用 Scylla 追踪并重建导入表最后用脚本修正 PE 对齐和入口校验。每一步都有很容易翻车的细节参数设错了重跑一遍是常事。3.1 第一步定位 OEP监控内存属性而不是乱猜在 x64dbg 中打开样本后不要急着停在入口分析。系统断点处先做两件事把所有访问冲突异常0xC0000005设为“忽略并继续”因为 Themida 的 VM 解释器会故意触发访问冲突来干扰调试然后把单步异常0x80000004保持为“通知但不中断”之外的形态避免壳的异常处理被调试器抢走。操作步骤按下面这个顺序在 x64dbg 命令行窗格输入bp VirtualProtect32 位样本也可以补一条bp VirtualAlloc。让程序运行每次断下后用调用栈窗口确认返回地址。壳自己的解密调用通常返回在系统 DLL 或驱动模块里目标模块的调用才是我们要的。观察 VirtualProtect 的flProtect参数。0x04 表示 PAGE_READWRITE0x40 表示 PAGE_EXECUTE_READWRITE。重点关注从 0x04 变成 0x40 的那一次那往往是壳把解密后的指令页“交还”给程序的瞬间。对上一步的目标地址下内存执行断点继续运行。如果命中后指令流看起来像普通编译代码就记录这个地址为 OEP 候选。这个流程的核心是OEP 是一个执行事件不是某个固定位置的静态字节。用内存属性变化作为触发器比在可疑区域逐个试硬件断点可靠得多而且能绕过大部分单步反调试。3.2 第二步用 Scylla 追踪式重建 IAT而不是直接搜索定位到 OEP 后程序应该暂停在原始入口附近。此时打开 Scylla选择调试中的进程把 ImageBase 自动识别出来的基址留下OEP 字段填当前 EIP 减去 ImageBase 得到的 RVA。Scylla 里有“IAT 搜索”和“追踪 IAT”两个按钮这个选择是最大的分岔路。Themida 重定向过的 IAT 用静态搜索找到的是一堆跳板地址直接修复后程序运行到第一条 API 调用就会崩。需要点的是“追踪 IAT”让 Scylla 模拟执行壳的跳板代码拿到真实的 API 地址。相关参数按这个经验设置参数作用推荐值Max Depth追跳板时的递归深度上限50 起步出现大量无效地址再升到 200Auto Trace自动识别跳板并跟踪开启Use IAT from trace用追踪结果而不是静态扫描结果开启追踪完成后点“获取导入表”检查列表里的 DLL 是否合理。纯 Win32 程序通常只有 kernel32、user32、ntdllMFC 程序会有 mfc*.dll。如果混进大量不认识的模块先不要急着修复回到追踪参数里抬升 Max Depth 再试一次。最后点“修复 dump”输出文件到新路径。3.3 第三步用 pefile 修正 SizeOfImage 与入口校验Scylla 修完的文件通常能加载但节表和对齐很可能不干净运行到一半会莫名其妙崩。常见症状是入口点没有落在任何节的范围内或者 SizeOfImage 小于 Sections 实际覆盖的区域。这里用 pefile 做一次快速体检加修复import sys import pefile def fix_dump(in_path, out_path): pe pefile.PE(in_path) opt pe.OPTIONAL_HEADER # 重新计算 SizeOfImage以最末节的结束位置向上对齐 sec_align opt.SectionAlignment last_end 0 for sec in pe.sections: end sec.VirtualAddress sec.Misc_VirtualSize if end last_end: last_end end opt.SizeOfImage ((last_end sec_align - 1) // sec_align) * sec_align # 入口点抽查AddressOfEntryPoint 必须落在某个节的范围内 ep opt.AddressOfEntryPoint if not any(sec.contains_rva(ep) for sec in pe.sections): print([!] OEP 没有落在任何节里脱壳文件大概率跑不起来) print( 请回到 Scylla 重新确认 OEP 值) pe.write(out_path) pe.close() print([] written:, out_path) if __name__ __main__: fix_dump(sys.argv[1], sys.argv[2])逻辑说明SectionAlignment是节在内存中的对齐粒度Misc_VirtualSize是该节在内存中的实际大小。Scylla dump 时往往把 RawSize 拉到文件对齐的整数倍但内存映射是按 VirtualSize 走的所以 SizeOfImage 要用各节的 VirtualAddress 加 VirtualSize 重新计算。contains_rva是 pefile 对每个节提供的方法直接判断入口是否落在节的虚拟地址区间里。运行方式是python fix_dump.py dump_1.exe dump_1_fixed.exe。修复后不要急着交付继续往下走最后用 PE 语义差异做整体验证。4. 工具链选型与脚本化实操调试器、Scylla 与模拟执行的组合工具选型不是越多越好。Themida 脱壳场景里工具之间是替代关系而不是叠加关系选错主调试器会浪费半天。这一章把三个关键选择讲清楚调试器选哪家、隐藏插件怎么配、模拟执行在什么情况下值得上。4.1 OllyDbg 与 x64dbg 的适用边界老壳选老刀新壳上 x64dbgOllyDbg 1.10 在 V1.8.X 时代积累了海量脚本很多脚本能自动绕过旧版反调试并直接停在 OEP 附近。但 V2.X 换了监控模式老脚本经常在中间某一步崩掉继续硬用只会浪费时间。x64dbg 的优势是同时支持 32/64 位异常处理和插件生态更现代Scylla 集成也顺。场景首选调试器理由V1.8.Xx86OllyDbg 1.10 老脚本脚本成熟OEP 定位快V1.8.Xx64x64dbgOllyDbg 不支持 x64V2.Xx86/x64x64dbg老脚本失效需要现代断点管理实际项目里我一般两种都开先让 OllyDbg 跑一把老脚本如果五分钟内没有到 OEP立刻切 x64dbg 手动流程不在脚本上恋战。4.2 关键调试器参数异常过滤、隐藏插件与心跳处理ScyllaHide 和 TitanHide 是当前实践中最常用的隐藏插件原理都是钩住 NtQueryInformationProcess、NtSetInformationThread 这几个关键查询把 PEB 里的 BeingDebugged 字段和内核调试端口信息伪装掉。V2.X 的心跳检测会周期性比对关键内存区域和调试寄存器隐藏插件只能降低被检测的概率不能彻底消除。异常过滤是另一个必须提前设好的参数。下表是常用的默认策略异常码含义处理方式0xC0000005访问冲突忽略并继续0x80000004单步异常交给调试器不交给壳0x80000003断点异常忽略并继续0x4000001F由壳主动触发的异常忽略并继续心跳问题的处理经验是不要在暂停状态下长时间停留。每做完一个断点操作就立即继续运行减少壳线程在时间窗口内做一致性校验的机会。如果必须长时间分析先把所有线程暂停再把目标线程切换出来看分析完立刻恢复。虽然有时会触发死锁但比被心跳踢掉强。4.3 模拟执行从 vmprotect 脱壳思路里能借什么、放弃什么网上搜 vmprotect 脱壳工具时能看到不少基于模拟执行的思路先别急着搬到 Themida 上。VMP 和 Themida 的 VM 指令集设计差异很大照搬工具链不现实但“用模拟器跑可疑跳板区、观察最后退出的地址”这个方法可以借鉴。当手动流程走到死胡同比如某个关键函数被病毒化保护无法在调试器里跟踪时可以用 Unicorn 加载指定代码段把所有跳板当作代码执行看它最终返回到哪里from unicorn import * from unicorn.x86_const import * def emulate_section(data, base, trampoline_start, trampoline_end): mu Uc(UC_ARCH_X86, UC_MODE_32) mu.mem_map(base, 0x4000) mu.mem_write(base, bytes(data)) # 只打印跳板区内的执行轨迹避免陷入 VM 解释器的大循环 def hook_code(uc, address, size, user_data): print(exec 0x%x size%d % (address, size)) mu.hook_add(UC_HOOK_CODE, hook_code, begintrampoline_start, endtrampoline_end) try: mu.emu_start(base trampoline_start, base trampoline_end) except UcError as e: print(stopped at 0x%x: %s % (mu.reg_read(UC_X86_REG_EIP), e)) emulate_section(data, 0x400000, 0x410000, 0x412000)这段是分析骨架不是直接能跑的生产脚本。它展示的是思路把不信任的代码放到模拟器里黑盒执行记录每条指令地址等它执行完跳板区后自然退出退出的目标地址就是还原后的真实跳转点。模拟执行的优势是不触发内核级反调试劣势是遇到外部 API 调用需要逐一 hook工程量大通常作为最后手段而不是首选。4.4 不同版本下的组合方案速查综合前面所有讨论给出一个可以直接复用的选择表场景推荐组合预期成本V1.8.X x86OllyDbg 1.10 老脚本 Scylla0.5-1.5 小时V1.8.X x64x64dbg ScyllaHide Scylla1-2 小时V2.X x86x64dbg TitanHide Scylla2-4 小时V2.X x64x64dbg TitanHide Scylla PE-bear3-5 小时SDK 虚拟化过重模拟执行半天到一天成本是按经验估算的前提是样本没有额外套壳。遇到多层保护的话每多一层加一倍时间。5. 避坑Themida/WinLicense 脱壳最容易翻车的 5 个现场这个壳我翻过车也看别人翻过车。下面几条都是高频事故每条都按“现象 → 原因 → 解决”写清楚省得你重走一遍。5.1 一启动就重启蓝屏现象在虚拟机里运行样本或附加调试器系统直接重启或者蓝屏停在SYSTEM_SERVICE_EXCEPTION。原因Themida 驱动检测到调试器或虚拟机中的某些硬件特征后主动调用KeBugCheckEx制造蓝屏。这通常出现在 V1.9-V2.0 的某些 Build 里。解决换用快照机制每次启动样本前保留干净快照蓝屏直接回滚。关闭内核调试串口和虚拟机的调试接口不在这个环境里开 Windbg 双机联调。如果还是蓝屏换 Windows 7 SP1 虚拟机老系统被识别成调试环境的概率低一些。5.2 附加后进程秒退现象用调试器的 Attach 模式挂上目标进程进程活不过几秒就退出但正常运行没事。原因壳在启动线程里做了反附加心跳附加动作会改写 PEB 的 BeingDebugged 标志心跳线程检测到后直接调用 ExitProcess。解决不要用 Attach用 x64dbg 的“打开”方式启动样本让壳在调试器加载时就开始解密流程。打开后先不要把程序停在入口把所有异常设成忽略后直接运行等它自己跑进我们预设的 VirtualProtect 断点。配合 ScyllaHide 隐藏 PEB 调试状态能进一步降低秒退概率。5.3 dump 出来的文件一运行就崩现象文件能被 DIE 识别成“已脱壳”但运行后在入口附近报 0xC0000005或者在加载 DLL 时直接退出。原因最常见的是 OEP 填错把 VM 解释器入口当成原始入口其次是 IAT 没有走追踪重建静态搜索出来的跳板地址在 dump 后变成悬空指针。解决回 Scylla 重新确认 OEP。判断方法很简单OEP 处的前几条指令必须是常见编译器的入口形态比如 VC 的push ebp; mov ebp, esp而不是一串无意义的寄存器移动。IAT 方面重新点“追踪 IAT”不要用“IAT 搜索”的结果。5.4 关键函数仍是 VM 黑匣子现象主程序逻辑已经还原但某个核心函数在反编译窗口里是几千行的指令搬运完全读不出业务逻辑。原因这个函数在编译期被 Themida SDK 标记整段代码转换成了 VM 字节码跟壳层解密无关普通脱壳流程对这部分无效。解决不要硬抠。先用运行对比判断这个函数的行为边界确定入参、出参和副作用然后在调用点做 hook 或内存 patch把黑匣子当外部模块用。如果一定要还原逻辑用模拟执行提取它的内存读写序列再反推算法这属于另一类工作量。5.5 脱完一次又提示还带壳现象第一轮脱壳完成后 DIE 扫描不再报警但程序运行一小段时间后内存里出现新的可执行代码DIE 再次识别出保护特征。原因样本是两层保护外层脱完后内层在运行期自解密重新生成壳代码。解决不要在第一轮停在“DIE 不报警”就行了。对 dump 文件再跑一遍定位 OEP、追踪 IAT、对齐修复的完整流程。每轮结束后用 Process Hacker 导出现场模块列表检查除了系统模块和目标模块外是否还有可疑的非系统 DLL 驻留。有的话继续第二遍脱壳。6. 进阶技巧用 PE 语义差异验证脱壳结果脱壳完成不等于可以交付。给队友或客户一个可用样本之前我习惯再做四层验证全部通过才算数。第一层看入口指令指纹。干净样本的 OEP 处应当是该编译器最原始的入口模板。VC 是push ebp; mov ebp, esp; push -1Delphi 是push ebx; mov ebx, eax加push esi。如果入口前五条全是寄存器互移、无来源的 push说明还在 VM 解释器里。用 x64dbg 打开修复后的文件停在入口单步五条就能判断。第二层看导入表语义。加壳时混进导入表的调试相关 API比如NtQueryInformationProcess、NtSetInformationThread在干净程序中极少直接调用。脱壳后如果这些调用还在导入表里成排出现大概率是 IAT 修复时把壳的逻辑也带了进来。正常业务程序只会导入GetProcAddress、LoadLibraryA和少量 Win32 API导表越干净越像原始文件。第三层看段表和熵。DIE 的熵曲线里Themida 代码段熵通常接近 7.9 甚至 8.0脱壳后代码段应该回落到 6.2-6.8。如果所有段的熵都还在 8 附近说明 dump 的还是壳代码。PE-bear 的 Section 标签会显示每段的熵值和 RawSize看到 RawSize 大到离谱、但 VirtualSize 很小的段直接回 Scylla 重新来过。第四层做运行对比。加壳样本运行后内存里常有壳的附加线程和驱动句柄脱壳样本的线程数明显减少且线程入口地址全部落在目标模块代码段内。用 Process Hacker 分别导出前后两轮的线程起始地址一缩一升就能看出壳线程是否清理干净。我自己的习惯是保留一份加壳前的 PE 头快照脱壳后逐字段做 diff入口点、节数、SizeOfImage、导入表 RVA。很多问题在 diff 里一眼就能定位比如入口点落在第三节的尾部而这一节的 Characteristics 还保留着MEM_WRITE权限说明 dump 时没清理 RWX 段补一下权限标记就能解决。脱壳越到后面越拼耐心脚本出错是常态能救场的还是对 PE 结构本身的熟悉程度。希望这篇能帮你在下一个 Themida/WinLicense 样本上少走一圈弯路早点下班。本文还有配套的精品资源点击获取
返回列表