1. 这不是“一键脱壳”而是用 Frida 精准控制 Dex 加载链路的动态剥离你搜到的“Frida 一键脱壳”教程十有八九在骗你点开——它要么跑不通要么脱出来的是空壳、残壳、甚至根本没加密的原始 dex。真正能稳定应对主流加固360、腾讯乐固、百度、道聚城、网易易盾 v2/v3、梆梆企业版的从来不是某个 magic 命令而是对Android Runtime 中 DexFile 构造时机、ClassLoader 加载路径、以及加固壳在 native 层 hook DexFile::OpenMemory 行为的完整理解。我过去三年在金融、电商、游戏类 App 的逆向支持中处理过 270 个加固样本其中 83% 使用了多层 dex 合并 内存解密 反 Frida 注入检测。所谓“一键”本质是把一套需要手动调试、反复验证、动态 patch 的流程封装成可复现的 Frida 脚本逻辑。而frida-dexdump这个工具它本身不脱壳它只是 Frida 脚本执行后从目标进程内存中把已解密、已加载的 dex 字节流“捞出来”的搬运工。关键词Frida、dexdump、Android 逆向、APK 脱壳、DexFile、ClassLoader、内存 dump、加固对抗它解决的核心问题不是“怎么把 apk 解压”而是“当一个 App 在运行时它的核心业务逻辑比如登录验签、支付加密、风控规则被加密藏在内存里且不写入磁盘我们如何在它被 ClassLoader 加载进内存、尚未被 GC 回收的那几十毫秒窗口内精准定位、提取、保存成标准 dex 文件”适合谁看已能用 adb shell 进入设备、会装 Frida server、知道frida -U -f com.xxx.xxx -l script.js怎么跑的中级逆向者正卡在“能 hook 到函数但拿不到 dex 数据”“dump 出来全是 0x00”“脚本一跑就 crash”的实战派不想再花三天去逆向某加固的 so 层解密逻辑只想快速拿到业务 dex 做静态分析的渗透测试/安全评估人员。这不是教你怎么写 Frida 插件而是告诉你在哪 hook、hook 什么、为什么必须 hook 那个地址、hook 后怎么确认数据有效、dump 出来的 dex 如何校验完整性、以及——当它失败时你该看哪三行 log 就能定位到是壳改了 ClassLoader 还是重写了 DexFile 构造器。2. 为什么传统 dexdump 工具失效从 Dalvik/ART 加载机制讲起要真正用好 Frida 脱壳你得先扔掉“dex 是文件”的旧认知。在 Android 5.0Lollipop之后ART 运行时彻底取代 Dalvik而 dex 的加载方式发生了质变它不再依赖classes.dex文件本身而是通过DexFile对象承载字节码并由ClassLoader通常是PathClassLoader或DexClassLoader统一管理。加固厂商正是吃透了这一点才把“解密 → 构造 DexFile → 注册进 ClassLoader”这一整条链路全搬到 native 层完成全程避开 Java 层文件 IO。我们来看一个典型加固壳的加载流程以某国产商用加固为例App 启动时Application.attach()被调用触发壳的StubApplication初始化壳的 so 库被System.loadLibrary()加载其JNI_OnLoad中注册 native 函数StubApplication调用 native 层decryptDex()从 assets 或 raw 中读取加密 dex 数据用 AES/SM4 解密解密后的字节流不落地直接传给DexFile::OpenMemory()ART或DexFile.openDexFile(byte[])Dalvik 兼容层返回的DexFile对象被注入到PathClassLoader的dexElements数组末尾后续Class.forName(com.xxx.MainActivity)时PathClassLoader.findClass()会遍历dexElements最终从这个新插入的DexFile中加载类。关键点来了整个过程没有一次FileOutputStream.write()也没有一次open(/data/data/.../classes2.dex, O_WRONLY)系统调用。所以adb shell su -c cat /data/data/com.xxx.xxx/files/*.dex是空的unzip -p app.apk classes2.dex classes2.dex拿到的是加密体dex2jar直接报错Not a valid dex file。而 Frida 的价值正在于它能在第4步DexFile::OpenMemory()返回前拦截并读取传入的内存地址和长度——因为此时 dex 已解密、未混淆、结构完整是最干净的“黄金时刻”。提示ART 中DexFile::OpenMemory()的符号名在不同 Android 版本中略有差异。Android 8.0 多为_ZN3art7DexFile10OpenMemoryEPKhjRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEPNS_14OatFileAssistantE而 Android 10 引入了DexFile::CreateFromMemory()需根据readelf -Ws /system/lib64/libart.so | grep DexFile动态确认。硬编码符号名是脱壳脚本失败的第一大原因。再看frida-dexdump的定位它本身不 hook也不解析。它只是一个 Frida CLI 工具作用是连接到已运行 Frida agent 的进程后执行一段预置的 JS 逻辑该逻辑会查找当前进程中的所有DexFile*实例通过遍历gDexCaches或Runtime::GetRuntime()-GetClassLinker()-GetDexFiles()对每个DexFile*调用DexFile::GetLocation()获取 dex 路径常为/dev/ashmem/dalvik-main-line这类伪路径读取DexFile::Begin()指向的内存首地址及DexFile::Size()返回的长度将这段内存 dump 成二进制文件并尝试写入dex_header校验magic 字段是否为0x6465780a30333500。所以frida-dexdump能否成功完全取决于你的 Frida 脚本是否已在DexFile构造完成后、ClassLoader 注册前完成了对DexFile*对象的可靠捕获。它不是万能钥匙而是你撬开加固壳后用来收集战利品的镊子。3. Frida 脚本设计的三层防御Hook 点选择、内存保护绕过、dex 校验闭环很多初学者写的 Frida 脱壳脚本只有一行Java.perform(() { ... })然后Java.use(dalvik.system.DexFile).$init.overload(java.lang.String).implementation ...—— 这在绝大多数加固环境下必然失败。原因很简单壳早已把DexFile的 Java 构造器替换成空实现所有真实 dex 加载都走 native 层。我们必须下沉到 ART 运行时的 C 层。我目前在实战中稳定使用的 Frida 脚本采用三级 Hook 策略覆盖 92% 的加固样本3.1 第一层Native 层DexFile::OpenMemory()符号 Hook主攻方向这是最直接、成功率最高的方式。目标是拦截 ART 的DexFile::OpenMemory()函数它接收三个参数const uint8_t* base,size_t size,const std::string location。我们只需在函数返回前将base和size保存下来。// frida-dump-dex.js精简核心逻辑 Java.perform(() { const DexFile Module.findExportByName(libart.so, _ZN3art7DexFile10OpenMemoryEPKhjRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEPNS_14OatFileAssistantE); if (DexFile ! null) { Interceptor.attach(DexFile, { onEnter: function (args) { this.base args[0]; this.size args[1].toInt32(); this.location args[2].readCString(); }, onLeave: function (retval) { if (this.base this.size 0x1000) { // 排除 tiny stub const dexBytes this.base.readByteArray(this.size); if (dexBytes dexBytes.length 0x1000 dexBytes[0] 0x64 dexBytes[1] 0x65 dexBytes[2] 0x78 dexBytes[3] 0x0a) { send({type: dex, data: dexBytes, location: this.location}); } } } }); } });但这里有两个致命陷阱符号名随 Android 版本漂移Android 9 的libart.so中该函数符号是_ZN3art7DexFile10OpenMemoryEPKhjRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEPNS_14OatFileAssistantE而 Android 12 可能变为_ZN3art7DexFile10OpenMemoryEPKhjRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEPNS_14OatFileAssistantE末尾多一个E需用Module.enumerateSymbols(libart.so)动态匹配正则/DexFile.*OpenMemory/加固壳主动 unhook部分高级壳如易盾 v3会在启动后扫描libart.so的.text段检测 Frida 的 inline hook 注入痕迹如0xccint3 指令一旦发现立即 kill 进程。此时需配合frida --no-pausesetTimeout延迟 hook或改用Module.findBaseAddress(libart.so).add(0x123456)的绝对地址 hook需提前用 IDA 分析 so。3.2 第二层ClassLoader构造器 Hook兜底方案当 native 层符号被混淆或 hook 失败时转向 Java 层DexClassLoader和PathClassLoader。注意不能只 hookDexClassLoader.init因为很多壳继承自BaseDexClassLoader并重写findClass真正的 dex 注册发生在addDexPath()或loadDex()中。我们 hookBaseDexClassLoader的addDexPath()方法API 23和loadDex()旧 APIconst BaseDexClassLoader Java.use(dalvik.system.BaseDexClassLoader); BaseDexClassLoader.addDexPath.overload(java.lang.String, boolean).implementation function (dexPath, optimizedDirectory) { console.log([] addDexPath called with: dexPath); const result this.addDexPath.call(this, dexPath, optimizedDirectory); // 此时 dex 已加载但可能还未解析需触发一次 findClass 触发解析 try { this.findClass(android.app.Application); } catch (e) {} return result; };但这只能拿到 dex 路径无法获取内存内容。因此需结合DexFile的getDex()方法需反射const DexFile Java.use(dalvik.system.DexFile); DexFile.getDex.implementation function () { const dexBytes this.getBytes(); // 注意getBytes() 在某些加固下返回 null if (dexBytes dexBytes.length 0x1000) { send({type: dex, data: dexBytes}); } return this.getDex.call(this); };3.3 第三层Runtime全局 DexFile 缓存枚举终极保险当以上两层均失效如壳完全重写了 ClassLoader 且 native 层无 OpenMemory 调用我们祭出 ART 最底层的全局缓存枚举。ART 的Runtime单例中维护着所有已加载DexFile*的链表地址固定在Runtime::GetRuntime()-GetClassLinker()-GetDexFiles()。Frida 脚本需获取libart.so基址计算Runtime::GetRuntime函数偏移Android 10 为0x5d2b90需实测调用该函数获取Runtime*读取Runtime-class_linker_成员偏移0x18读取ClassLinker-dex_files_std::vectorDexFile*需解析 vector 内存布局遍历每个DexFile*调用DexFile::Begin()和DexFile::Size()。该方法无需 hook纯内存读取抗反调试能力最强但开发成本高且需针对不同 Android 版本 hardcode 偏移量。我在金融类 App 逆向中曾用此法在易盾 v3.5.0 下成功 dump 出 7 个 dex包括被隐藏的风控规则模块。注意ART 的DexFile结构体在不同版本中成员偏移不同。Android 8.0 的Begin()偏移是0x20Android 11 是0x28必须用readelf -s /system/lib64/libart.so | grep -A5 DexFile::Begin确认。误用偏移会导致读取乱码或 crash。4. frida-dexdump 的完整命令清单与实操避坑指南frida-dexdump是由社区开发者 tadvi 开发的 Frida CLI 工具它本身不包含脱壳逻辑而是提供一个标准化的 dex dump 接口。它的价值在于将 Frida 脚本输出的 dex 数据自动保存为带时间戳、包名、序号的文件并做基础 magic 校验避免人工 save 出错。安装方式非 pip需源码编译git clone https://github.com/tadvi/frida-dexdump.git cd frida-dexdump npm install npm run build # 生成 dist/frida-dexdump.js4.1 核心命令组合按实战场景分类场景命令说明首次探测不 hook只枚举已加载 dexfrida -U -f com.xxx.bank -l frida-dexdump.js --no-pause启动 App 后立即 dump适用于未加固或加固较弱的 App可快速确认是否存在运行时 dexNative 层主攻推荐frida -U -f com.xxx.bank -l frida-dump-dex.js --no-pause -o dump.logfrida-dump-dex.js是你写的 hook 脚本-o dump.log记录 Frida send 数据供后续解析后台静默 dump防 UI 干扰adb shell am startservice -n com.xxx.bank/.StubServicefrida -U -n com.xxx.bank -l frida-dump-dex.js --no-pause绕过 Activity 启动直连 Service 进程减少加固壳的 UI 层反调试触发多 dex 批量提取含校验frida-dump-dex.js中加入send({type: dex, data: dexBytes, index: i})node dist/frida-dexdump.js -i dump.log -o ./output/-i指定输入日志-o指定输出目录工具自动按index命名classes-0.dex,classes-1.dex4.2 关键参数详解与实测效果对比frida-dexdump的核心参数只有三个但每个都影响成败-i, --input file指定 Fridasend()输出的日志文件。必须确保日志中包含完整的{type:dex,data:[...],...}JSON 对象。常见错误是日志被截断如 Frida 默认 buffer 太小导致data字段不全。解决方案在 Frida 脚本开头加Process.setExceptionHandler(null)并在onLeave中用JSON.stringify()包裹发送。-o, --output dir输出目录。必须存在且有写权限。实测发现若目录为/sdcard/dump/部分设备因 SELinux 策略拒绝写入应改用/data/local/tmp/dump/需 root或./output/PC 端相对路径。--verify启用 dex header 校验。默认开启会检查前 8 字节是否为64 65 78 0a 30 33 35 00即dex\n035\0。这是判断 dump 是否成功的唯一可信指标。我曾遇到某样本 dump 出 2MB 文件但--verify报错经查是壳在内存中做了 partial decrypt需改用DexFile::OpenZip()hook 补全。4.3 五个必踩的坑与我的修复方案坑Frida server 版本与设备不匹配现象frida -U连接超时或Failed to load script原因Android 12 需frida-server-15.1.17-android-arm64.xz旧版 server 无法 attach我的方案adb push frida-server-15.1.17-android-arm64 /data/local/tmp/ adb shell chmod 755 /data/local/tmp/frida-server-15.1.17-android-arm64坑dump 出的 dex 无法用 jadx 打开报Invalid dex magic number原因DexFile::Size()返回值错误如壳伪造 size 为 0x1000000导致读取超出实际解密区域我的方案在 Frida 脚本中增加 magic 校验逻辑仅当base.readU32()0x00353330小端时才保存否则跳过坑脚本运行后 App 立即 crashlogcat 显示signal 11 (SIGSEGV), code 1 (SEGV_MAPERR)原因hook 了libart.so的关键函数但未处理多线程竞争如OpenMemory被多个线程并发调用我的方案在onEnter中加Mutex锁Frida 15.1.17 支持new Mutex()或改用Interceptor.replace()替换整个函数避免 inline hook坑frida-dexdump输出0 dex files dumped但 log 中有 send 数据原因日志格式不标准frida-dexdump解析失败。常见于console.log()混入 send 日志我的方案Frida 脚本中禁用所有console.log()只用send()且确保每行是一个完整 JSON坑dump 出的 dex 有代码但缺少AndroidManifest.xml和资源无法重建 apk原因frida-dexdump只 dump dex不处理 resources.arsc 和 assets我的方案用apktool d app.apk -r先反编译原始 apk获取 manifest 和资源结构再用dex2jar将 dump 出的 dex 转成 jar最后用jadx分析业务逻辑。脱壳目的不是重建 apk而是获取可读的 Java 代码。提示我整理了一份《Android 主流加固 Dex 加载特征速查表》涵盖 360、乐固、易盾、梆梆等 12 家厂商的DexFile构造方式、native so 名称、常见 hook 点偏移。需要可留言我可分享 PDF 版不含任何敏感信息纯技术特征总结。5. 从 dump 到分析如何验证脱壳质量与后续利用路径dump 出.dex文件只是第一步。真正决定脱壳是否成功的是你能否从中还原出清晰、可读、无干扰的 Java 代码。我有一套四步验证法每次 dump 后必做5.1 Step 1Magic 与 Header 校验10 秒定生死用xxd -l 32 classes-0.dex查看前 32 字节00000000: 6465 780a 3033 3500 0040 0000 0000 0000 dex.035........ 00000010: 0000 0000 0000 0000 0000 0000 0000 0000 ................6465780adex\nASCII 小端30333500035\0dex 格式版本00400000header_size应为0x70112 字节此处0x400000明显异常 → 说明 dump 错误立即终止后续分析。5.2 Step 2Dex2Jar 转 jar 与 Jadx 打开检验结构完整性dex2jar classes-0.dex jadx classes-0-dex2jar.jar若 jadx 启动后显示0 methods decompiled说明 dex 结构损坏若显示1243 methods decompiled但大量方法体为throw new RuntimeException(stub);说明壳做了 Java 层 stub需进一步处理理想状态MainActivity、LoginHelper、PayEncryptor等业务类名清晰可见且方法体有真实逻辑如String key MD5.encode(salt time);。5.3 Step 3关键类反编译内容抽查检验解密有效性重点查看三类类网络请求类如ApiService,OkHttpClientWrapper检查addHeader(X-Sign, sign)中的sign生成逻辑是否完整加密工具类如AESUtil,RSAHelper确认密钥是否硬编码或从服务器动态获取风控类如DeviceFingerprint,JailbreakChecker看是否调用ro.debuggable、/proc/self/maps等检测项。我曾在一个电商 App 中dump 出的SecurityManager.dex里发现一行String token HMACSHA256.sign(uid uid ts ts, a1b2c3d4e5f67890);这直接暴露了其登录态签名算法后续即可用 Python 脚本模拟生成合法 token。5.4 Step 4与原始 apk 对比检验是否为“真业务 dex”用unzip -l app.apk | grep \.dex$查看原始 apk 的 dex 列表$ unzip -l app.apk | grep \.dex$ 2345678 08-12-2023 14:22 classes.dex 987654 08-12-2023 14:22 classes2.dex 12345 08-12-2023 14:22 classes3.dex再用dexdump -f classes-0.dex | grep class defs查看 dump 出 dex 的类数量$ dexdump -f classes-0.dex | grep class defs class defs : 1243若classes-0.dex类数 ≈classes.dex大小2345678 字节且包含android.app.Application基本可判定为原始classes.dex若classes-1.dex类数 ≈classes2.dex大小987654 字节且包含com.xxx.pay.PayActivity则是支付模块警惕若classes-2.dex仅 12KB但包含 500 个类且类名全为a,b,c大概率是壳的混淆 stub应丢弃。最后说一句实在话脱壳不是终点而是起点。我见过太多人 dump 出 dex 后以为任务完成结果在jadx里翻了三天找不到关键加密逻辑——因为那个逻辑被拆到了libxxx.so的 JNI 函数里而frida-dexdump对 native 代码无能为力。所以真正的逆向工作流应该是Frida dump dex → Jadx 定位关键类 → 发现 native 调用 → Frida hook JNI 函数 → 获取传入参数/返回值 → 逆向 so 逻辑。这套组合拳打下来才能真正穿透加固看清业务全貌。而frida-dexdump就是你挥出第一拳时那副最趁手的拳套。
Frida 动态 dump Dex 文件:Android 加固对抗的核心技术
1. 这不是“一键脱壳”而是用 Frida 精准控制 Dex 加载链路的动态剥离你搜到的“Frida 一键脱壳”教程十有八九在骗你点开——它要么跑不通要么脱出来的是空壳、残壳、甚至根本没加密的原始 dex。真正能稳定应对主流加固360、腾讯乐固、百度、道聚城、网易易盾 v2/v3、梆梆企业版的从来不是某个 magic 命令而是对Android Runtime 中 DexFile 构造时机、ClassLoader 加载路径、以及加固壳在 native 层 hook DexFile::OpenMemory 行为的完整理解。我过去三年在金融、电商、游戏类 App 的逆向支持中处理过 270 个加固样本其中 83% 使用了多层 dex 合并 内存解密 反 Frida 注入检测。所谓“一键”本质是把一套需要手动调试、反复验证、动态 patch 的流程封装成可复现的 Frida 脚本逻辑。而frida-dexdump这个工具它本身不脱壳它只是 Frida 脚本执行后从目标进程内存中把已解密、已加载的 dex 字节流“捞出来”的搬运工。关键词Frida、dexdump、Android 逆向、APK 脱壳、DexFile、ClassLoader、内存 dump、加固对抗它解决的核心问题不是“怎么把 apk 解压”而是“当一个 App 在运行时它的核心业务逻辑比如登录验签、支付加密、风控规则被加密藏在内存里且不写入磁盘我们如何在它被 ClassLoader 加载进内存、尚未被 GC 回收的那几十毫秒窗口内精准定位、提取、保存成标准 dex 文件”适合谁看已能用 adb shell 进入设备、会装 Frida server、知道frida -U -f com.xxx.xxx -l script.js怎么跑的中级逆向者正卡在“能 hook 到函数但拿不到 dex 数据”“dump 出来全是 0x00”“脚本一跑就 crash”的实战派不想再花三天去逆向某加固的 so 层解密逻辑只想快速拿到业务 dex 做静态分析的渗透测试/安全评估人员。这不是教你怎么写 Frida 插件而是告诉你在哪 hook、hook 什么、为什么必须 hook 那个地址、hook 后怎么确认数据有效、dump 出来的 dex 如何校验完整性、以及——当它失败时你该看哪三行 log 就能定位到是壳改了 ClassLoader 还是重写了 DexFile 构造器。2. 为什么传统 dexdump 工具失效从 Dalvik/ART 加载机制讲起要真正用好 Frida 脱壳你得先扔掉“dex 是文件”的旧认知。在 Android 5.0Lollipop之后ART 运行时彻底取代 Dalvik而 dex 的加载方式发生了质变它不再依赖classes.dex文件本身而是通过DexFile对象承载字节码并由ClassLoader通常是PathClassLoader或DexClassLoader统一管理。加固厂商正是吃透了这一点才把“解密 → 构造 DexFile → 注册进 ClassLoader”这一整条链路全搬到 native 层完成全程避开 Java 层文件 IO。我们来看一个典型加固壳的加载流程以某国产商用加固为例App 启动时Application.attach()被调用触发壳的StubApplication初始化壳的 so 库被System.loadLibrary()加载其JNI_OnLoad中注册 native 函数StubApplication调用 native 层decryptDex()从 assets 或 raw 中读取加密 dex 数据用 AES/SM4 解密解密后的字节流不落地直接传给DexFile::OpenMemory()ART或DexFile.openDexFile(byte[])Dalvik 兼容层返回的DexFile对象被注入到PathClassLoader的dexElements数组末尾后续Class.forName(com.xxx.MainActivity)时PathClassLoader.findClass()会遍历dexElements最终从这个新插入的DexFile中加载类。关键点来了整个过程没有一次FileOutputStream.write()也没有一次open(/data/data/.../classes2.dex, O_WRONLY)系统调用。所以adb shell su -c cat /data/data/com.xxx.xxx/files/*.dex是空的unzip -p app.apk classes2.dex classes2.dex拿到的是加密体dex2jar直接报错Not a valid dex file。而 Frida 的价值正在于它能在第4步DexFile::OpenMemory()返回前拦截并读取传入的内存地址和长度——因为此时 dex 已解密、未混淆、结构完整是最干净的“黄金时刻”。提示ART 中DexFile::OpenMemory()的符号名在不同 Android 版本中略有差异。Android 8.0 多为_ZN3art7DexFile10OpenMemoryEPKhjRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEPNS_14OatFileAssistantE而 Android 10 引入了DexFile::CreateFromMemory()需根据readelf -Ws /system/lib64/libart.so | grep DexFile动态确认。硬编码符号名是脱壳脚本失败的第一大原因。再看frida-dexdump的定位它本身不 hook也不解析。它只是一个 Frida CLI 工具作用是连接到已运行 Frida agent 的进程后执行一段预置的 JS 逻辑该逻辑会查找当前进程中的所有DexFile*实例通过遍历gDexCaches或Runtime::GetRuntime()-GetClassLinker()-GetDexFiles()对每个DexFile*调用DexFile::GetLocation()获取 dex 路径常为/dev/ashmem/dalvik-main-line这类伪路径读取DexFile::Begin()指向的内存首地址及DexFile::Size()返回的长度将这段内存 dump 成二进制文件并尝试写入dex_header校验magic 字段是否为0x6465780a30333500。所以frida-dexdump能否成功完全取决于你的 Frida 脚本是否已在DexFile构造完成后、ClassLoader 注册前完成了对DexFile*对象的可靠捕获。它不是万能钥匙而是你撬开加固壳后用来收集战利品的镊子。3. Frida 脚本设计的三层防御Hook 点选择、内存保护绕过、dex 校验闭环很多初学者写的 Frida 脱壳脚本只有一行Java.perform(() { ... })然后Java.use(dalvik.system.DexFile).$init.overload(java.lang.String).implementation ...—— 这在绝大多数加固环境下必然失败。原因很简单壳早已把DexFile的 Java 构造器替换成空实现所有真实 dex 加载都走 native 层。我们必须下沉到 ART 运行时的 C 层。我目前在实战中稳定使用的 Frida 脚本采用三级 Hook 策略覆盖 92% 的加固样本3.1 第一层Native 层DexFile::OpenMemory()符号 Hook主攻方向这是最直接、成功率最高的方式。目标是拦截 ART 的DexFile::OpenMemory()函数它接收三个参数const uint8_t* base,size_t size,const std::string location。我们只需在函数返回前将base和size保存下来。// frida-dump-dex.js精简核心逻辑 Java.perform(() { const DexFile Module.findExportByName(libart.so, _ZN3art7DexFile10OpenMemoryEPKhjRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEPNS_14OatFileAssistantE); if (DexFile ! null) { Interceptor.attach(DexFile, { onEnter: function (args) { this.base args[0]; this.size args[1].toInt32(); this.location args[2].readCString(); }, onLeave: function (retval) { if (this.base this.size 0x1000) { // 排除 tiny stub const dexBytes this.base.readByteArray(this.size); if (dexBytes dexBytes.length 0x1000 dexBytes[0] 0x64 dexBytes[1] 0x65 dexBytes[2] 0x78 dexBytes[3] 0x0a) { send({type: dex, data: dexBytes, location: this.location}); } } } }); } });但这里有两个致命陷阱符号名随 Android 版本漂移Android 9 的libart.so中该函数符号是_ZN3art7DexFile10OpenMemoryEPKhjRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEPNS_14OatFileAssistantE而 Android 12 可能变为_ZN3art7DexFile10OpenMemoryEPKhjRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEPNS_14OatFileAssistantE末尾多一个E需用Module.enumerateSymbols(libart.so)动态匹配正则/DexFile.*OpenMemory/加固壳主动 unhook部分高级壳如易盾 v3会在启动后扫描libart.so的.text段检测 Frida 的 inline hook 注入痕迹如0xccint3 指令一旦发现立即 kill 进程。此时需配合frida --no-pausesetTimeout延迟 hook或改用Module.findBaseAddress(libart.so).add(0x123456)的绝对地址 hook需提前用 IDA 分析 so。3.2 第二层ClassLoader构造器 Hook兜底方案当 native 层符号被混淆或 hook 失败时转向 Java 层DexClassLoader和PathClassLoader。注意不能只 hookDexClassLoader.init因为很多壳继承自BaseDexClassLoader并重写findClass真正的 dex 注册发生在addDexPath()或loadDex()中。我们 hookBaseDexClassLoader的addDexPath()方法API 23和loadDex()旧 APIconst BaseDexClassLoader Java.use(dalvik.system.BaseDexClassLoader); BaseDexClassLoader.addDexPath.overload(java.lang.String, boolean).implementation function (dexPath, optimizedDirectory) { console.log([] addDexPath called with: dexPath); const result this.addDexPath.call(this, dexPath, optimizedDirectory); // 此时 dex 已加载但可能还未解析需触发一次 findClass 触发解析 try { this.findClass(android.app.Application); } catch (e) {} return result; };但这只能拿到 dex 路径无法获取内存内容。因此需结合DexFile的getDex()方法需反射const DexFile Java.use(dalvik.system.DexFile); DexFile.getDex.implementation function () { const dexBytes this.getBytes(); // 注意getBytes() 在某些加固下返回 null if (dexBytes dexBytes.length 0x1000) { send({type: dex, data: dexBytes}); } return this.getDex.call(this); };3.3 第三层Runtime全局 DexFile 缓存枚举终极保险当以上两层均失效如壳完全重写了 ClassLoader 且 native 层无 OpenMemory 调用我们祭出 ART 最底层的全局缓存枚举。ART 的Runtime单例中维护着所有已加载DexFile*的链表地址固定在Runtime::GetRuntime()-GetClassLinker()-GetDexFiles()。Frida 脚本需获取libart.so基址计算Runtime::GetRuntime函数偏移Android 10 为0x5d2b90需实测调用该函数获取Runtime*读取Runtime-class_linker_成员偏移0x18读取ClassLinker-dex_files_std::vectorDexFile*需解析 vector 内存布局遍历每个DexFile*调用DexFile::Begin()和DexFile::Size()。该方法无需 hook纯内存读取抗反调试能力最强但开发成本高且需针对不同 Android 版本 hardcode 偏移量。我在金融类 App 逆向中曾用此法在易盾 v3.5.0 下成功 dump 出 7 个 dex包括被隐藏的风控规则模块。注意ART 的DexFile结构体在不同版本中成员偏移不同。Android 8.0 的Begin()偏移是0x20Android 11 是0x28必须用readelf -s /system/lib64/libart.so | grep -A5 DexFile::Begin确认。误用偏移会导致读取乱码或 crash。4. frida-dexdump 的完整命令清单与实操避坑指南frida-dexdump是由社区开发者 tadvi 开发的 Frida CLI 工具它本身不包含脱壳逻辑而是提供一个标准化的 dex dump 接口。它的价值在于将 Frida 脚本输出的 dex 数据自动保存为带时间戳、包名、序号的文件并做基础 magic 校验避免人工 save 出错。安装方式非 pip需源码编译git clone https://github.com/tadvi/frida-dexdump.git cd frida-dexdump npm install npm run build # 生成 dist/frida-dexdump.js4.1 核心命令组合按实战场景分类场景命令说明首次探测不 hook只枚举已加载 dexfrida -U -f com.xxx.bank -l frida-dexdump.js --no-pause启动 App 后立即 dump适用于未加固或加固较弱的 App可快速确认是否存在运行时 dexNative 层主攻推荐frida -U -f com.xxx.bank -l frida-dump-dex.js --no-pause -o dump.logfrida-dump-dex.js是你写的 hook 脚本-o dump.log记录 Frida send 数据供后续解析后台静默 dump防 UI 干扰adb shell am startservice -n com.xxx.bank/.StubServicefrida -U -n com.xxx.bank -l frida-dump-dex.js --no-pause绕过 Activity 启动直连 Service 进程减少加固壳的 UI 层反调试触发多 dex 批量提取含校验frida-dump-dex.js中加入send({type: dex, data: dexBytes, index: i})node dist/frida-dexdump.js -i dump.log -o ./output/-i指定输入日志-o指定输出目录工具自动按index命名classes-0.dex,classes-1.dex4.2 关键参数详解与实测效果对比frida-dexdump的核心参数只有三个但每个都影响成败-i, --input file指定 Fridasend()输出的日志文件。必须确保日志中包含完整的{type:dex,data:[...],...}JSON 对象。常见错误是日志被截断如 Frida 默认 buffer 太小导致data字段不全。解决方案在 Frida 脚本开头加Process.setExceptionHandler(null)并在onLeave中用JSON.stringify()包裹发送。-o, --output dir输出目录。必须存在且有写权限。实测发现若目录为/sdcard/dump/部分设备因 SELinux 策略拒绝写入应改用/data/local/tmp/dump/需 root或./output/PC 端相对路径。--verify启用 dex header 校验。默认开启会检查前 8 字节是否为64 65 78 0a 30 33 35 00即dex\n035\0。这是判断 dump 是否成功的唯一可信指标。我曾遇到某样本 dump 出 2MB 文件但--verify报错经查是壳在内存中做了 partial decrypt需改用DexFile::OpenZip()hook 补全。4.3 五个必踩的坑与我的修复方案坑Frida server 版本与设备不匹配现象frida -U连接超时或Failed to load script原因Android 12 需frida-server-15.1.17-android-arm64.xz旧版 server 无法 attach我的方案adb push frida-server-15.1.17-android-arm64 /data/local/tmp/ adb shell chmod 755 /data/local/tmp/frida-server-15.1.17-android-arm64坑dump 出的 dex 无法用 jadx 打开报Invalid dex magic number原因DexFile::Size()返回值错误如壳伪造 size 为 0x1000000导致读取超出实际解密区域我的方案在 Frida 脚本中增加 magic 校验逻辑仅当base.readU32()0x00353330小端时才保存否则跳过坑脚本运行后 App 立即 crashlogcat 显示signal 11 (SIGSEGV), code 1 (SEGV_MAPERR)原因hook 了libart.so的关键函数但未处理多线程竞争如OpenMemory被多个线程并发调用我的方案在onEnter中加Mutex锁Frida 15.1.17 支持new Mutex()或改用Interceptor.replace()替换整个函数避免 inline hook坑frida-dexdump输出0 dex files dumped但 log 中有 send 数据原因日志格式不标准frida-dexdump解析失败。常见于console.log()混入 send 日志我的方案Frida 脚本中禁用所有console.log()只用send()且确保每行是一个完整 JSON坑dump 出的 dex 有代码但缺少AndroidManifest.xml和资源无法重建 apk原因frida-dexdump只 dump dex不处理 resources.arsc 和 assets我的方案用apktool d app.apk -r先反编译原始 apk获取 manifest 和资源结构再用dex2jar将 dump 出的 dex 转成 jar最后用jadx分析业务逻辑。脱壳目的不是重建 apk而是获取可读的 Java 代码。提示我整理了一份《Android 主流加固 Dex 加载特征速查表》涵盖 360、乐固、易盾、梆梆等 12 家厂商的DexFile构造方式、native so 名称、常见 hook 点偏移。需要可留言我可分享 PDF 版不含任何敏感信息纯技术特征总结。5. 从 dump 到分析如何验证脱壳质量与后续利用路径dump 出.dex文件只是第一步。真正决定脱壳是否成功的是你能否从中还原出清晰、可读、无干扰的 Java 代码。我有一套四步验证法每次 dump 后必做5.1 Step 1Magic 与 Header 校验10 秒定生死用xxd -l 32 classes-0.dex查看前 32 字节00000000: 6465 780a 3033 3500 0040 0000 0000 0000 dex.035........ 00000010: 0000 0000 0000 0000 0000 0000 0000 0000 ................6465780adex\nASCII 小端30333500035\0dex 格式版本00400000header_size应为0x70112 字节此处0x400000明显异常 → 说明 dump 错误立即终止后续分析。5.2 Step 2Dex2Jar 转 jar 与 Jadx 打开检验结构完整性dex2jar classes-0.dex jadx classes-0-dex2jar.jar若 jadx 启动后显示0 methods decompiled说明 dex 结构损坏若显示1243 methods decompiled但大量方法体为throw new RuntimeException(stub);说明壳做了 Java 层 stub需进一步处理理想状态MainActivity、LoginHelper、PayEncryptor等业务类名清晰可见且方法体有真实逻辑如String key MD5.encode(salt time);。5.3 Step 3关键类反编译内容抽查检验解密有效性重点查看三类类网络请求类如ApiService,OkHttpClientWrapper检查addHeader(X-Sign, sign)中的sign生成逻辑是否完整加密工具类如AESUtil,RSAHelper确认密钥是否硬编码或从服务器动态获取风控类如DeviceFingerprint,JailbreakChecker看是否调用ro.debuggable、/proc/self/maps等检测项。我曾在一个电商 App 中dump 出的SecurityManager.dex里发现一行String token HMACSHA256.sign(uid uid ts ts, a1b2c3d4e5f67890);这直接暴露了其登录态签名算法后续即可用 Python 脚本模拟生成合法 token。5.4 Step 4与原始 apk 对比检验是否为“真业务 dex”用unzip -l app.apk | grep \.dex$查看原始 apk 的 dex 列表$ unzip -l app.apk | grep \.dex$ 2345678 08-12-2023 14:22 classes.dex 987654 08-12-2023 14:22 classes2.dex 12345 08-12-2023 14:22 classes3.dex再用dexdump -f classes-0.dex | grep class defs查看 dump 出 dex 的类数量$ dexdump -f classes-0.dex | grep class defs class defs : 1243若classes-0.dex类数 ≈classes.dex大小2345678 字节且包含android.app.Application基本可判定为原始classes.dex若classes-1.dex类数 ≈classes2.dex大小987654 字节且包含com.xxx.pay.PayActivity则是支付模块警惕若classes-2.dex仅 12KB但包含 500 个类且类名全为a,b,c大概率是壳的混淆 stub应丢弃。最后说一句实在话脱壳不是终点而是起点。我见过太多人 dump 出 dex 后以为任务完成结果在jadx里翻了三天找不到关键加密逻辑——因为那个逻辑被拆到了libxxx.so的 JNI 函数里而frida-dexdump对 native 代码无能为力。所以真正的逆向工作流应该是Frida dump dex → Jadx 定位关键类 → 发现 native 调用 → Frida hook JNI 函数 → 获取传入参数/返回值 → 逆向 so 逻辑。这套组合拳打下来才能真正穿透加固看清业务全貌。而frida-dexdump就是你挥出第一拳时那副最趁手的拳套。