1. 项目概述为什么选择Frida与DexDump进行脱壳在移动安全研究领域对加固后的安卓应用进行脱壳分析是逆向工程师的“基本功”也是理解应用内部逻辑、发现潜在安全问题的关键一步。市面上加固方案众多像360加固、腾讯乐固、梆梆加固等它们通过加密、混淆、虚拟机保护等技术将应用的原始Dex文件隐藏起来给静态分析带来了巨大障碍。这时候动态脱壳技术就成了破局的关键。我选择Frida配合DexDump脚本这套组合拳原因很直接它足够灵活、强大且“接地气”。Frida作为一个动态插桩框架允许我们在目标应用运行时注入JavaScript代码从而能够Hook到应用加载、解密Dex的关键函数。而DexDump脚本则是一个专门为Frida设计的“内存猎人”它的任务就是在Dex文件被解密并加载到内存的瞬间将其从内存中完整地“捞”出来保存为标准的.dex文件。这套方法不依赖于特定的加固版本只要应用最终要在ART/Dalvik虚拟机里运行就绕不开内存加载这一步理论上就存在被Dump的可能。相比于一些需要逆向脱壳机或依赖特定系统版本的“黑盒”工具FridaDexDump给了我们完全的控制权和透明度你能清楚地知道每一步在做什么遇到了什么问题以及如何去解决。这对于学习加固与脱壳原理以及后续的深度分析是无可替代的。2. 环境搭建与前期准备工欲善其事必先利其器。在开始实战之前一个稳定、完备的环境是成功的一半。这里我以分析一个典型的银行类APP为例带你走一遍完整的配置流程。银行APP通常采用高强度的商业加固是检验我们技术的好对象。2.1 核心工具链部署我们的工具链主要包含三部分运行在电脑上的Frida客户端、运行在手机上的Frida服务端以及我们的核心武器——DexDump脚本。1. 电脑端Frida环境安装在你的分析电脑Windows/macOS/Linux上打开命令行通过pip安装Frida-tools即可。这是最推荐的方式能确保版本匹配。pip install frida-tools安装完成后使用frida --version验证。同时建议安装frida的Python绑定方便后续编写自定义脚本pip install frida2. 手机端Frida-server部署这是最容易出错的环节。首先用adb shell getprop ro.product.cpu.abi命令查看你手机或模拟器的CPU架构通常是arm64-v8a。然后去Frida的GitHub Releases页面下载对应版本的frida-server-xx.x.x-android-xxx.xz文件。 下载后解压得到frida-server可执行文件。通过adb将其推送到手机的/data/local/tmp/目录并赋予执行权限adb push frida-server /data/local/tmp/ adb shell cd /data/local/tmp chmod 755 frida-server关键一步在手机端以root权限启动server。很多教程说用su -c但在部分系统上可能不生效。最稳妥的方式是先进入adb shell获取root权限再直接运行adb shell su cd /data/local/tmp ./frida-server 注意后面的符号让服务在后台运行。验证是否成功可以在电脑端执行frida-ps -U如果能看到手机上的进程列表说明连接成功。3. DexDump脚本获取与理解DexDump脚本有很多变种原理大同小异。我推荐使用一个功能比较完善的版本例如来自r0ysue或hanbinglengyue等安全研究员优化过的脚本。你可以从GitHub上搜索“DexDump”找到它们。核心原理是Hookdalvik.system.DexFile或libart.so中加载Dex的相关函数如OpenMemory在函数执行时将其内存地址和大小参数记录下来并将这块内存数据写入文件。注意不同Android版本、不同加固方案Hook的点可能不同。对于高版本Android尤其是Android 8.0以上和新型加固可能需要Hookart::DexFile::OpenMemory或dex2oat相关的底层函数。准备多个不同策略的脚本是明智之举。2.2 目标APP与测试设备选择目标APP选择一款你感兴趣的、经过加固的银行APP。可以从各大应用市场下载。请务必在法律允许和授权范围内进行测试建议使用自己开发或明确获得授权的应用进行练习。测试设备强烈推荐使用Root过的真实安卓手机或功能完整的模拟器如夜神、雷电并开启Root权限。分析银行类APP其反调试、环境检测机制通常非常严格。真机Root提供最真实的环境但某些银行APP会检测Root状态导致闪退。可能需要使用Magisk Hide等模块进行隐藏。模拟器环境可控快照功能便于反复测试。但许多加固和银行APP会检测模拟器特征。需要修改模拟器的设备指纹如build.prop来绕过检测。在开始前先将目标APP安装到测试设备上但不要急于启动。3. 脱壳实战一步步捕获内存中的Dex环境就绪目标锁定现在让我们进入核心的脱壳操作环节。这个过程就像一场耐心的狩猎我们需要在合适的时机布下陷阱。3.1 启动Frida并附着目标进程首先我们需要让目标APP运行起来并用Frida附着它。这里有两种常用方式方式一启动时注入Spawn这种方式在APP启动的早期就进行注入有机会捕获到最早期的类加载器初始化和Dex加载过程对于某些在启动时就完成所有解密加载的加固方案非常有效。frida -U -f com.example.bankapp --no-pause -l dexdump.js-U: 连接到USB设备。-f com.example.bankapp: 指定包名以Spawn方式启动应用。--no-pause: 启动后立即继续运行不中断。-l dexdump.js: 加载我们的DexDump脚本。执行后你会看到Frida连接成功并且APP开始启动。此时脚本已经开始工作如果Hook点正确控制台会开始输出捕获到的Dex信息。方式二附着已运行进程Attach如果APP已经启动或者你想在应用进入某个特定界面后再开始脱壳可以使用Attach模式。# 首先打开APP进入主界面 frida-ps -U | grep bank # 找到目标进程的PID frida -U -p PID -l dexdump.js或者直接用包名附着frida -U com.example.bankapp -l dexdump.js实操心得对于银行APP我通常先尝试Spawn方式。如果APP有强烈的反调试导致崩溃我会先尝试一些反反调试的Frida脚本例如禁用ptrace检测、隐藏Frida特征等或者改用Attach方式在APP完全启动并完成部分环境检测后再进行附着。3.2 DexDump脚本核心逻辑与Hook点解析一个典型的DexDump脚本其核心在于以下几个Hook点。理解它们你就能在脚本不工作时自己进行调试。1. Hookjava.lang.ClassLoader这是最上层的Hook点。目标是dalvik.system.DexFile或libcore.io.DexFile的loadDex或openDex方法。当Java层通过PathClassLoader或DexClassLoader加载Dex时会调用到这里。Java.perform(function() { var dexFile Java.use(“dalvik.system.DexFile”); dexFile.loadDex.implementation function(srcPath, outputPath, flags) { console.log(“[Java] loadDex called: “ srcPath); var result this.loadDex(srcPath, outputPath, flags); // 可以在这里尝试获取Dex数据但此时数据可能还未完全解密或不在理想的内存区域 return result; }; });这个点能告诉我们Dex从哪里加载但对于加固APPsrcPath可能是一个临时解密文件且此时Dex数据可能已被处理。2. Hooklibart.so中的OpenMemory(主流方法)这是更底层、更有效的Hook点。ART虚拟机通过OpenMemory函数将内存中的Dex数据映射为DexFile对象。我们需要在Native层进行Hook。var module_libart Process.findModuleByName(“libart.so”); var symbols module_libart.enumerateSymbols(); var openMemoryAddr null; for (var i 0; i symbols.length; i) { var symbol symbols[i]; if (symbol.name.indexOf(“OpenMemory”) ! -1 symbol.name.indexOf(“DexFile”) ! -1) { openMemoryAddr symbol.address; console.log(“Found OpenMemory at: “ openMemoryAddr); break; } } if (openMemoryAddr) { Interceptor.attach(openMemoryAddr, { onEnter: function(args) { // args[0] 可能是 dex起始地址 args[1] 可能是大小 this.dexBase args[0]; this.dexSize args[1]; console.log([Native] OpenMemory called. Base: ${this.dexBase}, Size: ${this.dexSize}); }, onLeave: function(retval) { // 有时在onLeave时数据才准备就绪 if (this.dexBase this.dexSize 0) { dumpDex(this.dexBase, this.dexSize); } } }); }dumpDex函数负责将指定内存区域的数据读出来并写入文件。它会检查Dex文件的魔数dex\\n或dey\\n来确认数据的有效性。3. 应对多ClassLoader与多次加载一个复杂的APP尤其是用了插件化或动态加载的可能会有多个ClassLoader实例Dex也会被多次加载。我们的脚本需要记录已经Dump过的Dex基址避免重复输出。同时要注意Dex可能在内存中被优化为OAT或VDEX格式脚本需要能识别并做相应处理比如尝试从OAT中提取Dex。3.3 执行脱壳与文件验证当脚本成功附着并运行后你需要在APP界面上进行尽可能多的操作点击登录、切换Tab、浏览功能页面等。目的是触发APP加载所有功能模块对应的Dex文件。脚本会在控制台输出类似以下信息[Native] OpenMemory called. Base: 0x7a3f5c2000, Size: 456780 [Dump] Dex detected! Saved to /data/data/com.example.bankapp/1.dex [Native] OpenMemory called. Base: 0x7a3f5d0000, Size: 102400 [Dump] Dex detected! Saved to /data/data/com.example.bankapp/2.dex ...同时Dex文件会被保存到手机存储的指定目录通常是APP的数据目录/data/data/package_name/下。文件验证使用adb pull将Dump下来的.dex文件拉到电脑上。使用file命令检查文件类型file 1.dex应该显示Dalvik dex file version 035或类似信息。使用反编译工具进行验证使用jadx-gui、GDA或bytecode-viewer直接打开拉取的Dex文件。如果能成功打开看到大量的Java类尤其是com.example.bankapp包下的类并且代码不是完全的混乱或加密状态说明脱壳基本成功。如果打开失败或只有零星几个类可能只Dump到了壳程序本身的Dex需要调整Hook点或策略。4. 深度避坑与对抗指南实战从来不会一帆风顺尤其是面对防护严密的银行APP。下面是我在无数次“翻车”中总结出的常见问题与对抗技巧。4.1 常见问题速查与解决方案问题现象可能原因排查步骤与解决方案Frida连接被拒绝或超时1.frida-server未运行或未以root运行。2. 手机USB调试未开启。3. 电脑ADB版本与手机不兼容。4. 端口冲突。1.adb shell进入ps | grep frida检查进程用su权限重新启动。2. 确认开发者选项和USB调试已开。3. 更新platform-tools。4. 重启adb server:adb kill-server adb start-server。注入后APP立即闪退1. APP检测到Frida。2. Hook点错误导致崩溃。3. 脚本存在语法错误或死循环。1.反反调试使用-fspawn模式配合--no-pause有时能绕过。使用专门隐藏Frida的脚本如修改frida-gum特征。2.精准Hook确认Hook的函数签名完全正确特别是Android版本差异。使用Module.enumerateSymbols()仔细查找。3.脚本调试先注入一个最简单的console.log脚本测试再逐步增加功能。控制台有Hook日志但无Dex输出或输出大小异常1. Hook时机不对Dex未解密。2. 获取的内存地址/大小参数不正确。3. Dex数据在内存中被分段或混淆。1.尝试不同Hook点从loadDex到OpenMemory再到dex2oat的编译过程。2.打印并验证参数在onEnter和onLeave都打印地址和大小对比变化。可能需要在onLeave时Dump。3.内存扫描如果参数不对可以尝试在函数调用前后对相关内存区域进行扫描搜索Dex魔数(64 65 78 0a)。Dump出的Dex文件无法反编译1. Dump的数据不完整或错误。2. Dex文件被抽取式加固函数体为空。3. 反编译工具不支持该Dex版本。1. 用010 Editor等二进制工具打开检查文件头是否完整。2.对抗抽取需要在函数被执行即代码被动态解密填充到内存的瞬间进行Dump。这需要Hookart::ArtMethod::Invoke等执行引擎函数技术难度更高。3. 尝试更新jadx到最新版。只能Dump出壳的Dex没有业务代码加固将业务Dex隐藏得很深可能通过自定义ClassLoader或直接在Native层加载。1.追踪ClassLoaderHook所有ClassLoader的子类构造函数和loadClass方法分析其父子关系。2.Native层监控监控dlopen、dlsym等函数看是否有动态加载的so库负责解密和加载Dex。3.内存暴力搜索在APP运行到主界面后对整个进程内存空间进行扫描搜索Dex魔数。4.2 高级对抗技巧1. 对抗Frida检测银行APP常用的检测手段包括检测frida-server的默认端口27042、检测/proc/self/maps和/proc/self/task/pid/fd中的frida特征字符串、检测进程名等。端口检测启动frida-server时使用非默认端口./frida-server -l 0.0.0.0:8080连接时指定-H 手机IP:8080。特征隐藏使用修改版的frida-gum库或者使用Frida的Interceptor在内存中实时抹去相关字符串特征。双进程保护有些壳会fork子进程在子进程中做敏感操作。需要附着到正确的子进程上或者使用frida的Child gating功能。2. 对抗反调试PTRACE_TRACEME检测这是经典手段。可以在libc的ptrace函数上做Hook让其直接返回0表示成功。TracerPid检测APP会读取/proc/self/status中的TracerPid字段。可以通过Hook文件读取相关函数如fopen,read在内存中返回修改后的内容。时间差检测在关键代码前后计算时间差如果过长则认为被调试。可以通过Hookgettimeofday、clock_gettime等函数来“加速”时间。3. 应对新型虚拟机保护VMP部分高级加固会将关键Java方法翻译成自定义的指令集在私有虚拟机中执行。对于这种保护内存Dump可能无效因为原始字节码可能根本不存在于内存中。思路转变重点从“获取代码”转向“理解逻辑”和“获取数据”。可以Hook虚拟机解释器的调度函数记录输入输出或者直接Hook这些受保护方法前后的Java API如网络请求、数据库操作、UI更新进行高层逻辑分析。5. 脱壳后的分析与修复成功Dump出Dex文件只是第一步如何让这些“生肉”变成可读、可分析的代码还需要后续处理。5.1 使用反编译工具分析Dex将Dex文件拖入jadx-gui是最快捷的方式。jadx会尝试将Dalvik字节码重构为Java代码。对于银行APP重点关注登录认证逻辑搜索login、auth、password、encrypt等关键词。网络通信模块查找OkHttpClient、Retrofit、HttpURLConnection相关的类分析其加密、签名参数构造过程。加解密算法查找Cipher、MessageDigest、SecretKeySpec等类的使用可能涉及AES、RSA、MD5、SHA等。核心业务接口寻找API接口URL的定义处。注意由于混淆的存在类名、方法名、字段名可能都是无意义的a、b、c。需要结合代码上下文逻辑、字符串常量jadx能很好还原以及动态调试来推测其真实含义。5.2 处理抽取加固与Dex修复如果你发现反编译出的方法体是空的或者大量方法指向同一个“壳”方法这很可能遇到了抽取式加固。Dex文件的结构是完整的但关键方法的代码体code_item被移除了只在运行时动态填充。动态Dump时机需要在方法第一次被解释执行或JIT编译时进行Dump。这需要更精细的Hook例如Hookart::interpreter::ArtMethod::Invoke或art::jit::JitCompiler::CompileMethod。有现成的工具如Youpk针对ART就是基于这个原理。Dex修复将动态Dump出来的方法体按照Dex格式写回到原始的Dex文件对应的code_item偏移处。这是一个非常精细的二进制操作需要深入理解Dex文件格式。有一些开源项目如dexfixer提供了参考但通常需要根据具体加固方案进行调整。5.3 整合多Dex与资源文件一个APP可能包含多个Dex文件classes.dex, classes2.dex, …以及资源文件resources.arsc、assets。完整的分析需要将它们整合。多Dex合并jadx-gui支持直接导入整个APK文件或包含多个Dex的目录。你也可以使用d2j-dex2jar工具将所有Dex合并成一个jar但可能会丢失一些信息。资源分析使用APKTool解包原始APK获取资源文件。将脱壳出的Dex与解包出的资源放在一起用jadx打开可以建立代码到资源ID如R.layout.xxx的引用关系使分析更直观。6. 从实战到原理理解加固与脱壳的博弈经过以上步骤你应该已经能够成功对多数加固APP进行脱壳。但作为一个研究者我们不能只停留在“能用”的层面更要理解背后的“为什么”。加固技术的演进脉络第一代 - 整体加密将整个APK或Dex文件加密运行时在内存中解密。对抗方式就是找到解密函数Hook或在内存明文时Dump。DexDump主要对付这种。第二代 - 抽取加密将关键方法的代码体抽走加密留下空壳。运行时按需解密填充。对抗方式需要Hook执行引擎在填充后Dump。第三代 - 虚拟机保护VMP将Java或Native代码转换为自定义指令集在自研虚拟机中执行。对抗难度极大静态分析几乎失效需要深入分析虚拟机解释器。第四代 - 混淆与混淆在代码中插入大量无意义指令、控制流扁平化、不透明谓词等增加人工分析的难度。对抗主要依靠动态调试和耐心。脱壳技术的核心思想无论加固技术如何变化其最终目的都是让代码在某个时刻、某个位置以CPU能够理解的指令或ART/Dalvik能够理解的字节码形态出现。脱壳技术的本质就是定位这个“时刻”和“位置”。Frida提供的动态插桩能力让我们有能力在这个“时刻”切入从“位置”上把数据拷贝出来。这就是为什么FridaDexDump的方法具有如此强的生命力和普适性。未来的挑战随着Android版本更新如Project Mainline模块化、硬件安全如TEE可信执行环境的普及以及加固方案与系统更深度的结合如定制ROM纯用户态的脱壳会越来越难。这要求安全研究者需要具备更底层的系统知识如Binder通信、内核模块、TrustZone等。但无论如何理解内存、理解进程、理解代码执行的生命周期这些基本原理永远不会过时。