☰
Android APK 逆向分析全链路:apktool、dex2jar 与 jd-gui 实战
2026/10/31 10:36:47 网站建设 项目流程

简介:这套Android逆向助手工具包面向具备一定Android基础的开发者与安全研究人员,将APK反编译、DEX转Java、代码查看与重签名等环节所需的常用工具集中整理,便于开展应用结构分析、安全审计、功能理解与性能排查。压缩包共69个文件、约15.21MB,以jar与exe工具主体、bat和sh批处理/脚本、txt说明文档为主,另含少量配置与证书类文件,目录组织相对完整,可减少逐个搜集与配置工具的时间。目前已有145人学习下载。其核心价值在于把apktool、dex2jar、jd-gui、autosign等组件放在同一套环境里,配合脚本与说明,读者可较快搭建反编译分析流程,查看资源与Java代码、验证修改结果并完成测试签名,适合作为逆向入门与日常分析的工具集合。

1. 从「拿到一个 APK」说起:三件套各管哪一段

同事把一个线上包丢过来,说本地代码和这包里跑的代码对不上,想知道里面那个第三方模块到底干了什么。这种时候,一个能反复用的 android逆向助手 比任何单点工具都值钱:apktool 把资源和 smali 拆出来,dex2jar 把 dex 翻成 jar,jd-gui 再把 jar 读成近似 Java 源码。三段接起来,黑匣子才被撬开一条缝。

它适合三类人:手里握着自有或已获授权 APK、需要核查第三方 SDK 行为的安全自查人员;接手没有源码的历史项目、只能从包体反推逻辑的维护者;以及想搞清楚资源混淆、多渠道打包到底往包里塞了什么的一线 Android 开发。下面按我实际干活的顺序,把这条链路一段段拆开,重点放在命令参数和翻车点上。

2. apktool 反编译:资源、smali 与 apktool.yml 三层产物怎么读

2.1 先想清楚这一趟是要资源还是要代码

apktool 做的事看着简单,其实分三块:把AndroidManifest.xml和res/下的二进制 AXML 还原成可读的 UTF-8 XML;把resources.arsc解析成资源表;把classes*.dex反汇编成 smali 文本。三块的耗时和产物体积差很远,一个几十兆的包全解出来,smali 目录能膨胀到几百兆、上十万个文件,搜索一次都要等半天。所以我一般不会上来就全解。

判断标准很直接:只想知道清单里注册了哪些组件、用了哪些权限、res/values/strings.xml里写了什么,用-s;只改一行代码逻辑、不想让 apktool 去碰资源表,用-r;要做完整的回编译改造,才用默认的全解模式。这个选择不是玄学,它直接决定了后面 dex2jar 那一步的输入从哪来——-s模式下 apktool 会把原始 dex 原样拷进输出目录,省得你再解压一遍 APK。

还有一种情况是系统包或定制 ROM 里的 APK,比如某厂商的桌面、设置这类。它们引用了框架资源,直接解会报资源找不到。常见做法是先apktool if把对应的framework-res.apk装进本地框架目录,再解目标包。框架版本装错,后面资源 id 全对不上,这是血泪经验。

2.2 四条命令与它们各自的产物

下面这组命令是我固定用的,放在一个脚本里按需调用:

# 1) 标准解码:资源 + smali 全部展开,改造场景用这个 apktool d -f app-release.apk -o out_full # 2) 只解资源:dex 原样保留,速度快,产物体积小 apktool d -s -f app-release.apk -o out_res # 3) 只解代码:资源不解析,适合只动 smali 的场景 apktool d -r -f app-release.apk -o out_dex # 4) 系统包:先装框架,再解目标 apktool if framework-res.apk apktool d -f SystemUI.apk -o out_sys

参数逐个说清楚。-f是强制覆盖已存在的输出目录,不加的话第二次跑会直接报目录已存在然后退出,脚本里一定要带。-o指定输出目录,别用默认的「包名」目录,多个包一起处理时容易互相覆盖。-s是 skip sources,跳过 dex 反汇编,产物里classes.dex是原封不动拷过来的二进制。-r是 skip resources,资源和清单保持二进制状态,只出 smali。

apktool if的 if 是 install-framework,把框架包解码后写入用户目录下的框架缓存,后续解码会自动按包名和版本号匹配。同一台机器上装多个框架版本时,用--frame-path指定独立的框架目录,比反复覆盖缓存干净得多。

解完之后先别急着翻代码,花两分钟看目录结构。out_full/AndroidManifest.xml是可读的,out_full/res/是资源,out_full/smali/是主 dex 的 smali,out_full/smali_classes2/、smali_classes3/对应classes2.dex、classes3.dex,out_full/original/里放着原始的AndroidManifest.xml和签名信息,out_full/apktool.yml是这次解码的元数据。unknown/目录里通常是 apktool 没认出来的文件,多数是加固产物或非标准资源,看到它基本可以判断这个包不简单。

2.3 apktool.yml 与资源表里值得盯的字段

apktool.yml是 apktool 回编译时的依据,改错一个字段,回编译出来的包就可能装不上。它长这样(值是占位,实际看你自己的包):

# out_full/apktool.yml 里几个必须核对的字段 versionInfo: versionCode: '<包体版本号>' versionName: '<包体版本名>' sdkInfo: minSdkVersion: '<最低支持版本>' targetSdkVersion: '<目标版本>' doNotCompress: - resources.arsc - png - META-INF/services/*

versionInfo和sdkInfo是从清单里读出来的,回编译时会写回去,一般不用动。真正要关注的是doNotCompress,它列出哪些后缀的文件在打包时不做压缩。有些应用把.so或者特定资源加进这个列表,是因为运行时要按偏移直接读,压了就会崩。你从别处拷一个apktool.yml过来用,这个列表对不上,回编译出来的包一启动就闪退,排查半天还以为是 smali 改错了。

资源 id 在out_full/res/values/public.xml里,形如public type="string" name="app_name" id="0x7f0f0001"。反编译阶段它是只读参考,回编译阶段它决定 id 分配。改资源时新增一条、删掉一条,都可能让 id 偏移,进而让 smali 里硬编码的0x7f...指向错误的资源。所以我的习惯是:只改值不改条目,真要增删资源,先在 public.xml 里手动占位,再去看 smali 里有没有引用。

清单这块,定位入口最快的方式是搜 intent-filter:

# 从可读清单里找 LAUNCHER 入口,-A3 带出后面三行 grep -n -A3 "android.intent.action.MAIN" out_full/AndroidManifest.xml

拿到入口类名之后,把它记下来。后面在 jd-gui 里翻 jar 的时候,这是第一个要打开的类,从onCreate往下读,比漫无目的地在几万个类里乱翻效率高一个数量级。

3. dex2jar 转换:多 dex 批量处理与失败分支的四个判断点

3.1 dex 到底有几个,别只转第一个

现代 APK 几乎不可能是单 dex。方法数超过 65536 就得分包,classes.dex、classes2.dex、classes3.dex依次排下去,运行时由PathClassLoader按顺序加载。apktool 解码时会把它们分别展开成smali/、smali_classes2/、smali_classes3/,但 dex2jar 只认二进制 dex,不认 smali 文本。所以要么用-s模式让 apktool 把原始 dex 拷出来,要么直接从 APK 里解压:

# 直接从 APK 抽取所有 dex,-j 去掉目录层级,-o 指定输出目录 mkdir -p raw_dex unzip -o -j app-release.apk 'classes*.dex' -d raw_dex ls -lh raw_dex/

-j是 junk paths,只取文件名不还原目录结构,因为 APK 里的 dex 本来就在根目录。-d指定解压目标。抽完先ls -lh看一眼有几个、每个多大,心里有个数——某个 dex 特别大,通常意味着核心业务都在里面。

只转classes.dex是最常见的翻车点。你在 jd-gui 里搜一个类名死活搜不到,回头一看它在classes2.dex里,白折腾半小时。批量处理不难,一个循环就够。

3.2 单文件参数与批量脚本

先看单文件怎么调:

# -f 覆盖已有产物,-d 保留调试信息(行号、局部变量名) ./d2j-dex2jar.sh -f -d classes.dex -o classes.jar # Windows 下把 sh 换成 bat,参数完全一致 # d2j-dex2jar.bat -f -d classes.dex -o classes.jar

-f是 force,输出文件已存在时不加会直接跳过或报错,脚本里必带。-o指定输出 jar 路径,不指定的话会在当前目录生成classes-dex2jar.jar这种默认名,多个 dex 一起转就会互相覆盖。-d是保留 debug info,效果是反编译出来的 Java 源码里能看到原始行号和部分局部变量名——如果这个包没被混淆过,加不加-d的可读性差距非常大,加了之后方法里的变量叫userId而不是v3。

批量脚本我一般这么写:

#!/usr/bin/env bash # 把当前目录所有 classes*.dex 批量转成 jar,失败的单独记录 set -u OUT_DIR="./jars" mkdir -p "$OUT_DIR" for dex in classes*.dex; do [ -e "$dex" ] || continue # 没有匹配到文件就跳过 base="${dex%.dex}" # 去掉后缀作为 jar 名 echo ">>> 转换 $dex" ./d2j-dex2jar.sh -f -d "$dex" -o "$OUT_DIR/${base}.jar" \ || echo "!!! $dex 失败" >> "$OUT_DIR/failed.txt" done ls -lh "$OUT_DIR"

set -u让脚本遇到未定义变量直接退出,避免路径拼错还继续跑。${dex%.dex}是 shell 的后缀截断,把classes2.dex变成classes2,输出就是classes2.jar。末尾的重定向把失败的文件名追加到failed.txt,跑完一眼就能看出哪个 dex 没转成功,不用在滚屏里翻。

3.3 转换失败先看堆栈的哪一行

dex2jar 失败时抛的堆栈很长,但真正有用的就两三行。我按出现频率排一下判断顺序:

第一种,com.googlecode.dex2jar.DexException后面跟着can't find ...或unsupported。这类多半是 dex 本身不标准——加固过的包、被改过头的 dex、或者从内存 dump 出来的残缺 dex。遇到这种,先确认这个 dex 是不是原始 APK 里带的,unzip -l app-release.apk | grep dex对一下大小;大小对不上就说明你手上的 dex 来路有问题。

第二种,java.lang.OutOfMemoryError。大 dex 转换很吃内存,默认堆不够。给 JVM 加参数的方式是设环境变量:

# 给 dex2jar 的 JVM 提到 2G 堆,再跑转换 JAVA_OPTS="-Xmx2g" ./d2j-dex2jar.sh -f -d classes.dex -o classes.jar

不同版本的启动脚本对JAVA_OPTS的读取方式可能不一样,不生效的话直接改d2j-dex2jar.sh里那行java调用,手工加上-Xmx2g。这是最土也最可靠的办法。

第三种,转完了但 jar 里全是空类或者只有字段没有方法。这通常不是 dex2jar 的锅,而是原包用了控制流混淆或者方法体被抽走了。判断方法是拿 smali 对照:out_full/smali/下对应类的.smali文件里,如果.method下面是空的或者只有.registers没有指令,那说明代码本来就不在 dex 里,转一百遍也转不出来。

第四种,转换成功但 jar 打不开或 jd-gui 报错。先看 jar 大小,几百字节的基本是空壳;再用unzip -l classes.jar | head看看里面的.class路径对不对。路径里出现奇怪的乱码,通常是包名里有非 ASCII 字符,这种情况换 jadx 直接吃 APK 更省事。

4. jd-gui 阅读:三个信号判断反编译结果能不能信

4.1 打开 jar 之后先看哪里

jd-gui 打开 jar 后左边是包树,右边是源码面板。第一次打开别急着往下滚,先做三件事:看包树的第一层,混淆过的包通常是一堆a、b、c单字母包,没混淆的能看到完整的公司域名倒序;用搜索定位前面从清单里拿到的入口类,从onCreate开始读;看类的总数,几千个类正常,几万个类就该考虑只导出关心的包。

导出全部源码是File → Save All Sources,会生成一个 zip,解压后是.java文件树。这个功能在需要全局 grep 的时候特别好用,比在 GUI 里一个个点快得多——导出来之后grep -rn "http" .一把梭,比什么都直观。缺点是遇到大包会卡很久,内存不够直接崩,所以只导关心的包更稳妥。

命令行场景用 jd-cli,适合塞进脚本:

# 把 jar 里所有 class 反编译成 java 文件,输出到 src 目录 jd-cli -od src classes.jar # 只反编译某个包,减少输出体积 jd-cli -od src -p com.example.module classes.jar

-od是 output directory,-p限定包前缀。批量处理多个 jar 时这个组合比开 GUI 高效,但要注意它和 jd-gui 用的是同一套反编译内核,jd-gui 读不出来的,jd-cli 一样读不出来。

4.2 三个信号判断这段反编译能不能信

反编译出来的 Java 代码只是「近似还原」,不是原始源码。判断可不可信,我一般看三个信号。

第一个信号是字符串常量。const-string在 smali 里是明确存在的,反编译到 Java 里应该原样出现在双引号中。如果你在 smali 里看到const-string v0, "https://api.example.com/v1/user",而 Java 里对应位置显示的是乱码或空字符串,说明这一段的还原出了问题,后面的逻辑判断都不能信。

第二个信号是方法体完整性。正常的反编译结果里,if/else、for、try/catch结构是清晰的。如果看到大量这样的结构:

// 典型的控制流平坦化产物,可读性极差 while (true) { switch (v0) { case 0: /* 一堆赋值 */ continue; case 1: /* 又跳回去 */ continue; } }

那基本可以确定原包做了控制流平坦化。这种情况下 jd-gui 的输出只能当索引用——看它调用了哪些方法、用了哪些常量,具体分支逻辑要回 smali 里看跳转表。

第三个信号是行号。如果转换时带了-d,Java 源码左侧会显示行号;点进去看行号是不是连续且合理的(比如一个方法 30 行,行号从 120 到 150)。行号跳跃剧烈或者全是 1,说明调试信息被剥了,你看到的变量名都是arg0、arg1,读起来会非常吃力,这时候不如直接看 smali 的寄存器操作。

4.3 拿 smali 做交叉验证

反编译结果可疑时,回到 smali 是最稳的。apktool 的产物里,每个类一个文件,路径就是包名展开。查一个方法在 smali 里的样子:

# 按类名找 smali 文件,比如查 com/example/module/NetHelper find out_full/smali* -path "*com/example/module/NetHelper.smali" # 看这个方法里所有字符串常量,反推接口地址和参数名 grep -n "const-string" out_full/smali*/com/example/module/NetHelper.smali

find里的smali*通配是为了把smali_classes2这些一起覆盖到。const-string那一行下面通常紧跟invoke-*,能看出这个字符串传给了谁。这种「先 grep 常量、再顺着调用往下看」的路子,比在 jd-gui 里从入口类一层层点进去快得多,尤其是在类名被混淆成a.b.c的时候。

资源这边也可以交叉验证。jd-gui 里如果看到R.string.xxx,去public.xml里查这个 id,再回res/values/strings.xml看真实文案。三方对上,说明资源和代码的映射是完整的;对不上,说明有资源被替换过或者 id 被重排过,这个包就不适合直接回编译,得先想办法把资源表对齐。

5. 避坑与排查:五个高频翻车点的现象、原因与解法

5.1 反编译阶段

坑一:解码时报资源表错误,中途退出。

现象是apktool d跑到一半抛出跟resources.arsc相关的异常,输出目录只建了一半,res/是空的。原因通常是两点:apktool 版本比目标包的资源格式老,认不出新的资源表结构;或者这个包引用了框架资源但本机没装对应框架。解决顺序是先apktool if framework-res.apk装框架再重试,还不行就换一个更新的 apktool 版本。别在原地反复重试同一个命令,那不会有任何变化。

坑二:Windows 下解码成功但 smali 目录不全。

现象是输出目录里res/正常,smali/缺了一大块或者干脆没有。原因多半是路径里带中文或空格,apktool 在写文件时对这类路径处理有问题。解决方式很土但有效:把 APK 拷到D:\work\apk\这种纯英文短路径下,输出目录也用英文,重新跑一遍。这个问题在新版本里改善了不少,但老版本上仍然常见。

5.2 转换与阅读阶段

坑三:jd-gui 里搜不到类,或者打开一片// ERROR。

现象是明明知道某个功能存在,搜类名搜不到,或者点进去方法体只有一行// ERROR。原因有三个层次:类在classes2.dex里没转(回去补批量脚本);字节码版本高于 jd-gui 支持范围(换 jadx 直接吃 APK);代码被加固或抽走(回 smali 确认.method下面有没有指令)。排查顺序就按这个来,从最便宜的检查开始。

5.3 回编译与签名阶段

坑四:重打包后安装报签名相关错误。

现象是adb install返回INSTALL_PARSE_FAILED_NO_CERTIFICATES或者INSTALL_FAILED_INVALID_APK。原因是回编译出来的 APK 没有签名,或者签名和对齐的顺序搞反了。这里有个容易记混的点:zipalign必须在apksigner之前做,先签名再对齐会把 v2 签名块破坏掉。正确顺序是apktool b→zipalign→apksigner sign→apksigner verify。这四步我后面会完整走一遍。

坑五:回编译报资源 id 找不到,或者装上去一启动就崩。

现象是apktool b阶段报某个资源引用不存在,或者编译过了但运行到某个界面直接闪退,logcat 里是Resources$NotFoundException。原因是改资源时删了条目或改了 public.xml,导致 id 偏移。解法是尽量只改值不删条目;必须删的时候,先在public.xml里把这条留着(改成空值也行),保证 id 分配不变,再去改 smali 里的引用。改完一定要跑一遍aapt dump badging确认包信息正常。

6. 回编译验证:从 smali 改动到装机自测的完整链路

改完 smali 或资源之后,从源码目录回到一个能装的包,中间有四步,顺序错了就白干:

# 1) 回编译,产物未签名 apktool b out_full -o unsigned.apk # 2) 4 字节对齐,必须在签名之前 zipalign -f -p 4 unsigned.apk aligned.apk # 3) 生成自用签名(仅测试用,别拿去做正式发布) keytool -genkeypair -v -keystore demo.keystore -alias demo \ -keyalg RSA -keysize 2048 -validity 10000 \ -storepass 123456 -keypass 123456 -dname "CN=demo" # 4) 签名并校验 apksigner sign --ks demo.keystore --ks-pass pass:123456 \ --out signed.apk aligned.apk apksigner verify -v signed.apk

-p 4里的 4 是对齐字节数,-p表示按页对齐共享库,这两个参数组合是 Android 官方推荐值。keytool那条里的-validity 10000是有效期天数,-dname随便填但要带CN=,不填会交互式追问,脚本里会卡住。apksigner verify -v输出里Verified using v1 scheme和v2 scheme两行都是 true 才算签全了,只签 v1 在新系统上可能被拒。

装上去之后别只看能不能打开。跑一次adb logcat -c && adb logcat | grep -E "AndroidRuntime|FATAL",把启动过程完整看一遍;再进到改动对应的那个界面,手动触发一次相关逻辑。如果改的是网络层,用抓包工具确认请求真的发出去了;如果改的是资源,截图对比一下显示效果。这一步不能省,我见过太多次「能启动但功能静默失败」的情况,光看安装成功没有任何意义。

最后一个技巧:把上面四步写成一个build.sh,参数化输入目录和输出名,每次改完一行命令跑完,中间任何一步失败就set -e退出。这个脚本本身不复杂,但它把「顺序不能错」这件事固化下来了,比记在脑子里靠谱。从那以后我每次改完 smali 都强制走一遍 zipalign → 签名 → verify → 装机自测,一步不跳,希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询