首页 / 资讯中心 / 文章详情

APK反编译与回编译全流程:apktool、jadx、smali与重签名实战

APK反编译与回编译全流程:apktool、jadx、smali与重签名实战 ★ FEATURED ARTICLE
APK 和 Dex 文件的反编译、回编译这件事在移动安全审计、老项目维护、竞品功能拆解、SDK 行为分析这些场景里几乎是绕不开的基本功。我这些年接手过的活儿里有相当一部分第一步就是把包拆开看看里面到底装了什么。很多人第一次接触这类工具链以为装个图形化工具点几下就完事结果一遇到资源混淆、多 Dex、签名校验、加固壳就卡住来回折腾好几天。这篇东西就是把我自己反复踩坑之后沉淀下来的完整流程写清楚从 APK 拆包、Dex 反编译成可读代码、修改后回编译、重签名到最终验证每一步为什么这么做、参数怎么选、哪里容易翻车都讲透。适合刚入行的移动安全测试人员、需要逆向排查线上崩溃的 Android 开发以及想搞清楚第三方 SDK 到底干了什么的技术负责人。哪怕你之前只听说过 apktool 和 jadx 这两个名字跟着走一遍也能独立完成一次完整的反编译与回编译闭环。1. 整体方案设计与工具链选型思路1.1 为什么反编译要拆成资源层和代码层两条线Android 应用的产物结构决定了这件事必须分两条线走。一个 APK 本质上是个 zip 包里面混着两类完全不同的东西一类是resources.arsc、res/目录下的 XML 和图片、AndroidManifest.xml这些资源与配置它们在打包时被编译成了二进制格式普通文本编辑器打开全是乱码另一类是classes.dex可能还有classes2.dex、classes3.dex里的字节码这是 Java/Kotlin 源码编译后的产物运行在 ART/Dalvik 虚拟机上。这两类内容的处理工具完全不同。资源层用apktool最顺手它能解码二进制 XML 和resources.arsc还原成人类可读的 XML 和原始资源文件而且回编译时能重新打包成二进制格式这是它不可替代的地方。代码层则要看你的目的如果只是想读懂逻辑用jadx直接反编译成 Java 代码可读性最好如果是要修改字节码再回编译就得用baksmali/smali走 smali 汇编这条路因为 jadx 反编译出来的 Java 代码是只读的改完没法直接编译回 Dex。我见过不少人图省事想用 jadx 改完代码再想办法编译回去最后都放弃了——jadx 的输出是给人看的不是给编译器吃的。所以正确的思路是资源改动走 apktool代码改动走 smali纯分析走 jadx。三条路各司其职别指望一个工具通吃。1.2 工具链组合与各自职责边界把整套流程用到的工具列清楚避免新手一上来就乱装一堆东西却不知道谁干什么工具核心职责输入输出是否可回编译apktool资源解码与重打包APK可读资源目录是jadx / jadx-guiDex 转 Java 源码APK/DexJava 代码否只读分析baksmaliDex 转 smaliDexsmali 文件否smalismali 转 Dexsmali 目录Dex是apksignerAPK 签名APK已签名 APK是zipalign字节对齐优化APK对齐后 APK是这张表是我建议你贴在显示器旁边的。核心逻辑一句话apktool 管资源smali 管代码apksigner 管签名zipalign 管对齐。jadx 是旁路的分析工具不参与回编译链路。1.3 方案选型的三个关键取舍第一个取舍是要不要用 apktool 全量解码。apktool 解码资源很强大但它对某些加固过的包、或者用了特殊资源混淆方案的包解码会失败或者解出来的东西回编译报错。我的经验是如果只是改代码不改资源完全没必要用 apktool 全量解码直接用 baksmali 从 APK 里抽 Dex 出来处理改完再塞回去这样能绕开大量资源层的坑。第二个取舍是jadx 还是 smali 做代码分析。纯读逻辑jadx 的 Java 输出效率高十倍但一旦涉及我要改这一行再跑起来就必须切到 smali。实际操作里我经常是先用 jadx 定位到关键类和方法记下方法签名再去 smali 里找对应位置改两者配合用效率最高。第三个取舍是签名方案。现在 Android 7.0 以上要求 APK Signature Scheme v2Android 9 以上很多场景要求 v3只签 v1 的包在新系统上装不上或者校验失败。所以重签名必须用 apksigner 而不是老的 jarsigner并且要显式指定启用 v1/v2/v3。这个后面实操部分会详细讲参数。2. 环境准备与工具安装的实操细节2.1 基础运行环境搭建这套工具链主体是 Java 生态所以第一步是把 JDK 装对。apktool、jadx、baksmali/smali、apksigner 全都依赖 Java 运行时。我建议用JDK 11 或 JDK 17这两个版本兼容性最好。JDK 8 虽然也能跑大部分工具但新版 apktool 和 apksigner 对高版本字节码支持更好用 8 偶尔会遇到Unsupported class file major version之类的报错。装完之后务必验证java -version javac -version两条命令输出的版本号要一致且都是 11 或 17。我踩过的坑是机器上装了多个 JDKjava指向 17 但JAVA_HOME指向 8导致某些工具启动时加载错版本报一堆莫名其妙的错。所以装完一定要echo $JAVA_HOMEWindows 下echo %JAVA_HOME%确认环境变量和实际 java 版本对得上。Android SDK 里的build-tools也要装因为 apksigner 和 zipalign 都在里面。不需要装完整的 Android Studio用命令行工具单独下 build-tools 就行。装好后把build-tools/版本号/目录加到 PATH这样 apksigner 和 zipalign 能直接调用。版本建议选 30.0.0 以上太老的 build-tools 里的 apksigner 不支持 v3 签名。2.2 apktool 安装与版本选择apktool 有两个东西要下一个是apktool.jar主程序一个是包装脚本Windows 是apktool.batLinux/macOS 是apktool脚本。很多人只下了 jar然后每次都要java -jar apktool.jar很麻烦。正确做法是把 jar 和脚本放同一目录脚本里指向 jar 的路径改对之后直接敲apktool就能用。版本选择上优先用最新稳定版。apktool 对 Android 新版本的资源格式支持是逐步跟进的用老版本解新包经常报Could not decode arsc file或者解出来的resources.arsc结构不对导致回编译失败。我一般去官方发布页拿最新的apktool_x.x.x.jar重命名成apktool.jar覆盖旧的。验证安装apktool --version能打印出版本号就说明脚本和 jar 都通了。如果报Error: Unable to access jarfile八成是脚本里的 jar 路径写错了打开脚本看一眼那行java -jar后面的路径。2.3 jadx 与 smali 工具的准备jadx 直接用 release 包里的jadx和jadx-gui脚本即可同样依赖 Java。jadx-gui 是图形界面适合人工浏览jadx命令行适合批量导出。我平时分析用 gui需要把整个工程导出成 Java 文件时用命令行jadx -d output_dir target.apkbaksmali 和 smali 是两个独立的 jar官方发布里通常叫baksmali-x.x.x.jar和smali-x.x.x.jar。这两个工具的版本必须严格配对用 2.5.2 的 baksmali 反汇编出来的 smali必须用 2.5.2 的 smali 汇编回去版本不一致会报语法错误或者生成错误的 Dex。这是新手最容易忽略的点我见过有人拿 2.4 的 smali 去汇编 2.5 baksmali 的产物折腾一下午找不到原因。2.4 一个容易忽略的前置检查APK 是否加固在动手反编译之前先花两分钟判断这个包有没有加固。方法很简单用 unzip 列出 APK 内容看classes.dex的大小和数量。如果classes.dex只有几十 KB但 APK 总体积好几 MB且lib/目录下有一堆.so文件那基本可以判定是加固过的——真正的业务代码被抽到 so 里或者加密藏在别处dex 只是个壳。unzip -l target.apk | grep -E classes.*dex|\.so加固包直接走常规反编译流程解出来的 Dex 里全是壳代码看不到真实逻辑。这种情况要么放弃要么走脱壳路线那是另一个话题涉及运行时 dump复杂度高很多。提前判断能帮你省下大量无效操作。3. APK 拆包与 Dex 反编译的核心操作3.1 用 apktool 解码资源与清单标准解码命令apktool d target.apk -o decoded_dir-o指定输出目录不加的话默认输出到以 APK 名命名的文件夹。解码成功后decoded_dir里会有AndroidManifest.xml已还原成可读 XML、res/原始资源、smali/如果 APK 里有 Dexapktool 会顺带用内置 baksmali 反汇编成 smali、apktool.yml重打包需要的元信息。这里有个细节值得说apktool 默认会把 Dex 也反汇编成 smali 放在smali/、smali_classes2/等目录。如果你只想改资源这一步顺带产出的 smali 可以不管但如果你要改代码这些 smali 就是你的工作对象。不过我更推荐代码改动单独用 baksmali 处理原因后面 3.3 会讲。解码时常见的两个参数-f强制覆盖已存在的输出目录。反复调试时很有用省得手动删。-r跳过资源解码只反汇编 Dex。当你只关心代码、不想被资源解码报错卡住时用这个。-r这个参数救过我很多次。有些包的resources.arsc用了非标准格式apktool 解码资源直接崩但我其实只想看代码加-r就绕过去了。3.2 多 Dex 场景的处理现代 APK 基本都是多 Dexclasses.dex、classes2.dex、classes3.dex……方法数超过 65536 就会分包。apktool 解码时会自动把它们分别反汇编到smali/、smali_classes2/、smali_classes3/。回编译时 apktool 也会自动按目录重新打包成多个 Dex这部分不用你操心。但如果你走 baksmali 手动路线就要自己管理多个 Dex。我的做法是先解压 APK 拿到所有 dexunzip target.apk classes*.dex -d dex_workspace然后对每个 dex 单独 baksmalijava -jar baksmali.jar d dex_workspace/classes.dex -o smali_out/ java -jar baksmali.jar d dex_workspace/classes2.dex -o smali_out2/改完之后再分别 smali 回 dex最后替换回 APK。多 Dex 的关键是保持 Dex 与 smali 目录的对应关系不乱改哪个 dex 的代码就只动哪个目录别混着来。3.3 baksmali 反汇编 Dex 的参数详解单独反汇编一个 Dexjava -jar baksmali.jar d classes.dex -o smali_out常用参数-o输出目录。--debug-info保留调试信息行号、局部变量名。默认是保留的如果你想要更干净的输出可以关掉但调试时建议保留报错行号对定位问题很有帮助。-r/--no-accessor不生成 accessor 方法输出更接近原始字节码。一般不用动。反汇编出来的 smali 是 Dalvik 字节码的汇编表示语法上每个方法是一段.method ... .end method每条指令对应一个虚拟机操作。看不懂没关系改代码时你只需要定位到目标方法改其中一两行指令或者常量即可不需要通读全部。3.4 jadx 反编译做代码定位的正确姿势jadx 的价值在于快速定位。打开 jadx-gui 加载 APK左侧是包结构树中间是反编译出的 Java 代码支持全局搜索类名、方法名、字符串常量。我通常的流程是在 jadx 里搜关键字符串比如某个提示文案、某个 URL、某个 SharedPreferences 的 key定位到使用它的类和方法。记下这个方法的完整签名比如com.example.foo.Bar.checkLicense(Ljava/lang/String;)Z。切到 smali 目录按包路径找到对应的.smali文件搜索方法名定位到具体指令位置。这个jadx 定位、smali 修改的组合是我用得最顺的。纯靠 smali 读逻辑太痛苦纯靠 jadx 又改不了两者结合效率最高。jadx 反编译偶尔会失败报Failed to decompile或者输出一堆// error。这通常是遇到了 jadx 不支持的字节码模式或者混淆过的控制流。这时候别死磕直接看 smali虽然难读但一定准确。4. 代码修改与回编译的完整实现4.1 smali 修改的典型场景与写法最常见的修改场景是改常量和改条件跳转。举个改常量的例子smali 里字符串常量是这样出现的const-string v0, trial_version改成const-string v0, full_version就完成了字符串替换。数字常量则是const/4 v0, 0x1这种0x1就是值 1改成0x0就是 0。改条件跳转是另一个高频操作。smali 里的条件跳转长这样if-eqz v0, :cond_0意思是如果 v0 等于 0 就跳到 :cond_0。如果你想让它无论如何都不跳可以把if-eqz改成goto或者把条件反过来。但要注意改跳转很容易破坏栈平衡或者让后续指令引用到未初始化的寄存器改完必须回编译验证不能想当然。注意smali 里寄存器v0、v1、p0、p1的分配是编译器算好的你新增指令时如果用到新寄存器必须同步修改方法头的.registers或.locals声明否则回编译会报寄存器越界。这是新手改 smali 最常见的翻车点。4.2 smali 回汇编成 Dex改完 smali 后用 smali 工具汇编回 Dexjava -jar smali.jar a smali_out -o classes_new.dexa是 assemble 的缩写smali_out是 smali 目录-o指定输出的 dex 文件名。如果原包是多 Dex就分别对每个 smali 目录执行输出对应的classes.dex、classes2.dex。汇编报错时错误信息通常会指出哪个文件哪一行有问题比如Invalid register、Label not found、Expected ...。按提示回去改就行。我遇到最多的是标签label问题——删了某段代码但没删对应的跳转标签或者标签重名。smali 的标签是:cond_0、:goto_0这种同一方法内不能重名。4.3 替换 Dex 并重新打包 APK拿到新的 Dex 后要把它塞回 APK。有两种路线路线一apktool 重打包。如果你是用 apktool 全量解码的直接把新 Dex 反汇编的 smali 覆盖到smali/目录然后apktool b decoded_dir -o rebuilt.apkapktool 会自动把 smali 汇编成 Dex 并打包。这条路适合同时改了资源和代码的情况。路线二直接替换 Dex。如果你只改了代码没动资源最干净的做法是拿原始 APK用 zip 工具把新的classes.dex替换进去zip -j target.apk classes_new.dex但这里有个坑APK 里的文件顺序和压缩方式会影响签名和安装直接zip替换可能导致某些校验失败。更稳妥的做法是用zip -X不额外写属性或者干脆解压整个 APK、替换 Dex、再重新打包。我一般用后者虽然多一步但最不容易出问题。4.4 zipalign 与 apksigner 重签名回编译出来的 APK 是未签名的直接装会报INSTALL_PARSE_FAILED_NO_CERTIFICATES。签名前先做字节对齐zipalign -v -p 4 rebuilt.apk aligned.apk-p 4表示按 4 字节对齐这是 Android 要求的页对齐。对齐的作用是让 APK 里的未压缩资源在内存映射时能直接按页加载减少运行时内存拷贝。不做对齐虽然也能装但会有性能损耗而且某些系统版本会拒绝安装。然后签名apksigner sign --ks mykey.jks --ks-key-alias myalias \ --v1-signing-enabled true --v2-signing-enabled true --v3-signing-enabled true \ --out signed.apk aligned.apk关键参数解释--ks密钥库文件。没有的话用keytool -genkeypair生成一个测试用途随便填信息即可。--v1/v2/v3-signing-enabled显式开启三代签名方案。v1 是 JAR 签名老系统兼容v2 是 APK Signature Scheme v2Android 7.0v3 是 v3 方案Android 9支持密钥轮换。三个都开最保险。--out输出文件名。签完验证一下apksigner verify --verbose signed.apk输出里会列出 v1/v2/v3 各自的验证结果全绿才算成功。如果 v2 或 v3 显示 false说明签名参数没生效检查是不是 build-tools 版本太老。提示重签名后 APK 的签名和原包不同如果应用内部有签名校验很多金融类、游戏类应用都有会检测到签名变化而拒绝运行或闪退。这种情况要么定位并 patch 掉校验逻辑在 smali 里找getPackageInfo配合GET_SIGNATURES的调用要么放弃。签名校验的 patch 是另一个深坑涉及多处校验点和 native 层校验不在本文展开。5. 常见问题排查与避坑经验实录5.1 反编译阶段的典型报错报错一Could not decode arsc file。这是 apktool 解码resources.arsc失败常见于资源格式被修改过或者 apktool 版本太老。解决办法升级 apktool 到最新版如果还不行加-r跳过资源解码只处理代码。报错二brut.androlib.AndrolibException: Could not smali file。apktool 内置的 smali 版本和 Dex 格式不匹配。升级 apktool 通常能解决或者改用独立 baksmali 处理 Dex。报错三jadx 反编译大量// error。遇到不支持的字节码或者混淆控制流。不影响 smali 修改直接看 smali 即可。5.2 回编译阶段的典型报错报错一Invalid register。smali 里用了超出.registers声明范围的寄存器。检查方法头的寄存器声明或者改用已声明的寄存器。报错二Label not found。跳转指令引用了不存在的标签。检查是不是删代码时把标签定义也删了或者标签名拼错。报错三Expected ... but found ...。smali 语法错误通常是某条指令的参数类型不对比如该用寄存器的地方写了常量。按报错行号回去看。5.3 安装与运行阶段的典型问题问题一INSTALL_FAILED_INVALID_APK。多半是没对齐或者签名不完整。先 zipalign 再 apksigner且确保 v2/v3 都开了。问题二装上了但一启动就闪退。常见原因有三个签名校验没 patch 掉、smali 改动破坏了逻辑、多 Dex 替换时目录对应错了。排查顺序是先看 logcat 的崩溃堆栈定位到具体类和方法再回 smali 检查改动点。问题三INSTALL_FAILED_UPDATE_INCOMPATIBLE。设备上已装了同包名但签名不同的版本。先卸载旧版再装或者用adb install -r加-d允许降级。5.4 一份问题速查表现象可能原因排查方向apktool 解码失败资源格式特殊/版本老升级 apktool 或加 -rsmali 汇编报寄存器错寄存器声明不匹配检查 .registers/.locals回编译 APK 装不上未对齐/未签名zipalign apksigner启动闪退签名校验/逻辑破坏看 logcat 堆栈定位多 Dex 应用部分功能异常Dex 替换错位核对 smali 目录与 dex 对应jadx 输出全是 error字节码不支持改用 smali 直接分析5.5 几条用血换来的实操心得第一条每次改动前先备份原始 APK 和原始 smali 目录。我吃过亏改到一半发现改错了想回退结果原始文件被覆盖只能重新从原始 APK 解一遍浪费大量时间。养成cp -r备份的习惯成本极低收益极高。第二条小步快跑改一处验一处。不要一次性改十几个方法再回编译一旦出错根本不知道是哪处改坏的。正确做法是改一个方法、回编译、装上去验证、通过再改下一个。虽然看起来慢但总时间反而更短。第三条善用 logcat 定位崩溃点。闪退时adb logcat里的堆栈会精确到类名和方法名拿着这个信息回 smali 找对应位置比盲目搜索快得多。过滤命令adb logcat | grep -E AndroidRuntime|FATAL第四条签名校验的 patch 要全面。很多应用的签名校验不止一处Java 层有、native 层也有甚至服务端还会校验。只 patch 掉 Java 层的一处往往还是闪退。判断方法是在 smali 里全局搜getPackageInfo、GET_SIGNATURES、signatures这些关键词把所有校验点都找出来逐个处理。第五条注意 Android 版本差异。同一个改过的包在 Android 8 上能跑在 Android 12 上可能因为分区存储、权限模型变化而行为不同。测试时至少覆盖一个低版本和一个高版本设备别只在一台机器上验证就下结论。这套流程我从最早的单一 Dex 应用一路用到现在的多 Dex、资源混淆、部分加固的复杂包工具在变、对抗在升级但资源走 apktool、代码走 smali、分析走 jadx、签名走 apksigner这个骨架一直没变。真正花时间的从来不是工具怎么用而是遇到报错时知道往哪个方向查——上面那些速查表和心得就是帮你把排查方向缩到最小的那部分。
阅读完成 · 觉得有帮助?
咨询建站