ARTICLE DETAIL

资讯详情

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

逆向分析必备:从UPX到VMProtect的脱壳实战指南

逆向分析必备:从UPX到VMProtect的脱壳实战指南 做网络安全攻防的朋友应该都遇到过这种场景从某个渠道拿到一个样本丢进沙箱一跑行为确实可疑可丢进IDA准备分析时却发现函数列表全是乱码字符串也搜不到任何有用的提示程序入口点藏得严严实实——大概率是加壳了。软件逆向里最绕不开、也最劝退新人的一块就是脱壳。这篇文章把脱壳从原理到实操完整讲清楚适合正在学逆向、做恶意样本分析、或者准备各类CTF和攻防演练的读者。你不用是高手只要会一点汇编、用过调试器就能跟着一步步把壳扒下来。1. 脱壳这件事到底是在做什么1.1 壳的本质给程序裹了一层“快递包装”壳不是病毒也不是恶意程序专属技术它本质上是一个包裹在原程序外面的小程序。程序运行的时候操作系统先跳进壳的代码壳负责在内存里把真正干活的代码解开、还原然后把控制权交还给原始程序。用户看到的执行入口是壳的入口原始程序的入口——OEPOriginal Entry Point——被藏起来了。压缩壳和加密壳是比例最高的两类。压缩壳像快递包装里的气泡膜主要目的是把程序体积缩小常见的有UPX、ASPack、NSPack加密壳则是给快递箱加了密码锁壳的代码会把原程序分段加密运行时逐段解密目的本身就是防分析、防篡改常见的有VMProtect、Themida、爱加密这类加固产品。还有一些保护壳会在运行前检查调试器、虚拟机、文件完整性和内存完整性这些都是壳的特性不是原程序的功能。理解这一点很关键脱壳的目标不是“消灭壳代码”而是想办法让原程序在内存里完整还原之后把它“拍”下来或者抓到OEP然后重建一个没有壳的、可以直接被静态分析的PE文件。1.2 为什么安全研究离不开脱壳脱壳在安全攻防里的位置有点像学骑车必须先学会上车。逆向一个加壳的恶意样本时如果不脱壳你在IDA里看到的可能只有壳的引导代码真正的恶意逻辑还在加密数据里躺着。勒索软件、远控木马、挖矿程序几乎都会加一层壳有些还会叠加多层就是为了让分析人员的效率降到最低。攻防演练和CTF比赛里逆向题也特别喜欢出加壳程序。出题人给一个加了ASPack或者UPX的二进制参赛者要快速识别壳类型、找到OEP、dump内存、修复导入表才能进入主程序开始真正的逆向。脱壳速度直接决定能不能拿分。尤其是AWD攻防这种时间紧、对抗强的场景能快速判断样本有没有加壳、壳是什么类型比闷头逆向一下午高效得多。另外SRC漏洞挖掘中失陷样本和恶意文档也经常加壳。脱壳可以说是分析链路的第一环这一环走不通后续的行为分析、C2提取、特征提取都没法做。2. 动手之前先认得壳——常见壳类型与识别方法2.1 常见壳的几大流派压缩壳、加密壳、保护壳先记结论壳通常分为压缩壳、加密壳、保护壳三类但实际做样本分析时更常见的分类是按工具名来认。下面是这三年我见过的高频壳按“新手友好度”排了个序。壳类型代表工具难度特点压缩壳UPX、ASPack、NSPack入门几乎不加密目的是压缩脱壳方法固定入门加密壳简单的自写壳、加花指令壳较低混淆程度一般ESP定律可破中等加密壳Enigma、PECompact、ExeStealth中有反调试需要结合调试器手工处理商业保护壳VMProtect、Themida、爱加密移动端高虚拟化反调试反虚拟机脱壳只是第一步UPX是学习脱壳最好的入门样本UPX本身开源壳代码逻辑简单压缩后体积小官方还提供了解压功能。很多恶意样本偷懒直接给EXE加UPX所以学会UPX脱壳就能处理掉一部分现实中的样本。ASPack则比UPX稍微绕一些入口点不是标准的pushad但仍可以用ESP定律处理。VMProtect这类商业壳难度陡增想完全脱壳很费精力但也不是没有思路后面我会展开说。2.2 用Exeinfo PE和PEiD快速识别壳识别壳不需要靠肉眼猜直接上工具扫一眼是最快的。PEiD是经典中的经典虽然已经很多年不更新了但识别老壳和高频壳非常准常见UPX、ASPack、NSPack都能一眼扫出。新样本推荐Exeinfo PE这个工具这些年更新很勤快对压缩壳、加密壳、加壳器的支持面更广还能显示入口点、节表、编译器字段在识别壳的同时给出不少有用信息。识别时直接把exe文件拖进Exeinfo PE窗口界面会显示类似UPX 3.96、ASPack 2.12这样的壳标识。如果没有识别出来可以点“More info”看节表和入口点特征再结合经验判断。例如UPX壳的节名通常叫UPX0、UPX1ASPack的节名是.aspackVMProtect的节名是.vmp0、.vmp1。节表特征的优先级有时比工具识别结果还要高因为加壳器修改节名是常有的事但节表里数据段、代码段的布局变化很难造假。移动端样本的壳识别则是另一个套路。Android APK的加壳标识通常出现在dex文件加载逻辑里像爱加密、360加固、腾讯乐固都有自己独有的特征字符串iOS的加固和反调试更复杂这里不展开。识别壳这件事宁可多花两分钟确认也不能省因为壳类型判断错了脱壳方案跟着就错了。3. 脱壳实操从UPX到VMProtect3.1 准备工作与环境搭建脱壳需要一个相对干净的环境。我个人的习惯是开一台Windows 10虚拟机安装一个尚未被杀软干扰的调试环境把所有样本和工具都放在同一个目录里快照保存好。需要的工具就这几样x64dbg或OllyDbg32位样本用OD更顺手64位样本用x64dbg、LordPE或Scylla做内存dump、ImportREC或Scylla修复导入表再加上一个能看PE结构的工具比如CFF Explorer。我常用Scylla来做dump和导入表修复这一步可以省掉ImportREC的单独安装但如果习惯用LordPEImportREC的组合流程也完全一样。环境搭好后先做一个最简单的实验用记事本写一个只有MessageBox调用的C程序分别用UPX和ASPack加壳拿这两个样本练习脱壳。自己造的样本不会因为恶意行为触发杀软报警可以放心练手。3.2 UPX脱壳命令行一条命令的“速通”玩法UPX比较特殊它自带解压功能。拿到一个UPX加壳的样本先别急着上调试器试试这条命令upx -d sample.exe如果样本的UPX节没被修改过、壳没有被二次加工这条命令可以直接脱壳成功。生成的sample.exe就是还原后的原始程序拖进IDA里就能看正常函数和字符串。现实中我会先跑一下upx -d不行再上调试器。这条命令背后做的事是把UPX节解压回原始代码段然后去掉UPX0、UPX1等节重新计算入口点。不过要注意不是所有UPX加壳样本都能这样直接脱。很多恶意样本会在UPX脱壳后追加一段自解压代码或者修改节名这样upx -d会报错此时就只能手动脱。还有一种情况是UPX加壳后又套了一层壳命令脱完一层后面还有一层。判断标准就是脱壳后的文件能不能正常分析如果发现导出表、导入表还是异常就继续下一层。3.3 手脱UPXESP定律内存断点理解脱壳的本质命令行脱壳只是解决了“能不能脱”的问题真正要理解脱壳原理还得手动脱一次。以32位UPX为例手动脱壳流程几乎是固定的第一步把UPX样本丢进x64dbg或OllyDbg断在入口点。UPX入口典型的指令序列是“pushad; sub esp, 0x...; call 某地址”pushad把寄存器压栈保存环境sub esp则给解压代码预留局部空间。第二步执行一次“ESP定律”在pushad之后ESP指向的栈顶就是所有寄存器压栈后形成的缓冲区。此时给这快栈区下一个硬件访问断点例如在OD里使用命令“hr esp”意思是当程序访问ESP指向的内存时立即断下。这样做的逻辑是壳代码解压完成后最终一定会通过popad恢复寄存器、跳转到OEP而popad会访问栈顶这几个保存寄存器值的地址所以硬件断点会在即将跳OEP时触发。第三步断下来后取消硬件断点往下单步跟踪几步。一般情况下很快就能看到一条“jmp 某个地址”或者“push 某个地址; retn”。这个地址就是OEP。如果OEP看起来像标准的C程序入口比如一上来就是push ebp; mov ebp, esp那基本就找到了。第四步在OEP处下断点重新运行到断点处然后用Scylla或LordPE把当前进程的内存dump下来保存成一个新的exe文件。这个新文件里包含了解压后的原始代码但它依赖的导入表还是指向原壳构造的数据需要修复。整个过程中最核心的“为什么”在于壳的代码运行完之后最终控制权必须交还给原始程序这个交还动作是一个确定的操作序列恢复寄存器、跳转OEPESP定律捕捉的就是“寄存器恢复”到“跳转OEP”之间的缝隙。理解了这一点后续换任何壳都能用这个思路去试。3.4 动态脱壳与内存DUMP拿一场实战说事前面其实是典型的“静态壳”手动脱壳思路。遇到不按套路出牌的壳就得换思路动态跟踪把程序跑到关键代码已经解压、还没开始执行恶意行为的那一刻然后dump内存。我举一个实际做过的例子一个加了ASPack的远控样本。ASPack入口不是UPX那种pushad开头而是一段循环解压代码直接单步会很痛苦。我的办法是在壳的内存区下访问断点或者用OD的“run trace”功能跑一段观察寄存器和EIP的变化。ASPack的解压逻辑会在解压完成后跳转到一个区域这个区域的数据是从加密状态变成明文状态的。我直接对.text段的第一个字节下内存访问断点断下时观察地址往往就已经接近OEP附近。另一种做法是直接内存搜索特征。例如程序解压完成后末尾通常会有“popad; jmp OEP”这段序列用调试器搜索当前进程内存中“61 5D 5E 5F C3”这一类的机器码字节模式直接定位到跳转指令。实操里搜索模式比无脑单步跟踪效率高得多特别是遇到加了几层壳或者带花指令的样本单步跟踪很容易陷入死循环或者踩到反调试陷阱。找到OEP后在调试器里让程序跑到OEP用Scylla选择当前进程填入OEP地址直接dump。dump之后不是立刻就能收工因为导入表通常是乱的下一节说修复。3.5 导入表修复脱壳后程序跑不起来的根源脱壳后最常见的翻车现场就是dump出来的exe一运行就报错错误提示往往指向“0xC0000005访问冲突”或者“无法定位程序输入点”。根本原因是导入表坏了。加壳程序在运行时动态加载API并填充导入表dump下来的文件里导入表地址和函数名对应关系已经被壳打乱甚至清空。不修复IAT文件没法运行。修复IAT的经典工具是ImportREC和Scylla。以Scylla为例操作流程大概是先切换到已经停在OEP的调试器进程在Scylla中选择对应PID第一栏填入OEP的RVA不是VA要注意Scylla填的是RVA相对于模块基址的偏移如果模块基址是0x400000OEP是0x405230就填0x5230点击“IAT Autosearch”再点“Get Imports”软件会自动扫描导入表并识别函数名。扫描结果里如果有很多无效函数可以手工调整扫描范围或者重新到更靠近OEP的指令位置运行程序。修复完成后点击“Dump”生成文件再点击“Fix Dump”在刚生成的文件基础上修复导入表最终得到的文件才是能跑的。实际测试中IAT Autosearch经常因为扫描范围不对导致修复失败。一个土办法是多试几次不同范围的自动搜索然后观察右侧函数解析列表的合理性如果发现像GetProcAddress、LoadLibraryA这类动态函数没有正确识别可以手动添加IAT地址范围。还是要强调不同壳IAT修复策略不一样比如VMProtect的IAT加密会在运行时动态计算傻乎乎地自动搜索基本找不到。这种情况就要用ORPIE脱壳机或者专门的IAT Hook修复插件属于进阶玩法。4. 工具链详解调试器、Dump工具与导入表修复工具怎么配合4.1 调试器的选择OllyDbg 还是 x64dbg工具选型这个问题每次给新人讲脱壳都要提一次。OllyDbg在32位时代的地位已经写进教科书尤其是配合插件体系比如HideDebugger、StrongOD这类反反调试插件在处理老壳和32位恶意样本时非常顺手。但64位程序它无能为力而且多年不更新Python脚本和自动化能力也比较弱。x64dbg是现在的推荐选择理由有三个一是同时支持32位和64位同一个界面逻辑不用来回切二是界面和大量功能都能脚本化配合自己写的一段脚本可以批量完成“加载样本、定位OEP、dump、修复IAT”的流水线三是它仍在持续更新对Windows最新版本兼容性更好。不过说句实话脱壳这件事工具是次要的关键是会不会用。奥利给式的“记流程”没有意义因为换个壳流程就变了。用得多了就懂了调试器的作用是动态观察程序由壳到原始代码交还控制权的全过程所以无论OD还是x64dbg核心把握两个功能就够了设置断点和查看寄存器栈。某些极端样本比如加了反调试反虚拟机还要用上调试器的“反反调试插件”才能继续这也是很多人栽跟头的地方。后面常见问题里再细说。4.2 Dump工具与导入表修复工具怎么配合用流程图来描述一句话就是调试器负责让程序在“刚刚到达OEP”这个状态暂停Dump工具负责把当前进程的原始内存完整保存为文件IAT修复工具负责在保存的文件基础上重建导入表最后CFF Explorer检查修复结果。具体到工具配合有两种习惯。一种是用LordPE dump再配合ImportREC修复这是老手惯用的组合。LordPE在dump时可以把“完整的内存镜像”抓下来包括所有节和未映射的区块缺点是它不修复IAT。另一种是我现在更常用的Scylla一站式方案因为它可以读取x64dbg的进程上下文一键完成dump和IAT修复。两者的本质是一样的关键步骤是dump之前必须确认停在OEP、dump时选择正确的模块基址、修复时填写正确的OEP。注意dump的内存如果包含了壳自己创建的区域可能带来额外的问题。比如有些壳在内存区域里留下了反调试线程或者数据陷阱dump下来的文件就会多出一些奇怪的节不一定会导致运行错误但会增加静态分析的干扰。遇到这种情况可以用PE工具把不必要的节删掉或者标记为不可执行。4.3 反调试对抗的思路脱壳过程中一定会接触到反调试。壳不让调试器附加、不让单步、不让下内存断点手段层出不穷检测PEB的BeingDebugged标志位、检测NtQueryInformationProcess、检测RDTSC时间差、检测窗口类名是不是调试器、检测虚拟机存在这些都属于静态特征绕过的范畴。恶意样本里的壳一般不会只做一层加壳时常常会把反调试选项选满导致调试器一加载就被踢出或者程序直接退出。遇到这种情况我一般先做三件事第一换插件HideDebugger或ScyllaHide把调试器的各种暴露特征隐藏起来第二用硬件断点替代软件断点硬件断点不会修改代码字节很多检测软件断点的手段就失效了第三遇到时间差检测别用单步循环跑改用“run to log”或者“trace into”条件式跟踪减少运行时间差。有一个最常见的坑提示一下调试器附加恶意样本时样本可能检测到自己跑在虚拟机里就直接退出倒不一定是反调试可能是反虚拟机逻辑。如果真想分析可以在虚拟机配置里做手脚但千万别把这种行为和“破解某软件”“绕过支付”混淆起来我们分析的是恶意样本不是琢磨怎么盗版方向别搞反了。5. 常见问题与排查技巧实录5.1 脱壳后程序闪退或报错这是新人问得最多的现象。闪退的根源通常是dump文件里有些内存位置没有正确映射常见情况有三种一是导入表没修好尤其是delay import和动态获取的函数没有还原程序启动调用某个API时失败直接退出。二是OEP没找对dump文件的入口点其实还在壳的代码里但壳的后续逻辑已经被dump破坏程序一启动就跑飞。检查方法是把dump下来的文件再次加载进调试器看看入口处的几条指令是不是标准的编译器入口序列。三是dump时少选了一些内存区块比如数据段的某些偏移没被包含程序运行时读取配置信息失败。解决办法是dump前扩大内存区域选择范围或者直接用完整的区块dump。排错时我习惯用一个最笨也最有效的办法把脱壳后的文件加载到CFF Explorer里看入口点RVA和节表是否合理再丢进调试器看入口反汇编是否流畅最后再运行看行为。三层检查下来大部分问题都能定位。5.2 OEP找不准怎么办前面说了ESP定律只是思路不是万能公式。遇到OEP找不准的情况先检查自己是否停在正确的时机很多壳在解压过程中会多次改变寄存器状态和跳转ESP定律断下的点未必就是真正要跳OEP的前一点。调低期望值换用几个辅助手段一是在内存窗口里监视.text段是否发生变化解压完成后.text段的内容会从乱码变成正常汇编指令这时手动翻翻反汇编窗口通常能找到编译器的典型入口序列二是用“Run to user code”类功能让程序运行时直接返回到非系统模块的代码壳跳OEP本质上就是从系统DLL返回用户代码三是用“hit trace”下条件日志记录所有访问某个特定内存地址的指令。再一个实用技巧搜“55 8B EC”机器码这是“push ebp; mov ebp, esp”的字节序列是VC、GCC等编译器常见的函数入口特征。当内存解压完成后直接在调试器的内存dump里搜索这串字节往往就能定位到OEP附近。搜索时注意用HEX模式、勾选所有区块。5.3 导入表修复总失败函数列表一片红执行IAT Autosearch后扫描结果里如果函数名识别为零或者大片无效地址基本就是IAT没有正确解密。原因多半是样本不是传统静态IAT而是用了动态解析、混淆调用或者加壳器会修改IAT内容。举个例子VMProtect壳的程序在OEP处导入表还没有完全构建好很多API是在运行过程中通过加密的GetProcAddress动态解析的。这时自动扫描自然失败。处理思路有两个。第一分析样本运行过程中壳解析了哪些API用调试器对GetProcAddress下断点记录调用参数和返回值把这些信息整理成一份“动态解析API表”然后手动填充到Scylla或ImportREC里。第二使用更高级的脱壳机比如Oreans UnVirtualizer或专门处理VMProtect的脚本但不能指望100%还原某些虚拟化壳是设计上就很难完全还原的有时只能让它变成“能运行但仍有壳特征”的折中状态。5.4 dump文件比原文件还大正常吗正常。内存dump会包含壳解压后的所有区块有时还会有壳自己申请的内存区域所以大小可能大于原加壳文件。但如果大得离谱比如几十MB就要注意dump选项里是否把所有内存区域都勾上了包括堆、栈和一些无关的映射区域。这种情况一般不会导致运行失败但会显著增加后续静态分析时的检索负载。推荐做法是dump时只勾选镜像映射区域Image也就是PE节对应的那几个区块其他的不选。另外dump完的节名可能还是UPX0、UPX1、.aspack这样可以在CFF Explorer里把节名改成.text、.rdata、.data方便之后分析工具正常识别。改完了顺便检查一下节的可执行属性别把数据节标成可执行也不要让代码节没有可执行属性Windows加载器对属性比较敏感一旦不匹配程序就崩。5.5 遇到反调试、反虚拟机程序直接退出这是老生常谈。处理顺序我总结为“两层三查”第一层是插件层开启ScyllaHide的所有隐藏选项隐藏调试器痕迹和虚拟机痕迹第二层是手动层找到检测点之后在调试器里单步跳过。做这个操作前一定要先定位检测代码。定位检测代码的方法对ExitProcess、TerminateProcess、NtTerminateProcess下断点样本退出时肯定会调用这些API中断返回调用栈就能找到检测代码的位置。检测点通常是一个条件跳转比如“jnz/je”之后跳到退出分支。把条件跳转改成相反方向或者把跳转指令nop掉即可绕过。有一个坑是壳可能不止检测一次而是散落在整个运行逻辑里所以每绕过一个点后要继续下断点观察是否再次触发退出直到进入原始代码为止。6. 从脱壳入门到安全研究进阶6.1 脱壳只是起点后续分析思路脱壳成功只是拿到了一张入场券。真正的工作是把脱壳后的样本丢进IDA开始分析恶意行为。动态行为可以再结合火绒剑、Process Monitor这类工具观察它创建了什么进程、操作了什么注册表、连了什么C2地址。有一个非常常见的现象脱壳后程序仍然会用各种方式检测自己是否在被分析比如判断调试端口、检查模块列表里有没有分析工具、通过时间差判断是否被放慢。这在样本分析里属于反分析对抗需要结合具体样本具体处理。如果是从攻防演练角度来学习脱壳我建议把脱壳能力当作“基本功”不要单独为脱壳而脱壳。脱壳只是一个环节后面还要学习汇编指令体系、PE格式、调试器脚本、沙箱行为分析甚至要会写自己的脱壳脚本。单独精通某一款壳的脱法没有意义因为恶意样本里的壳千变万化关键是要掌握“理解壳的行为模式”的能力。6.2 学习路径建议给刚入门的同学一条可以参照的路径。第一阶段学汇编和PE结构。不用太深能把C语言和汇编对应起来看懂push、call、ret、jmp这些指令即可。PE结构至少理解DOS头、NT头、节表、导入导出表。第二阶段用UPX壳做练习命令行脱壳成功一次再手动脱壳成功一次期间不断问自己“壳到底解压了什么”。第三阶段拿ASPack、NSPack这些老牌壳练手学会用Exeinfo PE识别壳用ESP定律、单步跟踪、内存断点等多种思路找OEP。第四阶段接触商业壳和加固壳学VMProtect、Themida、爱加密这类产品的分析思路。这个阶段建议搭配CTF逆向题练习因为CTF题目都是可控的、题目质量高而且题目会刻意设计各种壳和反调试技巧适合练手。同时可以看看网络安全学习路线图把逆向和Web、二进制、密码学几个方向的关系理清楚。学习路线没有统一标准但“汇编PE调试器实战样本”的组合是必修课。6.3 一些过来人的建议我个人做了这么多年的安全分析最大的体会是脱壳这东西眼高手低不行。看十篇教程不如亲手脱一个。第一次手动脱UPX可能需要卡上两个小时但只要脱通了后面的ASPack、NSPack就会变得很简单。遇到卡壳的样本先别急着问人要答案试着从这几个角度自我提问壳的入口在做什么解压完成了吗控制权交还给原始代码的关键跳转在哪导入表是否已经建立完整再有一个容易被忽视的细节保持工具版本更新。Exeinfo PE这种识别工具要经常更新签名库不更新的话新壳很容易识别不了。调试器和脱壳脚本也要保持版本一致我见过太多人在x64dbg的新版本上跑了老版本的脚本结果行为异常其实是脚本兼容性问题。还要提醒一句学脱壳技术目的是做安全分析、恶意样本研究、漏洞挖掘这类正当工作不要走到破解别人软件、绕过授权这种灰色路径上去。真正的攻防对抗是攻防演练这种合法合规的框架下进行的技术本身是中性的关键是用在什么场景。这条路很长把基础打牢、把案例做扎实后续做恶意样本分析也好、做漏洞研究也好都会觉得当初花时间啃“脱壳”这块硬骨头非常值。
返回列表