最近在分析一款叫 X Saver 的 Twitter 视频下载 App 时,我遇到的是 Play 商店常见的 split APK 形式,而不是传统的单一 APK。 本文记录一下完整的拆包、资源合并、重新打包的过程,以及中间踩过的坑,供之后遇到同类问题时参考。
先通过 ADB 查到 X Saver 在设备上的安装路径:
adb shell pm path twittervideodownloader.twitter.videoindir.savegif.twdown
系统返回三条路径,大致长这样:
.../base.apk.../split_config.arm64_v8a.apk.../split_config.xxxhdpi.apk再用 adb pull 拉到本地即可。
这其实就是典型的 Android App Bundle 拆分结果:
arm64-v8a 的 so 库等 ABI 相关内容xxxhdpi)的图标、图片等资源系统安装时会把它们视为一个 App 的不同 split 一起加载。
本文的目标是:把这些内容合并成一个可单独安装的“胖 APK”来方便后续逆向分析和调试。
使用的主要工具:
keytool、apksigner 用于签名(或 Android SDK 中的 apksigner)在工作目录下准备三个子目录(名字可按习惯):
XSaver/
base.apk
split_config.arm64_v8a.apk
split_config.xxxhdpi.apk
先对 base 和 dpi split 做反编译:
apktool d base.apk -o base
apktool d split_config.xxxhdpi.apk -o split_dpi
ABI split 只包含 so 库,直接解压即可(也可以 apktool d,看个人习惯):
mkdir split_abi
unzip split_config.arm64_v8a.apk -d split_abi
此时目录结构大致如下:
XSaver/
base/ # apktool 解包后的 base 工程
split_dpi/ # apktool 解包后的 dpi split 工程
split_abi/ # unzip 出来的 abi split 内容
后续所有修改都在 base/ 目录上进行。
split_config.arm64_v8a.apk 的主要价值就是 lib/arm64-v8a/*.so。
直接把它们合并到 base 即可:
# 如果 base/lib/arm64-v8a 不存在先建目录
mkdir -p base/lib/arm64-v8a
# 复制 so
copy split_abi\lib\arm64-v8a\* base\lib\arm64-v8a\
(Windows 也可以用资源管理器直接拖;注意别把 META-INF、AndroidManifest.xml 之类的乱丢进 base)
重点是 split_dpi 目录下的 res 内容。
我实际看到的结构类似:
split_dpi/res/
drawable-anydpi-v21/
drawable-anydpi-v24/
drawable-hdpi-v4/
drawable-ldrtl-xxhdpi-v17/
drawable-night-anydpi-v21/
drawable-xhdpi-v4/
drawable-xxhdpi-v4/
values/
values-hdpi/
...
关键点:这里是 apktool 解包后的 res,里面的
.xml已经是「纯文本 XML」了,可以安全交给 aapt2 重新编译。
如果你是直接 unzip split_config.xxxhdpi.apk 然后拷 res/*.xml 到 base,那里面的 XML 是 二进制格式,aapt2 会直接报错:
error: not well-formed (invalid token).
这是我一开始踩的一个坑,所以正确做法一定是:先 apktool d,再拷。
拷贝命令示例:
# 各种 drawable 目录
copy /y /s split_dpi\res\drawable-anydpi-v21\* base\res\drawable-anydpi-v21\
copy /y /s split_dpi\res\drawable-anydpi-v24\* base\res\drawable-anydpi-v24\
copy /y /s split_dpi\res\drawable-hdpi-v4\* base\res\drawable-hdpi-v4\
copy /y /s split_dpi\res\drawable-ldrtl-xxhdpi-v17\* base\res\drawable-ldrtl-xxhdpi-v17\
copy /y /s split_dpi\res\drawable-night-anydpi-v21\* base\res\drawable-night-anydpi-v21\
copy /y /s split_dpi\res\drawable-xhdpi-v4\* base\res\drawable-xhdpi-v4\
copy /y /s split_dpi\res\drawable-xxhdpi-v4\* base\res\drawable-xxhdpi-v4\
# values 相关
copy /y /s split_dpi\res\values\* base\res\values\
copy /y /s split_dpi\res\values-hdpi\* base\res\values-hdpi\
用资源管理器图形界面拷也完全可以,确认目标目录在 base\res 下即可。
原始的 base Manifest 中会有一些声明,告诉 Play / 系统这是一个需要 split 的应用,例如:
<meta-data
android:name="com.android.vending.splits.required"
android:value="true"/>
<meta-data
android:name="com.android.vending.splits"
android:resource="@xml/splits0"/>
以及 <manifest> 标签上的 split 属性:
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
android:requiredSplitTypes="base__abi,base__density"
android:splitTypes="">
在我们准备做「胖 APK」时,这些配置已经没意义了,反而有可能引发安装异常。
所以在 base/AndroidManifest.xml 中进行如下修改:
删除两条 <meta-data>:
com.android.vending.splits.requiredcom.android.vending.splits从 <manifest> 标签上删除:
android:requiredSplitTypes="base__abi,base__density"android:splitTypes=""修改后 <manifest> 头部就恢复成比较正常的形式:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="twittervideodownloader.twitter.videoindir.savegif.twdown"
android:versionCode="..."
android:versionName="...">
public.xml 位于 res/values/public.xml,主要记录「资源名 ↔ 资源 ID」。
这里的原则很简单:
只保留 base 自己的
public.xml,split 里的不管它,也不要拷。
原因:
public.xml 和 resources.arsc;如果把 split 的 public.xml 也合进去,很容易出现:
resource xxx already defined因此在合并时:
base/res/values/public.xml 保留原样;split_dpi 的 values 里不小心把 public.xml 也拷过去,建议直接删掉它。在 base/apktool.yml 中,可以看到类似配置:
doNotCompress:
- arsc
- so
- ...
有些版本的 apktool 可能会把 dex 加进 doNotCompress,这会导致回编译时产生奇怪问题。
在我的实践中,做了两点微调:
- dex 条目(让 aapt2 正常处理压缩方式)- so(保持 native 库不被压缩)最终 doNotCompress 里包含 arsc、so 等即可。
这一步不是绝对必须,但配合很多现有项目会更稳一点。
完成以上修改后,就可以尝试回编译:
apktool b base -o app-modified.apk
not well-formed (invalid token)这一般是:你在 base/res 里面放进了“二进制 XML”,而不是 apktool 解码过的文本 XML。
最常见的场景:
unzip split_config.xxxhdpi.apk,从里面的 res/drawable-anydpi-v21/*.xml 拷到 base;.xml 是 aapt2 编译后的二进制格式,第一字节根本不是 <,aapt2 会直接报错。解决方案就是前面提过的:
对 split APK 使用
apktool d反编译,然后从反编译后的目录split_dpi/res拷资源。
修正后重新 apktool b,就可以顺利生成 app-modified.apk 了。
app-modified.apk 是未签名的,需要用自己的证书重新签名:
如果还没有 keystore,可先创建一个:
keytool -genkeypair -v -keystore my-release-key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias x_saver_mod
使用 apksigner 签名:
apksigner sign \
--ks my-release-key.jks \
--ks-key-alias x_saver_mod \
app-modified.apk
安装到测试机:
adb install -r app-modified.apk
如果前面合并没出错,一般可以正常安装并运行。 在我的实测中,合并后的 X Saver 功能正常,说明:
如果只是逆向分析,不一定非要合成胖 APK
完全可以:
jadx 打开 base.apk 看 Java/Kotlin 逻辑;split_config.arm64_v8a.apk 里的 so;合并胖 APK 更多是为了方便自己装在不同设备上调试。
生产环境/分发不建议这么做
排查问题时,多关注 aapt2 报错信息
apktool b 的日志一眼看出来。这次对 X Saver 的 split APK 合并,主要经历了三个关键点:
Manifest 与 public.xml 的处理要克制:
public.xml 保留 base 的,忽略 split 的。掌握这一套之后,再遇到类似的 App Bundle 拆包场景,基本都可以按这个套路来处理、做成自己的胖 APK 版本,方便后续 Hook、日志、协议逆向等工作。