ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

Apktool实战手册:Android逆向解包、smali修改与重打包全解析

Apktool实战手册:Android逆向解包、smali修改与重打包全解析 只要你在 Android 逆向这个坑里待过几天就一定绕不开 Apktool。它是把 APK 解包成可读资源、smali 代码再重新编译回 APK 的那个“万能手术刀”。很多人觉得反编译就是把 APK 拖进工具看源码其实真正动手改资源、改逻辑、重新打包、重新签名这一整条链路Apktool 才是核心中的核心。这篇文章我会从环境准备、命令参数、资源处理、smali 改写到签名打包把我实际踩过的坑和常用套路一次性讲透适合刚接触 Android 逆向的初学者也适合做过几次但总在重打包阶段翻车的老手。1. 先搞明白 Apktool 到底在哪个环节干活1.1 反编译、重新编译、签名这三件事千万别混为一谈很多新手会把“反编译 APK”理解成“看 Java 源码”实际上 Apktool 做的并不是这件事或者说不止这一件事。它主要负责的是把 APK 里的资源文件res、AndroidManifest.xml、resources.arsc解包成可以阅读、修改的原始格式把 dex 文件反汇编成 smali 汇编代码。注意这里拿到的是 smali不是 Java 源码。想看 Java 源码一般要用 jadx 或 dex2jar 配合 jd-gui那是另一条路线改起来也不如 smali 灵活。重新编译就是反过来把改过的资源目录和 smali 目录打包回 APK。这个环节最容易出问题因为 Android 的资源编译规则非常严格资源 ID、public.xml、文件名、字符串转义任何一处出错apktool 都会在 build 阶段给你颜色看。签名是在重新编译之后做的Apktool 本身默认不负责签名或者说它只是帮你把产物生成出来最终能不能装到手机里取决于你是否正确签名。所以完整的链路是apktool decode解包- 修改 smali / 资源 / Manifest - apktool build重打包- 签名工具签名 - zipalign非必需但建议- 安装测试。这条链路里 Apktool 负责头尾两段签名和优化是它之外的工具很多教程把签名也塞进 Apktool 里讲其实容易误导人。1.2 它和 jadx、Android Studio 的定位差异Android Studio 是开发工具它把源代码编译成 APKApktool 是逆向工具把 APK 还原成接近工程的结构。jadx 更像是一个“阅读器”它能直接反编译 dex 为 Java 代码浏览非常舒服但你很难用 jadx 改完了再重新打包。dex2jar 就更单纯了只负责把 dex 转成 jar适合分析不适合修改。我自己的习惯是先 apktool d 把 APK 解包用 jadx 看整体逻辑定位关键代码然后根据 jadx 里的类名、方法名回到 smali 里精准改动。jadx 是导航apktool 是操作台两者配合而不是二选一。遇到加壳或资源混淆的 APPjadx 经常啥也看不到这时候 Apktool 反而能先解出资源文件帮你看清楚入口点、权限、组件声明再决定下一步怎么搞。2. 环境准备和最少必要命令2.1 JDK 版本和 apktool 包装脚本Apktool 是 Java 写的工具目前主要是 Kotlin所以必须先装 JDK。我建议装 JDK 11 或 JDK 17太老的 JDK 8 跑最新版 Apktool 有时会报 unsupported class version 之类的错。装完 JDK 后去官网下载 apktool.jar再根据官方文档写一个包装脚本。Windows 上我会新建一个apktool.bat内容大致是echo off java -jar C:\tools\apktool.jar %*macOS / Linux 就写个 shell 脚本或直接 aliasalias apktooljava -jar ~/tools/apktool.jar官方文档里还有apktool.bat的详细写法但核心就是让命令行能直接调用。注意apktool.jar路径里不要有空格一旦路径带空格脚本很容易在后续传参时出问题。我遇到过脚本能启动但无法传参的坑最后发现是路径引号没加全。2.2 解包、重打包、安装这三条命令压箱底这可能是你接下来三个月用得最频繁的三条命令apktool d xxx.apk -o output_dir apktool b output_dir -o new.apk apktool b output_dir --use-aapt2第一条命令把 APK 解包到 output_dir默认会包含AndroidManifest.xml、res、smali可能有 smali_classes2、smali_classes3 等对应 multidex、unknown、original等目录。第二条命令把修改后的目录重新打包。第三条指定用 aapt2 编译资源新版 Apktool 默认可能已经用 aapt2但老版本需要手动指定。如果你只关心资源不想处理 dex可以加-s参数表示 no source。反过来只要代码不要资源用-r。这两个参数一定要记牢apktool d xxx.apk -s # 只解资源跳过 dex 反汇编 apktool d xxx.apk -r # 只解 dex跳过资源解码还有-f是强制清除输出目录重解包时非常好用。如果上次解包一半失败输出目录残留下次不解-f会直接告诉你输出目录不为空。2.3 第一次解包后你会看到什么解包成功后目录结构大致长这样output_dir/ ├── AndroidManifest.xml ├── apktool.yml ├── original/ ├── res/ ├── smali/ └── unknown/AndroidManifest.xml已经从二进制 XML 转成明文 XML可以直接改权限、组件、应用名称。apktool.yml是 Apktool 自己生成的元信息里面记录了原 APK 的版本信息、minSdk、targetSdk、是否使用 aapt2 等重打包时会参考不建议随意删字段。original目录保留的是原始的签名文件和 AndroidManifest二进制版一般很少动。unknown是 Apktool 不认识的文件残留比如某些 APK 里的特殊目录。我第一次解包的时候最惊讶的是 smali 文件实在太多了全是.smali后缀的文本文件打开看一眼就能感受到“汇编风格”的阅读压力。但没关系后面我会说怎么定位想改的方法不用真的逐行读完所有 smali。3. 资源解码、修改与重编译的关键细节3.1 resources.arsc 是资源索引的中枢不要乱动APK 里的resources.arsc是一个二进制资源表记录着资源名称、类型、ID 和具体配置的映射关系。Apktool 解包时会把这张表解成可读的res/values/public.xml里面每一个资源都有固定的编号像0x7f0a001e这样的格式。你在 res 里改文件内容没问题但不要自己随意往 public.xml 里加资源或改 ID除非你完全知道后果。为什么不能乱动因为 Android 在运行时是通过资源 ID 来查找资源的代码里引用的是 int 值不是字符串名称。如果你手工改乱了 public.xml轻则资源错乱重则生成 APK 后安装没问题一运行就闪退。我自己吃过这个亏为了给一个 App 加一张启动图擅自复制了一条public记录并改了 ID结果整个 App 的资源索引错位启动后大量资源找不到黑屏。正确的改资源方式是在 res 目录里增删文件然后通过重编译让 Apktool 自动重新生成 public.xml。不要手工维护 ID 表也不要依赖旧版本的 public.xml 结构。3.2 XML 里的路径与资源引用改的时候要小心转义解包后的res目录里可能有各种类型的资源layout、drawable、values、mipmap等。XML 文件打开后是明文但里面很多属性值看起来像string/app_name、drawable/ic_launcher这些是资源引用。如果你修改了一个字符串资源最好检查它有没有被其他地方引用因为改错会影响整个 App 的展示逻辑。我实际改资源时遇到过一个大坑在res/values/strings.xml里把一条带英文引号的字符串改成了中文内容忘了处理引号转义结果重编译直接报 XML 格式错误。XML 的规则很严格、、、这些字符必须转义文本里的%系列格式化占位符也要注意尤其在多语言资源里Android 会把它当作格式化字符串处理。这里引入一个最常用的操作修改 App 显示名称。你在AndroidManifest.xml里搜索android:label如果是string/app_name就去res/values/strings.xml里改string nameapp_name。这算最简单的改造适合练手。3.3 重编译时 aapt 与 aapt2 的选择不能乱来Apktool 早期版本默认使用 aapt 编译资源现在新版更推 aapt2因为 aapt2 对版本兼容和资源压缩的处理更好。重编译时加--use-aapt2能解决很多莫名其妙的问题比如某些 AndResGuard 混淆过的资源、超大 arsc 文件、非标准资源目录等。但 aapt2 也有它的问题对 Java 环境更敏感JDK 版本太老或某些字符集设置不对会报java.lang.UnsupportedOperationException或编码错误。我的经验是默认先用 apktool 自带的资源编译方式报错就加--use-aapt2重试再不行就切换 JDK 版本。如果你重打包后报错aapt2相关的wwarning大多数情况不影响产物但如果报e开头就是 error说明资源编译确实失败了。注意区分日志级别。3.4 重打包后的签名v1、v2、v3 缺哪个都可能安装失败Apktool build 生成的 APK 是未签名的直接装到手机必失败。签名工具有两个选择旧的jarsigner来自 JDK新的apksigner来自 Android SDK Build Tools。我现在一律用 apksigner因为它原生支持签名方案 v2、v3而 jarsigner 只支持 v1。签名命令是这个样子的apksigner sign --ks mykey.jks --ks-key-alias mykey --ks-pass pass:123456 --key-pass pass:123456 --out signed.apk unsigned.apk如果目标 App 的 targetSdk 比较高比如 30 以上系统要求必须包含 v2 签名否则 Android 11 会提示“安装包解析失败”或“应用未安装”。反过来如果老设备或某些国内 ROM 只支持 v1你签 v2 可能又能装但某些系统校验更严。稳妥做法是 v1、v2 都保留apksigner 默认会启用当前 minSdk/targetSdk 下所有支持的签名方案。签名完建议顺手跑一遍 zipalign虽然 apksigner 对未对齐安装包能够修正但我习惯先 zipalign 再签名zipalign -f 4 input.apk aligned.apk apksigner sign ... aligned.apk顺序别搞反对齐之后再签名基本不会出问题签名之后再对齐会导致签名失效因为对齐会改动文件内容。4. smali 修改改资源简单改逻辑才有技术含量4.1 一个能直接上手的 smali 修改案例先看一个场景App 每次启动都会弹更新弹窗你不想让它弹想绕过。用 jadx 反编译后定位到弹窗的类和方法比如com/example/MainActivity;-showUpdateDialog()。接下来在 smali 目录里找到对应文件打开后把方法体改掉让它直接return-void。原始 smali 可能长这个样子.method public showUpdateDialog()V .locals 2 # 弹窗逻辑 invoke-direct {p0}, Landroid/app/AlertDialog$Builder;-init()V ... return-void .end method改成最简单的空实现.method public showUpdateDialog()V .locals 0 return-void .end method这就是最基础的 smali 修改。修改完了重打包、签名、安装弹窗就消失了。当然真实应用不一定这么简单很多弹窗函数带有参数和返回值定位方法可能藏在 Fragment、Service 甚至回调里但思路完全相同。4.2 smali 语法快速上手记住这四条规则smali 本质上就是 dex 反汇编出来的寄存器语言你不必学得很深常用规则就几条寄存器用v0、v1表示局部变量p0表示 this非静态方法p1、p2等表示方法参数。指令是opcode {regs}, args的格式比如const/16 v0, 0x1就是给 v0 赋常量 1。方法调用用invoke-*系列指令比如invoke-virtual、invoke-static、invoke-direct后面跟{寄存器}, 类;-方法签名。返回值用return、return-void、return-object根据方法返回类型而定。想快速定位方法不建议用文件搜索硬找太慢。直接在 smoke这里修正为 smali目录里配合 grep 搜索字符串或注释比如弹窗标题文本在 strings.xml 里可以看到string/update_tip再去 smali 里搜索update_tip就能快速定位。这个方法比搜类名可靠得多因为很多 App 会混淆类名但字符串资源往往还能保留。4.3 smali 目录有多个时别改错 dex现在的 App 基本都开启了 multidex解包后会出现smali_classes2、smali_classes3等目录。这意味着同一个工程被拆成多个 dex 文件smali 文件的物理位置并不一定对应逻辑上的类路径。如果你在 jadx 里看到一个类搜索 smali 时一定要在所有 smali 目录里搜索而不能只搜smali根目录。有一种情况最容易翻车同一个类在多个 dex 里存在重复定义业务上少见但部分壳和热修复机制会这么做。修改的时候要确定哪个是实际生效的一般通过查看调用方或运行时行为来判断比较费时。快速做法是先改你认为生效的那个重打包测试不行再看另一个。4.4 资源混淆、加固壳对 smali 的影响如果 App 用了加固壳Apktool 解出来的 dex 很可能只是壳的加载器真正的业务代码在加密的 so 或 dex 里smali 目录里基本找不到有意义的内容。这时 Apktool 只能用于看资源和 Manifest分析代码要靠脱壳工具不在本文范围。如果 App 做了资源混淆比如微信的 AndResGuardresources.arsc 里的资源名会被人为改成短字符Apktool 解码后你看到的文件可能是res/xxxxx这种无意义名称public.xml 大量 ID 混乱。遇到这种 App重编译时最容易报错--use-aapt2也不是万能药需要先想办法恢复资源映射或者干脆放弃资源修改只操作 smali。5. 全局配置、框架文件和高级用法5.1 框架文件 framework-res.apk 和 -t 参数什么时候用某些 App 深度定制了系统主题或引用了系统私有资源比如android:style/Theme.DeviceDefault、android:color/holo_blue_dark之类。Apktool 解码时通常自带一份框架文件但如果你解包的系统 App 或定制 ROM 资源包会提示 missing framework files这时就需要导入系统框架。命令是apktool if framework-res.apk-t参数则是在解码时指定使用某个框架 tag。我解包 MIUI、EMUI 这类 ROM 里的内置应用时几乎必用这个命令否则资源 id 解析范围不够解出来的资源会缺 id。注意framework tag 存在~/.local/share/apktool/framework/目录下不同系统路径不一样重装系统后别忘了重新导入。5.2 apktool.yml 里可以调整的配置项解包后的apktool.yml记录了原 APK 的关键信息我整理几个常用的minSdkVersion、targetSdkVersion影响重编译时 aapt 的版本处理改动过大可能装不上。versionName、versionCodeApp 版本信息去更新弹窗的另一种粗暴做法就是改这两个值。usesFramework引用框架版本信息系统应用修改时需要加版本号。renameManifestPackage有些工具用它做 Manifest 包名替换但 Apktool 本身不直接改包名改包名要动 smali 里的常量字符串这是个容易踩坑的操作。如果你修改了 manifest 里的package属性重编译大概率报错。现在很多 App 的包名不在 manifest 中而是在 applicationId 和 build 配置里改包名属于一个复杂工程不要想着改个 manifest 就完事。5.3 命令行常用参数这篇文的总结性干货最后把我常用的 Apktool 命令和参数状态表放这里方便你直接复制场景命令标准解包apktool d app.apk解包到指定目录apktool d app.apk -o out只解资源apktool d app.apk -s只解 dexapktool d app.apk -r强制覆盖输出目录apktool d app.apk -f使用 aapt2 重打包apktool b out --use-aapt2输出指定文件名apktool b out -o new.apk无资源模式重打包apktool b out --no-res无 dex 模式重打包apktool b out --no-src导入框架文件apktool if framework-res.apk这些参数是高频操作剩下的遇到再查也行别指望一次背完。5.4 集成到脚本的批量处理思路如果手里的 APK 不止一个比如要批量给多语言包改名、批量替换启动图手动一条条敲命令太累了。可以写个简单的 shell 脚本循环处理。下面是一个批量解包并重打包的小例子macOS/Linuxfor apk in *.apk; do apktool d $apk -o out_${apk%.apk} apktool b out_${apk%.apk} -o new_${apk%.apk}.apk done要注意的是如果原包结构复杂脚本很容易因为某个包的特殊性问题中断建议在脚本里加入单文件失败后继续处理的逻辑比如|| echo error on $apk。批量操作的精髓不是一次跑完而是让失败项足够清晰。6. 常见报错和排查技巧实录6.1 重编译报错Invalid file name这是资源文件名不合法导致的通常出现在解包后被修改过的资源文件里。例如把res/drawable/xxx.jpg改成了带中文或空格的文件名Android 资源系统不接受这种命名。排查方法是看 build 日志里哪一行报的FileNotFoundException或Invalid file name直接把文件名改回合法格式。还有一个类似问题资源文件路径包含大写字母Android 的资源名标准要求全小写如果你手误改成大写同样会报错。这种事很蠢但确实会发生尤其当你从 macOS 复制文件时文件名大小写很容易在 Windows 上出问题。6.2 重编译报错Could not decode arsc file解包阶段报这个说明 resources.arsc 文件格式特殊常见于资源混淆、劣质打包工具或加固壳。你可以先试apktool d app.apk -r绕过资源解码只看 dex。如果必须改资源需要借助其他工具先还原资源表或者找专业的脱修工具Apktool 本身没有魔法。我在解某个老版本 App 时遇到过这个报错原因是它用了自定义的资源压缩算法Apktool 官方修复之前无法解码。最后我只能放弃资源修改只用-r模式处理 dex。所以不必死磕一个工具搞不定的事换个思路也许更快。6.3 重编译报错duplicate entry重打包阶段报duplicate entry一般是res/下存在重复资源或.git残留、临时文件被一起打进去了。比如你解包后用了 Finder/Windows Explorer 浏览目录系统悄悄生成了Thumbs.db或.DS_Store文件这些文件可能被当作资源文件打进去造成重复或类型错误。解决办法是重打包前清理目录里的隐藏文件或者用命令行find . -name .DS_Store -delete之类清理掉。养成习惯解包后的目录只保留 Apktool 生成的目录不要在里面乱塞文件。6.4 重打包后安装失败INSTALL_PARSE_FAILED_NO_CERTIFICATES 或 INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION这类问题绝大多数是签名没有完成。记住apktool build 出来的 APK 是裸包不签名不可能安装成功。先检查是否执行了 apksigner再看 v1/v2 签名是否对齐。如果提示签名方案不受支持可能是 apksigner 版本太老无法识别较新的 v3 签名也可能是你用了自签的 v1 证书但 targetSdk 太高系统要求 v2。最省心的做法是生成一个新的 keystore用 apksigner 默认参数签名不要动额外的 flag。6.5 安装成功但启动闪退闪退原因比编译错误更难排查常见的有改动 smali 导致逻辑异常比如 register 数量不够。修改 Manifest 后丢了必要的组件声明或权限。签名不一致导致原有 App 的签名校验失败应用自校验。修改了资源 ID 或 public.xml运行时资源索引错位。so 库架构缺失比如原包只有 arm64你改过的设备模拟器是 x86。建议从小改动开始测试每次改完只验证一个点别一次改完再找问题。我之前图省事一次性改了名称、图标、逻辑三处出问题后根本分不清是哪一步搞坏的。后来学聪明了每改一步就重打包测试一次虽然多花时间但排错效率反而高很多。6.6 常见问题速查表现象原因快速处理解包就报错加固壳 / 资源混淆换工具或只解 dex编译报 aapt2 错误资源格式或路径问题加--use-aapt2或换 JDK编译报 UTF-8 编码错误strings.xml 有非法字符检查特殊符号转义重打包后安装失败未签名 / 签名方案缺失apksigner 重新签名重打包后闪退资源 ID 错乱 / smali 改坏回退到上一个可用版本排查找不到某个类多 dex 或加固所有 smali 目录都搜一遍7. 写在最后的几句经验Apktool 是一个“上限很高、下限很低”的工具刚接触时你只需要三条命令就能跑通解包到重打包的流程但真正把它用得顺手需要你熟悉 Android 资源编译机制、dex 文件格式以及一套调试思路。我个人的建议是先从改应用名称和启动图标这种无风险操作入手再尝试去更新弹窗、改版本号这类逻辑修改最后才考虑复杂场景。每一类操作都先备份原始的 APK 和解包目录尤其是AndroidManifest.xml和apktool.yml这两个文件改动错误造成的损失往往最大。另外Apktool 本身不提供回滚机制也没有断点调试能力它的定位就是“文本化 APK 内部结构”。所以修改前做好备份、修改时一次只动一点、重打包后立刻安装测试这三条习惯比任何高级技巧都重要。希望这篇文章能帮你少走点弯路。
RELATED READING

延伸阅读

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