简介:这套工具集面向Android逆向工程师、安全研究人员及需要审计APK的开发者,围绕脱壳、反编译与源码查看三个环节提供完整方案。包内共43个文件,以jar、bat、sh、exe为主,另有cfg配置、txt说明与示例apk,压缩包约40.51MB,跨平台脚本兼顾Windows与Linux环境。核心组件包括blackdex脱壳、apktool 2.2解包与Smali反编译、dex2jar 2.0转换Java字节码、jd-gui图形化查看类结构,以及Smali2Java还原代码逻辑,可组合成从脱壳到源码阅读的完整链路。已有295人学习,适合开发者自检代码质量、修复漏洞,也适用于安全人员检测恶意软件或分析混淆应用。使用时请遵守法律法规与软件版权,尊重知识产权。
1. 拿到一个加固 APK,先别急着反编译
很多人第一次接触 apk脱壳,是因为手里有个 APK 想看看它到底干了什么——可能是分析某个竞品的实现,可能是排查一个第三方 SDK 的隐私采集行为,也可能是自己公司的老项目源码丢了只剩安装包。结果用反编译工具一打开,classes.dex里只有几百个类,核心逻辑全不见了,字符串全是乱码,方法体里就一句native声明。这就是典型的加固壳在起作用。
这个方向要解决的核心问题就一句话:把被壳保护的真实 DEX 从内存或文件中还原出来,再交给反编译工具还原成可读源码。它适合三类人:做安全审计的、做竞品分析的、以及需要从遗留 APK 里抢救业务逻辑的开发者。整条链路是「脱壳 → 反编译 → 查看源码」,三步各有各的工具和坑,缺一步都拿不到能读的代码。下面按我实际干活的顺序拆开讲。
2. 脱壳:从内存里把真实 DEX 捞出来
2.1 先判断壳的类型,别上来就硬脱
加固方案分两大流派,判断错了后面全白干。第一类是整体加固:APK 里原始的classes.dex被加密或抽空,启动时由壳的Application在内存里解密并动态加载,代表特征是你的AndroidManifest.xml里application节点的android:name指向一个陌生的类,而classes.dex体积异常小。第二类是抽取壳(函数抽取):DEX 文件结构还在,但每个方法的字节码被抽走,运行时才回填,特征是反编译能看到类名和方法签名,但方法体是空的或者只有throw。
判断方法很直接,用unzip看 APK 内部结构:
# 列出 APK 内的 dex 文件及大小 unzip -l target.apk | grep -E "\.dex|lib/"如果只看到一个几百 KB 的classes.dex,但 APK 总大小有好几 MB,多出来的体积大概率在assets/下的加密文件和lib/下的 native 壳 so 里,这是整体加固。如果有多个classesN.dex且大小正常,但反编译后方法体为空,那是抽取壳。我一般还会顺手看一眼lib/目录下有没有名字可疑的 so,比如带protect、shell、jiagu这类字样的,基本能确认厂商方向。
提示:不要根据 so 文件名去搜厂商然后找现成的脱壳脚本,版本对不上会直接崩,老老实实按壳的行为特征来选工具。
2.2 内存 dump 的最小操作流程
整体加固的通用解法是运行时内存 dump:让 App 正常跑起来,壳把真实 DEX 解密加载进内存后,从进程内存里把 DEX 文件抠出来。核心工具是frida配合一个 dump 脚本。前提是设备已 root,或者用一台可调试的测试机。
先确认 frida 环境通了:
# 设备端启动 frida-server(需 root) adb shell su -c "/data/local/tmp/frida-server &" # 主机端确认能看到目标进程 frida-ps -U | grep -i target然后挂上 dump 脚本。下面是一个精简版的内存搜索逻辑,思路是遍历进程内存映射,找 DEX 文件头dex\n035或dex\n036:
# dump_dex.py - 在目标进程内存中搜索 DEX 魔数并落盘 import frida, sys JS_CODE = """ var ranges = Process.enumerateRanges('r--'); ranges.forEach(function(range) { try { // 在每段可读内存里搜 dex 魔数 var matches = Memory.scanSync(range.base, range.size, "64 65 78 0a 30 33 35 00"); matches.forEach(function(m) { // 读 DEX 头部拿到文件大小(偏移 0x20 处 4 字节小端) var size = m.address.add(0x20).readU32(); if (size > 0x1000 && size < 0x2000000) { var data = m.address.readByteArray(size); send({addr: m.address.toString(), size: size}, data); } }); } catch(e) {} }); """ def on_message(msg, data): if data: name = "dump_" + msg["payload"]["addr"] + ".dex" open(name, "wb").write(data) print("saved", name, msg["payload"]["size"]) process = frida.get_usb_device().attach("目标包名") script = process.create_script(JS_CODE) script.on("message", on_message) script.load() sys.stdin.read()逻辑说明:Process.enumerateRanges('r--')只遍历可读内存段,避免碰不可读区域导致崩溃。Memory.scanSync搜的是 DEX 文件头的魔数加版本号,64 65 78 0a是dex\n,后面30 33 35 00是035\0。拿到起始地址后,DEX 头部偏移0x20处是file_size字段,小端 4 字节,用它决定读多少。参数上,size的上下限0x1000到0x2000000是经验值,太小的是误命中,太大的是异常,按你目标 App 的规模可以调。
跑完之后你会得到一堆dump_xxx.dex,其中体积最大、类数量最多的那个通常就是主 DEX。用dexdump或直接丢进反编译工具验证一下类数量。
2.3 抽取壳的应对:主动调用回填
抽取壳光 dump 内存不够,因为方法体在内存里也是按需回填的,你没调用到的方法,字节码根本没还原。常见做法是主动调用触发回填:用 frida hook 住壳的ClassLoader或者遍历DexFile,对每个方法做一次artMethod的调用触发,让壳把字节码填回去,再 dump。
这块没有万能脚本,因为不同壳的回填入口不一样。我一般的思路是 hookdalvik.system.DexFile的loadClass或者defineClass,记录所有被加载的类,然后反射遍历每个类的getDeclaredMethods,对每个方法尝试setAccessible(true)后调用一次(注意参数要构造合法值,否则会抛异常,但异常本身也能触发回填)。这个过程容易把 App 搞崩,建议在测试机上跑,并且加 try-catch 包住每一次调用。
注意:主动调用有副作用,可能触发业务逻辑、发网络请求、写本地数据。跑之前把设备网络断掉,或者用一台干净的测试机,别在主力机上干。
3. 反编译:把 DEX 还原成能读的源码
3.1 工具选型:jadx 和 apktool 各管一段
脱壳拿到 DEX 之后,反编译工具的选择决定了你读代码的体验。我常用的组合是jadx看 Java 源码,apktool解资源,baksmali看 smali 字节码。三者定位不同:
| 工具 | 输入 | 输出 | 适用场景 |
|---|---|---|---|
| jadx | APK/DEX | Java 源码 | 快速读逻辑,首选 |
| apktool | APK | smali + 资源 | 改包、看 XML 和资源 |
| baksmali | DEX | smali | jadx 反编译失败时的兜底 |
jadx 的优势是直接把 DEX 转成近似 Java 的代码,可读性最好。但它对混淆过的代码、复杂的控制流、以及某些 lambda 表达式会反编译失败,输出// error或者直接跳过方法。这时候就得退回 smali 层面看。
3.2 jadx 命令行批量反编译
图形界面适合看单个类,但批量处理我习惯用命令行:
# 反编译单个 APK,输出到 out 目录 jadx -d out/ --show-bad-code target.apk # 只反编译指定的 dex(脱壳产物) jadx -d out_dex/ --show-bad-code dump_0x7f8a.dex # 导出为 gradle 工程结构,方便 IDE 打开 jadx -d out_proj/ --export-gradle target.apk参数说明:-d指定输出目录,--show-bad-code是关键,它让 jadx 在反编译失败时仍然输出部分代码而不是直接跳过,读起来虽然不完整但比没有强。--export-gradle会把结果组织成 Android 工程结构,用 IDE 打开后能跳转、能搜索,读大项目时效率高很多。
反编译完成后,重点看几个地方:AndroidManifest.xml确认入口和权限,Application类的onCreate看初始化逻辑,以及壳相关的类(通常包名很怪,比如单字母包名)确认脱壳是否干净。
3.3 反编译失败时的 smali 兜底
jadx 报错的方法,用 baksmali 转成 smali 看:
# 把 dex 转成 smali baksmali d dump_0x7f8a.dex -o smali_out/ # 反编译单个 class(需要先知道 smali 路径) # 直接看 smali_out/com/example/Foo.smalismali 是 Dalvik 的汇编,读起来累但信息完整。关键指令:invoke-*是方法调用,iget/iput是读写实例字段,sget/sput是静态字段,const-string是字符串常量。看 smali 时我一般先找const-string把关键字符串捞出来,再顺着字符串的引用往上找调用链,比从头读快得多。
提示:混淆过的代码里,类名方法名都是
a、b、c,别硬读。先找字符串常量、找网络请求的 URL、找 SharedPreferences 的 key,从这些有语义的锚点切入。
4. 查看源码:从能读到读懂的三个技巧
4.1 用字符串锚点定位核心逻辑
反编译出来的代码动辄几万个类,从头读是不可能的。我的习惯是先全局搜字符串。比如分析一个网络请求逻辑,搜https://、api/、token;分析加密逻辑,搜AES、RSA、Cipher、getInstance;分析存储,搜SharedPreferences、getString。
jadx 命令行支持导出后直接用grep -rn搜:
# 在反编译结果里搜关键字符串,定位文件和行号 grep -rn "api/v1/login" out/sources/ | head -20 # 搜加密相关调用 grep -rn "Cipher.getInstance" out/sources/ | head -20拿到文件和行号后,再回到 jadx 图形界面或者 IDE 里跳过去读上下文。这个方法比盲目读类结构快一个数量级。
4.2 交叉引用追调用链
定位到一个关键方法后,需要知道谁调用了它、它又调用了谁。jadx 图形界面里右键有「Find Usages」,命令行没有这个功能,但可以用grep模拟:搜方法名,看哪些文件引用了它。
# 假设关键方法是 checkSignature,搜所有引用 grep -rn "checkSignature" out/sources/ | grep -v "\.smali"顺着调用链往上追,通常能追到Application.onCreate或者某个Activity的入口,这样整个初始化流程就串起来了。往下追则能看到具体的实现细节,比如加密用的密钥从哪来、请求参数怎么拼。
4.3 动态验证:hook 关键方法看运行时值
静态读代码有个死穴:混淆和反射会让调用关系变得模糊,你以为是 A 调 B,实际运行时是 C 通过反射调的。这时候用 frida hook 一下,直接看运行时的方法参数和返回值,比读代码快。
// hook_check.js - 打印目标方法的入参和返回值 Java.perform(function() { var TargetClass = Java.use("com.example.a.b"); TargetClass.checkSignature.implementation = function(a, b) { console.log("arg1:", a); console.log("arg2:", b); var ret = this.checkSignature(a, b); console.log("ret:", ret); return ret; }; });逻辑说明:Java.use拿到目标类,.implementation重写方法体,在调用原方法前后打印参数和返回值。参数类型要跟原方法签名对上,不确定的话先用Java.use(...).class.getDeclaredMethods()把方法签名打出来看。这个脚本配合脱壳后的代码读,能快速验证你对某个方法的理解对不对。
5. 避坑:脱壳反编译里最容易翻车的五件事
5.1 dump 出来的 DEX 打不开,报 magic 错误
现象:frida dump 出一堆文件,用 jadx 打开提示not a valid dex file或者magic error。原因:内存里搜到的魔数命中不一定是 DEX 起始位置,可能是某个加密数据块里恰好出现了dex\n035字节序列;或者 DEX 在内存里是紧凑排列的,你读的file_size字段被壳改过。解决:先验证文件头,用xxd dump_xxx.dex | head -1看前 8 字节是不是6465 780a 3033 3500。是的话再看file_size字段是否合理。如果魔数对但大小不对,尝试固定读一个较大的值(比如 10MB)然后手动截断到实际 DEX 结束位置(DEX 末尾有map_off指向的 map 列表,可以据此判断真实结尾)。
5.2 脱壳后方法体还是空的
现象:DEX 能打开,类名方法名都在,但方法体是空的或者只有return-void。原因:这是抽取壳,你 dump 的时机太早,方法还没被调用回填;或者你 dump 的是磁盘上的 DEX 而不是内存里回填后的。解决:确认是抽取壳后,改用主动调用方案,遍历所有方法触发回填再 dump。另一个思路是 hook 壳的DexFile相关方法,在它回填完成的回调里 dump。时机很关键,dump 早了是空壳,dump 晚了进程可能已经退出。
5.3 jadx 反编译卡死或 OOM
现象:jadx 处理大 APK 时卡住不动,或者抛OutOfMemoryError。原因:jadx 默认堆内存不够,或者某个 DEX 里有畸形结构导致解析死循环。解决:加大 JVM 堆,jadx启动脚本里改-Xmx到 4G 或 8G。如果是单个 DEX 有问题,先用 baksmali 把它转成 smali 确认结构,再单独用 jadx 处理其他 DEX。批量场景下可以按 DEX 拆分处理,别一次性喂给 jadx。
5.4 反编译出来的代码变量名全是 a、b、c
现象:代码能读,但类名方法名变量名全是单字母,逻辑完全看不懂。原因:代码经过了混淆(ProGuard/R8),符号名被压缩了。解决:混淆是不可逆的,但可以靠字符串常量、API 调用序列、控制流结构来推断语义。我一般先找const-string里的有意义的字符串(URL、key、错误提示),以这些为锚点反推周围代码的用途。另外可以试试开源的混淆映射还原工具,但效果有限,别抱太大期望。
5.5 脱壳脚本在目标 App 上直接崩
现象:frida 脚本一加载,目标 App 闪退。原因:壳有反调试、反注入检测,检测到 frida 的痕迹就主动杀进程;或者你的内存扫描触碰了受保护区域。解决:先确认壳有没有反调试,常见特征是 App 启动后检测frida-server进程、检测/proc/self/maps里的 frida 相关 so、检测调试端口。应对思路是改 frida-server 的名字、用非标准端口、hook 掉检测函数让它返回正常值。内存扫描时把范围缩小到r--且排除[anon:libc_malloc]这类敏感段,减少触发概率。
6. 进阶:把脱壳和反编译串成可复用的流水线
单次脱壳反编译不难,难的是批量处理和版本迭代。我现在的习惯是把整条链路脚本化,形成一个可复用的流水线,这样每次拿到新版本 APK 只需要跑一遍脚本。
第一步是自动化脱壳。把 frida dump 脚本封装成接受包名参数的 Python 脚本,启动 App、等待加载、dump、退出,一气呵成。关键是要加一个「等待 DEX 加载完成」的判断,不能启动后立刻 dump,否则拿到的是空壳。我的做法是轮询内存里 DEX 的数量,连续几次稳定后再 dump。
第二步是自动筛选有效 DEX。dump 出来的文件里有很多误命中,用脚本过滤:读文件头验证魔数,读file_size验证大小,用dexdump -f看类数量,类数量少于 10 的直接丢弃。剩下的按类数量排序,取前几个作为候选。
第三步是批量反编译和索引。用 jadx 命令行批量处理候选 DEX,输出到统一目录,然后用grep建立字符串索引。我一般会建一个简单的倒排索引:把所有const-string和类名方法名提取出来,存成关键词 -> 文件:行号的映射,查的时候直接搜索引,比每次grep全目录快。
# 批量反编译 + 建索引的简化流程 for dex in candidates/*.dex; do jadx -d "out/$(basename $dex .dex)" --show-bad-code "$dex" done # 提取所有字符串常量建索引 grep -rn "const-string" out/ | sed 's/.*const-string[^"]*"\([^"]*\)".*/\1/' | sort -u > strings_index.txt这套流水线跑顺之后,从拿到 APK 到能开始读核心逻辑,大概十几分钟。剩下的时间全花在理解业务逻辑上,而不是跟工具较劲。
最后说个我踩过的坑:早期我总想找一个「万能脱壳工具」,试了一圈发现没有。每个壳的行为都不一样,同一个壳不同版本也不一样。后来我放弃了找万能工具,转而把精力放在理解壳的加载原理上——只要搞清楚了它怎么解密、怎么加载、怎么回填,针对性地写几十行 frida 脚本就能解决,比到处找现成脚本靠谱得多。工具会过时,原理不会。希望帮到你。
本文还有配套的精品资源,点击获取