ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

APK反编译与重打包实战:apktool、dex2jar、签名验证全流程

APK反编译与重打包实战:apktool、dex2jar、签名验证全流程

简介:这是一套面向 Android 开发与安全研究者的 APK 反编译工具集合,适合已具备基础 Android 开发经验、希望学习他人代码实现、进行恶意样本分析或二次开发的中高级技术人员使用。资源整合了 apktool、auto-sign、dex2jar 与 jd-gui 四个常用组件,覆盖从 APK 解包、资源与清单文件还原、DEX 转 JAR 到图形化查看 Java 源码的完整链路。压缩包为 rar 格式,内含 58 个文件,约 10.58MB,以 17 个 bat 与 14 个 sh 脚本承载各工具在 Windows 与 Linux 下的命令行入口,11 个 jar 构成 apktool、dex2jar、signapk 等核心程序,另有 3 个 exe 图形界面程序、9 个 txt 说明文档以及签名所需的 pem、pk8 密钥文件,开箱即可搭建本地反编译环境。目前已有 13978 人学习下载,工具链组合成熟、脚本齐备,便于读者按需查阅官方说明并快速上手实践。

1. 一个只剩 APK 的交接包,把我逼回了这套工具链

上周接手一个交接项目,仓库里只剩一个 40MB 的 release 包,源码、构建脚本、签名文件全丢了。要查的东西很具体:某个第三方 SDK 的版本号、一张图片资源被谁引用、清单里多出来的权限是谁塞的。这种时候 APK 反编译工具就是唯一入口——apktool 把资源和 smali 拆开,dex2jar 配 jd-gui 把 dex 还原成能读的 Java,auto-sign 负责改完之后重新签名装回设备。这套组合不算新,但在「只想快速看清一个包」的场景里依然最省事。下面把我跑通的流程、参数和翻车点摊开写,适合审计自有或已获授权应用、排查资源与 SDK 依赖的开发测试同学。

2. apktool 拆包与回编译:目录结构、开关参数与 smali 的可读边界

apktool 干的事,是把一个二进制 APK 拆成「人能看、也能再打回去」的工程目录。它内部做了两件独立的工作:用自带的解析器把AndroidManifest.xml、resources.arsc和各类 XML 还原成可读文本,用 baksmali 把classes.dex反汇编成 smali 汇编。这两件事可以分开做,也正因为能分开,后面排查问题时才能只拆一半、少踩很多坑。

先把定位说清楚:apktool 不产出 Java 源码,它产出的是 smali。smali 是可读、可改、可回编译的 Dalvik 汇编,寄存器、类型描述符、方法签名都是显式的。想读业务逻辑的 Java 形态,得走第 3 章的 dex2jar 路线;想改资源、改清单、改一个常量的返回值,apktool 这条链更直接。

2.1 环境准备与三种调用方式

apktool 本身是 Java 程序,跑之前先确认 JDK 在位。版本上没必要追新,JDK 8 到 17 都能覆盖绝大多数包,反而是 JDK 太新偶尔会遇到老版本 apktool 的反射告警。

# 先看 Java 是否可用,输出里要有 version 行 java -version # 方式一:官方 wrapper 脚本(Linux / macOS),脚本和 jar 放同一目录 chmod +x apktool apktool.jar ./apktool --version # 方式二:直接调 jar,适合在 CI 或多版本共存的机器上用 java -jar apktool_<version>.jar --version # 方式三:Windows 下用 apktool.bat,本质也是包了一层 java -jar apktool.bat --version

三种方式效果一致,区别只在路径管理。我一般在项目里固定放一份 jar,用完整路径调用,避免不同 shell 会话里apktool指向不同版本——这类「昨天还能拆今天报错」的玄学,十有八九是 PATH 里有第二份旧 jar。

框架资源(framework-res)是另一个容易被忽略的前置。系统应用或者带特殊主题的包,反编译时会引用厂商私有资源,apktool 找不到就会在回编译阶段报资源 ID 不存在。常规做法是先装一份对应系统版本的框架:

# 从设备或系统镜像里取 framework-res.apk 后安装到本地框架目录 apktool if framework-res.apk # 多版本共存时打标签,拆包时用 -p 指定 apktool if framework-res.apk -t android-33 apktool d target.apk -p android-33

if是 install-framework 的缩写,-t给这份框架起别名,-p在拆包时按别名取用。第三方应用绝大多数不需要这一步,只有碰到系统级包或定制 ROM 里的应用才用得上,别一上来就装一堆框架把自己绕进去。

2.2 拆包:一条命令背后的四类产物

最常用的拆包命令就一行,但每个开关都对应一个明确的取舍。

# 标准拆包:资源 + smali 全拆,-f 覆盖已存在的输出目录 apktool d app-release.apk -o out_full -f # 只拆资源,跳过 dex 反汇编,速度快很多,适合只查清单和图片 apktool d app-release.apk -o out_res -s -f # 只拆 dex,跳过资源解析,适合只想看 smali 逻辑 apktool d app-release.apk -o out_dex -r -f # 大包只反编译主 dex,减少等待时间 apktool d app-release.apk -o out_main --only-main-classes -f

-o指定输出目录,不给就按 APK 文件名建目录;-f强制覆盖,不加的话目录已存在会直接退出;-s是 skip sources 的缩写,只保留classes.dex原文件不做反汇编;-r是 skip resources,资源相关目录不解析;--only-main-classes只处理主 dex,分包里的classes2.dex、classes3.dex原样保留,适合先摸清主逻辑再决定要不要全量拆。

拆完的目录长这样,每类产物对应不同的排查目标:

产物路径内容典型用途
AndroidManifest.xml反编译后的明文清单查权限、组件导出状态、debuggable标记
res/解码后的图片与 XML 资源查图标、布局、字符串,替换后回编译
smali/、smali_classes2/各 dex 对应的 smali 目录定位常量、改返回值、追调用链
apktool.yml版本、压缩方式、资源 ID 偏移等元信息回编译失败的第一个排查点
original/原始清单与签名信息副本对照反编译结果是否失真
lib/、assets/原生库与资产文件,通常原样保留查 so 版本、看内置配置

original/这个目录值得单独说一句。apktool 会把原始AndroidManifest.xml和META-INF下的签名文件备份在这里,方便你比对反编译结果有没有丢东西。很多人拆完直接删掉,等回编译报错又找不到对照物,只能重新拆一遍,白等十分钟。

2.3 回编译与 smali 的可读边界

改完东西要打回去,命令同样简单,但参数选择直接影响产物能不能装。

# 标准回编译,输出未签名的 APK apktool b out_full -o repacked.apk # 资源解析报错时,换上 aapt2 解析器重试 apktool b out_full -o repacked.apk --use-aapt2 # 保留原始清单和签名块,只替换资源 apktool b out_full -o repacked.apk -c

-o是输出文件名,--use-aapt2切换资源编译器,-c是 copy original 的缩写,会沿用original/里的清单和签名块。第三种用法在「只换图片资源」时特别省事,能绕开一堆资源 ID 重排导致的引用错乱。

回编译出来的 APK 是未签名的,直接adb install会报INSTALL_PARSE_FAILED_NO_CERTIFICATES,这是正常的,签名流程见第 4 章。

现在说得现实一点:smali 的可读性是有上限的,能不能读懂,取决于这个包做了多少保护。

第一层是代码混淆。类名方法名变成a、b、a$a这种,交叉引用还在,但语义没了。这类包读 smali 的体验是「结构清晰、含义不明」,适合定位具体常量和方法调用,不适合通读业务。真要理解逻辑,还是得靠 Java 层面的反编译再配合调用链推断。

第二层是字符串加密。常见做法是把关键字符串塞进一个解密方法,运行时才还原。smali 里能看到的是const-string v0, "x9f2..."加一次静态方法调用。这时候光看 smali 没用,得找到解密函数,要么按它的算法在本地跑一遍,要么在设备上挂调试器看返回值。这一步是分水岭,很多人到这儿就卡住了。

第三层是资源混淆。资源路径被重写成res/a/b.png这种短名,resources.arsc里的 ID 也重排过。表现是 apktool 能拆开,但你不知道哪张图对应哪个界面。常规做法是按文件大小和图片尺寸筛,或者回编译后装上跑一遍,用布局检查工具反查。

提示:只对自有应用或已获得书面授权的应用做反编译。第三方商业包的保护措施往往对应明确的授权条款,动手前先确认边界,别把技术问题变成合规问题。

3. dex2jar 加 jd-gui:从 classes.dex 到能读的 Java,卡点在哪

apktool 给的是 smali,dex2jar 加 jd-gui 给的是 Java 形态的近似源码。这两条路不冲突,实际排查里经常是先用 jd-gui 快速定位到某个类,再回 smali 里改。理解 dex2jar 干了什么,比记住命令更重要:它把 Dalvik 字节码翻译成 JVM 字节码,产出一个标准 jar,jd-gui 再用自己的反编译器把 class 还原成 Java。翻译和还原是两次有损转换,所以结果一定是「接近」而不是「等于」原源码。

3.1 先从 APK 里精确取出 dex

dex2jar 的输入是 dex 文件,不是 APK。多 dex 的包要逐个处理,别指望它自己遍历。

# 只解出 dex 文件,其余条目不解,速度快 unzip -o app-release.apk 'classes*.dex' -d dex_out # 确认拿到了几个 dex,分包应用通常有 2 到 5 个 ls -lh dex_out # Windows 下没有 unzip 时,用 PowerShell 的 Expand-Archive 解全包再挑 # Expand-Archive -Path app-release.apk -DestinationPath apk_out

'classes*.dex'加引号是为了让 shell 把通配符交给 unzip 处理,不加引号在部分环境下会被当前目录的同名文件干扰。-d指定输出目录,-o覆盖已有文件。这一步看着琐碎,但它决定了后面转换的输入是否干净——直接把整个 APK 丢给 dex2jar 是不行的。

3.2 dex2jar 转换与参数说明

拿到 dex 之后逐个转 jar。Linux / macOS 用.sh,Windows 用.bat,参数一致。

# 单个 dex 转 jar,-f 强制覆盖已有输出,-o 指定 jar 路径 ./d2j-dex2jar.sh dex_out/classes.dex -f -o jars/classes.jar # 多 dex 逐个转,输出名带序号,避免互相覆盖 for f in dex_out/classes*.dex; do base=$(basename "$f" .dex) ./d2j-dex2jar.sh "$f" -f -o "jars/${base}.jar" done # 转换过程报异常但想继续,用 --skip-exceptions 跳过无法处理的指令 ./d2j-dex2jar.sh dex_out/classes.dex -f --skip-exceptions -o jars/classes.jar

-f强制覆盖输出文件,不加的话目标存在会直接失败;-o指定输出 jar 路径,不指定就按输入名生成在同目录;--skip-exceptions遇到无法翻译的指令时跳过而不是中断,代价是产物里对应方法体可能缺失。转换过程中刷出来的WARN大多是正常的,只要最后有Done且 jar 文件大小合理,就可以往下走。

多 dex 分开转是对的,但要知道后果:classes.jar和classes2.jar是两个独立的 jar,jd-gui 打开其中一个时,引用另一个 jar 里的类会显示成未解析状态。这是 dex2jar 路线的固有短板,不是操作失误。想省事的话,可以在 jd-gui 里把多个 jar 一起加进 classpath,或者直接换用能一次性加载整个 APK 的反编译工具做补充阅读。

3.3 jd-gui 打开与阅读技巧

jd-gui 是个 GUI 程序,双击 jar 就能开,但大包直接双击大概率卡死。原因是默认堆内存太小,加载几千个类就撑爆了。

# 给 jd-gui 更大堆内存,2G 起步,超大包给到 4G java -Xmx2g -jar jd-gui.jar # 打开指定 jar,省去手动选文件 java -Xmx2g -jar jd-gui.jar jars/classes.jar

-Xmx控制最大堆,32 位 JVM 用不了太大,确认是 64 位再加。打开之后有几个操作能明显提效:Ctrl+F全局搜索类名和方法名,比在目录树里翻快得多;Ctrl+Shift+T按类名跳转,前提是类名还没被混淆成单字母;右键菜单里的「查找用法」能追引用,但对跨 jar 的引用基本失效。

阅读上有几个判断要提前建立。一是 Kotlin 代码反编译后结构会很怪,协程、扩展函数、内联类还原出来经常是一堆带$的合成类,这不是工具坏了,是字节码层面的信息损失。二是 lambda 表达式会被还原成lambda$xxx$0形式的合成方法,读的时候要跟调用点对上。三是泛型信息在字节码里是擦除的,反编译出来的List往往不带类型参数,得靠上下文推。

还有一个现实问题:dex2jar 对新版 dex 格式的支持是滞后的。遇到Unsupported dex version这类报错,先别怀疑包有问题,大概率是工具版本跟不上 Android 的 dex 版本迭代。解法有两个,换新版的 dex2jar,或者改用直接解析 APK 的反编译工具。我的习惯是两条腿走路——dex2jar 加 jd-gui 用来快速浏览和搜索,遇到读不动的地方换另一个工具交叉验证,两边结论一致才敢下判断。

4. auto-sign 与现代签名链:从测试证书到 apksigner 的完整顺序

改完的 APK 不签名装不上,这是硬门槛。auto-sign 是一批老脚本的统称,核心逻辑是用内置的测试证书对 APK 做 v1 签名,一条命令搞定。它在「自己改着玩、装模拟器验证」的场景里确实方便,但放到今天有两个明显局限:用的是公开测试证书,任何设备都不会信任它为正式发布身份;只做 v1 签名,对 targetSdk 30 以上的包不够用。所以真正要落地,还是得走 keystore 加 apksigner 这条现代链路。auto-sign 可以留着当快速验证的快捷方式,别拿它当发布流程。

4.1 auto-sign 到底做了什么

这类脚本通常包含一个sign.bat/sign.sh、一个签名工具的 jar,以及一对测试密钥文件。执行时它做的动作是:把 APK 里的META-INF清掉,用测试私钥对每个条目算摘要写进CERT.SF和CERT.RSA,完成 v1 签名。

# 典型用法:传入待签名 APK,输出到指定文件 ./sign.sh repacked.apk signed_test.apk # Windows 下 sign.bat repacked.apk signed_test.apk

用起来是省事,但要知道自己在用什么。测试证书的私钥是公开的,意味着任何人都能伪造出「同一个签名」的包,所以它只适合本机调试。另外它不处理 zipalign,签完的包在部分设备上会因为未对齐而影响加载效率,甚至在某些校验严格的场景下装不上。

4.2 自己生成 keystore 的完整命令

正式一点的做法是自己生成密钥库,一次生成长期复用。下面的命令生成一个 2048 位 RSA 密钥、有效期 10000 天的 keystore。

# 生成 keystore,-dname 里的信息按自己项目填,CN 建议用项目代号 keytool -genkeypair -v \ -keystore demo-release.jks \ -keyalg RSA -keysize 2048 -validity 10000 \ -alias demo \ -storepass <store_password> -keypass <key_password> \ -dname "CN=demo, OU=dev, O=internal, L=city, ST=state, C=CN" # 查看 keystore 里的条目,确认 alias 和有效期 keytool -list -v -keystore demo-release.jks -storepass <store_password>

-keyalg和-keysize决定密钥强度,RSA 2048 是当前通用选择;-validity是有效天数,Google Play 要求至少到 2033 年,自己算好;-alias是条目别名,签名时要对上;-storepass和-keypass在 PKCS12 格式下通常要一致,不一致会告警。-dname里的字段不用太较真,但不能留空。

提示:这个 keystore 一旦用于发布就不能丢也不能换。换签名意味着已安装用户无法覆盖升级,只能卸载重装。生成后至少留两份异地备份,密码单独存。

4.3 对齐与签名顺序,以及 v1/v2/v3 的选择

这一段是血泪经验最集中的地方:顺序错了,包能装上但校验不过,或者干脆装不上。

正确顺序是先 zipalign 对齐,再 apksigner 签名。原因是签名会往 APK 里写入签名块并改变文件偏移,如果先签名后对齐,对齐操作会破坏已有签名。反过来,先对齐再签名,apksigner 会自己处理对齐后的偏移,v2 及以上签名不受影响。

# 第一步:4 字节对齐,-p 让 so 文件按页对齐,-c 只检查不写文件 zipalign -p -f 4 repacked.apk aligned.apk # 验证对齐结果,输出 Verification succesful 才算过 zipalign -c -v 4 aligned.apk # 第二步:用 apksigner 签名,默认会同时产出 v1 和 v2 apksigner sign \ --ks demo-release.jks --ks-key-alias demo \ --ks-pass pass:<store_password> --key-pass pass:<key_password> \ --out signed.apk aligned.apk # 第三步:验证签名并打印证书指纹 apksigner verify --print-certs signed.apk

zipalign的-p针对未压缩的 so 做页对齐,-f覆盖输出,4是对齐字节数,不能改。apksigner sign的--ks指定 keystore,--ks-key-alias对应生成时的别名,--ks-pass和--key-pass传密码,--out是输出文件。最后一个位置参数是输入 APK,位置别搞反。

签名方案的覆盖关系值得记一下:

方案引入版本覆盖范围备注
v1很早Android 7.0 以下必须基于 jar 签名,逐条目摘要
v2Android 7.07.0 到 9.0 优先使用整包签名,校验更快,防篡改更强
v3Android 9.09.0 及以上优先使用支持密钥轮换
v4Android 11与增量安装配合依赖单独的签名文件,常规打包用不到

实际打包时不用手动选,apksigner 会根据--min-sdk-version和--max-sdk-version自动决定生成哪几套。targetSdk 30 以上的包,v2 是硬性要求,只做 v1 会被系统拒绝安装。这也是 auto-sign 在今天不够用的直接原因。

5. 避坑与排查:五类翻车现场的现象、原因和解法

这一章按「现象、原因、解决」写,都是我在真实排查里反复遇到的。顺序按流程走,从拆包到反编译再到签名安装,碰到哪条对哪条。

5.1 拆包与回编译阶段

现象一:回编译报No resource identifier found for attribute或某个资源 ID 找不到。

原因通常是两类。一类是包里引用了厂商私有资源,本地没装对应框架;另一类是拆包时用了-r跳过资源解析,但 smali 里还引用了资源 ID,回编译时对不上。前者的判断方法是看报错里提到的属性名,如果是android:开头之外的自定义命名空间,基本就是缺框架。后者更常见,很多人只想看 smali 就加了-r,改完直接b,必然失败。

解决办法:缺框架就apktool if装上对应版本并用-p指定;用-r拆的包不要直接回编译,重新完整拆一遍再改。另外检查apktool.yml里的sdkInfo和versionInfo,这两个字段被手动改错也会导致资源编译异常。

现象二:回编译成功,装上去闪退,日志里是Resources$NotFoundException。

原因是资源 ID 在回编译时被重排了,而 smali 里硬编码的资源 ID 还是旧的。apktool 默认会尽量保持 ID 稳定,但如果原包做过资源混淆,或者拆包和回编译用的 apktool 版本不一致,映射就会错位。

解决办法:拆包和回编译必须用同一个 apktool 版本,这是硬性要求。如果原包资源被混淆过,尽量走-c保留原始资源的路径,只替换目标文件。排查时可以对照res/values/public.xml(如果有)里的 ID 和 smali 里的const值。

5.2 反编译与签名阶段

现象三:dex2jar 报Unsupported dex version,转换直接中断。

原因是 dex2jar 的版本落后于 dex 格式版本。新版 Android 工具链产出的 dex 会带更高的版本号,老版本工具解析不了文件头。

解决办法:换用更新的 dex2jar 版本;如果换版本还不行,改用能直接解析 APK 的反编译工具做交叉验证。这类问题不要在原地死磕,工具链迭代跟不上是常态,换路子比修工具快。

现象四:签名验证通过,但安装报INSTALL_FAILED_UPDATE_INCOMPATIBLE。

原因是设备上已装的版本用了不同签名的证书。反编译重打包必然换签名,系统把这当成另一个应用,拒绝覆盖。

解决办法:先卸载旧版本再装,注意卸载会清数据。如果要保留数据做对比,用adb install -r之前先确认签名一致,或者干脆装在另一台设备/另一个用户空间里。这个坑在测试机上尤其容易撞,因为测试机上往往已经装着正式包。

现象五:apksigner verify报DOES NOT VERIFY,或者提示 v2 签名缺失。

原因基本是两个:对齐和签名的顺序搞反了,先签名后 zipalign 破坏了签名块;或者用了jarsigner只做了 v1,而包的 targetSdk 要求 v2。前者在「照着老教程走」的场景里特别常见,后者是把老流程直接搬到新包上。

解决办法:严格按 align 再 sign 的顺序重做一遍;需要 v2 就用 apksigner,不要用 jarsigner。验证时加--print-certs看证书指纹,和自己的 keystore 对得上才算真签成功。如果verify通过但装不上,再看--min-sdk-version是不是设得比设备版本高。

6. 差分对比与签名验证:改完之后怎么证明自己没改坏

改包最怕的不是改不动,是改完之后说不清自己改了什么。尤其是排查「这个包和上一版差在哪」这类问题时,靠肉眼比对目录树完全不现实。我的做法是固定一套差分加验证的流程,每次改完强制跑一遍,五分钟换一个确定性。

6.1 用目录差分定位真实改动

思路很简单:把两个版本的 APK 分别拆到独立目录,用 diff 递归比对,过滤掉 apktool 自己生成的元信息噪声。

# 两个版本分别拆包,输出目录分开 apktool d app-v1.apk -o out_v1 -f apktool d app-v2.apk -o out_v2 -f # 递归差分,只输出有差异的文件名,-q 抑制相同内容 diff -rq out_v1 out_v2 | grep -v 'apktool.yml' # 只看 smali 目录的差异,快速定位代码改动范围 diff -rq out_v1/smali out_v2/smali | head -50 # 对单个文件看具体改动 diff -u out_v1/AndroidManifest.xml out_v2/AndroidManifest.xml

-r递归,-q只报文件名不报内容,两个参数配合能把噪声压到最低。过滤apktool.yml是因为它记录了拆包时间等信息,每次都不一样,留着会淹没真正的差异。head -50是防止输出太长刷屏,确认范围后再去掉限制看全量。

差分结果里最值得关注的是三类:清单文件的权限和组件变化、res/下的新增资源、smali/里新增或被修改的类。前两类直接对应功能变化,第三类往往能定位到具体 SDK 的引入。如果差异里出现大量 smali 文件同时变动但内容只是行号偏移,说明是编译期优化导致的,不用管。

6.2 四条命令确认包没被改坏

改完、签完,装之前先跑一遍验证。下面四条覆盖了版本信息、对齐、签名和证书身份,缺一条都可能留下隐患。

# 1. 看包的版本号、包名和 minSdk,确认改的不是这一版 aapt dump badging signed.apk | head -5 # 2. 检查 4 字节对齐,输出 Verification succesful 才算过 zipalign -c -v 4 signed.apk | tail -3 # 3. 验证签名方案和完整性 apksigner verify -v signed.apk # 4. 打印证书指纹,和自己的 keystore 比对 apksigner verify --print-certs signed.apk

aapt dump badging的输出里,package: name=是包名,versionCode和versionName是版本,第一行就能看完。zipalign -c是只检查模式,-v打详细结果,末尾必须是成功提示。apksigner verify -v会列出 v1、v2、v3 各自的验证结果,哪一项false都说明有问题。--print-certs输出 SHA-256 摘要,和keytool -list -v里的指纹逐位对上才算签的是自己的证书。

这四步里最容易跳过的是第二步。很多人签完直接verify通过就装了,结果在低端设备上加载慢或者偶发资源读取失败,回头查半天才发现是没对齐而不是代码问题。

验证项命令通过标准失败后果
版本信息aapt dump badging包名版本符合预期改错包,白忙一场
对齐zipalign -c -v 4输出 succesful加载变慢,偶发读取失败
签名完整性apksigner verify -vv1/v2 均 true安装被拒或运行校验失败
证书身份--print-certs指纹与 keystore 一致无法覆盖升级,用户数据丢失

差分和验证这两件事,单独看都不复杂,难的是养成固定动作。我现在的习惯是:任何一次改包,先拆两份做差分,改完按上面四条命令过一遍,全部通过才往设备上装。这套流程救过我两次——一次是差分发现包里多了一个不在预期内的权限,追下去是某个依赖版本被动升级带进来的;另一次是verify报 v2 缺失,避免了把只做了 v1 的包发出去。从那以后我每次改包都强制走一遍差分加四条验证,不再靠「装上去能跑就行」这种运气判断。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进