Post

Writeup of APKey Challenge of Hack The Box

Resolution of APKey Challenge of Hack The Box

🔬 “Analyze it, understand it and BREAK it!”..

Introduction

This challenge includes mobile reverse engineering, understanding of the Android basics like Java, Kotlin and Smali code, but it also will introduce us to a crypto obfuscated code which we’ll need to decrypt if we opt to solve it via static analysis only, otherwise it’s possible to resolve this challenge using Frida, as we’ll cover in this writeup.

I really hope you could enjoy this challenge as much as I did. So, let’s start with the setup!

The APK file

Once the APKey.apk file is downloaded, we are going to decompile it to read the code. To do so, we can use APKtool. I recommend always using latest version for better compatibility when decompiling.

  • Decompile the APK:
1
2
3
4
5
6
7
8
9
10
11
12
$: apktool v
2.12.1
$: apktool d APKey.apk -o APKey
I: Using Apktool 2.12.1 on APKey.apk with 8 threads
I: Baksmaling classes.dex...
I: Loading resource table...
I: Decoding file-resources...
I: Loading resource table from file: /home/hackermater/.local/share/apktool/framework/1.apk
I: Decoding values */* XMLs...
I: Decoding AndroidManifest.xml with resources...
I: Copying original files...
I: Copying unknown files...

Once decompiled, we can take a look at the source code in a readable format. Let’s take a look at the AndroidManifest.xml

  • AndroidManifest.xml:
1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:compileSdkVersion="30" android:compileSdkVersionCodename="11" package="com.example.apkey" platformBuildVersionCode="30" platformBuildVersionName="11">
    <application android:allowBackup="true" android:appComponentFactory="androidx.core.app.CoreComponentFactory" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.APKey">
        <activity android:name="com.example.apkey.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>
</manifest>

Here we have some intersting things:

  • Package Name: com.example.apkey.
  • The AndroidManifest.xml declares the MainActivity: com.example.apkey.MainActivity.
  • The application allows backup: android:allowBackup="true". * This attribute leads to sensitive data exfiltration when an attacker has access to a shell on the victim’s device. Therefore, it is considered a vulnerability if the app is in production.
  • And the minimum Android version allowed is 11 and the minimum Android SDK version is 30.

Static Analysis of the source code with Jadx

Once the APKey.apk is opened with Jadx, we can simply quickly navigate to the MainActivity clicking on the Home button on the top bar:

Jadx_MainActivity

Starting from the MainActivity we can take an overview of which functions are executed when the user starts the application. So after a review we can focus on the main fuction of this class:

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
public void onClick(android.view.View r5) throws java.security.NoSuchAlgorithmException {
            /*
                r4 = this;
                com.example.apkey.MainActivity r5 = com.example.apkey.MainActivity.this     // Catch: java.lang.Exception -> L88
                android.widget.EditText r5 = r5.f928c     // Catch: java.lang.Exception -> L88
                android.text.Editable r5 = r5.getText()     // Catch: java.lang.Exception -> L88
                java.lang.String r5 = r5.toString()     // Catch: java.lang.Exception -> L88
                java.lang.String r0 = "admin"

                boolean r5 = r5.equals(r0)     // Catch: java.lang.Exception -> L88
                r0 = 0
                if (r5 == 0) goto L7b
                com.example.apkey.MainActivity r5 = com.example.apkey.MainActivity.this     // Catch: java.lang.Exception -> L88
                c.b.a.b r1 = r5.e     // Catch: java.lang.Exception -> L88
                android.widget.EditText r5 = r5.d     // Catch: java.lang.Exception -> L88
                android.text.Editable r5 = r5.getText()     // Catch: java.lang.Exception -> L88
                java.lang.String r5 = r5.toString()     // Catch: java.lang.Exception -> L88
                java.lang.String r1 = "MD5"
                java.security.MessageDigest r1 = java.security.MessageDigest.getInstance(r1)     // Catch: java.security.NoSuchAlgorithmException -> L50 java.lang.Exception -> L88
                byte[] r5 = r5.getBytes()     // Catch: java.security.NoSuchAlgorithmException -> L50 java.lang.Exception -> L88
                r1.update(r5)     // Catch: java.security.NoSuchAlgorithmException -> L50 java.lang.Exception -> L88
                byte[] r5 = r1.digest()     // Catch: java.security.NoSuchAlgorithmException -> L50 java.lang.Exception -> L88
                java.lang.StringBuffer r1 = new java.lang.StringBuffer     // Catch: java.security.NoSuchAlgorithmException -> L50 java.lang.Exception -> L88
                r1.<init>()     // Catch: java.security.NoSuchAlgorithmException -> L50 java.lang.Exception -> L88

                r2 = 0
            L3a:
                int r3 = r5.length     // Catch: java.security.NoSuchAlgorithmException -> L50 java.lang.Exception -> L88
                if (r2 >= r3) goto L4b
                r3 = r5[r2]     // Catch: java.security.NoSuchAlgorithmException -> L50 java.lang.Exception -> L88
                r3 = r3 & 255(0xff, float:3.57E-43)
                java.lang.String r3 = java.lang.Integer.toHexString(r3)     // Catch: java.security.NoSuchAlgorithmException -> L50 java.lang.Exception -> L88
                r1.append(r3)     // Catch: java.security.NoSuchAlgorithmException -> L50 java.lang.Exception -> L88
                int r2 = r2 + 1
                goto L3a
            L4b:
                java.lang.String r5 = r1.toString()     // Catch: java.security.NoSuchAlgorithmException -> L50 java.lang.Exception -> L88
                goto L56
            L50:
                r5 = move-exception
                r5.printStackTrace()     // Catch: java.lang.Exception -> L88
                java.lang.String r5 = ""
            L56:
                java.lang.String r1 = "a2a3d412e92d896134d9c9126d756f"
                boolean r5 = r5.equals(r1)     // Catch: java.lang.Exception -> L88
                if (r5 == 0) goto L7b
                com.example.apkey.MainActivity r5 = com.example.apkey.MainActivity.this     // Catch: java.lang.Exception -> L88
                android.content.Context r5 = r5.getApplicationContext()     // Catch: java.lang.Exception -> L88
                com.example.apkey.MainActivity r0 = com.example.apkey.MainActivity.this     // Catch: java.lang.Exception -> L88
                c.b.a.b r1 = r0.e     // Catch: java.lang.Exception -> L88
                c.b.a.g r0 = r0.f     // Catch: java.lang.Exception -> L88
                java.lang.String r0 = c.b.a.g.a()     // Catch: java.lang.Exception -> L88
                java.lang.String r0 = c.b.a.b.a(r0)     // Catch: java.lang.Exception -> L88
                r1 = 1

                android.widget.Toast r5 = android.widget.Toast.makeText(r5, r0, r1)     // Catch: java.lang.Exception -> L88
            L77:
                r5.show()     // Catch: java.lang.Exception -> L88
                goto L8c
            L7b:
                com.example.apkey.MainActivity r5 = com.example.apkey.MainActivity.this     // Catch: java.lang.Exception -> L88
                android.content.Context r5 = r5.getApplicationContext()     // Catch: java.lang.Exception -> L88
                java.lang.String r1 = "Wrong Credentials!"
                android.widget.Toast r5 = android.widget.Toast.makeText(r5, r1, r0)     // Catch: java.lang.Exception -> L88

                goto L77
            L88:
                r5 = move-exception
                r5.printStackTrace()
            L8c:
                return

            */
            throw new UnsupportedOperationException("Method not decompiled: com.example.apkey.MainActivity.a.onClick(android.view.View):void");
        }
    }

But we have to be careful with this code in Jadx, because as it’s seen above the function, it’s shown this warn:

1
2
3
4
        /*
            Code decompiled incorrectly, please refer to instructions dump.
            To view partially-correct code enable 'Show inconsistent code' option in preferences
        */

Another approach we can take is to see the code from the “Simple” view, this is helpful to read and understand faster the code from a visual perspective:

Jadx_Simple_View

Understanding the code

From this point, we can identify some aspects of the execution flow:

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
@Override // android.view.View.OnClickListener
        public void onClick(View r5) throws NoSuchAlgorithmException {
        L47:
            e = move-exception;
            e.printStackTrace();
            return;
        L31:
            if (MainActivity.this.f928c.getText().toString().equals("admin") == false) goto L45;
            MainActivity r52 = MainActivity.this;     // Catch: Exception -> L47
            b r1 = r52.e;     // Catch: Exception -> L47
            String r53 = r52.d.getText().toString();     // Catch: Exception -> L47
            MessageDigest r12 = MessageDigest.getInstance("MD5");     // Catch: NoSuchAlgorithmException -> L39 Exception -> L47
            r12.update(r53.getBytes());     // Catch: NoSuchAlgorithmException -> L39 Exception -> L47
            byte[] r54 = r12.digest();     // Catch: NoSuchAlgorithmException -> L39 Exception -> L47
            StringBuffer r13 = new StringBuffer();     // Catch: NoSuchAlgorithmException -> L39 Exception -> L47
            int r2 = 0;
        L35:
            if (r2 >= r54.length) goto L37;
            r13.append(Integer.toHexString(r54[r2] & 255));     // Catch: NoSuchAlgorithmException -> L39 Exception -> L47
            r2 = r2 + 1;     // Catch: NoSuchAlgorithmException -> L39 Exception -> L47
            goto L35
        L37:
            String r55 = r13.toString();     // Catch: NoSuchAlgorithmException -> L39 Exception -> L47
        L42:
            if (r55.equals("a2a3d412e92d896134d9c9126d756f") == false) goto L45;
            Context r56 = MainActivity.this.getApplicationContext();     // Catch: Exception -> L47
            MainActivity r0 = MainActivity.this;     // Catch: Exception -> L47
            b r14 = r0.e;     // Catch: Exception -> L47
            g r02 = r0.f;     // Catch: Exception -> L47
            Toast r57 = Toast.makeText(r56, b.a(g.a()), 1);     // Catch: Exception -> L47
        L44:
            r57.show();     // Catch: Exception -> L47

            return;
        L39:
            e = move-exception;
            e.printStackTrace();     // Catch: Exception -> L47
            r55 = "";
        L45:
            r57 = Toast.makeText(MainActivity.this.getApplicationContext(), "Wrong Credentials!", 0);     // Catch: Exception -> L47
            goto L44
        }
    }
  • We can identify the first logic:
1
if (MainActivity.this.f928c.getText().toString().equals("admin") == false) goto L45;

If the username is not admin, the app will execute the L45 code, which it means that we cannot login as they are “Wrong Credentials!”.

1
2
3
4
L45:
    r57 = Toast.makeText(MainActivity.this.getApplicationContext(), "Wrong Credentials!", 0);     // Catch: Exception -> L47
    goto L44
    }

Therefore, admin must be the username.

  • In a second instance, we can also locate the following line, which make reference to the password.
1
if (r55.equals("a2a3d412e92d896134d9c9126d756f") == false) goto L45;

If the MD5 hash of the password is not a2a3d412e92d896134d9c9126d756f then the app will execute the code of L45 again (Wrong Credentials).

Second part of Static Analysis: Deobfuscate the code

Returing to the onClick() function in the Code view, we can locate the following code execution logic:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
L56:
        java.lang.String r1 = "a2a3d412e92d896134d9c9126d756f"
        boolean r5 = r5.equals(r1)     // Catch: java.lang.Exception -> L88
        if (r5 == 0) goto L7b
        com.example.apkey.MainActivity r5 = com.example.apkey.MainActivity.this     // Catch: java.lang.Exception -> L88
        android.content.Context r5 = r5.getApplicationContext()     // Catch: java.lang.Exception -> L88
        com.example.apkey.MainActivity r0 = com.example.apkey.MainActivity.this     // Catch: java.lang.Exception -> L88
        c.b.a.b r1 = r0.e     // Catch: java.lang.Exception -> L88
        c.b.a.g r0 = r0.f     // Catch: java.lang.Exception -> L88
        java.lang.String r0 = c.b.a.g.a()     // Catch: java.lang.Exception -> L88
        java.lang.String r0 = c.b.a.b.a(r0)     // Catch: java.lang.Exception -> L88
        r1 = 1
        android.widget.Toast r5 = android.widget.Toast.makeText(r5, r0, r1)     // Catch: java.lang.Exception -> L88
    L77:
        r5.show()     // Catch: java.lang.Exception -> L88
        goto L8c

So far we have already seen at the first lines of the MainActivity these following activities imported:

1
2
3
4
5
6
7
8
9
package com.example.apkey;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import c.b.a.b;
import c.b.a.g;

Then we find the c.b.a.b and c.b.a.g activities called during the L56 code logic. (The previous code snippet).

The c.b.a.g.a() is calling a function from the activity c.b.a.g and in other hand, the c.b.a.b.a(r0) is calling the c.b.a.b activity parsing the previous result. Finally in the L77, the r5.show() is showing the Flag. (Of course, correct credentials).

From Jadx we move to those activities and we found obfuscated code like this:

Obfuscated_Code_Jadx_b

Obfuscated_Code_Jadx_g

And yes… We can go and deobfuscate each part of the code but, I found a simpler way: Frida

Next step: Dynamic Analysis and Frida Hooking

To use Frida we need:

  1. An Android Device: it can be a physical rooted device or an Android Virtual Device Emulator
  2. Access to root execution via adb shell or the ability to inject the Frida Gadget. But this writeup focuses on Frida Server, so we’ll only take approach of the root execution in the Android Emulator.

Runtime execution bypass

So we proceed to install our APK file with:

1
$: adb install APKey.apk

APKey.apk installed on AVD

Then the second step is needed to be taken carefully to avoid errors in the process:

  1. Download the latest version of Frida Server: I use fridaDownloader - A powerful script to download the latest or custom version of Frida Gadget or Server from terminal.
  2. Download the latest version of frida and frida-tools inside a pip environment:
1
2
3
$: python3 -m venv env
$: source env/bin/activate
$: (env) pip install --upgrade frida frida-tools

Once we have all set, we need to push the frida-server binary into the device storage:

1
2
3
4
5
6
$: adb push frida-server-17.5.2-android-x86 /data/local/tmp/frida-server 
$: adb shell
## From the adb shell
$: su
#: chmod 0755 /data/local/tmp/frida-server
#: /data/local/tmp/frida-server &

Ensure that the version of Frida corresponds to the architecture of your device. For further info see: https://hackermater.gitbook.io/pentesting-notes/mobile-pentesting/dynamic-analysis#detect-android-devices-architecture

Bypassing the login and capturing the flag

Now comes the final and fun part: hook with Frida Server the functions and bypass the code logic.

We’ve previously seen that the username is admin, and that the password must match with the MD5 hash a2a3d412e92d896134d9c9126d756f. But we’ve also seen this code execution logic:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
L56:
        java.lang.String r1 = "a2a3d412e92d896134d9c9126d756f"
        boolean r5 = r5.equals(r1)     // Catch: java.lang.Exception -> L88
        if (r5 == 0) goto L7b
        com.example.apkey.MainActivity r5 = com.example.apkey.MainActivity.this     // Catch: java.lang.Exception -> L88
        android.content.Context r5 = r5.getApplicationContext()     // Catch: java.lang.Exception -> L88
        com.example.apkey.MainActivity r0 = com.example.apkey.MainActivity.this     // Catch: java.lang.Exception -> L88
        c.b.a.b r1 = r0.e     // Catch: java.lang.Exception -> L88
        c.b.a.g r0 = r0.f     // Catch: java.lang.Exception -> L88
        java.lang.String r0 = c.b.a.g.a()     // Catch: java.lang.Exception -> L88
        java.lang.String r0 = c.b.a.b.a(r0)     // Catch: java.lang.Exception -> L88
        r1 = 1
        android.widget.Toast r5 = android.widget.Toast.makeText(r5, r0, r1)     // Catch: java.lang.Exception -> L88
    L77:
        r5.show()     // Catch: java.lang.Exception -> L88
        goto L8c

So we can create a function in JavaScript to bypass the conditional boolean for the password, and directly force the application to give us the flag.

  • Frida code snippet:
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
Java.perform(function () {

    var class_b = Java.use("c.b.a.b");
    var class_g = Java.use("c.b.a.g");

    var onClickListener = Java.use("com.example.apkey.MainActivity$a");

    onClickListener.onClick.implementation = function (view) {
        console.log("[+] Button clicked. Generating flag directly...");

        try {
            var encryptedString = class_g.a();

            console.log("[*] Input (g.a): " + encryptedString);

            var flag = class_b.a(encryptedString);

            console.log("[!] FLAG FOUND: " + flag);

        } catch (err) {
            console.log("[-] Error calling methods: " + err);
        }
    };
    
    console.log("[+] Script loaded. Press ANY button in the App to see the flag.");

});

How this works and how to use it?

  1. With Java.use("com.example.apkey.MainActivity$a") we hook this specific class that handles the button click.
  2. We use .implementation to replace the original password check with our own code to bypass the code logic.
  3. We manually call class_g.a() and pass it to class_b.a(). This forces the app to run the decryption functions regardless of the input!
  4. Finally, we print in console with console.log(...) the flag obtained.
1
$: frida -U -f com.example.apkey -l solve.js

Frida Usage:

  • -U for USB debugging.
  • -f directly call the app by its package name.
  • -l loads the JS script.

So we PWNED the app:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$: frida -U -f com.example.apkey -l solve.js
     ____
    / _  |   Frida 17.5.2 - A world-class dynamic instrumentation toolkit
   | (_| |
    > _  |   Commands:
   /_/ |_|       help      -> Displays the help system
   . . . .       object?   -> Display information about 'object'
   . . . .       exit/quit -> Exit
   . . . .
   . . . .   More info at https://frida.re/docs/home/
   . . . .
   . . . .   Connected to Android Emulator 5554 (id=emulator-5554)
Spawned `com.example.apkey`. Resuming main thread!
[Android Emulator 5554::com.example.apkey ]-> [+] Script loaded. Press ANY button in the App to see the flag.
[+] Button clicked. Generating flag directly...
[*] Input (g.a): 1UlBm2kHtZuVrSE6qY6HxWkwHyeaX92DabnRFlEGyLWod2bkwAxcoc85S94kFpV1
[!] FLAG FOUND: HTB{m0r3_0bfusc4t1on_w0uld_n0t_hurt}

Frida_Android_Emulator_Bypass


I hope you found this writeup useful. If you have any doubts or tips, reach out to me on LinkedIn and let’s connect!

Happy Hacking!!

Author: Mateo Fumis

This post is licensed under CC BY 4.0 by the author.