I recently came up against my first split APK during an Android app security assessment. My usual toolkit doesn’t support split APKs, so I hacked together a solution to allow me to instrument and analyse an Android app that was split over multiple APKs. I’ve published patch-apk.py to Github, but read on for the gory details!
It turns out Android has supported split APKs since version 5/Lollipop (June 2014, API level 21), but this was the first time I’d come across one in many years of mobile app pentesting. APK splitting allows an app to be split across multiple APK files, for example one might contain the main code while another contains visual resources that are optimised for a given screen resolution, or native libraries that are compiled for a given architecture.
We can identify whether an app uses split APKs by checking if the
adb shell pm path command returns multiple APK paths. Using a COVID Symptom Tracker app as an example we get the following output:
$ adb shell pm path com.joinzoe.covid_zoe package:/data/app/com.joinzoe.covid_zoe-NW8ZbgI5VPzvSZ1NgMa4CQ==/base.apk package:/data/app/com.joinzoe.covid_zoe-NW8ZbgI5VPzvSZ1NgMa4CQ==/split_config.arm64_v8a.apk package:/data/app/com.joinzoe.covid_zoe-NW8ZbgI5VPzvSZ1NgMa4CQ==/split_config.en.apk package:/data/app/com.joinzoe.covid_zoe-NW8ZbgI5VPzvSZ1NgMa4CQ==/split_config.xxhdpi.apk
This app was chosen purely for using split APKs and being high on the top apps list on Google Play at the time of writing!
Patching with Objection
One of my main go-to tools for mobile app testing is objection. Unfortunately we get an error if we attempt to use
objection patchapk to patch
base.apk from an App Bundle and install it back on the device.
$ adb install-multiple base.objection.apk split_config.arm64_v8a.apk split_config.en.apk split_config.xxhdpi.apk adb: failed to finalize session Failure [INSTALL_FAILED_INVALID_APK: /data/app/vmdl1660002818.tmp/1_base.objection.apk signatures are inconsistent]
Looking at the APKs it wasn’t entirely clear what the signature inconsistency might be. It was possibly related to the
resources.asrc file, but I’m not familiar with the format. Either way, the contents of the split APK files looked simple enough to smash them together into one.
Simply copying files from the split APKs into the base APK directory and rebuilding didn’t work. A quick look at
AndroidManifest.xml revealed that the
<application> element had the attribute
android:isSplitRequired set to
true. I set this to false, then removed the following
<meta-data android:name="com.android.vending.splits.required" android:value="true"/> <meta-data android:name="com.android.vending.splits" android:value="@xml/splits0"/>
This still didn’t work, but the error hinted that the
android:extractNativeLibs attribute of the
<application> element was the problem. After setting this attribute to
true I managed to build a single APK, patch it using
objection patchapk, and get it up and running on my Android device.
After patching the app in this way it appeared to work correctly, but there were various icons missing from the UI compared to the corresponding iOS app I’d been working on. While this didn’t affect the functionality of the app, it was the result of me hacking the APKs together.
Looking more closely at the resources from the extracted APKs I noticed that the file
res/values/public.xml was very different across the different APKs. In particular, some of the resource names from the split APKs weren’t present in
base.apk, although the corresponding resource IDs were.
Using the same symptom tracker app from earlier as an example, if we look inside
res/values/public.xml of the APK
split_config.xxhdpi.apk we can see an entry with the name
exo_icon_stop and the id
0x7f0800bf. The same file in
base.apk did not include an entry with this name, but it did have an entry with the same ID as follows:
<public type="drawable" name="APKTOOL_DUMMY_bf" id="0x7f0800bf" />
The entry with the matching id in
base.apk had been given a dummy name by
apktool. Presumably because the actual resource name was defined in the split APK and therefore could not be resolved by
Fixing Resource IDs
The dummy resource names generated by
apktool aren’t a problem in many cases because Android apps tend to refer to resources by their ID. In this case, however, certain images were loaded by name.
With hundreds of resource identifiers in the app, I started hacking together some Python to automate fixing of resource IDs. The first step was to parse
base.apk and get the name and id of all entries where the name was prefixed with
Next, I needed to map those IDs to the correct names from the split APKs. This was done by parsing the
res/values/public.xml of each split APK and retrieving the name of each entry with an id matching one of those loaded from
With the mappings of dummy names to IDs to real names I could update
base.apk to give all entries with dummy names their correct names.
The final step was to process all XML files in the
res directory of
base.apk and replace all references to one of the dummy resource names with the real resource name.
With the resource identifiers fixed, the patched APK worked great.
Wrapping It Up
I’ve been automating some of my mobile app pentesting tasks recently. One of the main tasks I’ve automated is that of getting an app setup for proxying and instrumentation:
- Working out the package name and APK path on the device.
- Pulling the APK.
- Patching the APK to support user-installed CA certificates.
- Injecting a frida/objection instrumentation gadget.
- Uninstalling the original app.
- Installing the patched app.
Originally I had no intention of releasing this as it was largely just a dirty wrapper script automating several other tools.
Then I came across a split APK.
I’m not aware of any other easy method of patching a split APK and it’s definitely not something I’d want to be doing manually, so I’ve incorporated that into my original “pull-patch-push” script, tidied things up a touch, and released it on Github: patch-apk.py.