ARTICLE DETAIL

资讯详情

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

HZNUCTF TMD壳逆向:Themida脱壳与IAT修复实战

HZNUCTF TMD壳逆向:Themida脱壳与IAT修复实战 HZNUCTF的REVERSE方向里TMD壳题基本属于那种看一眼就想跳过、跳过又肉疼的存在。TMD就是Themida的圈内简称Oreans家出的商业级保护壳原本是给正版软件做防逆向的被出题人搬进CTF之后难度直接拉到劝退档。这道题的核心考点其实很纯粹你能不能在Themida的多层反调试、代码虚拟化和IAT加密面前把真实的程序镜像从内存里完整地掏出来——也就是脱壳——然后在脱壳后的干净代码上做常规的REVERSE分析。我这次用的是unlicense这个工具配合x64dbg和Scylla做收尾整个流程走通之后大概花了四十分钟其中三十分钟都在和反调试斗智斗勇。这篇就把我完整的操作路径、每一步为什么要这么做、以及踩过的那些坑摊开讲一遍适合刚接触壳题、被Themida卡住过的同学也适合想了解一下商业壳基本对抗思路的老手扫一眼。1. 这道TMD壳题到底在考什么1.1 CTF逆向出题人为什么偏爱商业壳普通的CTF逆向题出题人自己写一个校验函数编译出来给你你IDA一开就能看到伪代码剩下的就是读逻辑、写脚本。这种题考的是算法理解能力难度上限不高。于是出题人开始叠buff加花指令、混淆控制流、字符串加密、反调试。再往上叠一层就是直接套商业壳。Themida之所以会被选中是因为它有几个别的壳给不了的好处。第一它是真实世界存在的保护方案你在野外的样本里能见到不是出题人手搓的玩具练它有实际迁移价值。第二它自带的虚拟机保护会把关键代码转成自定义字节码静态反汇编看到的就是一堆无意义的指令流逼着你走动态路线。第三它的反调试手段成熟且全面几乎覆盖了所有常见的调试器检测点对新手来说是极好的抗压训练。从出题角度看好处也很明显加壳只需要一行命令但解题方要付出的成本是几倍的。题目体积小、分发方便难度却天然拉满性价比极高。所以近几年各类CTF的REVERSE题里Themida、VMProtect、ASPack、Enigma这些壳的出现频率都不低。1.2 Themida的三层防线加密、虚拟机、反调试想把这道题做掉得先知道自己在跟什么东西对抗。Themida的防护大致可以拆成三层一层套一层。最外层是区段加密与入口点混淆。原始程序的代码段和数据段会被加密存储PE头被大幅改写区段名可能保留.themida这类特征也可能被抹成一个空字符串。真正的入口点被劫持到壳自己的解密stub上你的调试器一断下来看到的是一段跟原程序毫无关系的启动代码。中间层是代码虚拟化。Themida会把选定的函数通常是关键逻辑或者它自己认为敏感的部分转换成一套自定义的虚拟机指令运行时由一个内置的VM解释器逐条解释执行。这就是为什么你dump出来的代码可能还是看不明白——因为一部分逻辑压根就不是x86指令。最内层是反调试与反dump。这一层最烦人手段包括但不限于直接读取调试寄存器DR0-DR7判断有没有硬件断点、调用NtSetInformationThread把当前线程从调试器视野里隐藏、用rdtsc做时间差检测、枚举窗口标题找调试器、检测父进程是不是调试器、检测PEB里的BeingDebugged标志、检测堆标志位。反dump方面它会在运行过程中反复修改自己的内存页属性甚至抹掉PE头让你dump出来的镜像没法直接用。注意这三层不是串行关系而是交织在一起的。你在调试过程中遇到的莫名其妙跑飞很可能不是壳本身的问题而是反调试检测到你之后主动把执行流带偏了。1.3 为什么我最后选了unlicense这条路面对Themida理论上你有三条路可走。第一条是硬啃手动下断点、找OEP、dump、修IAT。这条路对Themida来说极其痛苦因为Themida的OEP不是通过跳转到代码段这种简单标记暴露的它的解密和跳转过程有大量间接跳转和VM过渡靠ESP定律基本没戏。第二条是改题分析出题人的加壳方式找到壳的弱点定向绕过。这条路只有在出题人加壳姿势不规范的时候才走得通。第三条是用工具找现成的脚本或者专用工具让它自动完成OEP定位、内存dump和IAT重建你只负责分析脱壳后的程序。unlicense就属于这一类。我选它的理由很实际CTF是限时赛题目的价值在于考你的分析能力而不是考你手搓脱壳脚本的耐力。用工具把机械劳动省掉把时间花在真正的算法还原上这才是合理的资源分配。而且unlicense这类工具本身的工作过程也值得看一遍你能从它的日志里学到Themida的很多内部行为特征。2. 开干之前环境、样本与信息侦察2.1 工具清单与分工动手之前先把工具备齐避免分析到一半发现缺东西。我这次用到的东西不多但每一个都不能少。工具作用备注x64dbg主调试器负责附加进程、观察执行流、下断点32位样本用x32dbg版本unlicenseThemida脱壳工具自动定位OEP、dump内存、重建IAT版本要和壳版本匹配ScyllaIAT修复与区段重建的兜底方案unlicense失败时手动用PE工具PEiD / DIE / CFF Explorer静态识别壳类型、看区段、看导入表DIE对壳的识别更准IDA / Ghidra脱壳后的静态分析选自己顺手的Python 3写求解脚本建议装pwntools和z3备用这里有个容易被忽略的点位数一定要对齐。Themida的32位版本和64位版本在实现上差异很大脱壳脚本也完全不能通用。拿到样本第一件事就是用DIE看一眼是PE32还是PE32别拿x64dbg去调32位程序也别指望32位的脱壳脚本能搞定64位壳。实操心得工具版本和壳版本的匹配度比什么都重要。Themida 2.x和3.x的内部结构改动不小拿2.x的脚本去脱3.x的壳大概率会卡在OEP定位那一步日志里只给你一句OEP not found看不出原因。备好两三个版本的脚本轮着试。2.2 用静态信息快速判断壳的种类和版本这一步很多人跳过结果后面走弯路。拿到样本先别急着扔进调试器先做静态侦察。用DIE打开重点看三个地方区段表。Themida的典型特征是存在.themida区段或者区段名被清空。如果是较新的版本可能会看到.boot、.mackt这类不常见的名字。WinLicense同门产品则常带.winlice。入口点所在区段。正常程序的入口点通常在.text而加壳程序的入口点会落在最后一个可执行区段或者一个独立的stub区段里且该区段的原始大小和虚拟大小往往差很多。导入表。Themida会加密IAT静态看到的导入函数数量会异常少通常只剩下LoadLibraryA、GetProcAddress、VirtualAlloc这几个因为壳需要靠它们来动态加载真正的API。判断出版本之后再决定用哪个脚本。如果DIE给出的版本信息模糊就去看入口点的机器码特征或者直接看壳区段的大小——不同版本的stub体积有比较稳定的差异。2.3 样本边界哪些事能做哪些事别碰这一点我必须说清楚。脱壳技术在CTF里是完全正当的技能训练因为它处理的是出题人主动给你、并且期望你逆向的样本。但同样的手法用在别人的商业软件上就完全是另一回事了。我在整个分析过程中给自己划的线是只处理比赛提供的样本以及自己编译、自己加壳的练习程序。不触碰任何来源不明的、或者明确属于他人商业产品的二进制文件。分析过程中产生的任何结论只用于理解防护机制本身不用于绕过软件的授权验证。这不是场面话。逆向技能的价值在于理解系统怎么运转而不是在于能撬开多少把锁。把这条线划清楚你学起来反而更踏实因为你知道自己在做什么、为什么做。3. unlicense脱壳全流程实操3.1 Themida为什么让ESP定律彻底失效先解释一下为什么必须用工具而不是手动找OEP。上一代加壳工具比如UPX、ASPack的思路很朴素把原始程序压缩或加密存起来运行时在内存里解压然后跳到原始入口点执行。这类壳有个共性特征——解压完成后会有一个明显的恢复现场动作典型表现就是pushad之后popad栈指针ESP会回到一个可预测的位置。所以在栈上对ESP指向的地址下硬件访问断点断下来的时候基本就在OEP附近了这就是著名的ESP定律。Themida完全不吃这一套。它不依赖单一的恢复现场序列而是把解密过程拆散、混在大量垃圾代码和VM指令里中间穿插着多次内存分配、页属性修改和间接跳转。你根本找不到那个经典的特征点。更狠的是它会在执行过程中读取ESP附近的值来判断有没有人在这块内存上下了断点一旦命中就直接改道。对ASPack这类老壳社区里有现成的专用脱壳工具能一键搞定对Themida你只能依赖更聪明的自动化工具。3.2 unlicense的工作机制它到底绕过了什么我没有unlicense的源码但从它的运行日志和最终产出可以推断出它的大致工作路径理解这一点对排查问题很有帮助。第一步是反反调试。工具在附加进程之后会先处理掉一批已知的检测点比如在关键的检测函数上下断点并直接跳过、修改PEB中的相关标志位、以及在部分场景下对时间检测做屏蔽。这一步是隐式的你在日志里通常只看到它很快就完成了加载。第二步是定位OEP。它不依赖单一特征而是结合了多种启发式判断监控从壳区段向非壳区段的大跨度跳转、监控内存页属性从可写变为可执行的事件、以及分析跳转目标的代码特征是否符合编译器生成的函数序言。多条线索交叉验证之后才确定OEP候选。第三步是内存dump。找到OEP之后它把整个映像从内存里按区段抓下来同时处理掉Themida在运行期对PE头和区段的篡改重新构造一份结构完整的PE文件。第四步是IAT重建。这是决定dump出来的文件能不能用的关键。Themida把原始IAT加密了运行期才动态填充。工具需要在程序跑到OEP、IAT已经被填好但还没被壳清掉的这个时间窗口内扫描内存中指向系统DLL的指针数组反推出每个导入函数的名称重新写回导入表。提示这四步里最容易出问题的是时间窗口。如果dump时机偏早IAT还是空的偏晚壳已经把关键内存清了。所以工具经常会跑几次才成功这不是你的操作问题。3.3 实操命令与过程记录下面是我这次的完整操作序列。参数是基于我手上这版工具的常见用法不同版本可能有差异以你实际拿到的工具的帮助信息为准。先把样本放好确认位数和壳版本die sample.exe # 确认是PE32还是PE32以及壳的版本输出里能看到区段表有明显的Themida特征位数是32位版本落在3.x区间。接下来用x32dbg加载让程序跑到壳的初始化完成; x32dbg 里的典型操作序列示意 ; 1. 附加或打开样本后先进入系统断点 ; 2. 让程序自由运行到壳解密完成附近 ; 3. 不要手工下断点交给工具接管实际执行时我是先让unlicense以调试模式启动样本而不是先开x32dbg再附加——这一点值得强调。让工具自己启动样本比先开调试器再附加成功率高因为附加方式下进程已经跑了一段某些初始化阶段的检测点已经错过了工具来不及拦。工具跑起来之后日志会依次输出几个关键节点[] 进程已启动PID 0x1A2C [] 已安装反调试绕过钩子 [*] 正在扫描OEP候选... [] 发现候选OEP: 0x004012A0 (置信度 高) [] 正在dump内存映像... [] 已导出: sample_dump.exe [*] 正在重建IAT... [] IAT重建完成共修复 87 个导入项 [] 输出完成这里有一处细节值得留意候选OEP的置信度。如果它输出的是中或者低大概率dump出来的文件有问题别急着往下走先看看是不是壳版本没匹配上。我第一次跑的时候置信度是低dump出来的文件用IDA打开函数全是断的换了个脚本版本之后变成高一次就过了。工具跑完之后先做一次自检。用DIE打开dump出来的文件看导入表是不是恢复到了正常数量看区段名是不是回到了.text、.rdata、.data这种标准命名。这两项都正常基本可以认为脱壳成功。3.4 dump之后的修复IAT与区段对齐如果unlicense没能完整重建IAT就得手工收尾这时候Scylla上场。Scylla的工作方式是它在你dump的那个时间点扫描进程内存找出所有指向系统DLL导出函数的指针然后用启发式算法反推函数名。使用流程是先在调试器里让程序停在OEP附近再用Scylla附加依次点IAT Autosearch和Get Imports检查识别出来的导入表有没有明显的错误项比如地址落在非DLL区域、函数名是乱码确认无误后点Fix Dump选择之前dump出来的文件生成修复后的可执行文件。这一步最常见的两个坑识别出大量无效项。原因通常是停的位置不对IAT还没被完全填充。往前跑一点或者往后跑一点再试。修复后程序跑不起来。多半是区段对齐问题。Themida在运行期可能改过区段的虚拟地址导致dump出来的区段布局和PE头里的记录对不上。这时候用CFF Explorer手工调整区段的VirtualAddress和SizeOfImage让它们自洽。实操心得判断IAT修复是否成功的标准很简单——把修复后的文件扔进IDA看导入表里有没有printf、scanf、strlen、memcmp这类跟字符串处理有关的函数。CTF逆向题的校验逻辑几乎一定会用到这些如果导入表里能看到它们说明修复到位了直接开分析。4. 脱壳之后才是真正的REVERSE把check逻辑扒出来4.1 定位main的几条高性价比路径脱壳完成游戏才刚开始。这时候你面对的是一个干净的、能用IDA正常反编译的程序跟普通逆向题没区别了。定位主逻辑我有几个固定的习惯按优先级排第一优先是字符串交叉引用。看IDA的字符串窗口找flag、correct、wrong、congratulation这类字眼双击跳过去看引用它的函数。CTF题的出题人十个里有九个会在校验成功或者失败的时候打印提示这几乎是最快的入口。第二优先是导入表反查。找scanf、fgets、read的调用点输入读取的位置往往离校验逻辑不远顺着往下翻几屏就能看到。第三优先是入口点回溯。如果字符串被加密了、导入表也被处理过那就从OEP开始顺着__libc_start_main或者CRT的初始化链一路回溯到main。这条路慢但最通用。这道题我是靠字符串进去的。脱壳后的镜像里保留了input your flag:这样的提示串交叉引用直接定位到校验函数前后不到两分钟。4.2 还原校验算法的实际操作进去之后看到的逻辑不算复杂是典型的CTF套路读取输入做一轮长度检查和字符集检查然后逐字节参与运算最后跟一个硬编码的字节数组比较。一个很实用的技巧是先看数据结构再看运算。IDA反编译出来的代码里硬编码的字节数组会以unk_或者byte_开头的变量形式出现长度往往就等于flag长度。先把这个数组和它的长度记下来你就知道flag有多长了后面的分析会顺畅很多。另一个技巧是识别运算模式。CTF里最常见的三种异或代码里会出现^通常配合一个固定key或者一个和下标相关的key。加减和-成对出现注意有没有取余或者溢出截断。查表替换出现一个大数组配合索引本质是S盒替换。看到运算之后别急着抄代码先确认运算方向和边界。比如是input[i] ^ key[i]还是key[i] ^ input[i]是逐字节循环还是分块处理有没有i % 4这种分组逻辑。这些细节抄错一个脚本就跑不出正确结果。4.3 写出能跑的求解脚本确认算法之后写求解脚本就是纯体力活了。原则很简单逆着来。加密是异或解密还是异或加密是先加后异或解密就先异或后减。下面这个脚本是我这道题用的结构你把字节表和运算换掉就能直接用# 逆向校验算法求解脚本 # 说明enc是脱壳后从IDA里抄出来的硬编码比较数组 # key是对应运算里用到的固定密钥 enc [ 0x3C, 0x2A, 0x1F, 0x37, 0x5B, 0x11, 0x48, 0x26, 0x72, 0x0D, 0x64, 0x2E, 0x19, 0x53, 0x07, 0x6A, 0x31, 0x4C, 0x22, 0x0F ] key 0x5A flag [] for i, b in enumerate(enc): # 原程序里的运算假设是 ((input[i] ^ key) i) 0xFF # 逆运算先减i再异或key tmp (b - i) 0xFF flag.append(tmp ^ key) print(bytes(flag).decode(latin-1))跑出来是一串可打印字符直接就是flag本体。如果输出里有大量不可打印字符说明运算方向或者边界条件搞错了回去检查一下是不是漏了取余、或者key的下标不同步。实操心得脚本写完先别急着提交拿几组已知的中间值手工验算一下。比如你知道第0个字节的结果手动算一遍看跟脚本对不对得上。这一步花三十秒能省掉十分钟的反复调试。另外写完的脚本一定要存下来这类题目的算法改动很小下次遇到类似的可以直接改字节表复用。5. 踩坑记录与排查速查表5.1 常见故障现象与对应处理脱壳过程中遇到的问题高度集中在几类我整理成表方便对照。现象可能原因处理方式工具卡在扫描OEP不动壳版本与脚本不匹配或反调试拦截未生效换脚本版本确认位数对齐改用以工具启动进程的方式dump出的文件IDA打不开PE头被壳篡改dump时机偏早重新dump或用CFF Explorer手工修复头结构能打开但没有导入表IAT未重建或重建失败用Scylla在OEP处手动重建导入表里有大量乱码项停的位置不在IAT填充完成的窗口内微调暂停位置前后各试几次修复后程序一运行就崩区段虚拟地址不对齐检查SizeOfImage与各区段VirtualAddress是否自洽调试过程中程序无故跑飞触发反调试执行流被重定向检查是否漏了某个检测点换工具钩子方案脱壳成功但逻辑看不懂关键函数被VM虚拟化改用动态跟踪在关键API上下断点观察数据流这张表里的每一项我基本都撞过一遍。最耗时间的是最后一项——脱壳成功不代表万事大吉如果出题人把校验函数扔进了VM你静态还是看不出来只能靠动态跟踪。5.2 反调试对抗的几个实用技巧对付Themida的反调试有几个小技巧能显著降低踩坑概率。先用工具整体的绕过方案再考虑单点对抗。很多人一上来就手动处理检测点下了十几个断点之后反而把执行流搅乱了。正确做法是先让工具把大部分检测点处理掉只在工具明显失效的地方做针对性处理。注意检测的时机。Themida的反调试不是只在启动时执行一次很多检测是在运行过程中周期触发的。所以你不能只关注入口点要在整个调试过程中保持警惕。如果程序跑着跑着突然卡死或者跳飞多半就是某个周期检测命中了。用日志代替猜测。现代调试器和工具都有日志功能把断点命中、内存访问、模块加载这些事件全记下来出问题的时候翻日志比盯着屏幕猜有效得多。硬件断点比软件断点安全。软件断点会修改内存中的指令字节改成0xCCThemida有CRC校验能检测到。硬件断点走的是调试寄存器不改内存隐蔽性更好。不过要注意Themida也会检测调试寄存器的占用情况用完记得清掉。注意调试寄存器相关操作在两套主流调试器里的实现细节不同涉及具体实现的部分我这里不展开你在实际操作时以自己所用工具的文档为准。5.3 关于玄学问题的一点经验解释脱壳过程中有些现象看起来毫无道理比如同一个脚本跑两次一次成功一次失败或者dump出来的文件有时候能跑有时候不能。这类玄学其实都有技术原因只是表象上不好归因。一种可能是时序敏感。前面提过dump和IAT重建依赖一个时间窗口而这个窗口的边界可能受系统负载影响。机器忙的时候线程调度延迟变大工具抓取内存的时机就偏了。所以遇到过这种情况先关掉其他占资源的程序重跑几次看看稳定不稳定。另一种可能是内存布局随机化。如果系统开启了地址空间随机化每次加载的基址都不同某些依赖固定地址的启发式判断就会失效。这种一般在启动参数或者环境变量里可以控制具体看你所用的分析环境。还有一种可能是壳自身的多路径解密。Themida在某些配置下会根据运行环境选择不同的解密路径导致每次执行的中间状态不完全一致。这类情况下多跑几次、取成功的那次结果是完全可以接受的做法——CTF里没人要求你必须100%复现。6. 赛后复盘这类壳题的训练路径做完这道题我把整个流程重新梳理了一遍发现真正花时间的不是脱壳本身而是理解Themida到底在做什么。工具能帮你省掉机械劳动但它不能帮你理解为什么要这么做。如果你想让下次遇到壳题时更从容我建议按这个顺序练。第一步从老壳开始建立手感。拿UPX和ASPack练手手动找OEP、手动dump、手动修IAT把整个流程的每个环节都亲手过一遍。这些壳的保护强度低能让你专注于理解加壳这件事的本质是什么而不是一上来就被反调试搞得晕头转向。第二步过渡到中等强度的壳。挑一些有简单反调试、但没有虚拟机保护的壳练习动态跟踪和特征识别。这个阶段的目标是学会在调试器里读流程知道程序在干什么而不只是看它跳到哪里。第三步再碰Themida和VMProtect这类硬骨头。到这个时候你已经有了判断能力什么样的现象是反调试引起的什么样的现象是脱壳工具的问题什么样的情况是必须动态跟踪的。带着这套判断力去看工具日志学习效率会完全不一样。第四步一定要自己动手加壳。这一步被绝大多数人忽略但我认为是性价比最高的。你用工具给自己写的小程序加一个Themida壳然后观察壳前后的差异、观察运行期的行为你会对壳的每一个动作有直观认识。这种我出题我来解的视角转换比看十篇教程都管用。最后再说一点我个人在比赛中的取舍。遇到壳题先花两分钟判断壳的类型和强度如果是UPX这种直接命令一行解决如果是ASPack用现成工具如果是Themida这种先确认有没有匹配的自动脱壳方案有就上没有就评估一下当前时间还够不够硬啃不够就果断跳过先去做别的题。CTF是资源分配的游戏在一道壳题上耗掉全部时间导致其他题没做是最不划算的选择。把脱壳的能力练扎实让它变成你工具箱里一个两分钟内能决定要不要用的工具而不是一个必须死磕的关卡这才是这道题真正想让你学到的东西。
返回列表