最近有个朋友拿着一个套了360加固的App来找我,问能不能把里面的业务逻辑还原出来看看。我说能,但需要先脱壳,他当时一脸懵:脱壳是什么?难不难?会不会把手机搞坏?我花了大概一个多小时,从环境搭建到dump出dex,带着他完整走了一遍,全程没让他写一行代码,最后他惊喜地发现业务类全都在。这篇文章就把这套完全可复制的流程写下来,给所有零基础想做Android反编译、脱加固壳的人一条能走通的路。
先说清楚,这是一篇技术学习与安全研究方向的实操教程,面向的目标是:你自己写的测试App、已开源App、或者你有明确授权做安全评估的样本。不要拿它去搞未经授权的破解和分析,技术工具没有原罪,但使用边界必须自己守住。
1. 先从根上理解:加固和脱壳到底是怎么回事
1.1 为什么用jadx打开加固App,看不到业务代码
正常情况下,一个APK就算代码写得再烂,你用jadx一拖,基本能把Java层逻辑看个八九不离十。Activity、Fragment、网络请求封装、工具类,清晰得很。但当你拖入一个套了360加固(或者其他同类加固方案)的APK时,画风突变:首页只有屈指可数的几个类,application节点被换成了一个壳入口,业务类一个都找不到。
这不是jadx坏了,而是加固的机制导致的。加固的本质是“加密存储 + 运行时解密”。开发者的真实dex会被加密成一个文件,塞进assets目录或者so库里。App启动后,壳代码先接管进程,做一堆环境检测和安全校验,然后才从加密文件里解密出真实的dex字节流,再通过DexClassLoader、InMemoryDexClassLoader这类机制,把真正的dex加载进ART虚拟机里执行。
也就是说,从磁盘上看,这个App是“空心”的;只有当它跑起来,内存里才会出现完整的真实dex。所以静态反编译当然什么都看不到,因为你看到的classes.dex只是壳的启动器,真正的业务逻辑被锁在了壳后面。
1.2 脱壳就是趁真实dex在内存里“活着”的时候抓住它
理解了加固原理,脱壳的思路就顺理成章了:既然真实dex最终必须出现在内存里,那我就在App运行期间,把这段内存抓下来,保存成dex文件,再丢回jadx反编译。这就是俗称的脱壳。
整个过程有三件事最核心:
- 找到真实dex加载的时机(不能太早,壳还没解密;也不能太晚,某些临时dex可能已经被回收)
- 定位dex在内存中的位置(靠文件特征、内存段扫描、Hook关键类加载逻辑)
- 把内存字节完整保存下来,并保证dex文件的完整性(包括修复校验和等)
零基础路线最怕的就是第一关“时机”和第二关“定位”太玄学。好在社区已经有现成的工具把这两步做成了“一条命令”,你要做的不是发明轮子,而是学会正确使用轮子。
2. 工具选型:为什么零基础首选frida-dexdump
2.1 主流的脱壳方案,横向比个明白
在没有现成工具的时代,脱壳是件极其痛苦的事:你得先反编译壳本身,搞明白它的解密算法,然后写脚本手动调DexClassLoader,或者在IDA里动态调试so层逻辑。这对零基础无异于劝退。
现在主流方案大概分三类:
- 基于Frida生态的工具,代表是frida-dexdump。核心思路是用Frida把JS脚本注入目标进程,然后扫描进程内存,按dex文件的魔法头特征把所有疑似dex的内存段全部dump下来。优点是操作简单、对大多数加固有效、不需要定制系统;缺点是对某些变种加固会失效,且dump出来的文件会有冗余噪声。
- 基于定制ROM的方案,代表是FART、BlackDex。思路是在系统框架层做文章,在ART虚拟机加载类的时候主动脱壳。优点是兼容性极强,连很多VMP类加固都能扒出点东西;缺点是要给模拟器刷定制系统,光是环境搭建就可能劝退新手。
- 基于静态分析硬破解的方案,比如直接还原壳的解密算法。这种方案对分析者的逆向功底要求极高,不推荐零基础尝试。
对于“零基础 + 第一次脱壳”这个场景,frida-dexdump是性价比最高的:环境要求低,模拟器就能跑;命令简单,一条命令搞定;社区活跃度高,遇到问题好搜答案。
2.2 frida-dexdump的原理,说人话版
frida-dexdump的原理其实不复杂。dex文件有固定的文件头,也就是我们常说的magic字节,通常是dex\n035\0或更高版本。无论壳怎么加密磁盘上的文件,只要它把dex解密后交给ART虚拟机执行,内存里就一定有一块区域符合dex文件的特征。
frida-dexdump做的事情就是:
- 通过Frida注入目标进程
- 扫描进程的所有内存段,寻找匹配dex文件头特征的内存区域
- 对候选内存块做合法性校验(比如dex文件头部字段是否合理、文件大小是否与头部声明匹配)
- 把验证通过的内存块原样保存成dex文件
这就是为什么你会看到它有时候会dump出很多个dex,其中一部分可能是系统框架的dex、壳自身的dex、还有你真正想要的业务dex。没关系,都保存下来,后面用jadx逐个看就行。
理解这一步,你后面排查问题就会容易得多:比如dump出来的全是一些无关系统类,你该知道是时机不对;比如发现dex很小,可能是dump到了不完整的dex碎片。
3. 环境准备:一张清单加几条命令
3.1 你需要的东西,一个都不能少
工欲善其事必先利其器,零基础脱壳的环境准备就四块:电脑端工具、模拟器环境、客户端Frida、设备端frida-server。每一块都必不可少,但它们都不难。
实际操作前,我建议你准备好这些:
- 一台Windows 10/11或macOS电脑,内存别低于8GB
- Genymotion模拟器(我强烈推荐用模拟器而不是真机,原因很简单:模拟器自带root,不用解锁BL、不用为root权限提心吊胆)
- adb命令行工具,装Android Platform Tools即可
- Python 3.8以上环境
- Frida客户端、frida-tools、frida-dexdump,全部通过pip安装
- 与电脑端Frida版本严格一致的frida-server,放进模拟器里运行
这里有个重点:frida-server的版本必须和电脑端Frida的版本一致。这是新手踩坑率最高的一步,后面我会专门讲。
3.2 安装过程详解,每一步都给你说清楚
第一步,安装Python和pip。Windows用户去官网下载Python安装包,安装时一定勾选Add Python to PATH。macOS可以用brew install python3。装完打开终端,执行python --version和pip --version确认能跑。
第二步,安装Frida工具链。终端执行:
pip install frida frida-tools frida-dexdump装完以后,执行frida --version,记下版本号。比如我的结果是16.7.19,后面下载frida-server就要找16.7.19版本。
第三步,安装Genymotion并创建一台Android 9的虚拟设备。下载安装Genymotion Desktop客户端的时候,它会要求你安装VirtualBox,这个是底层依赖,按提示装就行。装好后启动设备,等它完全开机。
第四步,验证adb连接。终端执行adb devices,能看到设备就代表通信正常。如果看不到,大概率是adb路径没配好,或者Genymotion的adb bridge没启动。
第五步,下载frida-server。打开GitHub上frida官方仓库的releases页面,找到和刚才版本号完全一致的tag,下载android-x86_64的压缩包。Genymotion是x86架构,所以选x86_64;如果是真机,一般是arm64,别下错。
第六步,把frida-server推入模拟器并启动。依次执行:
adb push frida-server-16.7.19-android-x86_64 /data/local/tmp/frida-server adb shell chmod 755 /data/local/tmp/frida-server adb shell "su -c /data/local/tmp/frida-server &"如果提示Permission denied,别急,先adb shell进去,输入id看自己是不是root用户。Genymotion不同镜像版本的su行为不太一样,有些进去就是root,有些必须su。如果su -c不行,直接/data/local/tmp/frida-server &也许就成功了。
第七步,验证Frida连通。电脑终端执行frida-ps -U,如果能看到模拟器里的进程列表,说明Frida客户端和服务端通信正常,环境全部就绪。
4. 实操全流程:一条命令把壳拔下来
4.1 找到目标App的包名
脱壳前先得让工具知道该操作哪个App。包名就是Android里每个应用唯一的身份标识,比如com.example.app。获取方法有几种:
- 用aapt命令:
aapt dump badging xxx.apk | grep package - 用jadx打开APK,看AndroidManifest.xml里的package属性
- 或者最简单的,模拟器里安装好App,用
frida-ps -U列出进程名,找到自己目标
顺带观察一个细节:如果Manifest里application的name被换成了com.stub.StubApp这类带stub关键词的类,基本可以确认这包确实加了加固,而且这个类在后续分析里也是个重要线索。
4.2 启动脱壳命令
找到包名后,在电脑终端执行:
frida-dexdump -U -f com.example.app -o dump这里解释一下参数含义:-U表示连接通过USB/adb连接的目标设备;-f表示spawn方式,也就是由Frida来拉起这个App,而不是去attach一个已经在跑的进程;dump是输出文件夹名。
执行后你会看到类似下面的输出:
Start: 15:30:00 Spawned com.example.app. PID: 2345 Dumping dex files... [INFO] Dumping 4 dex files ... Done: 15:30:05几秒钟到几十秒不等,视App大小和dex数量而定。完成后,当前目录会生成一个dump文件夹,里面放了若干个dex文件。这些就是壳加载到内存里的所有dex原始数据。
4.3 用jadx验证脱壳成果
打开jadx,把dump目录里的dex文件拖进去,然后按包名路径找你的业务类。如果能看到com.example.app下的Activity、Fragment、网络请求类等,恭喜,脱壳成功。
如果看到的全是壳类或系统类,大概率是dump时机太早,壳还没把真实dex解密加载完。我遇到这种情况的补救办法是:先手动正常打开App,等页面完全加载,然后用attach模式重新来一次:
frida-dexdump -U -n com.example.app -o dump2-n参数指定的是进程名称。这种方法能覆盖到App运行中后期才按需加载的dex,特别是那些用到某个功能才动态加载进来的模块。
4.4 备选方案:如果frida-dexdump收获寥寥
frida-dexdump也不是万能的。某些加固方案做了更强的混淆和反调试,或者dex在内存中不是连续完整存在,而是分片加密、按方法动态解密执行,这时候扫描整段内存就变得困难。
遇到这种情况,可以尝试进阶路线:用Frida脚本主动遍历ClassLoader列表,沿着ClassLoader对象里的pathList字段,找到Element数组,再从Element里拿DexFile对象,反射调用相关方法把dex内容取出来。这个方案的本质是“通过ART虚拟机自身的加载信息来还原dex”,对一部分frida-dexdump无能为力的加固有效。
不过这个方案对新手并不友好,需要你懂一点Java反射和Frida脚本的语法。第一次不建议碰它,先把基础方案跑通再说。真要深入了,后面可以单独写一篇进阶篇。
5. 脱壳之后的三个坎,你得提前知道
5.1 第一坎:dump出来的可能是odex/vdex,不是标准dex
Android 8.0以上,ART虚拟机的运行时dex格式可能不是我们熟悉的classes.dex,而是odex/vdex这些衍生格式。jadx虽然能解析一部分,但偶尔会报错打不开。
遇到这种情况,思路是先把文件转换成标准dex。社区里常见的工具是vdexExtractor、oat2dex等。转换时要注意:不同的工具对不同Android版本的兼容性不同,Genymotion的Android 9和Android 7生成的文件格式有差异,需要试错。不过大多数常规App在Android 9上dump出来的依然是可以直接解析的标准dex,这个问题只在部分场景出现,不用太焦虑。
5.2 第二坎:脱壳成功但代码被混淆过
脱壳只是把“门”打开了,但门后面还有一道“雾”。很多App在加固之前会先做ProGuard/R8混淆,类名、方法名、变量名全变成a、b、c、d。你看到这种代码别以为脱壳失败了——脱壳解决的是“代码在不在内存里”的问题,混淆解决的是“代码好不好读”的问题,这是两个完全不同的层面。
应对混淆代码,我的经验是:先把jadx里按包名排序,肯定能找到一些没被混淆的工具类或三方库类;再通过AndroidManifest里的入口Activity和Service,找到那些必须保留名字的类,这些类是手动梳理逻辑的锚点;最后用jadx的导出源码功能,把可读的Java源码导出来,放到Android Studio或IDEA里做交叉搜索,效率会高不少。
5.3 第三坎:字符串加密和动态加载
有些App对关键字符串做了一层加密,运行时会解密后拼接使用。你在静态反编译的代码里看到的是一堆类似new String(Base64.decode("xxx"))的代码,或者是一堆native方法调用。这类保护已经不属于加固脱壳的范畴,更多是代码层面的对抗,需要结合动态调试和hook工具去还原。
对零基础读者来说,这三个坎可以放一放,先把脱壳这条主链路跑通,再按需进阶。
6. 常见问题与排查技巧实录
6.1 问题速查表
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| frida-ps -U 提示unable to connect | 电脑端Frida和frida-server版本不一致 | 两边版本号必须严格一致,重新下载frida-server并推入设备启动 |
| spawn后App秒退 | 目标App存在Frida检测机制 | 尝试attach模式(-n参数),或换一台更干净的设备环境 |
| adb devices列表是空的 | adb服务异常或模拟器未完全启动 | 执行adb kill-server和adb start-server,重启adb服务 |
| dump目录只有1个很小的dex | dump时机太早,壳还没来得及解密真实dex | 改用attach模式,或先手动打开App让页面完全加载再脱壳 |
| jadx无法打开dump文件 | 文件是odex/vdex等非标准dex格式 | 用vdexExtractor等工具转换后再用jadx打开 |
| 看到大量a、b、c类名 | 加固之外还做了代码混淆 | 这不算脱壳失败,按混淆分析思路处理 |
| dump下来的dex有很多,内部全是系统类 | 扫描到了ART虚拟机的框架dex | 不奇怪,逐个查看,业务dex通常集中在和目标包名有关的路径下 |
6.2 我踩过的三个坑,希望你绕开
第一个坑:frida-server版本不匹配。这是新手最容易遇到的。我有一台电脑上Frida客户端更新到16.7版本,模拟器里的frida-server还停留在16.6,结果frida-ps直接报unable to connect。排查了十分钟才想到版本问题。解决方式就是重新下载16.7的frida-server,推上去重启。强烈建议:在环境配置完成后,先在模拟器里跑一次frida-ps,确认没问题再开始脱壳。
第二个坑:Genymotion的root权限在不同镜像上有差异。有些镜像进去就是root,有些必须su -c。遇到Permission denied先别慌,adb shell进去跑一下id看看当前用户,再决定启动方式。如果su路径不统一,也可以试试在Genymotion的shell里直接以root执行。
第三个坑:dump时机拿捏不准。加固App启动后,壳代码要先解密、加载、校验,真实dex进入内存是需要时间的。我第一次实操时,App刚spawn完就立刻dump,结果只得到一个壳的dex,差点以为工具坏了。后来改成让App多跑几秒、甚至打开几个页面后再脱壳,结果完全不一样。如果你也遇到这个问题,优先考虑是不是时机没选对。
7. 脱壳技能还能怎么用在真实场景里
这部分算是给愿意深入的人一个方向提示。脱壳之外,大多数安全分析和逆向流程里,你会频繁遇到类似的手段:抓包、Hook、动态调试、so层分析。脱壳解决的是“看见Java层代码”的问题,但这只是第一步。你用frida-dexdump成功一次之后,顺手可以试试frida自带的一些Hook能力,比如hook一个方法看参数和返回值,简单且收获感极强。
还有一个很实际的应用场景:App升级后,你之前积累的分析笔记失效了。很多商业App每次发版都会重新加固、重新混淆,但包名和核心类名不会大变。有了脱壳这条流水线,你可以快速把新版本的dex导出来,和自己之前的笔记做diff,看看到底改了什么。这对做合规分析、版本对比、历史追溯都很有用。
我自己在做一些App的隐私行为审查时,标准流程就是:脱壳 → 反编译 → 导出Java → 在IDE里全局搜索隐私相关API调用 → 模块级梳理行为路径。脱壳是这套流程里最前置、最有“破防感”的一步,也是最容易让新人获得成就感的一步。
说到底,脱壳这件事真不难,难的是环境偶尔抽风、时机不对、工具版本打架,这类问题解决多了,你的排查能力自然就长上来了。我个人的体会是,第一次跑通frida-dexdump看到业务类出现的那一刻,你会觉得前面所有的环境折腾都值了。后面如果你想继续深挖,可以试着写自己的Frida脱壳脚本、研究不同加固厂商的加载时机差异,甚至尝试对着FART源码理解系统层脱壳的原理。路很长,但起点就是眼前这条命令。