The Challenge
Our lab administrator has just passed out from a strange virus. Please help us find the password to his messaging app so we can identify what he was working on and save his life.
Herald.apk
This challenge was labelled “mobile” and “reverse”, and had 58 solves by the end of the competition.
The Solution
TL;DR is at the end
First things first, let’s make sure this is a normal APK:
$ file Herald-e3081153dbcbc3f2bcd6aa0453e8ec6f7055deaf5762aee0a794e28e58b8bb12.apk
Herald-e3081153dbcbc3f2bcd6aa0453e8ec6f7055deaf5762aee0a794e28e58b8bb12.apk: Zip archive data, at least v0.0 to extract, compression method=store
APK are just fancy ZIP files, so…
$ unzip -l Herald-e3081153dbcbc3f2bcd6aa0453e8ec6f7055deaf5762aee0a794e28e58b8bb12.apk
Archive: Herald-e3081153dbcbc3f2bcd6aa0453e8ec6f7055deaf5762aee0a794e28e58b8bb12.apk
Length Date Time Name
--------- ---------- ----- ----
55 1981-01-01 01:01 META-INF/com/android/build/gradle/app-metadata.properties
8153072 1981-01-01 01:01 classes.dex
5672 1981-01-01 01:01 lib/arm64-v8a/libbetter.so
911696 1981-01-01 01:01 lib/arm64-v8a/libc++_shared.so
[ ... 282 lines omitted ... ]
168416 1981-01-01 01:01 lib/x86_64/libyoga.so
6 1981-01-01 01:01 META-INF/androidx.activity_activity.version
6 1981-01-01 01:01 META-INF/androidx.appcompat_appcompat-resources.version
6 1981-01-01 01:01 META-INF/androidx.appcompat_appcompat.version
6 1981-01-01 01:01 META-INF/androidx.arch.core_core-runtime.version
6 1981-01-01 01:01 META-INF/androidx.autofill_autofill.version
6 1981-01-01 01:01 META-INF/androidx.cardview_cardview.version
6 1981-01-01 01:01 META-INF/androidx.coordinatorlayout_coordinatorlayout.version
6 1981-01-01 01:01 META-INF/androidx.core_core.version
6 1981-01-01 01:01 META-INF/androidx.cursoradapter_cursoradapter.version
6 1981-01-01 01:01 META-INF/androidx.customview_customview.version
6 1981-01-01 01:01 META-INF/androidx.drawerlayout_drawerlayout.version
6 1981-01-01 01:01 META-INF/androidx.fragment_fragment.version
6 1981-01-01 01:01 META-INF/androidx.interpolator_interpolator.version
6 1981-01-01 01:01 META-INF/androidx.lifecycle_lifecycle-livedata-core.version
6 1981-01-01 01:01 META-INF/androidx.lifecycle_lifecycle-livedata.version
6 1981-01-01 01:01 META-INF/androidx.lifecycle_lifecycle-runtime.version
6 1981-01-01 01:01 META-INF/androidx.lifecycle_lifecycle-viewmodel-savedstate.version
6 1981-01-01 01:01 META-INF/androidx.lifecycle_lifecycle-viewmodel.version
6 1981-01-01 01:01 META-INF/androidx.loader_loader.version
6 1981-01-01 01:01 META-INF/androidx.recyclerview_recyclerview.version
6 1981-01-01 01:01 META-INF/androidx.savedstate_savedstate.version
6 1981-01-01 01:01 META-INF/androidx.swiperefreshlayout_swiperefreshlayout.version
6 1981-01-01 01:01 META-INF/androidx.transition_transition.version
6 1981-01-01 01:01 META-INF/androidx.vectordrawable_vectordrawable-animated.version
6 1981-01-01 01:01 META-INF/androidx.vectordrawable_vectordrawable.version
6 1981-01-01 01:01 META-INF/androidx.versionedparcelable_versionedparcelable.version
6 1981-01-01 01:01 META-INF/androidx.viewpager2_viewpager2.version
6 1981-01-01 01:01 META-INF/androidx.viewpager_viewpager.version
6 1981-01-01 01:01 META-INF/com.google.android.material_material.version
1515 1981-01-01 01:01 META-INF/kotlin-stdlib-common.kotlin_module
67 1981-01-01 01:01 META-INF/kotlin-stdlib-jdk7.kotlin_module
268 1981-01-01 01:01 META-INF/kotlin-stdlib-jdk8.kotlin_module
4810 1981-01-01 01:01 META-INF/kotlin-stdlib.kotlin_module
24 1981-01-01 01:01 META-INF/okhttp-urlconnection.kotlin_module
277 1981-01-01 01:01 META-INF/okhttp.kotlin_module
355 1981-01-01 01:01 META-INF/okio.kotlin_module
24 1981-01-01 01:01 META-INF/react-native-screens_release.kotlin_module
926 1981-01-01 01:01 kotlin/annotation/annotation.kotlin_builtins
3685 1981-01-01 01:01 kotlin/collections/collections.kotlin_builtins
200 1981-01-01 01:01 kotlin/coroutines/coroutines.kotlin_builtins
758 1981-01-01 01:01 kotlin/internal/internal.kotlin_builtins
13050 1981-01-01 01:01 kotlin/kotlin.kotlin_builtins
2301 1981-01-01 01:01 kotlin/ranges/ranges.kotlin_builtins
2338 1981-01-01 01:01 kotlin/reflect/reflect.kotlin_builtins
218 1981-01-01 01:01 okhttp3/internal/publicsuffix/NOTICE
37730 1981-01-01 01:01 okhttp3/internal/publicsuffix/publicsuffixes.gz
2540 1981-01-01 01:01 AndroidManifest.xml
1260 1981-01-01 01:01 res/-0.xml
1396 1981-01-01 01:01 res/-Y.xml
[ ... 645 lines omitted ... ]
840 1981-01-01 01:01 res/zq.xml
429512 1981-01-01 01:01 resources.arsc
920104 1981-01-01 01:01 assets/index.android.bundle
88755 1981-01-01 01:01 META-INF/CERT.SF
1375 1981-01-01 01:01 META-INF/CERT.RSA
88681 1981-01-01 01:01 META-INF/MANIFEST.MF
--------- -------
70835418 951 files
Well, that looks like a legitimate APK.
Wikipedia has a quick rundown of the different files and directories within, if you’re not familiar.
Thankfully, the challenge designer was generous enough to compile the app for all architectures, including x86_64
.
This means that running the app in a desktop emulator like Genymotion will have no performance penalty (if there was only ARM support, the emulator would not be able to take advantage of hardware CPU emulation).
So let’s throw the app into a virtualized budget phone running Android 10 and see what we find.
We get a basic login screen (with subpar UX): two text fields for username and password, and a login button.
We also get an About tab at the bottom - unfortunately that only leads to a static Lorem Ipsum screen.
Entering random credentials gives you a “Wrong Username/Password combination” error, as expected.
Looking back at the challenge, our aim seems to be to crack this authentication somehow: considering that no data besides this APK was given, the flag is presumably provided by the app if the correct password is given.
Here is where I turned my attention to the APK itself.
First, I decompiled the app using jadx
, a popular decompiler for Android apps.
$ jadx -d decompiled --deobf --deobf-parse-kotlin-metadata --fs-case-sensitive Herald-e3081153dbcbc3f2bcd6aa0453e8ec6f7055deaf5762aee0a794e28e58b8bb12.apk
INFO - loading ...
INFO - processing ...
ERROR - finished with errors, count: 4
The errors reported at the end seem to be the typical issues that decompilers tend to encounter, nothing important in this case.
From here I spent a little while browsing the decompiled source code (in VSCode), and one of the native libraries caught my eye (there were matching libraries for the other architectures too):
lib/x86_64/libhermes-executor-common-debug.so
lib/x86_64/libhermes-executor-common-release.so
lib/x86_64/libhermes-executor-debug.so
lib/x86_64/libhermes-executor-release.so
These debug libraries seemed out of the ordinary, and especially so in a CTF.
Generally, any debug-enabled binaries include debugging information that makes reverse engineering way easier.
$ file -rk decompiled/resources/lib/x86_64/*debug.so
[...]/libhermes-executor-common-debug.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=efaca929a1e4ee83b85d3e898471ea9bb3d74e7d, stripped
[...]/libhermes-executor-debug.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=77c2c13b10f0a847b909b67ea4bf3caca12020a4, stripped
[...]/libreact_debug.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=546799eff4ef23b1f8780f9bf3d3aa5a0ccdc220, stripped
[...]/libreact_render_debug.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=1dede243b90f0779a8729b47d0a56591fc9619a4, stripped
Seeing that the binaries did not include debugging symbols turned me off this path, however.
And I am glad it did, because the solution turned out to be much easier.
Taking a better look at the Java source code, I found that com.herald
contained very little code.
Here’s com/herald/MainApplication.java
:
|
|
Based on the abundance of “React Native” terminology, and seeing little other code, I figured this app was likely a React Native app.
I had never worked with React Native before, let alone tried to decompile an app built with it.
So I did what anyone in my shoes would do, and searched online for “react native decompile mobile”.
DuckDuckGo returned a Stack Overflow answer that recommends using react-native-decompiler, which seemed to be exactly what I was looking for.
After many failed attempts to run the decompiler (I actually put this aside to work on other chals before I finally got it working), I was finally able to download and run it:
$ git clone https://github.com/nomi9995/react-native-decompiler
[...]
$ cd react-native-decompiler
$ npm run build
[...]
$ cd ..
$ node react-native-decompiler/out/main.js -i decompiled/resources/assets/index.android.bundle -o react-decompile
Reading file...
[!] No modules were found!
[!] Possible reasons:
[!] - The React Native app is unbundled. If it is, export the "js-modules" folder from the app and provide it as the --js-modules argument
[!] - The bundle is a binary/encrypted file (ex. Facebook, Instagram). These files are not supported
[!] - The provided Webpack bundle input is not or does not contain the entrypoint bundle
[!] - The file provided is not a React Native or Webpack bundle.
Well that’s annoying. All that time and effort, and it doesn’t even work.
After doing a bit more research, I realized that the app uses the Hermes JS engine (hence the native libraries from before), which is apparently what react-native-decompiler
means when it says “binary/encrypted file”.
Back to the internet, where I found this Stack Overflow answer that suggests using hbctool
to decompile the file.
As their README explains: by default, React Native apps ship with their Javascript source code, which can be extracted and un-minified to “decompile” them. However, Facebook has recently created their own JS engine called Hermes. If the developer opts in, their app will contain HermesJS bytecode instead of the minified source code - which means that there is no source code to extract.
Therefore, we need to use this tool to decompile the bytecode into something human-readable.
Their README also specifies which bytecode versions it supports; as of the competition, they were 59, 62, 74, and 76.
With no easy way to check which bytecode version was in-use, I figured I may as well try it and see what happens.
(side note: turns out you can just run file
on it and it’ll tell you)
$ git clone https://github.com/bongtrop/hbctool
[...]
$ cd hbctool
$ poetry install
Creating virtualenv hbctool in ~/ctf/insomnihack-2022-teaser/Herald/hbctool/.venv
Installing dependencies from lock file
Package operations: 1 install, 0 updates, 0 removals
• Installing docopt (0.6.2)
Installing the current project: hbctool (0.1.5)
$ poetry run hbctool
Usage:
hbctool disasm <HBC_FILE> <HASM_PATH>
hbctool asm <HASM_PATH> <HBC_FILE>
hbctool --help
hbctool --version
Alright, so far so good. Usage seems simple enough.
$ poetry run hbctool disasm ../decompiled/resources/assets/index.android.bundle ../hermes-decompile
[*] Disassemble '../decompiled/resources/assets/index.android.bundle' to '../hermes-decompile' path
Traceback (most recent call last):
File "<string>", line 1, in <module>
File "~/ctf/insomnihack-2022-teaser/Herald/hbctool/hbctool/__init__.py", line 61, in main
disasm(args['<HBC_FILE>'], args['<HASM_PATH>'])
File "~/ctf/insomnihack-2022-teaser/Herald/hbctool/hbctool/__init__.py", line 33, in disasm
hbco = hbc.load(f)
File "~/ctf/insomnihack-2022-teaser/Herald/hbctool/hbctool/hbc/__init__.py", line 29, in load
assert version in HBC, f"The HBC version ({version}) is not supported."
AssertionError: The HBC version (84) is not supported.
Welp. Looks like this app uses bytecode version 84, which clearly isn’t supported.
Maybe there’s a workaround?
Going back to hbctool
’s GitHub page, I check the Issues tab and find what I was looking for right away:
“The HBC version (84) is not supported.”
As a very unstable and non-tested workaround, you can use my basic POC for version 84. It uses almost the same python source code as the 76 version and headers/def files from the original hermes source code (v0.10.0). It seems to work with basic bytecode.
You can find it here: https://github.com/niosega/hbctool/tree/draft/hbc-v84
I will maybe do more tests and create a proper Pull request in the future.
Third time’s the charm, right?
$ cd ..
$ rm -rf hbctool # without -f it complains about git's read-only files
$ git clone https://github.com/niosega/hbctool
[...]
$ git checkout draft/hbc-v84
[...]
$ cd hbctool
$ poetry install
[...]
$ poetry run hbctool disasm ../decompiled/resources/assets/index.android.bundle ../hermes-decompile
[*] Disassemble '../decompiled/resources/assets/index.android.bundle' to '../hermes-decompile' path
[*] Hermes Bytecode [ Source Hash: fcea8fb1a251839a7811a5cdcfc8f975e0b3d67b, HBC Version: 84 ]
[*] Done
Finally!
hbctool
gave us three files: instruction.hasm
, metadata.json
, and string.json
.
$ rg INS *
$
Yeah, didn’t think so. Always worth a shot though.
I didn’t really know what to do with these files; metadata.json
was large enough to cripple even xed (my basic text editor of choice).
hbctool
’s README kindly asks us to read a blog post for more information, so I figured I’d start there.
It explained that instruction.hasm
contained the application logic, and demonstrated a simple example of modifying the instructions.
Well, might as well look to see what the app is doing.
Opening instruction.hasm
in VSCode, I first searched for password
. Suprisingly there were 10 matches.
hbctool
seems to insert comments that tell you the contents of strings that are referenced, which means you don’t need to constantly lookup the string for every ID you see.
Most matches seemed to be useless, but one quickly stood out.
It was located in a function named tryAuth
:
|
|
Based on the UI messages and the function name, it seemed as though this function was responsible for the entire authentication process.
And, more importantly, there’s a mention of a ‘decodedFlag’ near the bottom.
I tried following the control flow starting from the top, but the jump instructions’ targets weren’t apparent to me, so I went back to the ‘decodedFlag’, and noticed the Jmp
instruction directly above it.
What if… what if we just comment that out?
|
|
It’s definitely not that easy. It can’t be… but there’s only one way to find out.
So. I guess now we recompile the APK?
Except, we can’t quite do that. Not yet.
You see, I used jadx
earlier to decompile the Java portion of the app, and that’s how I got this index.android.bundle
. But jadx
is destructive, meaning it is not designed to be easily reversed.
We need apktool
for this (which we could have used from the start, if we knew this was a React Native app).
apktool d
will “decode” the app into a directory, in a way that is reversible.
$ apktool d -s -o ../apk-mod ../Herald-e3081153dbcbc3f2bcd6aa0453e8ec6f7055deaf5762aee0a794e28e58b8bb12.apk
I: Using Apktool 2.6.0 on ../Herald-e3081153dbcbc3f2bcd6aa0453e8ec6f7055deaf5762aee0a794e28e58b8bb12.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...
I: Loading resource table from file: ~/.local/share/apktool/framework/1.apk
I: Regular manifest package...
I: Decoding file-resources...
I: Decoding values */* XMLs...
I: Copying raw classes.dex file...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...
Now we can use hbctool asm
to re-assemble the HermesJS bytecode.
$ poetry run hbctool asm ../hermes-decompile ../apk-mod/assets/index.android.bundle
[*] Assemble '../hermes-decompile' to '../apk-mod/assets/index.android.bundle' path
[*] Hermes Bytecode [ Source Hash: fcea8fb1a251839a7811a5cdcfc8f975e0b3d67b, HBC Version: 84 ]
[*] Done
Next, we rebuild the APK with apktool
.
$ cd ..
$ apktool b apk-mod/ -o Hermes-modded.apk
I: Using Apktool 2.6.0
I: Checking whether resources has changed...
I: Building apk file...
I: Copying unknown files/dir...
I: Built apk...
Now, you might think you could just drag-and-drop Hermes-modded.apk
into the emulator to install it.
Unfortuntely, that would give you (from Genymotion at least) a very unhelpful generic error message. ADB would give you this error code: INSTALL_PARSE_FAILED_NO_CERTIFICATES
.
This is because our re-built APK isn’t signed. This makes sense; how is apktool
supposed to sign the APK without the original developer’s private key?
So we’ll sign it with our own key (check out this simple guide if you don’t already have a keypair - I used mykey
for alias_name
).
$ jarsigner Hermes-modded.apk mykey
Enter Passphrase for keystore:
jar signed.
Warning:
The signer's certificate is self-signed.
Now we need to uninstall the app from the emulated phone (otherwise, Android will complain about mismatched certificates, because your modded APK is signed with a different keypair than the original APK was).
Finally, we can drag-and-drop the APK into Genymotion to install it.
Enter admin
for the username, anything you want for the password, and hit login.
Hiding in the background lies our precious flag.
It was, in fact, this easy.
Except for the parts that weren’t.
TL;DR
apktool
hbctool
(v84-compat fork)- Ctrl-F
tryAuth
- Patch out
jmp
instruction - Rebuild + sign + install
- Try logging into
admin
, get flag