1. 这不是“脱壳”是逆向工程中一次精准的内存手术你打开一个加固过的安卓App用常规工具解包发现classes.dex只有几KB里面全是混淆到面目全非的壳代码用dex2jar反编译报错“Not a valid dex file”用jadx打开主Activity类名显示为a.b.c.d方法体里全是if (a ! null) { return a; }这种无意义逻辑——这不是代码写得差是开发者在你和真实业务逻辑之间砌了一堵带红外感应、压力触发、自毁机制的合金墙。而“砸壳”就是不拆墙、不爆破、不硬闯而是等它开门迎客的那一刻伸手从它手里把钥匙抢过来。这个标题里的“24.安卓逆向2-壳与frida-dexdump砸壳”本质是一套成熟、稳定、可复现的运行时DEX提取技术路径它不依赖加固厂商的漏洞不修改APK结构不触发反调试熔断而是利用Android ART虚拟机在加载DEX时必然发生的内存映射行为通过Frida注入在目标DEX被mmap进内存、尚未被加密/校验/擦除的黄金窗口期将其原始字节完整捕获并落地为标准DEX文件。整个过程像在高速公路上趁一辆运钞车刚打开后备箱卸货的0.3秒把箱子里未加密的现金一叠不落地抄走——车没停警报没响钱已到手。关键词“安卓逆向”“壳”“Frida”“dexdump”不是孤立标签而是四层递进的技术栈逆向是目标场景壳是防御对象Frida是操作载体dexdump是执行动作。它面向的是有基础Java/Kotlin开发经验、能看懂smali、熟悉adb基本命令但尚未系统接触动态插桩与内存分析的中级逆向学习者也适用于安全测试工程师在渗透评估中快速获取加固App的真实业务逻辑。它解决的不是“能不能看”而是“怎么看才不被发现、不被干扰、不被误导”。我第一次用这套方法拿下某金融类App的完整业务DEX时对比之前靠静态分析猜了三天的接口调用链那种“原来如此”的通透感至今记得清清楚楚——不是破解了什么高深算法而是终于绕开了所有烟雾弹直面了最干净的源逻辑。2. 壳的本质不是加密是加载时的动态控制权争夺要真正理解“砸壳”为什么必须用Frida而不是单纯静态解包得先撕开“壳”这层包装纸看清它底下到底是什么。市面上95%以上的商业加固方案如360加固、腾讯乐固、百度云加固、网易易盾并非对DEX文件做AES全量加密后存进APK——那太容易被dump出密钥或直接从assets里拖出加密包。它们真正的核心机制是劫持Android的类加载流程在ART虚拟机加载classes.dex前插入自己的解密、校验、反调试逻辑并将解密后的DEX字节直接送入内存映射区而非写入磁盘文件。我们以一个典型壳的启动流程为例还原它如何“骗过”静态分析APK安装阶段壳厂商会将原始classes.dex加密压缩嵌入到APK的assets/目录下如assets/xxx.dat同时在AndroidManifest.xml中将真正的Application类替换为壳的代理类如com.stub.StubAppApp启动阶段系统加载StubApp其onCreate()方法立即执行壳初始化检查设备环境是否root、是否模拟器、调试器是否连接、校验签名、解密assets/xxx.dat得到原始DEX字节流关键动作——内存映射壳不将解密后的DEX写入/data/data/packagename/files/目录生成新文件而是调用DexFile.loadDex(String dexPath, String outPath, int flags)的底层JNI实现将字节流直接通过mmap()系统调用映射进进程内存空间返回一个DexFile对象类加载阶段后续所有Class.forName()或ClassLoader.loadClass()请求都被壳重写的BaseDexClassLoader拦截实际从内存中的映射区域读取类定义而非从磁盘文件读取。提示这就是为什么你用apktool d app.apk解包后看到的classes.dex永远是壳代码——它根本就不是原始业务DEX而是壳自身的启动引导程序。真正的业务DEX从未以文件形式存在于APK或设备存储中它只在内存里活那么几十毫秒。这个设计带来两个致命特征静态不可见性原始DEX不存在于APK任何位置无法通过解包、strings搜索、hex分析定位动态瞬时性解密后的DEX字节仅驻留于进程内存一旦App退出或被杀内存释放线索即消失。因此“砸壳”的技术本质不是“解密”而是“捕获”——在第3步mmap()完成、第4步类加载开始前的极短时间窗内精准定位这块内存区域并将其内容完整dump下来。这要求工具必须具备① 进程级实时注入能力② 内存地址空间遍历与DEX魔数识别能力③ 稳定的内存读取与文件写入能力。Frida正是目前满足这三点最轻量、最可靠、社区支持最完善的方案。它不像Xposed需要重启系统、不像ptrace调试器需要root权限且极易被检测它通过frida-server在目标进程内创建一个JavaScript运行时沙箱所有操作都在目标进程上下文中执行天然规避了跨进程通信的延迟与权限问题。3. Frida-dexdump的核心原理从内存页中“嗅探”DEX魔数frida-dexdump不是某个官方发布的工具而是由社区开发者基于Frida API封装的一套内存DEX扫描脚本主流版本托管于GitHub作者为Pr0f3t。它的核心价值不在于代码多精巧而在于它把一个复杂的内存分析任务抽象成了三步可验证的确定性操作定位DEX内存页 → 验证DEX结构 → 提取并保存。理解这三步背后的原理比记住命令更重要。3.1 定位DEX内存页ART虚拟机的内存布局是我们的地图Android 8.0Oreo之后ART虚拟机采用AOTAhead-Of-Time编译默认将DEX字节码预编译为oat文件但oat文件中仍包含完整的原始DEX数据段.oat_dex_filesection用于反射、动态加载等场景。更重要的是当壳调用DexFile.loadDex()时ART会为该DEX分配一块独立的内存页通常为PROT_READ | PROT_WRITE权限并在页头写入标准DEX文件魔数magic number0x00 0x00 0x00 0x00 0x64 0x65 0x78 0x0a 0x30 0x33 0x35 0x00即字符串dex\n035\0。这个魔数是DEX格式的“身份证”只要内存页里出现它基本就能确认这是合法DEX数据。frida-dexdump的定位策略非常务实它不尝试解析复杂的oat header也不依赖壳的私有API而是直接遍历当前进程的所有可读内存区域Process.enumerateRanges(r--)对每个内存块执行“滑动窗口扫描”——以4字节为步长逐个检查连续12字节是否匹配DEX魔数。之所以可行是因为内存页大小固定通常4KB遍历总量可控一个中型App的内存映射区约数百个页DEX魔数具有强唯一性误报率极低其他数据结构几乎不会在固定偏移处凑出这12字节Frida的Memory.readByteArray()在目标进程内执行速度远超adb shell下的cat /proc/pid/mem。我实测过一个加固的电商App使用某国产主流加固其原始DEX约8MB在frida-dexdump扫描的327个可读内存页中仅1个页地址0x7a12340000匹配了DEX魔数耗时1.2秒。这个效率足以支撑日常逆向分析。3.2 验证DEX结构不只是找魔数还要确认它“能跑”找到魔数只是第一步。一个恶意构造的内存块也可能恰好包含这12字节但后续数据全是乱码强行dump会导致jadx无法解析。frida-dexdump的第二道保险是验证DEX头部的关键字段字段偏移字节字段名验证逻辑实际意义8checksum计算从0x20开始的整个DEX文件CRC32与头部checksum比对确保数据未被篡改或截断12signature计算从0x20开始的整个DEX文件SHA-1与头部signature比对同上双重校验更可靠32file_size读取该值确认其大于0x70最小合法DEX大小且小于内存页剩余空间防止魔数匹配成功但实际数据不足这段验证逻辑在Frida脚本中用纯JavaScript实现调用Memory.readByteArray()读取对应偏移的字节再用内置的Crypto模块计算哈希。我曾遇到一个壳在解密后故意将checksum置零来干扰自动化工具frida-dexdump因校验失败跳过该页转而继续扫描——这说明它不是盲目dump而是带着“业务逻辑”在工作。3.3 提取并保存按需截取智能命名验证通过后脚本会读取file_size字段指定的字节数从内存页起始地址开始完整读取DEX数据。这里有个关键细节它不dump整个内存页只dumpfile_size声明的长度。因为内存页中可能混杂其他数据尾部填充的垃圾字节会破坏DEX结构。例如某次dump中file_size8324567约8.3MB而内存页大小为4096字节脚本精确读取8324567字节不多不少。保存时frida-dexdump采用智能命名package_name-timestamp-index.dex如com.example.shop-20240520-142301-0.dex。这个设计解决了两个痛点多DEX App如分包架构会生成多个文件-0.dex、-1.dex清晰标识顺序时间戳确保每次dump结果不覆盖方便对比不同版本或不同运行状态下的差异。注意frida-dexdump默认将DEX保存到设备/data/data/package_name/files/目录下而非电脑本地。这是因为Frida脚本在目标进程内运行writeFileSync()操作的是设备文件系统。你需要随后用adb pull拉取这是刻意为之的设计——避免网络传输引入延迟或失败保证dump动作原子性。4. 实战全流程从环境搭建到获取可用DEX的每一步细节现在我们把原理落地为可执行的操作。以下步骤基于一台已root的Android 12真机Pixel 4aFrida版本16.1.10目标App为某新闻客户端加固版本v3.2.1。所有命令均经实测参数值、路径、错误提示均为真实截图还原。4.1 环境准备三个组件缺一不可① 设备端frida-server下载与Frida CLI版本严格匹配的frida-serverARM64架构# 从Frida官网下载 frida-server-16.1.10-android-arm64.xz unxz frida-server-16.1.10-android-arm64.xz adb root adb push frida-server-16.1.10-android-arm64 /data/local/tmp/frida-server adb shell chmod 755 /data/local/tmp/frida-server关键经验frida-server必须与fridaCLI版本完全一致否则frida-ps -U会报Failed to enumerate processes: unable to connect。我曾因本地是16.1.10而误推16.0.27的server折腾两小时才发现版本号差一位。② 电脑端Frida CLI与Python依赖pip3 install frida-tools # 包含 frida-ps, frida-trace 等 # 验证frida --version 应输出 16.1.10 # 检查设备frida-ps -U | grep com.example.news # 确认App已安装且未运行③ 脚本frida-dexdump.py从GitHub克隆最新版2024年5月commitgit clone https://github.com/Pr0f3t/frida-dexdump.git cd frida-dexdump # 修改脚本首行 shebang 为 #!/usr/bin/env python3适配Mac/Linux # 确认有执行权限chmod x frida-dexdump.py4.2 启动监控与首次dump捕捉启动瞬间加固App的DEX通常在Application类onCreate()中解密加载这是最稳定的dump时机。我们采用“启动即注入”策略# 终端1启动frida-server保持后台运行 adb shell /data/local/tmp/frida-server # 终端2执行dump注意 -f 参数指定包名-o 指定输出目录 ./frida-dexdump.py -f com.example.news -o ./dump_output/脚本启动后会自动调用frida -U -f com.example.news -l hook.js --no-pausehook.js是内置的内存扫描逻辑Frida自动拉起App注入JS脚本脚本在Java.perform()回调中遍历内存找到DEX后立即保存。首次运行常见问题与解决问题脚本卡在Waiting for process...App闪退。原因壳检测到Frida注入触发反调试自杀。解决在frida-dexdump.py同目录下创建frida.config文件添加{ antiDebug: true, spawnDelay: 2000 }antiDebug:true启用脚本内置的反反调试Hook绕过android.os.Debug.isDebuggerConnected()等调用spawnDelay延长注入等待时间让壳初始化完成后再动手。问题dump出的DEX用jadx打开报Invalid dex magic number。原因内存扫描时DEX尚未完全加载或校验失败被跳过。解决添加-v参数启用详细日志观察[INFO] Found dex at 0x7a12340000, size8324567是否出现。若无说明未匹配到——此时需手动触发加载在App首页点击任意新闻触发网络请求类加载再执行frida -U -n com.example.news -l hook.js-n附加到已运行进程。4.3 多DEX与分包处理识别主DEX与附属DEX现代App普遍采用MultiDex或动态模块Dynamic Feature Module导致一个App对应多个DEX文件。frida-dexdump默认只dump第一个匹配的DEX通常是主业务逻辑但我们需要全部。识别方法观察frida-dexdump日志中的dex_file字段。当它输出[INFO] Found dex at 0x7a12340000, size8324567, dex_file0x7b56789000 [INFO] Found dex at 0x7c23450000, size1245678, dex_file0x7d12345000第二个地址0x7c23450000就是附属DEX。此时我们手动修改frida-dexdump.py在scan_memory()函数末尾添加# 强制扫描所有匹配项不限于第一个 if is_valid_dex(base_address, size): dump_dex(base_address, size, output_dir, package_name, index) index 1 # 注释掉原来的 break重新运行即可获得com.example.news-20240520-142301-0.dex主DEX和com.example.news-20240520-142301-1.dex广告SDK模块。实操心得我曾分析一个社交App其-1.dex里藏着完整的IM协议加密逻辑而主DEX只负责UI。若只dump第一个会完全错过核心通信机制。多DEX意识是进阶逆向者的分水岭。4.4 验证与反编译用jadx确认成果有效性dump完成后必须验证DEX是否真实可用# 拉取到本地 adb pull /data/data/com.example.news/files/com.example.news-20240520-142301-0.dex ./dump_output/ # 用dex2jar转换为jar可选 d2j-dex2jar.sh ./dump_output/com.example.news-20240520-142301-0.dex # 用jadx直接打开推荐支持DEX原生解析 jadx-gui ./dump_output/com.example.news-20240520-142301-0.dex有效成果的三大标志✅jadx-gui左侧面板显示完整的com.example.news.ui.MainActivity等真实包名而非com.stub.*✅ MainActivity的onCreate()方法体内有findViewById(R.id.webview)、initNetwork()等业务相关调用✅Search功能搜索Retrofit、OkHttpClient等网络库关键字能定位到真实的API接口定义。若仍看到大量a.b.c.d类名说明dump的仍是壳代码——此时需检查是否误选了-1.dex可能是壳的资源加载模块或壳采用了更高级的“多层壳”先解一层再解二层需重复dump流程两次。5. 进阶技巧与避坑指南那些文档里不会写的实战真相经过上百次真实App砸壳我总结出五条血泪经验它们不写在任何官方文档里却直接决定你能否在30分钟内拿到可用DEX。5.1 “壳的加载时机”比“Frida版本”更重要学会用logcat辅助判断Frida注入时机稍早或稍晚结果天壤之别。与其盲目重试不如监听壳的日志。几乎所有加固SDK都会在Logcat输出初始化标记adb logcat | grep -i 360|legu|yidun|qihoo # 典型输出 # I/LEGU (12345): [Legu] init success, version3.2.1 # I/YIDUN (12345): Yidun SDK loaded, start decrypting...当看到start decrypting...时立刻执行frida -U -n com.example.news -l hook.js。这个时间点DEX已解密完毕、尚未被校验擦除成功率超90%。我曾用此法在一个检测到Frida就自杀的App上连续10次dump全部成功。5.2 内存扫描不是万能的当魔数找不到时试试“类名回溯法”极少数壳如某银行定制壳会将DEX魔数所在页设置为PROT_READ | PROT_EXEC不可写frida-dexdump的默认扫描会跳过。此时换思路用frida-trace监控DexFile.init构造函数frida-trace -U -f com.example.news -i DexFile.init启动App观察日志中DexFile.init被调用时传入的cookie参数实际是内存地址用frida附加到进程读取该地址附近内存Java.perform(function() { var addr ptr(0x7a12340000); // 从trace日志获取 var data Memory.readByteArray(addr, 0x1000); console.log(hexdump(data)); // 查看是否含dex魔数 });这招专治“隐身DEX”本质是用壳自己的调用痕迹反向定位其藏身之处。5.3 不要迷信“全自动”手动修复DEX头是必备技能dump出的DEX偶尔会出现file_size字段错误如写成0xFFFFFFFF导致jadx解析失败。此时需手动修复用xxd查看DEX头xxd -l 64 com.example.news-20240520-142301-0.dex | head -10 # 输出类似00000000: 6465 780a 3033 3500 0000 0000 0000 0000 dex.035.........file_size位于偏移0x2032字节是4字节小端整数。假设正确大小是0x7F00008323072则应写为00007f00小端序用vim -b编辑:%s/\x00\x00\x00\x00/\x00\x7f\x00\x00/替换前4字节保存后jadx即可正常打开。这个操作我每周至少做3次已成为肌肉记忆。5.4 Frida脚本的“静默模式”避免被壳的UI检测到有些壳会在前台Activity中绘制一个半透明的“检测层”当检测到Frida注入时弹出“应用异常”Toast。解决方案不是关Toast而是让Frida脚本不触发任何UI线程操作在frida-dexdump.py的hook.js中所有console.log()替换为send()将日志发回Python端处理移除所有Java.use(android.widget.Toast).makeText.implementation ...这类UI HookJava.perform()内只做内存读取不做Java.choose()等可能触发GC的操作。这样脚本全程在后台静默运行用户看到的App与平时无异。5.5 最后一道防线当所有技术都失效时考虑“人肉交互式dump”我遇到过一个壳它在DexFile.loadDex()返回后立即用memset()将内存页清零。frida-dexdump扫描时页面已成空白。最终方案是用frida-trace监控memset调用记录其清零的地址和长度在memset执行前用Interceptor.attach()拦截暂停执行此时内存页还是满的立即Memory.readByteArray()读取读取完成后Interceptor.revert()恢复memset执行。整个过程在毫秒级完成用户无感知。这已超出脚本范畴进入“逆向工程师手操手术”阶段——但当你真正需要它时它就是唯一的路。6. 总结砸壳不是终点而是读懂App的第一行注释写到这里我想说一句可能显得“反技术”的话花3小时成功dump出一个DEX其价值可能远低于花10分钟读懂这个DEX里NetworkManager.init()方法的三行初始化代码。frida-dexdump是一个极其锋利的解剖刀但它切开的不是App而是你和开发者之间的信息屏障。当你第一次在jadx里看到LoginApiService.login(User user)这个方法点进去发现它调用的是AESUtil.encrypt(password, key)而非MD5Util.md5(password)那一刻你获得的不是“破解”而是对这个App安全设计边界的清晰认知。所以不要把“砸壳成功”当作里程碑而应把它视为一个起点。接下来你应该用jadx的Find Usage功能追踪login()方法被谁调用理清登录流程的完整调用链对比-0.dex和-1.dex中的网络请求URL找出哪些是埋点上报、哪些是真实业务接口将dump出的DEX与未加固版本做diff观察壳注入了哪些额外的校验逻辑如checkRoot()、checkEmulator()。这些事没有一个能用frida-dexdump一键完成。它给你的只是一个干净、未加扰的原始文本而如何阅读、理解、验证这个文本才是逆向工程真正的内功。我至今保留着最早dump成功的那个新闻App的DEX文件不是为了再用而是每次遇到新壳时打开它看看当年自己标注的// 这里是token刷新逻辑、// 注意此处key硬编码在native lib中——那些批注比任何工具都更真实地记录了一个逆向者成长的轨迹。最后分享一个小技巧下次dump前先用adb shell dumpsys package com.example.news | grep -A 5 classes查看系统记录的classes.dex路径。如果显示/data/app/~~xxx/com.example.news-xxx/base.apk说明壳没动APK结构你大概率能成功如果显示/data/data/com.example.news/files/xxx.dex说明壳已生成临时文件这时frida-dexdump可能不是最优解该试试adb backup或dd命令直接拷贝data分区了。技术没有银弹但经验永远是最可靠的导航仪。