十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

DexProtector内存态对抗:从匿名段定位到DEX重组实战

DexProtector内存态对抗:从匿名段定位到DEX重组实战 1. 从“壳”到“态”为什么内存态对抗才是DexProtector绕过的分水岭先聊点实际的。DexProtector这壳在国内外的商用加固里出场率一直不低游戏、金融、SDK都有它的身影。很多人一上来就盯它的DEX加密、指令抽取、资源混淆觉得把这些静态层扒掉就完事大吉。但真实对抗里静态还原往往只解决“能看”解决不了“能跑”。真正卡脖子的是它在内存里布下的那道匿名段防线。匿名段这词听着玄乎其实说穿了就一句话DexProtector在运行时会把一部分敏感代码或数据映射到没有文件偏移对应关系的内存区域。正常的DEX加载流程ArtMethod的dex_pc_、dex_code_item_offset_等字段都指向DEX文件内部的偏移落盘后能通过解析文件来恢复语义。但匿名段的代码你在文件里根本找不到它的来路它就像一栋没有房产证登记的建筑——你拆墙能拆出钢筋但没法从城建档案里查出它的图纸。所以做DexProtector的内存态绕过核心命题不再是“解密哪个文件”而是“还原哪块内存”。这和很多入门教程里教的“dump内存DEX然后修复checksum”完全是两码事。那些教程默认DEX在内存里是一整块连续、有完整header的镜像用frida-dexdump这类工具扫一下dex\n035\0魔数就能捞出来。可DexProtector偏偏不按这套牌理出牌它会切割、重排、部分抹除header字段甚至让某些方法的code_item根本不在DEX镜像内而是散落在匿名mmap里由JIT或解释器直接消费。这时候你需要的是一套内存态的重组思维而不是dump思维。我们要做的是把ArtMethod的运行时结构当成“索引”把散落在各处的code_item、dex_cache、class_definitions当作“零件”重新拼出一份逻辑上自洽、可被静态工具解析的DEX。能做到这一步才算真正意义上绕过了DexProtector的内存防护。这篇文章我就把自己在实际对抗中摸出来的几条路径摊开讲——怎么定位匿名段、怎么绕过检测、怎么重组DEX以及每一步里那些文档不会告诉你的坑。适合已经懂frida基础、想往深水区走的逆向工程师。如果你只是拿壳练手玩静态分析可以收藏了以后再看。2. 匿名段检测机制拆解它到底在防谁又是怎么发现的想绕检测必须先搞清楚对方在查什么。DexProtector的匿名段检测不是单一手段而是一套组合拳每一下都打在“你的内存和正常加固DEX不一样”这个事实上。2.1 匿名段的本质一份不存在于文件系统的“幽灵DEX”先明确什么叫匿名段。Linux下用mmap创建映射时可以传一个MAP_ANONYMOUS标志表示这块内存不关联任何文件。DexProtector会把部分DEX数据拷贝进这类匿名映射中然后通过修改ArtMethod的字段让执行引擎跳过去取指令。检测方最直接的抓手就是查/proc/self/maps。如果你在frida脚本里枚举内存范围会发现某些区域标记为rw-p或r-xp但路径一栏是空的——这就是匿名段。正常加载的DEX无论是oat还是vdmfvdex文件maps里都能看到对应的文件路径。一个连文件路径都没有的“可执行代码区”搁谁看都可疑。但匿名段本身不等于恶意系统库、JIT code cache也常常使用匿名映射。所以DexProtector的检测不会傻到看见匿名段就崩它会叠加第二层判断这段内存里是否出现DEX结构特征。2.2 检测触发点从“看地图”到“查户口”DexProtector的检测逻辑大致可以拆成四步每一步都是递进关系动态库与系统API完整性校验检测进程是否被注入so、是否hook了art::DexFile::Open、mmap、mprotect等关键入口。这层属于常规操作任何加固都会做用来拦掉最基础的dump脚本。匿名段的可写可执行属性检查正常JIT代码段虽然也是匿名映射但大多数情况下W^X写与执行互斥是开启的。DexProtector会因为自身需要把某些匿名段属性设成rwx于是它就反向利用这个特征来检测那些为了注入而修改内存属性的外部模块。内存特征扫描在关键执行点比如ArtMethod::Invoke、ClassLinker::LoadClass扫描当前线程栈和堆搜DEX魔数64 65 78 0adex\n、类描述符字符串如Lcom/xx/开头的ASCII串一旦发现就大概率判定有人在dump或hook。完整性自我校验DexProtector会把关键的header字段如checksum、signature、file_size做加密存储运行期动态校验。如果你修改了内存中的DEX数据但没同步修复校验和它会在某个随机时机直接abort进程。这四步组合起来让很多粗放的frida脚本死得很惨要么一启动就被Anti-debug拦掉要么刚dump到一半就收到SIGABRT。我的经验是对抗这套检测要从“避免触发”转向“接受触发后的恢复”——不会有人能做到100%不被发现但你可以让它的检测结果变得不可信或者让崩溃点变得可控。这就要聊到DEX重组了。3. 内存态绕过实战定位、摘除校验、重组三阶推进下面进入正题。这节我按实际操作的顺序来组织先定位匿名段拿到完整的“零件清单”再处理运行期校验让重组后的DEX能落地最后讲如何把碎片拼回一个结构合法的DEX文件。3.1 定位匿名段与碎片DEXfrida脚本的三个阶段很多教程会教你用Process.enumerateRanges(r--)然后扫魔数。这条路在DexProtector面前经常空手而归它的DEX镜像头部被裁剪掉了或者魔数被打散。我的实践中定位匿名段更适合分三步走第一步枚举ArtMethod筛选出dex_pc落在匿名段的样本。在ArtMethod的结构里dex_pc_字段指向方法字节码在DEX code_item中的偏移。通过Java.vm.getEnv()拿不到原始结构得从art::ArtMethod的硬编码偏移读。以arm64为例ArtMethod对象里offset 0x00: declaring_class_ (GCRoot, 8字节) offset 0x08: access_flags_ (u4) offset 0x0c: dex_code_item_offset_ (u4) offset 0x10: dex_method_index_ (u4) offset 0x14: method_index_ (u2) offset 0x16: hotness_ (u2) offset 0x18: imt_index_ (u4) offset 0x1c: ptr_sized_fields_ (8字节含data_或entry_point_from_jni_)entry_point_from_jni_和entry_point_from_quick_compiled_在绝大多数情况下指向art_quick_generic_jni_trampoline或art_quick_invoke_stub这种跳板而非直接指向可执行代码。真正有意义的是dex_code_item_offset_它记录了方法在DEX文件中对应code_item的起始位置。当它在标准DEX范围内时你可以顺着文件解析一旦它指向了不是由DexFile管理的内存地址就说明这个方法被特殊处理了。第二步比对DexFile的地址空间找出越界的code_item。拿到方法的dex_code_item_offset_后结合ClassLinker中的DexFile对象地址可以算出当前DEX镜像的边界。如果code_item_offset明显落在镜像之外——比如DexFile镜像范围是0x7000xxxx-0x7010xxxx但某个方法的偏移是0x7058f2a0——那这个方法的真实字节码大概率被挪到了匿名段offset只是某种映射值或标志位。这一步的产出是一张清单哪些方法在标准DEX内、哪些方法指向匿名段、匿名段的基址是多少。这张清单就是后续DEX重组的“零件目录”。第三步对指向匿名段的方法做内容嗅探。匿名段虽然是匿名映射但内容并非完全没有规律。DexProtector为了保证Art解释器能解析字节码在匿名段里保留的仍然是一份结构上可被Art识别的code_item序列——它只是把这段数据从DexFile镜像里拆了出去未必做了二次加密。所以你可以直接用上一步的地址在内存里把code_item的完整数据读出const codeItemBuf Memory.readByteArray(anonAddr, codeItemSize);读取前需要先解析前4个字节insns_size_in_code_units_然后反推出整个code_item的长度code_item结构: u2 registers_size_ u2 ins_size_ u2 outs_size_ u2 tries_size_ u4 debug_info_off_ u4 insns_size_in_code_units_ u2 insns[insns_size_in_code_units_]实际读取时注意对齐和大小端arm64下全小端倒没什么大坑。真正的问题是debug_info_off和tries_size在匿名段里可能被置为0或随机值因为DexProtector觉得反正没人会解析运行时内存里的debug信息。重组阶段如果直接照搬这些字段静态分析器就会因为长度不匹配而解析失败。3.2 运行期校验摘除让修改后的内存不再被“追杀”DexProtector的完整性校验分两种时机一种是懒校验在特定方法第一次被调用时才检查另一种是随机定时校验由独立线程周期性触发。绕过思路也分两种我倾向于“物理删除校验源”而不是“欺骗校验逻辑”。校验源是什么是DexProtector自己注入到进程里的so逻辑它通过art::Runtime、art::DexFile等C对象来访问DEX数据。只要DEX数据还在可读内存里它就能对比header的checksum和实际内容的哈希值。所以我们要做的是让“被修改后的DEX”也通过校验——最直接的办法是修改内存中的DEX头字段让它对被修改后的内容做自我声明。具体操作上我通常在frida里做三件事Patch checksum用Memory.patchCode把DEX header的checksum字段改为0或者改成用Adler32重新计算过的值。DexProtector如果检查的是文件级别checksum这一步就让它误以为内容没变如果它校验的是内部签名表则需要进一步patch。Patch signature字段DEX header的signatureSHA-120字节是另一个校验点。部分版本会运行期重新计算DEX的SHA-1并与header比对。先置0再观察是否触发崩溃如果仍崩溃说明它是独立存储的校验引用需要去DexProtector的so里找到校验函数的总控逻辑。Hook校验函数定位sha1或adler32相关函数在它读取DEX数据时返回预先算好的“原装”值。这一步不改变内存内容只改变校验函数看到的输入。难点在于DexProtector会内联或使用自定义实现不一定走系统库。有朋友可能会问是不是只要把checksum改成0就行实测下来不完全。因为DexProtector对DEX的校验不是只看header的self-checksum它会用独立的dex_file_verifier重新解析你的DEX。如果你改过的内容破坏了DEX结构Verifier抛错同样会杀掉进程。这就引出核心观点**绕过校验最好的方式不是让校验函数“觉得对”而是让被校验的内存“真的对”。**所以第三步的本质是配合第3.3节的DEX重组让最终留在内存里的DEX在结构上自洽校验自然通过不需要在对抗校验逻辑上花太多精力。3.3 重组DEX把碎片拼回可解析的结构体重组的思路其实很朴素用ArtMethod下标当“目录页”用code_item当“正文”用class_defs当“索引”把整份DEX重建出来。DEX文件格式里header之后依次是string_ids、type_ids、proto_ids、field_ids、method_ids、class_defs、data。DexProtector静态层已把这些表加密内存中恢复后它们是完整可读的。但匿名段破坏了“data区内偏移”与“实体数据”的一致性——code_item的实地址跑到了DexFile镜像外。重组分四步**第一步从运行时重建标准DEX骨架。**找到art::DexFile对象把begin()和end()之间的内存完整读出来。大多数情况下这段内存里的header、string_ids、type_ids等表是完整的DexProtector不会连这些基础表都打散否则ART无法加载类。这一步会得到一份“缺了部分code_item数据的半成品DEX”。**第二步从ArtMethod列表提取匿名段code_item并回填。**遍历所有已加载的ArtMethod对每个方法用dex_code_item_offset_去判断它是否指向DexFile镜像内。如果指向镜像外则是匿名段数据读取后把它按偏移对应关系写回到半成品DEX的对应位置。这里有个关键细节匿名段中code_item的偏移值和DexFile镜像中原本的偏移不一定一致。DexProtector在移动code_item后会修改ArtMethod的dex_code_item_offset_来指向新的匿名段位置。所以重组时必须以ArtMethod的当前值为准来反推它对应DEX文件中的期望偏移而不是用匿名段里的数据本身。**第三步修复data_off、data_size以及各类表的条目数量。**运行时DexFile的header中data_off和data_size字段通常没被修改因为ART依赖它们定位data区但class_defs里的class_data_off、code_off字段可能会被DexProtector替换为随机值或0。这些字段需要通过ArtMethod的declaring_class_和name_反向匹配回class_def表逐个补全。**第四步重算header的checksum和signature。**这一步在上文3.2已经提过放到重组里再说一遍是为了一次讲完整套流程。注意如果你要的是“在内存中跑通”而不是“落盘后能被jadx打开”那checksum可以随便填但如果你想要一份可以保存到磁盘、后续离线分析的DEX就必须用标准算法重算。重组完成后再用frida-dexdump扫描整个内存能看到一个完整且魔数匹配的DEX镜像——但这时的镜像已经不是简单dump出来的原始内存而是你通过运行时信息“缝合”出来的逻辑DEX。4. 重组链路中的三个致命坑对齐、引用、Verifier重组思路说清楚只用了四步实操时每一步都埋着能把人整崩溃的暗坑。这里把最常见的三个单独拎出来讲每个都是我或我身边朋友实际踩过的。4.1 偏移对齐DexProtector并不保证code_item的4字节对齐标准DEX文件里code_item要求4字节对齐debug_info、annotations都依赖这个对齐才能正确遍历。DexProtector把code_item挪进匿名段时很可能直接把数据平铺存放只保证当前CPU能访存不做对齐处理。这在运行时完全没问题因为ARM64支持非对齐访问性能有损耗但不报错。可一旦你把这段数据回填到重组DEX静态解析器如baksmali会严格按对齐校验直接拒绝解析。判断方法很笨但有效把匿名段里读出的code_item地址对4取模如果余数非0你就要么在回填时做一次memmove对齐要么主动修改DEX的data_off把整个data区重建一遍。我倾向于后者因为只要静态工具能打开后期修偏移的机会多对齐不修连开都开不了。4.2 引用完整性字符串索引、类型索引的重映射匿名段里的字节码其中的指令操作数可能引用string_ids、type_ids、method_ids等索引。DexProtector在做静态加固时通常会重排这些表——把某些id的条目藏起来让自动dump出来的DEX即使能解析索引也全部错乱。举个真实例子一个方法里明明应该调用Landroid/util/Log;-e(...)但解析出来后索引却指向了Ljava/lang/Object;-hashCode()。这种错位在运行时不会暴露因为ArtMethod的dex_method_index_已经被DexProtector修正过可静态分析时你看到的就是“驴唇不对马嘴”。绕坑的方法只有一个**不要直接信任命令行dexdump的输出而是对照ArtMethod的运行时索引与DexFile镜像中method_ids条目做一次diff。**具体操作是用frida遍历全部ArtMethod读取每个方法的dex_method_index_再在内存DEX的method_ids表里找对应条目确认索引是否一致。偏差超过一定阈值就需要建立一份“运行时索引→DEX文件索引”的映射表在重组阶段做重映射。这一步工作量大但没有捷径。DexProtector的重排逻辑每次版本都可能变写死映射规则没有任何复用价值。我的做法是写一个半自动脚本先dump所有ArtMethod与索引的对应关系导出CSV再和静态分析结果比对手工修正差异较大的类。4.3 DexFileVerifier重组成果的“审稿人”即便上面两步都做对了还有最后一关——ART的DexFileVerifier。这是Dalvik时代就存在的DEX合法性校验器它会重新解析整个文件检查header里的file_size是否和实际解析结果一致每个class_def的class_data_off是否指向合法的class_data_item每个code_off是否指向合法的code_item字符串、类型、方法、字段索引是否在表范围内DexProtector为了让自己的匿名段方案不触发Verifier通常在加载完DEX后会绕过或篡改Verifier结果。但我们在内存里重组出的这份新DEX在下次系统触发Verifier时就会以“外来户”的身份接受审查。一旦某个code_off指向了已修复的位置但内容仍未对齐Verifier会返回一个致命错误进程直接崩溃。所以我在每次重组完成后会强制在frida里调一次art::DexFileVerifier::Verify通过art的JNI或直接构造std::string错误缓冲区拿到校验结果再决定是否放行const verifier Module.findExportByName(libart.so, _ZN3art15DexFileVerifierC2EPKNS_6DexFileEPKc); // 使用frida构造调用或直接调用Java层的dalvik.system.DexFile的静态方法做试加载如果没有现成的导出符号可以用更粗暴但有效的验证方式把重组后的DEX写入文件通过dalvik.system.PathClassLoader加载一次成功加载则不崩失败则根据异常信息定位到具体偏移。这一步网上少有教程提到但它恰恰决定了你的重组产物是“看起来像样”还是“真正能用”。建议每一位做DexProtector对抗的朋友都把Verifier当成金标准别只信自己的解析结果。5. 自动化重组脚本设计从手工frida到一键复原等跑通一次手工流程后你一定会想着自动化原因很现实DexProtector版本更新频繁今天刚摸清内存布局明天它变了手工再走一遍太费时间。我的方案是做成一个frida脚本加Python后处理的组合整个链路分成采集、离线重组、验证三段。5.1 采集端frida脚本导出运行时全量信息采集端需要从目标进程导出以下几类数据所有已加载DexFile的基址、大小遍历Runtime::GetRuntime()-GetClassLinker()-GetBootClassPath()以及OpenDexFilesFromOat里的DexFile实例每个DexFile对应的class_defs数量、method_ids数量等表头信息所有ArtMethod的关键字段dex_code_item_offset_、declaring_class_、dex_method_index_、access_flags_所有匿名段的内存数据按地址偏移读取存为独立bin文件这一步的frida脚本核心代码如下function dumpDexFileInfos() { // 遍历 DexFile 的容器 // 通过 libart.so 导出符号拿到 Runtime 单例 const artRuntime Module.findExportByName(libart.so, _ZN3art7Runtime9instance_E); // 读取 Runtime 对象中的 class_linker_再遍历 dex_files_ // 实际偏移视 Android 版本而定需要配合本地调试确定结构体布局 }注意这里的结构体偏移在不同安卓版本、不同DexProtector版本下都不一样。所以采集端不宜硬编码太多偏移推荐先用Instance工具或jnitrace摸清目标进程的结构体布局再写死到脚本里。自动化不是一步到位的先手工探测后固化脚本。5.2 离线重组端Python处理原始内存产出DEX拿到信息后Python端做重组合逻辑def rebuild_dex(header_bin, code_items, art_method_info): # 1. 解析header复制原有DexFile镜像 # 2. 把 code_items 按 art_method_info 中的 dex_code_item_offset_ 映射回 data 区 # 3. 依据 art_method_info 重建 class_defs 中的 code_off # 4. 重算 checksum 与 sha1 # 5. 输出最终DEX代码逻辑不复杂真正麻烦的是字节级别的偏移计算。我的建议是先用frida-tools的dexdump做一遍自动解析把报错信息汇总成列表然后逐项修复。Python脚本第一次跑出来的DEX十有八九是残废的别灰心有报错才说明结构问题暴露了出来。5.3 验证端模拟ART加载自动判定重组是否成功验证方式和手工阶段一样还是靠Verifier。但自动化后可以做得更闭环每次重组完DEX立刻启动一个最低限度的ART环境比如用Android模拟器里的app_process跑一个只做加载的测试APK把DEX作为额外DexFile塞进去能加载则通过报错则输出错误偏移。这一步我推荐用QEMU用户态加Android系统镜像来做成本低且不污染真实手机环境。实测一个重组DEX的验证时间大约在5秒左右比在真机上启动完整App快得多。6. 对抗升级的纵深思考DexProtector下一代会怎么防最后聊点防御方视角的猜测这能帮你看清自己当前绕过的易碎程度。DexProtector目前的设计思路还是“文件不落地内存有碎片”凡是围绕“跑起来后恢复原貌”的攻击手法它都在防。但它有个结构性弱点只要它还在用ART解释器或JIT就必须让ArtMethod结构保持正常语义——解释器要能读到dex_pcJIT才能生成代码。所以它再怎么折腾ArtMethod里总有一层可被解析的“中间表示”。这就是我们上面所有手法的根基。下一代它有几种可能升级方向VMP式指令翻译把字节码转成一堆自定义opcode不再依赖ART解释器。到那一步ArtMethod里指向code_item的offset可能只是诱饵真正的执行逻辑在VMP解释器内部。届时再谈DEX重组就意义不大了得转向“虚拟指令集还原”那是另一个量级的工作。按页级加密按需解密只在方法即将执行前解密出几条指令执行完立即抹除。这会让我们上面“匿名段数据可读”的假设失效届时需要改成hook解释器入口在读取字节码的瞬间抓取明文。硬件辅助的可信执行环境隔离直接把代码迁移到TEE里用户态完全不可见。这个一旦落地单纯靠frida逆向就没戏了得上内核级或硬件级方案。说这些不是劝退而是想表达一个观点**DexProtector对抗的本质是和ART执行模型赛跑。**只要ART还在用DexFile格式管理类的元数据和方法字节码就一定存在可以从运行时反射回文件的窗口。所谓的“绕过”无非是在这个窗口里做文章。我在实际项目里最深的体会是与其满世界找“通杀脚本”不如吃透ART源码里每一个结构体的语义。DexProtector再强的壳最后还是得build在ArtMethod、DexFile、ClassLinker这些基础组件上。理解了这块地基换任何壳你都能快速找到自己的突破口。如果你正在折腾DexProtector建议先别急着写frida脚本花两天时间把art/runtime/dex_file.h和art/runtime/art_method.h过一遍再动手效率会高很多。我当初就是从“照着网上的dump脚本跑一天一无所获”变成“看完源码后半小时定位问题”的。这条路没有捷径但很值得走。
返回列表