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 (*.RSA
, *.SF
and MANIFEST.MF
). As it turned out, this is also what apktool does:
There is no META-INF dir in resulting apk. Is this ok?
Yes.
META-INF
contains apk signatures. After modifying the apk it is no longer signed. You can use-c / --copy-original
to retain these signatures. However, using-c
uses the originalAndroidManifest.xml
file, 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 original/META-INF
by 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 pip
.
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!