frida-dexdump精准脱壳360加固App实战指南
1. 为什么360加固的壳不是“铁壁”而frida-dexdump是那把精准的手术刀你有没有遇到过这样的情况手头一个老版本的金融类App业务逻辑藏在某个隐藏Activity里但反编译出来全是com.stub.*包名、一堆a.b.c.d的混淆类classes.dex只有几十KB真正干活的代码全在assets/或lib/里动态加载点开JADX一看onCreate()里就一行ShellApplication.attachBaseContext()——这基本就是360加固V2/V3时代的典型特征。它不靠花哨的虚拟机指令混淆而是用Native层SO加载Java层反射调度DEX内存解密运行时校验四重组合拳把原始DEX像保险柜一样锁在内存里等App真正跑起来、解密完成那一刻才短暂存在。传统静态脱壳工具比如dex2jar直接读取APK里的DEX根本拿不到它因为原始DEX压根没写进APK文件而一些通用内存dump方案又容易触发360的反调试钩子一动就闪退。这时候frida-dexdump就不是“又一个工具”而是一套基于运行时上下文感知的精准捕获机制。它不硬刚360的校验逻辑而是等壳自己把原始DEX解密好、加载进内存、准备执行的那一刹那用Frida Hook住DexFile.loadDex()或BaseDexClassLoader的关键入口把内存里那个“活”的DEX字节数组原样抠出来。整个过程就像在流水线上盯住一个特定工位不拆机器、不关电源只等零件组装完成、质检通过、即将下线的0.5秒内用高速相机拍下它的高清正脸照。我实测过360加固V2.1.0.422021年主流版本用frida-dexdump在小米12Android 12上一次成功dump出的DEX反编译后包结构清晰、方法名可读、字符串未加密和原始开发时的classes.dex几乎一致。这不是玄学是利用了壳自身工作流的必然时序——再严密的防护也得让CPU执行解密后的字节码而Frida就卡在这个执行前的临界点。这个方案特别适合两类人一是做安全评估的渗透测试人员需要快速验证加固有效性而不是花三天去逆向壳的SO二是做兼容性适配的开发比如要给老App加个无障碍服务但找不到目标控件ID必须看真实布局逻辑。它不追求“全自动无感”但胜在路径最短、原理透明、失败可追溯。下面我就带你从零开始把这套流程走通每一步都附上我在真机上截的真实界面连Frida脚本里那个关键Hook点的堆栈回溯截图都给你标清楚。2. 环境搭建不是填坑而是为后续操作铺一条“无干扰通道”很多人卡在第一步不是因为命令敲错而是环境里埋着几个隐形地雷ADB权限不对、Frida版本不匹配、手机Root状态不稳定。我踩过最深的坑是某次用Magisk 25.2刷完Rootadb shell里能su但Frida却报Failed to find process——查了两小时才发现是Magisk Hide被360加固主动识别并屏蔽了Frida Server的进程名。所以环境准备的核心目标不是“装上就行”而是确保Frida能以最高权限、最低干扰的方式Attach到目标进程。下面所有步骤我都按小米12Android 12 Magisk 25.2 360加固App包名com.xxx.finance实测通过。2.1 手机端Root与Frida Server的“静默共存”首先确认Root有效性adb shell su id # 应输出 uid0(root) gid0(root)如果卡在su命令说明Root未生效需重刷Magisk或检查SU权限管理App如Magisk Manager是否对ADB Shell授权。接着下载对应架构的Frida Server小米12是ARM64处理器去 Frida Releases 下载frida-server-16.3.9-android-arm64.xz注意选对版本16.3.9是目前对360加固兼容性最好的稳定版新版16.4有概率触发其反调试。解压得到frida-server二进制文件用ADB推送到手机adb push frida-server /data/local/tmp/ adb shell chmod 755 /data/local/tmp/frida-server关键来了不要直接后台运行./frida-server 。360加固会扫描/proc/[pid]/cmdline发现frida-server字样就杀进程。正确做法是重命名并前台运行adb shell mv /data/local/tmp/frida-server /data/local/tmp/zygote64 adb shell /data/local/tmp/zygote64此时终端会卡住这是正常现象Frida Server在前台监听另开一个CMD窗口继续操作。这步的本质是让Frida Server的进程名伪装成系统Zygote进程绕过360的进程名关键词检测。2.2 电脑端Python环境与frida-dexdump的“轻量级依赖”电脑端无需复杂IDE一个干净的Python 3.9环境足矣避免用Anaconda其自带的OpenSSL版本常与Frida冲突# 创建独立虚拟环境推荐 python -m venv frida_env frida_env\Scripts\activate # Windows # 或 source frida_env/bin/activate # macOS/Linux # 安装核心依赖仅3个无冗余 pip install frida-tools10.8.2 # 注意版本10.8.2与frida-server 16.3.9完全匹配 pip install requests # 用于后续上传dump结果可选 git clone https://github.com/Pr0214/frida-dexdump.git cd frida-dexdump pip install -e . # 本地安装便于后续修改脚本为什么强调frida-tools10.8.2因为10.9版本重构了DeviceManager对Android 12的USB设备枚举逻辑变更会导致frida-ps -U无法列出进程。我试过10.12frida-ps返回空列表但降级到10.8.2后立刻显示com.xxx.finance进程。这不是bug是Frida团队对新Android版本的适配节奏问题——作为实战者我们选最稳的不追新。2.3 验证通道用最简命令确认“手术刀”已就位环境搭完必须用最小闭环验证# 终端1已运行 /data/local/tmp/zygote64前台 # 终端2 frida-ps -U # 应看到类似输出 # PID Name Identifier # 1234 com.xxx.finance com.xxx.finance如果看不到进程先检查ADB连接adb devices是否显示device而非unauthorized再检查Frida Server是否真在运行adb shell ps | grep zygote64。若看到进程但frida-ps无响应大概率是frida-tools版本不匹配立刻降级。这一步看似简单但它是后续所有操作的地基——地基不牢dump出来的DEX极大概率是损坏的我曾因版本不匹配dump出一个12KB的无效DEX反编译报Invalid magic number。提示所有操作务必关闭手机上的“USB调试安全设置”开关设置→开发者选项里该选项会强制ADB连接需用户点击确认而Frida Attach是后台行为无法弹窗交互开启后必失败。3. frida-dexdump的三大核心机制不是黑盒而是可调试的精密仪器很多教程把frida-dexdump当黑盒用python dump.py -p com.xxx.finance一跑完就完事。但当你遇到dump失败、dump出的DEX反编译报错时这种用法会让你彻底抓瞎。frida-dexdump真正的价值在于它的三层可干预设计Hook点选择、内存扫描策略、DEX校验逻辑。理解这三层你才能在360加固升级后快速适配而不是等作者更新脚本。3.1 Hook点选择为什么DexFile.loadDex()比BaseDexClassLoader更可靠frida-dexdump默认Hook的是DexFile.loadDex(String, String, int)这是最稳妥的选择。原因在于360加固的DEX加载流程壳SO在attachBaseContext()中调用System.loadLibrary(xxx)加载自定义SOSO内C代码解密原始DEX字节数组存入内存某块区域Java层通过DexFile.loadDex()将这块内存映射为DexFile对象最后由DexClassLoader加载该DexFile。关键点在于第3步loadDex()的第二个参数optimizedDirectory通常为空null而360加固为了规避检测不会把解密后的DEX写入磁盘所以它传入的是内存地址如/dev/ashmem/dalvik-main-line...。此时loadDex()的name参数第一个就是原始DEX的绝对路径如/data/data/com.xxx.finance/files/xxx.dex但这个路径在APK里根本不存在——它是壳动态创建的临时路径。frida-dexdump正是通过Hook这个函数在它执行前拿到name参数指向的内存地址再用Memory.readByteArray()读取对应长度的字节。对比BaseDexClassLoader的findClass()它发生在类加载阶段此时DEX已加载进内存但360加固会对findClass()做大量Hook检测一旦发现非系统调用就抛异常。我实测过HookfindClass()有70%概率触发360的SecurityException导致App崩溃。而loadDex()是系统API壳无法轻易拦截Hook成功率接近100%。3.2 内存扫描策略如何从GB级内存中精准定位DEX魔数即使Hook成功loadDex()传入的name参数也不一定直接指向DEX起始地址。360加固常把解密后的DEX字节数组放在一块更大的内存缓冲区中前面可能有壳自己的元数据如校验码、版本号。frida-dexdump的--scan参数就是干这个的它会在Hook获取的内存地址附近默认±1MB范围扫描0x6465780A即ASCII的dex\n魔数。这个范围不是乱设的——Android App的Dalvik Heap大小通常在128MB~512MB而解密缓冲区一般不超过4MB±1MB足够覆盖。但要注意扫描范围过大会拖慢dump速度过小则可能漏掉。我遇到过一个加固变种把DEX放在缓冲区末尾而loadDex()传入的地址是缓冲区开头此时默认扫描会失败。解决方案是手动扩大范围python dump.py -p com.xxx.finance --scan 2097152 # 扫描±2MB2097152就是2MB210241024单位是字节。这个值怎么定看loadDex()调用时的name参数长度如果name是/data/data/.../xxx.dex约40字符而实际DEX大小是3.2MB那缓冲区至少要4MB扫描范围设为2MB是安全的。3.3 DEX校验逻辑为什么dump出的DEX有时反编译报“Invalid header”frida-dexdump最后一步是校验读取的字节数组是否为合法DEX检查魔数0x6465780A、校验和checksum、签名signature是否匹配。但360加固有个阴招它解密后的DEX校验和是错的因为壳在解密时只还原字节不重新计算校验和重算太耗时影响启动速度。所以dump出来的DEX用JADX打开会提示Invalid dex file, bad checksum。解决方案在脚本里--fix-checksum参数。它会自动计算正确的校验和并写入DEX头部。原理很简单DEX头部0x8~0xC是校验和4字节小端先将此处置0计算整个DEX的Adler32校验和再写回去。我写了个小函数验证过对3.2MB的DEX计算耗时200ms完全可接受。没有这个参数你dump出来的DEX就是废的——别信网上说“用010 Editor手动改”效率低还易出错。注意--fix-checksum不能修复签名错误signature但签名错误不影响反编译阅读只影响dex2oat编译。对于逆向分析校验和正确就足够了。4. 实战全流程从启动App到获取可读DEX的完整链路含截图关键点现在进入最核心的实操环节。我会以360加固的com.xxx.finance版本2.5.1为例全程记录每一步命令、手机界面变化、Frida控制台输出并标注截图中的关键信息。所有截图均来自小米12真机确保100%可复现。4.1 启动App并等待壳初始化完成在手机上手动点击App图标启动不要急着执行dump命令。观察App启动过程首屏是360加固的白色启动页带360 Logo持续约1.5秒然后跳转到自家Splash页蓝色背景品牌Slogan此时壳已完成SO加载和DEX解密Splash页消失后主Activity才出现。这个Splash页就是你的信号灯必须等到Splash页完全显示、且无任何卡顿后再执行dump命令。因为壳的解密是在Splash页显示前完成的但loadDex()调用可能稍晚于SO加载。我测试过Splash页刚出现就dump有30%概率dump到空数据等它稳定显示2秒后再dump成功率100%。这步无法自动化必须人工判断——逆向不是写程序是和壳“斗智斗勇”。4.2 执行dump命令与实时监控在电脑终端执行python dump.py -p com.xxx.finance --output ./dumped/ --fix-checksum --scan 1048576参数详解-p com.xxx.finance指定目标进程--output ./dumped/输出目录会自动生成com.xxx.finance_20240520_143022.dex格式文件--fix-checksum强制修复校验和必加--scan 1048576扫描±1MB1MB1048576字节适配大多数场景。执行后Frida控制台会输出类似[*] Attached to com.xxx.finance [] Hooked DexFile.loadDex [] loadDex called with name/data/data/com.xxx.finance/files/xxx.dex, optimizedDirectorynull [] Found dex magic at 0x7a12345678 (offset 0x1234) [] Read 3245678 bytes from memory [] Fixed checksum: old0xabcdef01, new0x98765432 [] Saved to ./dumped/com.xxx.finance_20240520_143022.dex重点看[] loadDex called with...这一行它证明Hook已生效[] Found dex magic...表示扫描成功定位到DEX起始[] Fixed checksum...确认校验和已修复。如果这里卡住超过10秒说明App未进入可dump状态回到4.1步重新等待。4.3 验证dump结果三步法确认DEX可用性dump完成后别急着反编译先做三步验证文件大小检查用ls -lh ./dumped/看文件大小。如果只有几十KB说明dump失败可能是扫描范围太小或Hook未触发正常应与原始APK的classes.dex大小相近如3.2MB。魔数验证用xxd -l 8 ./dumped/*.dex查看前8字节应为00000000: 6465 780a 0000 0000 dex.....6465780a就是dex\n。JADX快速预览双击用JADX打开看左侧包结构是否展开。如果展开后全是com.stub.*说明dump的是壳的Stub DEX不是原始DEX——这意味着你Hook错了目标需检查是否-p参数输错包名或App有多个进程如com.xxx.finance:push要指定主进程。我截了一张JADX成功打开的截图左侧树状图清晰显示com.xxx.finance.ui.login.LoginActivity、com.xxx.finance.data.api.BankApiService等真实包名右侧代码窗里onCreate()方法体完整字符串如正在验证身份...未加密。这才是有效的dump结果。4.4 常见失败场景与针对性修复附真实报错截图实战中不可能一帆风顺以下是三个最高频失败场景及我的修复方案场景1Error: unable to find process with name com.xxx.finance截图显示Frida控制台红色报错。原因ADB连接不稳定或进程名不匹配。修复执行adb shell ps | grep xxx确认进程是否存在若存在但名字是com.xxx.finance:ui则用-p com.xxx.finance:ui若ps无输出重启ADBadb kill-server adb start-server。场景2[!] Exception: Error: access violation accessing 0x7a12345678截图显示内存访问异常。这是360加固的反调试手段它在DEX解密后立即释放内存或修改页属性。修复加--timeout 30参数延长Hook等待时间默认10秒或改用--hook-classloader参数HookBaseDexClassLoader的pathList字段需修改脚本见下文。场景3dump出的DEX用JADX打开报java.lang.IllegalArgumentException: Invalid dex file截图显示JADX错误弹窗。90%是校验和未修复。修复确认命令中加了--fix-checksum若仍报错手动计算校验和用Python脚本读取DEX文件跳过前12字节头魔数版本对剩余部分计算Adler32写入0x8~0xC位置。实操心得每次dump前先用frida-trace -U -f com.xxx.finance -i DexFile!loadDex跑一次看是否能捕获到loadDex调用。如果捕获不到说明壳用了其他加载方式如自定义ClassLoader此时需深入分析SO逻辑frida-dexdump就不适用了。5. 进阶技巧当标准方案失效时如何定制化突破360加固的新变种360加固每年迭代2-3个大版本V4开始引入“多DEX分片加载”和“运行时动态混淆”标准frida-dexdump可能失效。这时不能等作者更新要自己动手改造。以下是我针对V4.0.12023年新版本摸索出的两个有效方案已在3款App上验证成功。5.1 方案一HookDexPathList字段绕过loadDex()的检测V4.0.1加固把loadDex()调用包装在一层try-catch里并在catch中检测Frida Hook痕迹。但DexPathList是BaseDexClassLoader的私有字段存储了所有已加载的DexFile列表且访问它不触发壳的检测。改造思路先HookBaseDexClassLoader的构造函数获取实例用Java.use(dalvik.system.DexPathList).$init.overload(java.io.File, java.io.File, java.lang.String, java.lang.ClassLoader)获取DexPathList从DexPathList的dexElements数组中遍历每个DexElement读取其dexFile字段指向的内存。我修改了dump.py的hook_dexfile()函数新增hook_dexpathlist()分支。关键代码def hook_dexpathlist(classloader): DexPathList Java.use(dalvik.system.DexPathList) # 获取DexPathList实例 path_list classloader.pathList # 获取dexElements数组 elements path_list.dexElements.value for i in range(elements.length): element elements[i] dex_file element.dexFile.value # 从dexFile对象中提取内存地址 dex_bytes Memory.readByteArray(dex_file.address, dex_file.size) save_dex(dex_bytes, fdex_{i}.dex)执行时加参数--hook-dexpathlist即可。此方案成功率95%且不依赖loadDex()调用时机。5.2 方案二内存全量dump 智能DEX扫描应对“无固定入口”场景某些V4变种完全不用DexFile而是用Unsafe.allocateMemory()申请大块内存把DEX字节直接memcpy进去然后用DexFile的native方法加载。此时DexFile对象甚至不经过Java层。终极方案是用Memory.scanSync()对整个进程内存0x7000000000-0x7fffffffff扫描dex\n魔数对每个命中地址读取0x20字节验证DEX头完整性检查0x20处的file_size字段是否合理读取完整file_size字节校验魔数和校验和。我写了一个独立脚本full_mem_dump.py用frida的Process.enumerateRanges()先获取所有可读内存段再逐段扫描。对3GB内存的App扫描耗时约45秒但100%找到DEX。截图显示扫描日志中Found dex at 0x7a12345678, size3245678和之前一致。5.3 方案三结合Logcat动态定位解决“多进程加载”难题有些金融App把核心业务放在com.xxx.finance:core进程中而主进程只负责UI。此时-p com.xxx.finance会dump到UI进程的Stub DEX。正确做法启动App后立即执行adb logcat | grep DexFile观察哪条日志出现在com.xxx.finance:core进程中用frida-ps -U | grep core确认进程PID直接-p com.xxx.finance:coredump。我截了一张Logcat截图红色高亮行显示I/DexFile: loadDex from /data/data/com.xxx.finance/files/core.dex in pid 5678而5678正是com.xxx.finance:core的PID。这比盲目尝试高效十倍。最后分享一个小技巧dump完成后用jadx-gui打开DEX按CtrlShiftF全局搜索360或Qihoo如果搜不到任何结果说明dump成功如果搜到大量com.qihoo.xxx包名说明dump的是壳的代码不是原始DEX——这是最快速的成败判断法。