简介:「Android逆向助手」面向具备一定 Android 基础的开发者与安全研究人员,整合 apktool、dex2jar、jd-gui、autosign 等常用组件,用于 APK 反编译、资源提取、Java 源码查看、重打包签名及基础安全审计,可服务于功能分析、性能排查与逆向学习等场景。压缩包共 69 个文件,约 15.21MB,包含 18 个 jar、17 个 bat、14 个 sh、8 个 exe 及若干 txt 说明与签名相关文件,兼顾 Windows 与脚本调用方式,便于按工具链组织使用。已有 145 人学习下载。读者可借助这套工具较快搭建从反编译到源码阅读、再到签名安装测试的完整环境,并结合说明文件理解各组件用途与调用关系;同时需注意逆向分析的版权与合规边界。
1. 从一份丢失源码的旧包说起:android逆向助手把四步手工活压成一条链
某公司接手了一套三年前上线的安卓客户端,原始工程早就找不到了,线上只剩一个 APK。产品要改一行文案,顺带定位一个只在部分机型上出现的崩溃。这时候 android逆向助手 这类整合包就派上用场:它把 apktool 解包、dex2jar 转 jar、jd-gui 读 Java、重新签名回装这几步固定下来,目录和批处理脚本都配好,解压即用。适合两类人——手里只有 APK 没有源码的维护者,以及想搞清楚第三方 SDK 到底在后台干了什么的开发者。不适合拿它去动别人的商业包,这条边界先划清楚,后面所有操作都建立在你对目标包有处置权限的前提上。
2. 三条链路的分工:apktool、dex2jar 与 jd-gui 到底谁管哪一段
先把 APK 的骨架摊开看。一个典型的安装包里有二进制格式的 AndroidManifest.xml、res/ 下的资源、resources.arsc 资源索引表、一个或多个 classes*.dex、lib/ 下的 so,以及 META-INF 里的签名文件。这堆东西分属两个世界:资源世界是可读可改的 XML 与图片,代码世界是 Dalvik 字节码。apktool 和 dex2jar 分别服务这两个世界,jd-gui 则是代码世界的显示器。
2.1 资源层与代码层为什么必须分开走
apktool 干两件事。第一,把 resources.arsc 和二进制 XML 还原成带可读标签的明文,让你能在 res/values/strings.xml 里直接改文案;第二,把 dex 反汇编成 smali,一种寄存器式的类汇编语言,语法严格但可读性差。这两类产物落在同一个工程目录里,可改法和验证方式完全不同——改 res 里的一个字符串,只需要回编译;改 smali 里的一条指令,你得自己保证 .registers 和 .locals 声明与新增代码的寄存器用量对得上,否则打包不报错,装上就崩。
dex2jar 走的是另一条路。它只关心 dex 里的类结构,把 Dalvik 指令映射回 JVM 字节码,产出一个标准 jar。这个 jar 不参与回编译,纯粹给反编译器读。所以流程上我一般先用 apktool 解一次拿到全貌,判断问题在资源还是在逻辑;如果落在逻辑上,再单独走 dex2jar 路线把 Java 结构看清楚。
| 工具 | 输入 | 输出 | 适合干的事 | 不适合干的事 |
|---|---|---|---|---|
| apktool | APK | smali + 明文资源 + 可读清单 | 改资源、改清单、改 smali、回编译 | 通读大段 Java 逻辑 |
| dex2jar | dex / jar | jar | 把字节码搬回 JVM 世界 | 处理重度混淆或新指令集 |
| jd-gui | jar / class | 可读 Java | 全文搜索、类跳转、看调用链 | 反编译失败时给有效提示 |
| zipalign + apksigner | 未对齐 APK | 可安装 APK | 对齐与签名 | 修改代码逻辑 |
有人会问,jadx 一个工具就能从 apk 直接出 Java,为什么还要这一套。理由很实际:jadx 强在“读”,但它不产出可回编译的 smali 工程,改完要重新打包还是得回到 apktool。所以常见组合是 apktool 管改和打,dex2jar + jd-gui 管读和搜,两边交叉验证。整合包的价值就在于把这两条链路的工具、脚本、输出目录统一到一棵树里,省掉每次翻文档找参数的时间。
2.2 目录约定与环境变量:让后续命令都能一行跑完
工具散落在不同目录是效率杀手。我一般先固定一个根目录,把所有可执行入口塞进 PATH,工作区按时间戳隔离,避免两次分析互相覆盖产物。
# 固定工具根目录,后续脚本全部从这里取二进制 export ARH_HOME="$HOME/android-reverse-helper" export PATH="$ARH_HOME/bin:$ARH_HOME/tools/jd-gui:$PATH" # 每次分析开一个独立工作区,产物不互相污染 export ARH_WS="$ARH_HOME/workspace/$(date +%Y%m%d_%H%M%S)" mkdir -p "$ARH_WS"/{unpack,dex,jars,signed} # 确认关键工具都在 PATH 上,缺一个后面就断链 for t in apktool d2j-dex2jar.sh apksigner zipalign; do command -v "$t" >/dev/null || echo "缺少工具: $t" doneARH_HOME是根,ARH_WS是本次分析的沙箱,{unpack,dex,jars,signed}四个子目录分别对应解包产物、抽出的 dex、转换后的 jar、签名后的安装包。这段循环是自检,缺工具时立刻报出来,比跑到一半卡在command not found强。
还有个容易忽略的前提:apktool、dex2jar、jd-gui 都是 JVM 系工具,java 版本要能覆盖它们的字节码版本。老版本 jd-gui 在新 JDK 上跑不起来是常态,这类工具通常自带或要求特定 JDK,不要拿系统里最新的 JDK 硬套。
# 记录实际使用的 Java 版本,排错时这是第一个要看的变量 java -version 2>&1 | head -n 3 # 无图形界面的机器上 jd-gui 起不来,需要命令行反编译器兜底 ls "$ARH_HOME/tools/jd-gui" | head参数说明:java -version的输出要留档,因为同一个 jar 在不同 JDK 上打开结果可能不同;ls这行是确认工具目录里有没有命令行前端。很多整合包会额外带一个 jd-cli 之类的命令行版,无头服务器上就靠它批量导出源码。
3. apktool 解包与回编译:从 APK 到可改的 smali 工程
解包是整个流程里最不容易出错的一步,但也最容易因为参数选错,导致后面该看的东西没解出来。apktool 的选项不多,关键是搞清楚每个开关放弃了什么。
3.1 反编译参数怎么配:-r、-s、--only-main-classes 的取舍
-r表示不反编译资源,res/ 保持原始二进制形态;-s表示不反汇编 dex,不产出 smali;--only-main-classes只处理主 dex,把 classes2.dex 之后的代码跳过。三个开关都是“减负”用的,用错了就是自断手脚。
# 全量反编译:资源和代码都要,第一次分析用这个 apktool d "$ARH_WS/app.apk" -o "$ARH_WS/unpack" -f # 只改资源时:跳过 dex 反汇编,速度快很多 apktool d "$ARH_WS/app.apk" -o "$ARH_WS/unpack_res" -s -f # 只读代码结构时:跳过资源,避免 resources.arsc 解析报错中断整个流程 apktool d "$ARH_WS/app.apk" -o "$ARH_WS/unpack_src" -r -f-o指定输出目录,-f强制覆盖已有目录。第二行的-s是不反汇编 dex,适合“我只想改个图标和文案”的场景;第三行的-r是不反编译资源,适合“这包资源有奇怪的自定义格式,apktool 解析到一半就退出”的场景。注意-r之后回编译时资源是原样塞回去的,所以这条链路不能用来改资源。
--only-main-classes要慎用。它只处理主 dex,产物里 smali 目录只有一个,回编译时其余 dex 会被原样保留——听起来很省事,但如果你的改动点恰好落在 classes2.dex 里的某个类,你会找不到它,然后误判成“这个类被删了”。
解包完成后先别急着改,花两分钟确认三件事:
# 1. 清单里的入口和版本信息,这是后面验证改动是否生效的基准 grep -E 'package=|versionCode|versionName' "$ARH_WS/unpack/apktool.yml" # 2. dex 数量,决定后面 dex2jar 要批量处理几个文件 find "$ARH_WS/unpack" -name 'classes*.dex' | wc -l # 3. 反编译期间有没有告警,W: 开头的行要逐条看 apktool d "$ARH_WS/app.apk" -o "$ARH_WS/unpack_check" -f 2>&1 | grep -i '^W:'apktool.yml里记录了解包时探测到的 sdkInfo、versionCode、版本名,回编译时会原样写回清单,这是改动前后对比的锚点。第二步数 dex,是因为后面 dex2jar 是按文件循环的,漏一个就漏一整包类。第三步重跑一次抓告警行,W:开头的通常是“某个资源没解出来”“某个 9-patch 畸形”,这类告警在回编译阶段会变成具体报错,提前知道能省一轮排查。
3.2 改完 smali 之后:zipalign 与 apksigner 的顺序不能反
回编译只是把目录重新压成 APK,产物没有有效签名,直接装会被系统拒绝。顺序是固定的:先 zipalign 对齐,再 apksigner 签名。反过来做,对齐操作会破坏签名块,最终得到一个“签名校验失败”的包。
# 第一步:回编译,产物是未签名未对齐的 APK apktool b "$ARH_WS/unpack" -o "$ARH_WS/unsigned.apk" # 第二步:4 字节对齐;较新的 build-tools 支持 -P 16 做 16KB 页对齐 zipalign -p -f -v 4 "$ARH_WS/unsigned.apk" "$ARH_WS/aligned.apk" # 第三步:签名,v1/v2 方案由 minSdk 自动决定 apksigner sign \ --ks "$ARH_HOME/keys/debug.keystore" \ --ks-key-alias demo \ --ks-pass pass:android \ --key-pass pass:android \ --out "$ARH_WS/signed/app-signed.apk" \ "$ARH_WS/aligned.apk" # 第四步:验证签名,这一步必须做,不能靠安装成功来倒推 apksigner verify -v --print-certs "$ARH_WS/signed/app-signed.apk"-p是页对齐,让 so 文件在 APK 内的偏移与内存页边界一致,直接影响加载效率;-f覆盖输出;4是 4 字节对齐,这个值不要改。-P 16是针对 16KB 页大小设备的对齐参数,只在目标机型或构建环境明确要求时加,加错了会导致旧设备安装异常。
签名参数里,--ks指向密钥库,--ks-key-alias是别名,两处密码分开给。--out必须和输入文件不同名,否则 apksigner 会拒绝覆盖。--print-certs打印证书指纹,用来确认用的确实是你那把钥匙,而不是整合包里自带的某个来历不明的测试证书。
调试用的密钥库可以自己生成,不要用别人包里带的:
keytool -genkeypair -v \ -keystore "$ARH_HOME/keys/debug.keystore" \ -alias demo -keyalg RSA -keysize 2048 -validity 10000 \ -storepass android -keypass android \ -dname "CN=demo, OU=dev, O=demo, L=NA, ST=NA, C=NA"-validity 10000是天数,别设太短,否则某天突然装不上还得重新签一遍。-dname里的字段全是占位值,仅用于本地调试,不要填真实信息。
回编译阶段最常见的失败是资源相关:某个 9-patch 的边框像素不合法、某个字符串里的格式化占位符顺序被打乱、某个自定义属性的命名空间丢了。这类报错会明确指到文件和行号,照着改就行。真正折磨人的是“编译通过、安装通过、启动就崩”,那基本都出在 smali 改动上,下一章的排查章会展开。
4. dex2jar + jd-gui:把 dex 变成能搜索的 Java 源码
apktool 给的 smali 能改但不好读,一个方法动辄几十行invoke-virtual,靠肉眼跟调用链基本不现实。dex2jar 把 dex 转成 jar,jd-gui 再把 jar 展现成 Java,这条链路解决的是“看懂”的问题。
4.1 多 dex 拆包与批量转换脚本
dex2jar 的输入是单个 dex 文件,所以第一步是从 APK 里把 dex 抽出来。APK 本身就是 zip,直接按通配符解压即可,注意要覆盖 classes.dex、classes2.dex、classes3.dex 这类全部命名的文件。
# 从 APK 里抽出所有 dex,通配符要写成 classes*.dex unzip -o "$ARH_WS/app.apk" 'classes*.dex' -d "$ARH_WS/dex" # 逐个转换,输出文件名与输入保持一致,方便回头对号入座 for f in "$ARH_WS"/dex/classes*.dex; do base=$(basename "$f" .dex) d2j-dex2jar.sh -f "$f" -o "$ARH_WS/jars/${base}.jar" done # 列出产物,数量应与抽出的 dex 数量一致 ls -lh "$ARH_WS/jars"-f是强制覆盖输出,批量跑的时候必须有,否则第二次执行会卡在“文件已存在”的交互提示上。-o指定输出路径,这里刻意保留classes/classes2的前缀,是为了后面在 jd-gui 里定位某个类时,能立刻知道它属于哪个 dex。最后的ls是自检,抽了 4 个 dex 只出了 3 个 jar,说明有一个转换失败了,得单独看那个文件的报错。
Windows 环境下脚本后缀是.bat,路径分隔符要换,别的不用动。如果整合包的 bin 目录里同时放了两个平台的启动器,注意别混着调,混用会出现“找不到主类”这种莫名其妙的问题。
转换完成之后用 jd-gui 打开 jar。jd-gui 是图形界面,打开后左侧是包树,右侧是反编译出的 Java 源码,支持全文搜索和 Ctrl+点击跳转。这一步的实操技巧不多,但有两个习惯值得养成:一是打开后先看包结构,混淆过的包在顶层会呈现大量单字母包名,一看就知道后面要费劲;二是搜索时优先搜字符串常量而不是方法名,字符串在混淆里通常保留原样,是定位功能点最可靠的锚。
4.2 转换成功但 jd-gui 里读不出东西的几种情况
第一种是打开 jar 后右侧空白或弹INTERNAL ERROR。原因多半在 dex2jar 这一侧:它对某些较新的字节码形态覆盖不全,转换时类文件结构合法但方法体丢了。判断方法是换一个反编译器打开同一个 jar,如果别的工具能出源码,就是 jd-gui 的问题;如果都出不来,问题在 jar 本身。
第二种是类名全是a、b、c,方法名全是a()、b()。这不是工具坏了,是包做了名称混淆。这时候靠 jd-gui 硬读效率极低,正确做法是回到 apktool 的产物,从 AndroidManifest.xml 里的入口 Activity 出发,顺着 smali 里的字符串常量反向定位。混淆能改名字,改不掉字符串。
第三种是反编译出的 Java 语法错乱,出现大量goto或无法识别的表达式。这通常是控制流混淆导致的,反编译器还原不出结构化语法。这时候放弃 Java 层,直接看字节码:
# 只看类结构和方法签名,不看方法体 javap -p -classpath "$ARH_WS/jars/classes.jar" com.example.target.Foo # 连字节码一起看,适合确认某个方法到底调了什么 javap -p -c -classpath "$ARH_WS/jars/classes.jar" com.example.target.Foo-p显示私有成员,-c反汇编方法体。字节码比 smali 抽象层级高一点,比 Java 低很多,正好卡在“看得懂又不会被反编译失败挡住”的位置。参数里的类名要用完整包路径加点号,不是斜杠。
提示:同一个 jar 用两个不同的反编译器各打开一次,是比较省事的交叉验证手段。两边都能还原的方法可信度高,只有一边能还原的,看的时候要多留个心眼。
5. 避坑与排查:五条翻车记录
下面这几条都是实际踩出来的,顺序按发生频率排。每条都按现象、原因、解决三段写,照着对号入座基本能覆盖八成问题。
5.1 回编译成功但装不上:签名方案与对齐顺序
现象很典型:apktool b顺利结束,adb install报INSTALL_PARSE_FAILED_NO_CERTIFICATES或者干脆Failure [INSTALL_FAILED_INVALID_APK]。
原因有两个方向。一是根本没签名,apktool 的产物天生没有签名块,很多人第一次用时以为回编译完就能装。二是签了但顺序错了,先 apksigner 后 zipalign,对齐工具重写了文件布局,把签名块覆盖掉,apksigner verify会明确告诉你签名无效。
解决就是把顺序钉死:apktool b→zipalign -p -f -v 4→apksigner sign→apksigner verify -v。验证这一步别省,它是唯一能在安装前发现签名问题的环节。另外注意--min-sdk-version会影响签名方案的选择,如果目标包的 minSdk 很低而你又手动指定了高版本,可能出现低版本设备装不上的情况,这个参数一般交给 apksigner 自己从清单里读,不要手填。
5.2 改完 smali 启动闪退:寄存器声明没跟着改
现象是安装正常、点图标就闪退,logcat 里是一行java.lang.VerifyError或者Invalid register。
原因在 smali 的寄存器模型。每个方法开头有.registers N或.locals N声明,这个数字必须不小于方法体内实际用到的寄存器总数。你在方法里插了几条指令、用了一个没人用过的高位寄存器,声明却没同步加上去,校验器在类加载阶段就会拒绝这个方法。
解决分两步。先看报错里指的方法名,回到对应 smali 文件,把方法体里出现过的最大寄存器编号找出来,确保.registers的值大于它。二是优先复用已有寄存器而不是新开,比如插入日志时用方法里已经承载参数的寄存器,改完立刻回编译装一次验证,不要攒一堆改动最后一起测——一起测崩了根本不知道是哪处改坏的。血泪经验是每次只改一个方法,改完就装。
5.3 多 dex 包只处理了主 dex:关键类凭空消失
现象是在 jd-gui 里搜某个明明在跑的类,搜不到;或者在 apktool 产物里找不到某个功能对应的 smali,误以为功能被裁掉了。
原因是这类包把代码拆成了 classes.dex、classes2.dex、classes3.dex,而 apktool 加了--only-main-classes,或者 dex2jar 的循环通配符写成了classes.dex这种固定名。主 dex 通常只放启动必需的类,业务逻辑大量分布在后续 dex 里。
解决是解包时不用--only-main-classes,抽 dex 时通配符写classes*.dex,转换后核对 jar 数量和 dex 数量是否一致。装完之后如果还要继续排查,建议在 jd-gui 里同时打开多个 jar,虽然要切窗口,但比漏看一整包类强。
5.4 改字符串导致回编译失败:格式化占位符被破坏
现象是只改了strings.xml里一句文案,回编译直接报错,指向某个字符串资源,提示格式化相关。
原因是这些字符串里带%1$s、%d这类位置化占位符,aapt2 在编译时会做格式校验。你改文案时手滑删掉一个%1$s,或者把%1$s写成%s,占位符数量和代码里getString(R.string.x, arg)传入的参数个数对不上,编译期就拦下来了。
解决是改文案时只动文字部分,%开头的占位符原样保留,包括序号和类型字符。批量改多语言目录时尤其要小心,values-zh、values-en里的占位符必须一一对应,少一个就整体编译失败。真拿不准就把原字符串整行复制出来,只替换中间的汉字部分。
5.5 资源解析中断:畸形 9-patch 与自定义资源
现象是 apktool 解包跑到一半退出,报Could not decode file或者直接抛异常,产物目录不完整。
原因是包里存在不符合规范的 9-patch 图片,边框像素只画了三边,或者自定义资源的格式 apktool 不认识。这类文件在正常构建时被容忍,但反编译器的解析更严格。
解决是先加-r跳过资源反编译,至少把代码部分拿到手,确认分析主线不受影响;再单独用-s只解资源,缩小范围定位到具体文件。定位到之后把那几个文件从工程里剔除或者替换成合法版本,再跑一次全量解包。如果是第三方 SDK 塞进来的资源导致的,通常可以直接删掉对应的 res 子目录,只要没有代码引用它,回编译不会报错。
6. 进阶:把整套流程收进一个脚本,并验证结果没被改坏
手敲命令跑一两次还行,多轮迭代就容易漏步骤。我现在的做法是把解包、转换、回编译、对齐、签名串成一个带set -euo pipefail的脚本,任何一步非零退出就整体停下,不给“带着错误继续跑”的机会。
#!/usr/bin/env bash set -euo pipefail APK="$1" WS="${ARH_WS:?需要先导出 ARH_WS}" apktool d "$APK" -o "$WS/unpack" -f unzip -o "$APK" 'classes*.dex' -d "$WS/dex" for f in "$WS"/dex/classes*.dex; do d2j-dex2jar.sh -f "$f" -o "$WS/jars/$(basename "$f" .dex).jar" done # 改动在这里插入,脚本本身不负责改代码 apktool b "$WS/unpack" -o "$WS/unsigned.apk" zipalign -p -f -v 4 "$WS/unsigned.apk" "$WS/aligned.apk" apksigner sign --ks "$ARH_HOME/keys/debug.keystore" --ks-key-alias demo \ --ks-pass pass:android --key-pass pass:android \ --out "$WS/signed/app-signed.apk" "$WS/aligned.apk" apksigner verify -v "$WS/signed/app-signed.apk"set -euo pipefail三件套里,-e让单条命令失败即终止,-u让未定义变量直接报错,-o pipefail让管道中任一环节失败都能被捕获。这三条加起来,能挡掉大部分“上一步失败了但脚本继续往下跑,最后产物看起来正常其实缺东西”的情况。
比脚本更值得固化的是验证清单。改完一个包,我会按下面这张表逐项过一遍,任何一项对不上就回退重来:
| 校验项 | 怎么查 | 期望结果 |
|---|---|---|
| versionCode / versionName | 读回编译后的 apktool.yml | 与改动前一致,除非刻意改过 |
| dex 数量 | unzip -l看 classes*.dex 条数 | 与原始 APK 相同 |
| 入口 Activity | 解析 AndroidManifest.xml | 未被意外改动 |
| 签名指纹 | apksigner verify --print-certs | 与本地密钥库一致 |
| 冷启动 | 装到测试机后完整走一遍主流程 | 无闪退、无白屏 |
第三项容易被忽略。apktool 回编译时如果清单里有它不认识的属性,某些版本会静默丢弃,结果就是入口变了或者权限少了,表现为“装得上但打不开”。第四项是防止拿到一个签名对但证书不对的包,尤其多人协作时,别人签过的包混进来很难发现。第五项是最终验证,前面四项全过但启动就崩的情况我也遇到过,原因在第 5.2 节那种寄存器问题上。
从那以后我每次改完包,都强制走一遍apksigner verify加一次冷启动录屏,两个都过了才算完。这套习惯救过我好几次——有一次回编译产物看着一切正常,验证时发现 dex 数量少了一个,追下去是脚本里的通配符在某个 shell 下没展开,差一点就把残包发出去了。希望帮到你。
本文还有配套的精品资源,点击获取