Patching Android apps: what could possibly go wrong

Author: dnet

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/brututilJar_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 original AndroidManifest.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!