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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package com.herald;

import android.app.Application;
import android.content.Context;
import com.facebook.react.PackageList;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.soloader.SoLoader;
import java.util.List;

public class MainApplication extends Application implements ReactApplication {
	private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
		/* class com.herald.MainApplication.C11371 */

		/* access modifiers changed from: protected */
		@Override // com.facebook.react.ReactNativeHost
		public String getJSMainModuleName() {
			return "index";
		}

		@Override // com.facebook.react.ReactNativeHost
		public boolean getUseDeveloperSupport() {
			return false;
		}

		/* access modifiers changed from: protected */
		@Override // com.facebook.react.ReactNativeHost
		public List<ReactPackage> getPackages() {
			return new PackageList(this).getPackages();
		}
	};

	private static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
	}

	@Override // com.facebook.react.ReactApplication
	public ReactNativeHost getReactNativeHost() {
		return this.mReactNativeHost;
	}

	public void onCreate() {
		super.onCreate();
		SoLoader.init((Context) this, false);
		initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
	}
}

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.”

niosega commented 16 days ago

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
Function<tryAuth>4087(3 params, 13 registers, 0 symbols):
	LoadThisNS          	Reg8:2
	GetByIdShort        	Reg8:0, Reg8:2, UInt8:1, UInt8:121
	; Oper[3]: String(121) 'state'

	GetById             	Reg8:0, Reg8:0, UInt8:2, UInt16:4142
	; Oper[3]: String(4142) 'username'

	LoadConstString     	Reg8:1, UInt16:801
	; Oper[1]: String(801) 'admin'

	JStrictNotEqual     	Addr8:38, Reg8:0, Reg8:1
	GetByIdShort        	Reg8:0, Reg8:2, UInt8:1, UInt8:121
	; Oper[3]: String(121) 'state'

	GetById             	Reg8:3, Reg8:0, UInt8:3, UInt16:4120
	; Oper[3]: String(4120) 'password'

	GetById             	Reg8:4, Reg8:2, UInt8:4, UInt16:3485
	; Oper[3]: String(3485) 'decodedText'

	NewArrayWithBuffer  	Reg8:0, UInt16:28, UInt16:28, UInt16:9398
	Call2               	Reg8:0, Reg8:4, Reg8:2, Reg8:0
	JStrictEqual        	Addr8:105, Reg8:3, Reg8:0
	GetByIdShort        	Reg8:0, Reg8:2, UInt8:1, UInt8:121
	; Oper[3]: String(121) 'state'

	GetById             	Reg8:0, Reg8:0, UInt8:2, UInt16:4142
	; Oper[3]: String(4142) 'username'

	JStrictEqual        	Addr8:45, Reg8:0, Reg8:1
	GetEnvironment      	Reg8:0, UInt8:1
	LoadFromEnvironment 	Reg8:0, Reg8:0, UInt8:6
	GetById             	Reg8:3, Reg8:0, UInt8:5, UInt16:4460
	; Oper[3]: String(4460) 'Alert'

	GetById             	Reg8:0, Reg8:3, UInt8:6, UInt16:5067
	; Oper[3]: String(5067) 'alert'

	LoadConstString     	Reg8:1, UInt16:1772
	; Oper[1]: String(1772) 'Wrong Username/Password combination'

	Call2               	Reg8:0, Reg8:0, Reg8:3, Reg8:1
	GetById             	Reg8:0, Reg8:2, UInt8:7, UInt16:3904
	; Oper[3]: String(3904) 'setPrint'

	Call2               	Reg8:0, Reg8:0, Reg8:2, Reg8:1
	Jmp                 	Addr8:66
	GetEnvironment      	Reg8:0, UInt8:1
	LoadFromEnvironment 	Reg8:0, Reg8:0, UInt8:6
	GetById             	Reg8:3, Reg8:0, UInt8:5, UInt16:4460
	; Oper[3]: String(4460) 'Alert'

	GetById             	Reg8:1, Reg8:3, UInt8:6, UInt16:5067
	; Oper[3]: String(5067) 'alert'

	LoadConstString     	Reg8:0, UInt16:800
	; Oper[1]: String(800) 'You are not the admin, Liar!'

	Call2               	Reg8:0, Reg8:1, Reg8:3, Reg8:0
	GetById             	Reg8:1, Reg8:2, UInt8:7, UInt16:3904
	; Oper[3]: String(3904) 'setPrint'

	LoadConstString     	Reg8:0, UInt16:1312
	; Oper[1]: String(1312) 'Attack attempt detected'

	Call2               	Reg8:0, Reg8:1, Reg8:2, Reg8:0
	Jmp                 	Addr8:21
	GetById             	Reg8:1, Reg8:2, UInt8:8, UInt16:3839
	; Oper[3]: String(3839) 'decodedFlag'

	NewArrayWithBuffer  	Reg8:0, UInt16:43, UInt16:43, UInt16:9512
	Call2               	Reg8:0, Reg8:1, Reg8:2, Reg8:0
	GetEnvironment      	Reg8:0, UInt8:1
	LoadFromEnvironment 	Reg8:0, Reg8:0, UInt8:6
	GetById             	Reg8:1, Reg8:0, UInt8:9, UInt16:3819
	; Oper[3]: String(3819) 'Keyboard'

	GetById             	Reg8:0, Reg8:1, UInt8:10, UInt16:3822
	; Oper[3]: String(3822) 'dismiss'

	Call1               	Reg8:0, Reg8:0, Reg8:1
	LoadConstUndefined  	Reg8:0
	Ret                 	Reg8:0
EndFunction

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?

68
	;Jmp                     Addr8:21

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

  1. apktool
  2. hbctool (v84-compat fork)
  3. Ctrl-F tryAuth
  4. Patch out jmp instruction
  5. Rebuild + sign + install
  6. Try logging into admin, get flag