ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

HZNUCTF Themida脱壳:unlicense定位OEP与IAT修复

HZNUCTF Themida脱壳:unlicense定位OEP与IAT修复 1. 先搞清楚这道题在考什么Themida 加壳的硬骨头在哪HZNUCTF 的 REVERSE 方向里TMD 这道题一上来就给人下马威。TMD 就是 Themida 的缩写圈里做逆向的人提到这个名字第一反应基本都是这玩意儿不好啃。这道题给的是一个被 Themida 加壳的 Windows PE 可执行文件要求我们脱壳、还原出真实的程序逻辑再从中找到 flag。说实话如果只是普通的压缩壳比如 ASPack、UPX 那种几分钟就能搞定但 Themida 属于商业级保护壳反调试、代码虚拟化、IAT 加密一条龙硬怼下去很容易陷进去出不来。我把这道题的解题主线定成一句话用 unlicense 工具配合手工定位 OEP完成 dump 和导入表修复把壳剥掉露出原始代码。整篇文章我会从壳的识别讲起再到环境准备、OEP 定位、unlicense 实操、IAT 修复、结果验证、报错排查最后延伸到不同壳的应对差异。适合已经会一点 x64dbg 基础操作、想迈过带壳程序这道坎的 CTF 选手看也适合平时做授权场景下软件分析的同学参考。全程只针对题目环境和自研样本别拿去干别的。1.1 从题目文件到壳类型判定拿到文件的第一件事永远是查壳而不是急着上调试器。很多人一上来就 x64dbg 打开然后发现断点根本断不下来、程序跑着跑着就崩浪费一两个小时才想起来该查壳。这个顺序不能反。我一般用三个工具交叉验证DIEDetect It Easy、ExeinfoPE、PE-bear。DIE 的识别库更新比较勤对 Themida 的版本判断相对准ExeinfoPE 对老壳的签名库更全PE-bear 则是用来看 PE 结构本身的不依赖签名纯看区段和入口点。判定的几个硬指标摆在一起看观察项正常程序的典型表现Themida 加壳后的表现区段名.text/.rdata/.data/.rsrc出现 .themida、.winlice、.boot 之类或区段名被抹成随机字符入口点位置通常在 .text 区段开头附近落在最后一个区段且该区段往往可读可写可执行导入表导入函数几十上百个只有 LoadLibraryA、GetProcAddress 等寥寥几个区段权限.text 一般只读可执行大量区段带有可写可执行组合文件大小与代码量成正比明显膨胀常有几 MB 的空区段只要这三项里中了三项基本可以坐实是强壳。这道题的样本一打开导入表里就孤零零几个 API入口点在尾部区段区段全是 RWX不用犹豫Themida 实锤。注意不要只看区段名就下结论。现在的保护壳流行把区段名伪装成 .text、.data 这类正常名字真正靠谱的判断依据是权限组合、入口点位置和导入表规模三个一起看。1.2 Themida 到底给逆向制造了哪些障碍理解壳做了什么比记住该按哪个键重要得多。Themida 的保护手段可以拆成四层层层叠加。第一层是代码加密与压缩。原始程序的 .text 在磁盘上不是明文入口执行的第一段代码是壳的解密 stub它先把原始代码解密到内存再跳过去。这就是为什么静态分析工具在磁盘文件上看不到有意义的汇编全是垃圾字节。第二层是反调试。壳里内置了大量探测逻辑检查 PEB 里的 BeingDebugged 标志、检查 NtGlobalFlag、扫描调试器进程名和窗口标题、用 rdtsc 测时间差、故意触发异常看调试器是否吞掉。你要是老老实实开着调试器跑它要么直接退出要么走进一段假的执行流让你在错误的地方下断点。第三层是代码虚拟化与混淆。Themida 会把一部分关键代码翻译成它自己的字节码交给一个内置解释器执行。你看到的是一堆 push/pop 和调度循环逻辑被拆得七零八落。这部分想完全还原成本极高题目一般不会在这种地方藏关键逻辑所以我们要做的是先绕过它把非虚拟化的原始代码拿到手。第四层是IAT 加密。原始程序运行时会调用大量系统 API但壳把导入表加密了只有在运行时才动态解密并填充。所以你 dump 出来的内存镜像导入表是坏的直接运行会提示找不到入口点或者立刻崩溃。这四层决定了脱壳的标准动作定位 OEP → dump 内存镜像 → 修复导入表 → 修正重定位 → 验证运行。unlicense 这个工具的价值就在于把中间几个机械但容易出错的环节自动化。1.3 为什么不能全指望一键脚本我在早期做壳题的时候特别迷信一键脱壳机。结果就是遇到了变种就完全抓瞎因为我不知道脚本内部到底干了什么出错之后连从哪查起都不知道。所以这道题我的建议是先用手工方式走一遍全流程搞清楚每一步在解决什么问题然后再用 unlicense 去加速。这样即使工具在某些变种上失灵你也能退回手工路线。而且比赛时间有限工具跑得快是优势但前提是你知道它跑出来的是不是对的。2. 环境搭建与工具选型别在第一步就翻车2.1 工具清单与我为什么这么配我把这道题实际用到的工具列一下并说明每一项的选型理由。工具不在多在于每一样都清楚它解决什么问题。工具作用选型理由x64dbg动态调试、定位 OEP开源、插件生态好、反反调试脚本丰富DIE / ExeinfoPE查壳交叉验证签名避免误判版本PE-bear查看 PE 结构不依赖签名库直接看区段和目录表ScyllaIAT 修复与 x64dbg 集成度高重建导入表很顺手Python pefile脚本化分析区段、校验批量处理、写校验脚本必备unlicense主脱壳工具自动化 dump 与修复流程本题指定工具虚拟机 快照隔离运行样本崩了、挂马了直接回滚不污染主力机关于 x64dbg 和 OllyDbg 的选择我现在的做法是32 位样本优先 x64dbg 的 32 位版本因为它对现代 Windows 兼容更好插件多而 OllyDbg 在很多新系统上跑起来问题一堆。这道题是 32 位 PEx64dbg 完全够用。2.2 unlicense 工具在这道题里的定位先说清楚我对 unlicense 的理解。从名字和用法看它是一套针对加壳样本的辅助脱壳工具核心能力是在合适的时机把目标进程的内存镜像 dump 出来并按一定规则重建导入表减轻手工找 OEP 和修 IAT 的负担。它可能以独立程序、调试器插件或者脚本集合的形式存在具体形态看你手上拿到的版本。我手上的这套关键的几个配置维度是这样的不同版本的选项命名可能略有差异以你实际打开后的界面为准目标进程选择指定要 attach 的进程或者直接以调试方式启动样本。dump 模式完整镜像 dump带 PE 头推荐或仅 dump 某个区段。OEP 来源手动填入你找到的 OEP 地址或者让工具尝试自动识别。IAT 处理自动扫描并重建导入表或者保留原始未处理状态留给 Scylla 手工修。重定位修正开关项决定是否修复基址重定位表。输出路径dump 文件的落地位置。提示工具名带unlicense这类字眼容易让人误以为它能万能解壳。实际上它的自动化是建立在壳的常规行为模式上的一旦壳刻意打乱这些模式比如假 OEP、动态区段工具就会失准。手工确认永远是最后一道保险。2.3 隔离环境与操作习惯这一步我单独拎出来讲因为它太容易被忽视。带壳样本在分析过程中会做各种奇怪的事写注册表、创建子进程、触发异常、甚至尝试检测你所在的虚拟机。全部丢在主力机上跑出了问题是不可逆的。我的习惯是虚拟机里做全流程操作前先打快照样本和工具都放在一个固定目录里统一管理。每完成一个关键步骤比如 dump 成功就再打一次快照这样出错时回滚成本最低。另外提醒一句样本来源一定要清楚。CTF 题目给的附件是明确的安全环境可以放开手。来源不明的样本即使技术上好奇也不要随手双击运行。3. 定位 OEP脱壳真正考验功力的地方OEP 就是原始程序的入口点Original Entry Point。壳在把控制权交还给原始代码的那一刻程序计数器指向的地址就是 OEP。找到它脱壳就成功了一半。3.1 先跟壳的反调试周旋Themida 的反调试在第一段 stub 里就会密集触发。常见的有读fs:[0x30]拿 PEB取BeingDebugged字段判断用NtQueryInformationProcess查调试端口用rdtsc两次时间差判断单步花指令制造大量跳转迷惑 IDA 的静态反汇编。我的应对思路分两步走。第一步让调试器尽量隐身用 ScyllaHide 这类插件把常见的调试器痕迹PEB 标志、窗口标题、进程名、调试对象句柄都清理掉能解决一大半探测。第二步观察异常流在调试器里把异常处理交给程序自己x64dbg 的异常设置里把大部分异常类型设为不忽略或由程序处理因为壳常常故意触发除零、非法访问等异常来改变执行路径如果你让调试器吞掉异常逻辑就走偏了。我在这一步踩过的坑一开始把所有异常都设成忽略结果壳走进了一个完全不同分支跑了半天都没到 OEP还以为是壳没脱成功。后来改成让程序自己处理异常位置一下就对了。异常设置是 Themida 分析里最影响结果的一个开关。3.2 用内存访问断点锁住原始代码段反调试搞定之后就要想办法让 OEP 自己撞到断点上。最经典也最稳的方法是在原始代码段上下内存访问断点。原理很直接壳在解密阶段会把原始代码写进它最终要执行的区段。我们先通过区段权限和大小判断哪个区段是原始 .text 的落地位置通常是那个体积最大、权限为 R-X 的区段。然后在这个区段上设内存访问断点读/执行。当壳解密完成、CPU 第一次执行到这块代码时断点命中那个位置就非常接近 OEP 了。具体操作上x64dbg 里在内存窗口找到目标区段右键设置内存断点选访问或者执行。执行类断点会更精准因为壳写完代码后还要跳过去执行跳的那一下就是我们要的时刻。如果你发现区段位置不好判断还有两个备选招数栈上返回地址断点在壳的 stub 里很多地方会先 push 一个地址再 ret那个被 push 的地址往往是原始代码的入口。在栈顶下硬件断点观察它被 ret 到的位置。API 断点法原始程序启动时通常会调GetCommandLineA、GetVersion、GetStartupInfoA这类初始化 API而壳的 stub 一般不会。在这些 API 上下断点命中后回看调用栈的返回地址往往就是 OEP 附近。3.3 怎么确认就是这里断点命中不等于找到了正确的 OEP还要做特征校验。我一般看这几个信号代码风格突变壳的代码充满花指令和跳转原始代码则规整得多函数序言是标准的push ebp; mov ebp, esp或sub esp, xxx。区段归属变化OEP 应该落在原始代码段里而不是壳自己的区段。调用规律OEP 附近常有对__security_init_cookie的调用或者先调GetCommandLineA再进main。栈帧平衡如果前面有pushad附近应该能找到对应的popad且栈恢复平衡。把这几个信号凑齐基本可以确认。我在做题时习惯把候选 OEP 的地址记下来逐个用这些特征筛宁慢勿错——因为 OEP 填错后面 dump 出来的东西全是废的得从头再来。3.4 到达 OEP 后的第一次 dump 实操确认 OEP 之后先别急着 dump。正确顺序是在 OEP 处下断点让程序跑到 OEP 停下此时再执行 dump。因为只有到了这个时刻壳才把该解密的都解密完、该填充的 IAT 也填充好了。第一次 dump 我用 unlicense 的完整镜像模式把整块内存连同 PE 头一起 dump 下来。这样做的原因是后续修复 IAT 和重定位都需要完整的 PE 结构信息只 dump 单个区段会丢掉目录表。dump 出来的文件先另存一份原始备份后面所有修改都在副本上做方便对比回滚。4. unlicense 脱壳全流程实操4.1 参数配置与几个关键数值的确定配置环节我按前面列的维度逐项过一遍并解释每项背后的取值逻辑。OEP 地址填我在第 3 步确认的地址。如果工具支持相对基址偏移输入注意区分 VA虚拟地址和 RVA相对虚拟地址。VA ImageBase RVA。比如 ImageBase 是 0x400000你找到的 OEP VA 是 0x4012A0那 RVA 就是 0x12A0。填错这个工具会 dump 到错误位置。dump 模式选完整镜像。理由是后面要用 PE 解析器读取目录表。区段处理如果工具有重建区段选项打开它。原始程序的区段布局和加壳后的布局不一样重建能让 dump 出来的文件区段对齐正确减少后续修正。IAT 大小估算这个值影响导入表重建的准确性。估算方法是从 OEP 处的代码往前/往后找call dword ptr [xxxxxxxx]形式的间接调用收集目标地址看它们大致落在哪个区间。这个区间的范围就是导入表大概的大小。宁大勿小填稍微大一点工具扫描时能覆盖全。重定位修正这个要看你 dump 之后文件是否还需要在非默认基址加载。如果程序不支持 ASLR 或者你能确保在默认基址加载可以先关掉。开启的好处是更通用坏处是万一重定位表本身被壳破坏了修正反而会引入错误。我的习惯是先关掉试跑跑不起来再开。4.2 完整操作步骤下面是我做题时实际走的流程按顺序执行虚拟机打快照把样本、unlicense、x64dbg、Scylla 都放到同一个目录。用 DIE 和 ExeinfoPE 确认壳类型和版本记录关键信息。x64dbg 载入样本加载 ScyllaHide配置异常处理为程序自行处理。观察壳的反调试行为确认没有提前退出或走错分支。在原始代码段上设执行断点跑起来等命中命中后校验 OEP 特征。记录 OEP 地址换算成 RVA。打开 unlicense按 4.1 的配置填好参数attach 到目标进程。在 OEP 处让进程停下触发 dump。确认 dump 文件生成大小合理一般和原始程序量级相近不会只有几 KB。用 PE-bear 打开 dump 文件检查区段布局和导入表状态。如果导入表是坏的用 Scylla 或 unlicense 的 IAT 修复功能重建。修正入口点把 dump 文件的 AddressOfEntryPoint 改成 OEP 对应的 RVA。这一步经常被忘忘了的话文件打开还是从壳的代码开始跑。保存拿到脱壳后的文件。在干净环境里运行验证逻辑是否正常。第 12 步我用一个简单的 Python 脚本做避免手改出错import pefile path dumped.exe pe pefile.PE(path, fast_loadTrue) # 将入口点改为我们确认的 OEP 的 RVA oep_rva 0x12A0 pe.OPTIONAL_HEADER.AddressOfEntryPoint oep_rva pe.write(dumped_fixed.exe) print(entry point -, hex(oep_rva))这个脚本只做一件事——改入口点。别小看这几行很多dump 出来跑不起来的问题根源就在这里。4.3 IAT 修复与重定位处理IAT 修复是脱壳里最容易出错的一步也是最讲究方法的一步。dump 出来的内存镜像里导入表可能处于三种状态之一完全损坏全是指向壳内地址的指针、部分损坏、已经被壳在运行时填成了真实地址。修复的核心思路是扫描代码段里的间接调用/跳转指令收集它们引用的地址然后反查这些地址属于哪个模块的哪个导出函数重建一份导入表。Scylla 做这件事很成熟操作路径是设置 OEP → 点击 IAT Autosearch 自动扫描 → 检查扫描结果 → 点 GetImports 解析每个地址对应的模块和函数名 → 对识别不出来的条目手工指定或直接无效化 → 最后 Fix Dump生成修复后的文件。unlicense 的 IAT 处理如果和 Scylla 能力重叠我会用两者交叉验证各自扫一遍对比结果只保留双方都认可、并且函数名对得上的条目。函数名对不上或者模块是空的条目果断删掉宁可少几个导入也不能留错的错的导入会导致加载失败。重定位这块判断标准很简单如果修复后的文件在默认基址能正常加载运行就说明重定位不需要额外处理如果在非默认基址加载报错再回头修重定位表。实际做题时多数情况下默认基址加载就够了。修复完成后我会用 pefile 做一个校验脚本检查导入表是否结构完整import pefile pe pefile.PE(dumped_fixed.exe) if hasattr(pe, DIRECTORY_ENTRY_IMPORT): for entry in pe.DIRECTORY_ENTRY_IMPORT: print(entry.dll.decode(errorsignore), len(entry.imports)) else: print(no import directory found, IAT repair failed)能正常列出 DLL 和函数数量说明导入表结构至少是通的。4.4 验证脱壳结果验证这一步千万别省。我见过太多人 dump 完就直接交结果文件跑不起来回头查了半天是入口点没改。验证分三个层次第一层静态验证。用 PE-bear 打开看区段名是否正常、导入表是否可读、入口点是否指向代码段。三项正常文件结构就没大问题。第二层动态验证。在干净环境不带调试器直接双击运行看程序是否能正常启动、界面/输出是否符合预期。这一步能暴露 IAT 修复不完整的问题比如缺了某个 API 就崩溃。第三层逻辑验证。这道题最终是要出 flag 的脱壳之后把文件丢进 IDA找到校验逻辑输入正确的内容看能不能得到正确结果。只有这一步过了才算真正脱成功。如果第二层就崩了回到 IAT 修复那步补条目如果第二层正常但逻辑不对说明你可能脱错了 OEPdump 的是壳的代码需要重新定位。5. 常见报错与排查速查表5.1 典型问题一览下面这张表是我做题过程中实际遇到过的坑以及对应的排查方向。现象可能原因排查方向调试器一开程序就退出反调试生效检查 ScyllaHide 配置清理 PEB 标志和窗口信息断点始终不命中OEP 位置判断错误或断点类型不对换用栈返回地址断点、API 断点交叉验证dump 文件只有几 KBdump 模式选成了单区段或 dump 时机太早改完整镜像模式确保在 OEP 停下后再 dump脱壳后运行提示找不到入口点入口点没改成 OEP或改成了 VA 而非 RVA用 pefile 检查 AddressOfEntryPoint运行后立即崩溃IAT 修复不完整或有错误条目用 Scylla 重新扫描删除识别不出的条目程序能跑但逻辑不对dump 的是壳的代码OEP 错了重新定位 OEP核对代码风格特征IDA 反汇编结果全是花指令静态分析未脱壳先完成动态脱壳再分析脱壳后文件工具扫描 IAT 结果为 0 条OEP 未设置或进程状态不对确认在 OEP 断点命中状态下执行扫描5.2 几条只可意会的实操经验经验一OEP 宁可反复确认不要赌。我在这道题上第一次尝试时看到一个位置代码很规整就直接当成 OEP dump 了结果脱出来跑不通。后来重新按特征校验了一遍才发现那是壳解密后的一个中间跳板。多花十分钟确认能省掉后面半小时返工。经验二IAT 修复的取舍。Scylla 自动扫描出来的结果不是越多越好。有些条目它会给出一个看起来像但实际错误的函数名这种错误条目会让文件在加载时报错。我的原则是能明确对上的留对不上的删。缺几个导入通常不影响主逻辑运行但错一个就可能直接崩。经验三dump 文件永远留一份原始备份。所有修复都在副本上做每做完一步就存一版带序号的文件比如 dump_01_raw.exe、dump_02_entry_fixed.exe、dump_03_iat_fixed.exe。这样一旦某一步引入新问题你能快速定位是哪一步搞坏的不用从头重做。经验四工具和手工要互为验证。unlicense 给出的结果我会用 Scylla 或 pefile 脚本再验一遍。单一工具的自动化输出在没有交叉验证之前只能当作候选结果不能当作最终结果。提示上面这些工具的作用是帮助我们理解加壳与还原的原理仅适用于 CTF 竞赛题目和获得明确授权的自研样本分析。对不属于自己的软件做同样的操作可能触碰法律和平台规则。6. 从 Themida 延伸不同壳的应对思路差异做完这道题我对壳的理解更完整了。顺带说一下另外两类常见的壳方便你在别的题目里快速切换思路。6.1 压缩壳ASPack 这类其实好对付ASPack 是典型的压缩壳它的目标主要是减小体积保护强度远低于 Themida。它的特点是入口点在一个解压 stub 里stub 解压完原始代码后跳到 OEP跳转逻辑相对固定。应对 ASPack 的经典方法是ESP 定律入口 stub 处会有pushad保存寄存器解压完成后会有popad恢复。在pushad之后对栈顶下硬件断点popad执行时会命中命中点附近往下单步几步就能落到 OEP。这种方法对付压缩壳几乎百试百灵不需要像 Themida 那样跟反调试大战三百回合。对比项ASPackThemida主要目的压缩体积保护与反逆向反调试强度几乎没有极强代码虚拟化无有IAT 加密简单或没有强加密运行时动态填充脱壳难度低ESP 定律可解高需工具手工结合典型解法手工单步找 OEP定位 OEP 专业工具 dump 与修复6.2 .NET 程序的脱壳是另一套逻辑如果你的目标是 .NET 程序那思路完全不同。.NET 编译出来的是 IL 中间代码加壳工具比如某些混淆器做的是把 IL 加密运行时由一段 native 加载器解密并交给 CLR 执行。de4dot 这类工具专门用来处理 .NET 混淆和壳。常见的 .NET 处理流程是用 de4dot 自动识别混淆器类型它会尝试解密和还原 IL然后你得到一份可读的 .NET 程序集再拖进 dnSpy 或者 ILSpy 看逻辑。这里的脱壳和 native 程序的思路完全不同——native 是内存 dump 加 IAT 修复.NET 是 IL 解密加符号还原。我把三种情况放一起对照一下方便你形成判断直觉壳类型代表核心脱壳动作常用工具压缩壳ASPack、UPX找 OEP直接 dumpx64dbg Scylla或 UPX -d强保护壳Themida、WinLicense反调试 定位 OEP dump 重建 IATx64dbg Scylla unlicense.NET 混淆壳各类 IL 保护器解密还原 IL去混淆de4dot dnSpy / ILSpy判断出壳的类别解题策略基本就定下来了。这比无脑上工具效率高得多——你得先知道自己在打哪种怪才知道该带什么装备。我在准备 HZNUCTF 这类比赛时的体会是壳题的分数看着诱人但它的时间投入波动特别大。Themida 这种强壳状态好的时候一个多小时能全部走通状态差的时候卡在反调试或者 IAT 修复上能耗你三四个小时。所以我的策略是先花十分钟把壳的类型和版本摸清楚评估一下用工具直接跑的成功率如果自动流程能通就快速拿分如果通不了就果断切手工别在工具上死磕。这道题的 unlicense 工具本质上就是帮你省掉手工 dump 和修表的时间但它能不能跑出正确结果还是看你给的 OEP 对不对、环境有没有被反调试污染。把这些前置条件做扎实工具才能真正发挥价值。
返回列表