Patching Android apps: what could possibly go wrong
Many tools are timeless: a quality screwdriver will work in ten years just as fine as yesterday. Reverse engineering tools, on the other hand need constant maintenance as the technology we try to inspect with them is a moving target. We’ll show you how just a simple exercise in Android reverse engineering resulted in three patches in an already up-to-date tool.
I got an Android application to test that had issues with emulators and rooted devices, so I started it on a physical, unrooted device. That’s not too bad, one just has to enable the
debuggable flag and then JDB can be used to set breakpoints and examine internals.
This can be done by hand using apktool: just disassemble the APK, edit
AndroidManifest.xml, rebuild and (re)sign the APK. Objection makes this much easier, just use
patchapk with the
--enable-debug flag, so I did:
$ objection patchapk -s victim.apk --enable-debug ... Rebuilding the APK with the frida-gadget loaded... Rebuilding the APK may have failed. Read the following output to determine if apktool actually had an error: W: invalid resource directory name: /tmp/tmp14pitz7m.apktemp/res navigation brut.androlib.AndrolibException: brut.common.BrutException: could not exec (exit code = 1): [/tmp/brut<em>util</em>Jar_587774932932996925.tmp ...
It seemed that
apktool has some issues with resources, but I don’t need to touch those, so I just added the
--skip-resources which results in resources being copied as-is without decoding and (re)encoding. Or so I thought:
$ objection patchapk -s victim.apk -d -D ... Unpacking victim.apk App already has android.permission.INTERNET Traceback (most recent call last): File "/home/dnet/.local/bin/objection", line 10, in <module> sys.exit(cli()) File "/home/dnet/.local/lib/python3.7/site-packages/click/core.py", line 764, in __call__ return self.main(*args, **kwargs) File "/home/dnet/.local/lib/python3.7/site-packages/click/core.py", line 717, in main rv = self.invoke(ctx) File "/home/dnet/.local/lib/python3.7/site-packages/click/core.py", line 1137, in invoke return _process_result(sub_ctx.command.invoke(sub_ctx)) File "/home/dnet/.local/lib/python3.7/site-packages/click/core.py", line 956, in invoke return ctx.invoke(self.callback, **ctx.params) File "/home/dnet/.local/lib/python3.7/site-packages/click/core.py", line 555, in invoke return callback(*args, **kwargs) File "/home/dnet/.local/lib/python3.7/site-packages/objection/console/cli.py", line 344, in patchapk patch_android_apk(**locals()) File "/home/dnet/.local/lib/python3.7/site-packages/objection/commands/mobile_packages.py", line 168, in patch_android_apk patcher.flip_debug_flag_to_true() File "/home/dnet/.local/lib/python3.7/site-packages/objection/utils/patchers/android.py", line 413, in flip_debug_flag_to_true xml = self._get_android_manifest() File "/home/dnet/.local/lib/python3.7/site-packages/objection/utils/patchers/android.py", line 240, in _get_android_manifest return ElementTree.parse(os.path.join(self.apk_temp_directory, 'AndroidManifest.xml')) File "/usr/lib/python3.7/xml/etree/ElementTree.py", line 1197, in parse tree.parse(source, parser) File "/usr/lib/python3.7/xml/etree/ElementTree.py", line 598, in parse self._root = parser._parse_whole(source) xml.etree.ElementTree.ParseError: not well-formed (invalid token): line 1, column 0 Cleaning up temp files... Failed to cleanup with error: [Errno 2] No such file or directory: '/tmp/tmpesy509ma.apktemp.objection.apk'
After minutes of debugging, the real issue surfaced: skipping resource decoding also meant that
AndroidManifest.xml was left in its compiled Android binary XML format, resulting in the above message where the built-in Python XML parser tries to read the binary format. To spare others from this experience, I wrote a tiny patch and submitted it as a pull request so that this incompatible combination of command line parameters is detected early and a helpful message is displayed.
This still left me with a situation where I needed to find an alternate solution. As it turned out, others already had the same problem with
apktool and the solution was there: use AAPT2 and
apktool even provided a command line switch for that called
--use-aapt2, it’s just that it couldn’t be used through Objection so I wrote another tiny patch that made it possible to pass this through and submitted it as a pull request. This made it possible to enable debugging, I set some breakpoints and started playing with the app… then it promptly crashed with the following backtrace
10-16 11:14:24.138 9824 9824 E AndroidRuntime: FATAL EXCEPTION: main 10-16 11:14:24.138 9824 9824 E AndroidRuntime: Process: com.example, PID: 9824 10-16 11:14:24.138 9824 9824 E AndroidRuntime: java.lang.IllegalStateException: Module with the Main dispatcher is missing. Add dependency providing the Main dispatcher, e.g. 'kotlinx-coroutines-android' 10-16 11:14:24.138 9824 9824 E AndroidRuntime: at f.a.b.p.n(SourceFile:4) 10-16 11:14:24.138 9824 9824 E AndroidRuntime: at f.a.b.p.b(SourceFile:1) 10-16 11:14:24.138 9824 9824 E AndroidRuntime: at f.a.Z.a(SourceFile:11) 10-16 11:14:24.138 9824 9824 E AndroidRuntime: at f.a.c.a.a(SourceFile:3) 10-16 11:14:24.138 9824 9824 E AndroidRuntime: at kotlinx.coroutines.CoroutineStart.invoke(SourceFile:10)
First, I thought, this must have been an issue with
apktool, so I tried to narrow the range possible causes by using my time machine: 5 years ago I wrote a blog post about quick and dirty Android binary XML edits so I tried to follow that by doing nothing but deleting the
META-INF directory and (re)signing the APK with no other modifications. Yet I had the just same crash as above.
Searching on the web resulted in issues with references to the
META-INF/services directory, and listing the contents of the original APK revealed that indeed there were many other files in the
META-INF directory besides those needed for JAR-style signature verification (
MANIFEST.MF). As it turned out, this is also what apktool does:
There is no META-INF dir in resulting apk. Is this ok?
META-INFcontains apk signatures. After modifying the apk it is no longer signed. You can use
-c / --copy-originalto retain these signatures. However, using
-cuses the original
AndroidManifest.xmlfile, so changes to it will be lost.
Since Objection uses
apktool as well, I wrote a third patch and submitted it as a pull request. The only thing needed was a check to see if there were any files in the
META-INF directory of the original APK (carefully saved to the subdirectory
apktool) that have nothing to do with signature verification. If anything matched this filter, they got appended to the APK after
apktool processed it but before signing it with
jarsigner by Objection.
After these three patches, I could finally produce an APK that ran fine on a physical device and could be tampered with both using JDB and the Frida gadget injected by the
patchapk command of Objection. On 19th October 2019, version 1.8.0 of Objection was released with all three of my patches included, so now you can enjoy these improvements as well just by installing Objection using
Thanks to the Objection team for developing and maintaining such a great tool for reverse engineering mobile apps and also being quick to accept and merge pull requests!