ARTICLE DETAIL

资讯详情

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

360加固逆向:DEX解密与ELF修复原理深入剖析

360加固逆向:DEX解密与ELF修复原理深入剖析 360加固这类国产加固方案在Android安全分析里几乎绕不开。不管你是做恶意样本分析、加固SDK自研还是做合规检测只要手里的APK套了一层加固必然要面对两个硬骨头一个是DEX怎么从加密状态还原成可分析状态另一个是ELF里那些被改造过的结构怎么理顺。这篇文章不打算写那种照着点按钮的“一键脱壳”流水账而是把背后的原理拆开讲清楚——加密包为什么长这样、DEX和ELF的结构在加固里各自扮演什么角色、所谓“修复”到底在修什么。需要先申明边界本文只讨论加固方案的技术原理与安全分析方法用于自研防护评估、恶意代码分析等合法场景。对他人App做逆向务必先获得授权任何绕过授权机制的行为都不在讨论范围内。1. 加固壳的两条主流技术路线整体DEX加密与抽取式加固先说结论现在市面上主流加固方案虽然细节不同但骨架高度相似。360加固在早期版本和后来的迭代中核心思路一直围绕两条路线展开——整体加密和抽取式加固。理解这两条路线的差异是后续所有分析和修复工作的前提。1.1 整体加密把DEX锁进资源文件整体加密的路子比较直白。APK在加固阶段原始dex文件会被加密密文被放到assets或者特定资源目录下同时APK里会替换进一个壳的Application入口。原始Application会被改名、隐藏真实类名写进壳的配置里。用户在桌面点击图标启动App时系统先实例化的是壳的Application。壳的Application在attachBaseContext阶段做两件事第一件是System.loadLibrary加载对应的so文件这个so就是解密执行体第二件是从so里拿到解密的输出再通过自定义的ClassLoader加载真实DEX。这两件事的顺序很关键。因为落盘到APK里的原始DEX是密文Java层到这一步还拿不到可执行的字节码必须先靠native层完成解密动作再交给Art虚拟机去解释执行。所以天然分成两层。从分析者的视角看整体加密的突破口在运行时——不管加密算法多复杂最终在进程内存里一定会出现明文DEX文件镜像。这个镜像可能是不完整的也可能会被拆分但只要虚拟机还要执行Java代码内存里一定存在还原后的DexFile对象。1.2 抽取加固把方法体从DEX里“抽走”整体加密有个副作用只要拿到运行时的内存dump整个DEX能一次性还原防护强度相对有限。所以后来主流的加固方案都在往“抽取”方向走。抽取式加固的思路是不再满足于把整个DEX加密存放而是对每个方法单独处理。在加固阶段将特定方法的code_item从DEX中摘除或者填充无意义指令。原指令被转移存储到so层或者其他加密数据区。运行时在方法首次被调用、类被加载等时机native层钩子会把原始指令回填到内存里的method对应的代码区再继续执行。这样做对分析工作影响很大你即便拿到了DEX完整的内存镜像也只能看到静态的class信息、字段、方法声明真正的方法体可能是空的也可能只有几条恢复用的跳板指令。想看到完整逻辑必须顺着方法体和native层的对应关系去补齐这就是“修复DEX”这件事的核心来源。1.3 壳入口总是藏在so里不管是整体加密还是抽取加固都绕不开一个事实解密逻辑必须放在native层否则审计者用Java层工具就能直接跟到密钥和算法。这就是ELF在加固体系里拥有极高地位的原因。壳的so文件通常有一个相对简单的初始化链路linker加载so之后先执行.init_array里面的构造函数再把控制权转移给壳的JNI_OnLoad或者由Application主动调用某个JNI函数触发后续动作。加固so里往往还集成了反调试、反内存dump、环境检测等逻辑这些逻辑和真正的解密代码混在一起。所以分析ELF不是可选动作而是必须动作。不把so里的入口函数和调用关系理顺就不知道解密DEX的触发时机也不知道抽取方法体是从哪块内存拿回来的。2. DEX和ELF文件结构速记解密与修复都不能偏离格式约束很多新手拿到一个加固包第一反应是找工具第二反应是找脱壳脚本唯独忘了先把静态格式吃透。实际上DEX和ELF这两个格式里的关键字段直接决定了你分析到哪一步、修复时改哪里。2.1 DEX header是解密后的第一张地图DEX文件开头是固定的magic通常为“dex\n035”或“dex\n037”。紧跟在magic后面的整个header区域记录了文件尺寸、各索引区的偏移和数量。重点看这几个偏移字段string_ids_off / string_ids_size字符串索引区涉及类名、方法名、字段名的字符串都在这。type_ids_off / type_ids_size类型索引描述类、数组、原始类型的字符串引用。method_ids_off / method_ids_size方法索引每个method_id包含class_idx、proto_idx、name_idx。class_defs_off / class_defs_size类定义区指向每个类的详细定义。data_off / data_size数据区保存code_item、debug信息等实际字节码。这些字段指向的是文件内偏移。所有官方解析工具和反编译工具都依赖这套表结构。如果加固过程中篡改过索引表结构或者dump出来的DEX是从内存某个地址直接拷贝的那么header和真实数据之间可能出现错位直接丢给jadx或者IDA往往解析失败。判断一个DEX是否“健康”第一步不是关心有没有壳而是先检查header是不是完整、索引区是否和data区对齐。这是修复工作最基础的分诊动作。2.2 code_item位置和“修复”粒度每个方法最终的执行指令存放在code_item里。Art虚拟机执行一个方法时核心就是读取code_item中的insns指令数组。code_item的结构里有一组字段需要记住registers_size参数加局部变量的寄存器数量ins_size入参寄存器数量outs_size方法调用时传入的参数寄存器数量tries_size异常try区域的数量debug_info_off调试信息偏移insns_size指令数量insns[]实际的16位指令单元数组抽取式加固做手脚的地方就是insns[]。加固器可以把整个insns清空、置成nop或者重定向到一个跳板地址。运行时再由native回填真实指令。修复时的“方法体修复”本质就是把被抽走的insns恢复到code_item正确位置并确保insns_size与真实指令长度一致。如果只恢复了指令内容但长度字段不对最终执行时会越过边界轻则解析错乱重则崩溃。2.3 ELF头是壳入口的定位器ELF格式的套路同样固定。文件最开头是e_ident魔数通常为0x7f 45 4c 46也就是ELF三个字母。再往后是ELF头里的关键字段e_type标识可执行文件还是共享对象e_machine标识ARM、ARM64还是x86架构e_entry是程序入口点。但Android平台上的native库更关心的是程序头表和节头表。程序头表里的PT_LOAD段描述运行时要映射到内存的块linker加载so就是按PT_LOAD来的。节头表则保存符号表、字符串表、重定位表等在静态分析时要用的信息。加固对ELF的改造手段常见有几种修改ELF头的某些字段让静态解析器无法直接load。删除或者混淆节头表只保留程序头导致IDA等工具加载后看不到导出符号。把真正的导出函数藏在动态符号表之外只保留一个入口其余逻辑运行期展开。对敏感函数做控制流混淆或者整段代码加密运行时在内存里解密。“ELF修复”这个概念之所以存在就是因为经过这些改造的so文件已经不是一份标准的、可被分析工具直接读懂的ELF了。想看清楚这个so内部做了什么必须先把它还原成标准结构或者通过运行时行为去推算它原本的逻辑。3. 解密对象三连运行时内存还原、DEX镜像校验与修复粒度“解密”这个词在不同上下文里意思差别很大。在360加固的方案里可以拆成三个层次去看整体DEX密文的还原、抽取方法体的指令回填、以及ELF中相关逻辑的重建。三个层次分别对应不同的分析动作。3.1 为什么运行时还原后仍然不能直接反编译整体加密的情况下只要壳的Application跑起来内存里就会存在一个解密后的DEX镜像。理论上把这个镜像从进程内存中提取出来丢掉原始APK里的密文DEX这个环节就算完成了。但实践里经常碰到几个坑第一dump时机的问题。壳可能不会在Application启动第一时间解密全部DEX而是按需解密。如果你dump太早只能拿到一部分类dump太晚可能加载了大量已经回填过指令的DEX内存布局变得复杂。第二多个DEX叠加的问题。一个App可能有classes.dex、classes2.dex、classes3.dex加固后它们可能被合并成一个整体存储运行时再拆分还原。dump出来之后需要按DEX header去切割才能得到多个独立的DEX文件。第三内存对齐的问题。DEX在文件系统里有严格的对齐要求整个文件必须按4字节对齐。内存镜像如果是从堆里某个非对齐地址直接抓的可能头尾多出或缺少字节导致解析器报错。这些都不是算法层面的难题而是工程层面的琐碎问题。处理它们没有捷径只能先校验DEX header合法性再按结构逐个区域核对。3.2 校验与重建索引的基本思路拿到一个DEX镜像我先建议先做几步机械动作第一步检查起始4字节是不是“dex\n”魔数。很多内存dump工具输出的是完整DexFile对象可能包含前面的Object header需要跳过。第二步读取file_size字段看和实际文件长度是否一致。如果长度大于file_size说明后面带了附加数据如果长度小于file_size说明dump不完整。第三步检查各索引区偏移是否超过file_size。一旦发现有偏移超过文件尾基本可以判断这个DEX被截断过或者被加固器改写过头。第四步随机抽几个method_id解析出对应的class_idx、proto_idx、name_idx再跨索引区解析字符串看能否拼出一个正常的方法声明。这一步做完绝大多数“坏DEX”都能发现具体的异常点。修复工作也不是什么魔法就是把异常点定位出来尽量把结构恢复成标准形态。比如class_def里指向class_data的偏移错了就按真实的内存地址修正method索引里的name_idx指向了无效字符串就查字符串表重建字符串。这整套流程和“DEX解密”其实是一体的加密过程破坏的是可读性修复过程恢复的也是可读性。核心价值都在于让静态分析工具能够按照标准格式重新加载这份数据。3.3 抽取方法体跟踪的换算关系抽取式加固比整体加密更麻烦。你从内存里抓到的DEX可能已经包含了所有类定义但大量被抽走的方法体依然是空壳。要判断一个方法是不是被抽了有个很直接的办法看它的insns_size和insns内容。如果insns_size异常大或者异常小且内容全是nop或者第一个指令直接是一个跳转到某个固定地址的分支那八成就是被处理过。修复抽取方法时关键是把方法体对应的原始指令找回来。常见的找回路径有几条壳在运行时会把指令回填到内存你可以在方法执行后再dump一次对比执行前后的insns差异。壳的native层可能保存了一个方法地址到指令密文的映射表分析so后可以从映射表直接找到每个方法的原始指令。某些方案会把指令按方法粒度加密存放修复时需要在so里找到加解密函数逆出密钥后再逐个解密。无论哪种路径本质上都会落到一件事把so里存储的二进制数据按照DEX code_item的格式要求塞回正确的位置。所以抽取式的“DEX修复”和ELF分析是深度绑定的。你不可能脱离ELF单独谈抽取修复。4. ELF修复的具体指向从不可直接读到与DEX可联动很多人把ELF修复理解成“把加固so改成能看的so”这个说法太笼统。真正在分析场景里ELF修复通常包含三个明确的子任务恢复ELF基本结构、定位壳初始化入口、解析出加密数据表的生成规则。4.1 加固so在二进制层面做了什么360加固的so文件从体积上就很有辨识度通常体积不小因为里面塞了大量加解密、反调试、虚拟机检测相关的代码。这类so在静态层面上往往有这些特征节头表可能缺失或者节的数目被改成一个极小值。动态符号表中只保留了极少量导出项JNI_OnLoad都不一定直接暴露。入口函数可能被包在.init_array里的多个构造函数中调用关系复杂。符号字符串表里全是乱码或者被加密成非ASCII字符串。静态分析工具面对这种文件时最常见的反应是“加载失败”或者“识别为未知CPU架构”。这些只是表象本质是工具依赖的节头表、符号表信息被破坏了导致解析器无法按常规路径定位代码和数据。修复ELF的第一步通常是绕过或者重建这些被破坏的信息。绕过是比较务实的手段——既然linker加载so实际上只看程序头表那么加载后程序头表在内存里的形态就是可用的。在内存态下借助调试器或者运行时工具去转储能拿到一份已经映射好的、方便IDA分析的镜像。4.2 需要梳理清楚的三类ELF入口我分析加固so时习惯先把入口梳理成三条线第一条是静态入口线。从ELF头的e_entry出发结合.init_array里的构造函数指针找到最初执行的那些函数。加固的初始化逻辑大概率藏在这里。第二条是JNI入口线。壳在Java层一定会调用System.loadLibrary随后虚拟机会找JNI_OnLoad。所以JNI_OnLoad往往是一个必经点从这里跟进能摸到壳暴露给Java层的API面。第三条是动态注册函数线。很多加固so在JNI_OnLoad里会调用RegisterNatives做动态注册把Java层native方法和so里的函数绑定起来。分析动态注册的参数能直接看出哪些native函数负责解密、哪些负责指令回填。这三条线不是孤立存在的它们在运行时会汇合。最典型的场景是某个Java native方法从Java层传入一个DEX文件路径或者byte数组native函数内部完成解密之后再通过Art API注册进虚拟机。这一整套调用关系离开了ELF层面的分析根本理不清。4.3 DEX和ELF联动修复的边界实际项目里DEX修复和ELF修复经常是交替进行的不存在先修完DEX再修ELF这种线性顺序。举个常见例子抽取式加固的so里保存着一个“方法索引表”表的每一项记录某个方法在DEX中的method_idx、类的class_def偏移以及加密指令的存储位置。分析so时你会拿到这张表修复DEX时你要靠这张表去回填每个方法体。没有ELF分析表就找不到没有DEX结构知识表里的method_idx也解释不了。反过来也一样如果你先把DEX修复好了再回头看ELF很多so里的加密字符串、函数命名规律突然就能对上了。因为DEX里Java层的调用关系正好可以印证so里哪些函数是解密DEX用的、哪些是反调试用的。边界在哪里我的经验是DEX修复强调结构对齐ELF修复强调行为关联。DEX修复的“对错”是解析器能不能正常加载ELF修复的“对错”是你能不能把so里的逻辑映射到具体行为上。先修DEX修到能过解析器再回去分析ELF效率最高。5. 合法分析场景下的工具链与避坑经验工具这块我不想重复网上一抓一大把的列表。只说我实际项目里用下来比较顺手的组合以及这几个工具在“DEX解密与ELF修复”任务里的合理分工。5.1 工具分工参考工具主要用途在加固分析里的角色jadxJava层反编译还原DEX后的类结构、调用关系IDA ProELF/ARM指令分析梳理so入口、解密函数的调用链010 Editor二进制模板解析手工检查DEX header、ELF header、code_itemunidbgJNI模拟执行在无设备环境下调用so里的解密函数动态调试器运行时内存与寄存器观察捕捉内存中的明文DEX、跟踪指令回填时机这几类工具之间是互补关系。纯静态分析卡住时我会靠010 Editor手工过一遍结构字段判断到底是文件被截断还是索引被篡改。要看so里的解密逻辑时IDA是主力。要验证一个猜测的时候unidbg这类模拟执行工具比真机调试更快省去了适配、反调试对抗的麻烦。5.2 容易被忽略的坑第一个坑是版本差异。360加固不同版本的实现差异非常大老版本可能只是整体加密新版本可能是抽取加VMP的混合方案。网上很多脱壳脚本只适配特定版本拿新版样本直接跑结果必然是不完整或者直接崩。分析前建议先看so文件里的版本特征把样本归类再决定用什么思路。第二个坑是反调试对抗。加固so里普遍存在ptrace反调试、时间差检测、文件完整性校验。你以为自己在正常分析实际上so早就检测到调试环境故意返回假数据或者故意触发崩溃。处理办法没有统一公式只能逐项识别并绕过这也是为什么模拟执行环境越来越受欢迎——它相对可控不用跟真实设备上的反调试缠斗。第三个坑是方法体修复后的验证。指令回填以后光看jadx能出结构还不够。虚拟机执行时还会做校验比如CRC32校验、方法长度检查。修复完的DEX如果只过了静态解析器但运行时校验不过App一样会崩。我习惯修复后做两轮验证第一轮用反编译工具确认结构第二轮在可控环境里跑一次目标流程确认执行逻辑真实可用。5.3 个人实践心得做了几年加固对抗方向的工作最大的体会是这类任务拼的不是某个奇技淫巧而是对两种格式的熟练度和对加固实现路径的预判能力。一旦你能在脑海里把DEX的code_item和ELF的so函数地址关联起来大部分加固方案在你面前就是透明的。我也越来越重视自动化但这里的自动化不是指无脑跑脚本而是把自己分析过程中的判断逻辑沉淀成可复用的校验工具。比如写一个脚本自动检查DEX header合法性自动识别被抽取的method列表自动对比两次dump的insns差异。这些工具能极大节省重复劳动让你把精力集中在真正需要人脑判断的逆向环节。最后想说的是研究加固技术最忌讳的是“为了破解而去破解”。对加固方案做技术分析正确的出发点应该是理解和评估安全防护本身——保护自己开发的App、分析恶意软件、审计第三方SDK的安全性。无论是掌握DEX格式还是理解ELF修复本质上都是二进制分析能力的一部分。这套能力用在正道上才是它真正的价值所在。
返回列表