Post

TISC 2023 - (Level 3) KPA

Description

We’ve managed to grab an app from a suspicious device just before it got reset! The copying couldn’t finish so some of the last few bytes got corrupted… But not all is lost! We heard that the file shouldn’t have any comments in it! Help us uncover the secrets within this app!

Attached files

kpa.apk

Solution

Using the file command, kpa.apk was identified to be an Android mobile application.

1
2
$ file kpa.apk
kpa.apk: Android package (APK), with gradle app-metadata.properties, with APK Signing Block

I then used apktool to break the mobile app down into its various components:

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
$ wget https://github.com/iBotPeaches/Apktool/releases/download/v2.8.1/apktool_2.8.1.jar
$ java -jar apktool_2.8.1.jar d kpa.apk
I: Using Apktool 2.8.1 on kpa.apk
Exception in thread "main" brut.androlib.exceptions.AndrolibException: brut.directory.DirectoryException: java.io.EOFException
        at brut.androlib.res.ResourcesDecoder.hasManifest(ResourcesDecoder.java:70)
        at brut.androlib.res.ResourcesDecoder.decodeManifest(ResourcesDecoder.java:102)
        at brut.androlib.ApkDecoder.decode(ApkDecoder.java:95)
        at brut.apktool.Main.cmdDecode(Main.java:190)
        at brut.apktool.Main.main(Main.java:93)
Caused by: brut.directory.DirectoryException: java.io.EOFException
        at brut.directory.ZipRODirectory.<init>(ZipRODirectory.java:55)
        at brut.directory.ZipRODirectory.<init>(ZipRODirectory.java:38)
        at brut.directory.ExtFile.getDirectory(ExtFile.java:49)
        at brut.androlib.res.ResourcesDecoder.hasManifest(ResourcesDecoder.java:68)
        ... 4 more
Caused by: java.io.EOFException
        at java.base/java.io.RandomAccessFile.readFully(RandomAccessFile.java:471)
        at java.base/java.util.zip.ZipFile$Source.readFullyAt(ZipFile.java:1512)
        at java.base/java.util.zip.ZipFile$Source.findEND(ZipFile.java:1595)
        at java.base/java.util.zip.ZipFile$Source.initCEN(ZipFile.java:1641)
        at java.base/java.util.zip.ZipFile$Source.<init>(ZipFile.java:1479)
        at java.base/java.util.zip.ZipFile$Source.get(ZipFile.java:1441)
        at java.base/java.util.zip.ZipFile$CleanableResource.<init>(ZipFile.java:718)
        at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:252)
        at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:181)
        at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:195)
        at brut.directory.ZipRODirectory.<init>(ZipRODirectory.java:53)
        ... 7 more

However, apktool reported that there seems to be some issues decompressing the mobile app. To dig deeper into the issue, the zip command was used like so:

1
2
3
$ zip -T kpa.apk      

zip error: Unexpected end of zip file (kpa.apk)

The description did mention that the last few bytes were corrupted. To investigate further, the mobile app was opened using hexeditor and navigated to the end of it.

The last few bytes were compared against an uncorrupted zip file and the following changes were made and saved:

The zip command was then used to fix the file:

1
2
3
4
5
6
$ zip -FF kpa.apk --out fixed_kpa.apk 
Fix archive (-FF) - salvage what can
...

$ zip -T fixed_kpa.apk                
test of fixed_kpa.apk OK

After fixing the mobile app, the apktool could finally be used:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ java -jar apktool_2.8.1.jar d fixed_kpa.apk
I: Using Apktool 2.8.1 on fixed_kpa.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...
I: Loading resource table from file: /home/kali/.local/share/apktool/framework/1.apk
I: Regular manifest package...
I: Decoding file-resources...
...
I: Decoding values */* XMLs...
I: Baksmaling classes.dex...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...

Before making changes to the mobile app, I opened the mobile app in jadx-gui to analyse how it works:

1
$ jadx-gui $(pwd)/fixed_kpa.apk

The main part of the mobile app is found in com > tisc.kappa > MainActivity. In it, the method that pertains to the flag can immediately be sieved out:

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
public void M(String str) {
	char[] charArray = str.toCharArray();
	String valueOf = String.valueOf(charArray);
	for (int i2 = 0; i2 < 1024; i2++) {
		valueOf = N(valueOf, "SHA1");
	}
	if (!valueOf.equals("d8655ddb9b7e6962350cc68a60e02cc3dd910583")) {
		((TextView) findViewById(d.f3935f)).setVisibility(4);
		Q(d.f3930a, 3000);
		return;
	}
	char[] copyOf = Arrays.copyOf(charArray, charArray.length);
	charArray[0] = (char) ((copyOf[24] * 2) + 1);
	charArray[1] = (char) (((copyOf[23] - 1) / 4) * 3);
	charArray[2] = Character.toLowerCase(copyOf[22]);
	charArray[3] = (char) (copyOf[21] + '&');
	charArray[4] = (char) ((Math.floorDiv((int) copyOf[20], 3) * 5) + 4);
	charArray[5] = (char) (copyOf[19] - 1);
	charArray[6] = (char) (copyOf[18] + '1');
	charArray[7] = (char) (copyOf[17] + 18);
	charArray[8] = (char) ((copyOf[16] + 19) / 3);
	charArray[9] = (char) (copyOf[15] + '%');
	charArray[10] = (char) (copyOf[14] + '2');
	charArray[11] = (char) (((copyOf[13] / 5) + 1) * 3);
	charArray[12] = (char) ((Math.floorDiv((int) copyOf[12], 9) + 5) * 9);
	charArray[13] = (char) (copyOf[11] + 21);
	charArray[14] = (char) ((copyOf[10] / 2) - 6);
	charArray[15] = (char) (copyOf[9] + 2);
	charArray[16] = (char) (copyOf[8] - 24);
	charArray[17] = (char) (copyOf[7] + Math.pow(4.0d, 2.0d));
	charArray[18] = (char) ((copyOf[6] - '\t') / 2);
	charArray[19] = (char) (copyOf[5] + '\b');
	charArray[20] = copyOf[4];
	charArray[21] = (char) (copyOf[3] - '\"');
	charArray[22] = (char) ((copyOf[2] * 2) - 20);
	charArray[23] = (char) ((copyOf[1] / 2) + 8);
	charArray[24] = (char) ((copyOf[0] + 1) / 2);
	P("The secret you want is TISC{" + String.valueOf(charArray) + "}", "CONGRATULATIONS!", "YAY");
}

Here’s my summary on how it works:

  1. A password is received and it performs 1024 iterations of SHA1 hashing on it.
  2. If it matches the hardcoded hash, it then performs a list of operations on the password to obtain the flag.

Knowing that the hash was not crackable, I had a feeling that the password may have been hidden somewhere else in the mobile app.

The MainActivity class also referenced another class called sw:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.tisc.kappa;

/* loaded from: classes.dex */
public class sw {
    static {
        System.loadLibrary("kappa");
    }

    public static void a() {
        try {
            System.setProperty("KAPPA", css());
        } catch (Exception unused) {
        }
    }

    private static native String css();
}    

The css method seemed suspicious and something was indicating that it could possibly output the password in question. To capture the password, the way I went with was to modify the mobile app’s behaviour such that it will log the password using Android’s logging utility.

Returning back to the files produced by apktool, the smalli file of the sw class is opened:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.method public static a()V
    .locals 2

    :try_start_0
    const-string v0, "KAPPA"

    invoke-static {}, Lcom/tisc/kappa/sw;->css()Ljava/lang/String;

    move-result-object v1

    invoke-static {v0, v1}, Ljava/lang/System;->setProperty(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
    :try_end_0
    .catch Ljava/lang/Exception; {:try_start_0 .. :try_end_0} :catch_0

    :catch_0
    return-void
.end method

On line 11, I added a statement that uses the android.util.log method to log the return value of the css() method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.method public static a()V
    .locals 2

    :try_start_0
    const-string v0, "KAPPA"

    invoke-static {}, Lcom/tisc/kappa/sw;->css()Ljava/lang/String;

    move-result-object v1

    invoke-static {v0, v1}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I 
    
    invoke-static {v0, v1}, Ljava/lang/System;->setProperty(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
    :try_end_0
    .catch Ljava/lang/Exception; {:try_start_0 .. :try_end_0} :catch_0

    :catch_0
    return-void
.end method

.method private static native css()Ljava/lang/String;
.end method

Next, the smalli file of the MainActivity class is opened:

1
2
3
4
5
6
# virtual methods
.method protected onCreate(Landroid/os/Bundle;)V
    .locals 0

    invoke-super {p0, p1}, Landroidx/fragment/app/e;->onCreate(Landroid/os/Bundle;)V
...

Because the onCreate() method runs when the app opens, I added code to call the a() method of the sw class, which will call the modified css() method:

1
2
3
4
5
6
7
8
9
10
11
12
# virtual methods
.method protected onCreate(Landroid/os/Bundle;)V
    .locals 1

    new-instance v0, Lcom/tisc/kappa/sw;
  
    invoke-direct {v0}, Lcom/tisc/kappa/sw;-><init>()V
  
    invoke-static {}, Lcom/tisc/kappa/sw;->a()V

    invoke-super {p0, p1}, Landroidx/fragment/app/e;->onCreate(Landroid/os/Bundle;)V
...

After modifying the mobile app to log the password, the next step would be to rebuild it using apktool:

1
2
3
4
5
6
7
8
9
10
$ java -jar apktool_2.8.1.jar b fixed_kpa -o modified_kpa.apk           
I: Using Apktool 2.8.1
I: Checking whether sources has changed...
I: Smaling smali folder into classes.dex...
I: Checking whether resources has changed...
I: Building resources...
I: Copying libs... (/lib)
I: Building apk file...
I: Copying unknown files/dir...
I: Built apk into: modified_kpa.apk

Before installing the modified mobile app, I used uber-apk-signer to sign it:

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
$ wget https://github.com/patrickfav/uber-apk-signer/releases/download/v1.3.0/uber-apk-signer-1.3.0.jar
$ java -jar uber-apk-signer-1.3.0.jar -a modified_kpa.apk 
source:
        /home/kali/Desktop
zipalign location: BUILT_IN 
        /tmp/uapksigner-12553720549848037434/linux-zipalign-33_0_21301436328789540783.tmp
keystore:
        [0] 161a0018 /tmp/temp_12910698771914416700_debug.keystore (DEBUG_EMBEDDED)

01. modified_kpa.apk

        SIGN
        file: /home/kali/Desktop/modified_kpa.apk (2.76 MiB)
        checksum: 95539db827fe5b236b8f9f4efb89fa971fd5243814e7c218987c54341c72a786 (sha256)
        - zipalign success
        - sign success

        VERIFY
        file: /home/kali/Desktop/modified_kpa-aligned-debugSigned.apk (2.82 MiB)
        checksum: 2dc74886482beadfca997d9671dfb13515f1db3e607742b013e29c549b0fcfc1 (sha256)
        - zipalign verified
        - signature verified [v2, v3]
                Subject: CN=Android Debug, OU=Android, O=US, L=US, ST=US, C=US
                SHA256: 1e08a903aef9c3a721510b64ec764d01d3d094eb954161b62544ea8f187b5953 / SHA256withRSA
                Expires: Fri Mar 11 04:10:05 SGT 2044

[Sun Oct 01 18:04:09 SGT 2023][v1.3.0]
Successfully processed 1 APKs and 0 errors in 0.53 seconds.

To run the mobile app, I used a physical smartphone which was running Android. After connecting it, the adb devices command was performed to see if the smartphone was being recognised by my host:

1
2
3
$ adb devices                                                
List of devices attached
R58TA27S0BP     unauthorized

It was recognised, but the host was not authorized to interact with it as USB debugging was not enabled. I enabled it by turning on “Developer mode” and toggling “USB debugging” to enabled.

After doing so, the host can now interact with the smartphone:

1
2
3
$ adb devices
List of devices attached
R58TA27S0BP     device

Next, the mobile app is installed:

1
2
3
4
5
6
$ adb install modified_kpa-aligned-debugSigned.apk 
Performing Incremental Install
Serving...
All files should be loaded. Notifying the device.
Success
Install command complete in 2145 ms

Since we know that the password will be logged, it will be optimal to clear the logs first:

1
$ adb logcat -c

I opened the mobile app on the smartphone, which triggered the logging of the password:

1
2
$ adb logcat | grep KAPPA                           
10-01 18:04:35.455  6952  6952 E KAPPA   : ArBraCaDabra?KAPPACABANA!

With the password, the flag can now be generated using the following code:

Flag.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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import java.util.Arrays;

class Flag {

    public static void main(String[] args) {
        M("ArBraCaDabra?KAPPACABANA!");
    }

    public static String N(String str, String str2) {
        try {
            MessageDigest messageDigest = MessageDigest.getInstance(str2);
            messageDigest.update(str.getBytes());
            byte[] digest = messageDigest.digest();
            StringBuilder sb = new StringBuilder();
            for (byte b2 : digest) {
                String hexString = Integer.toHexString(b2 & 255);
                while (hexString.length() < 2) {
                    hexString = "0" + hexString;
                }
                sb.append(hexString);
            }
            return sb.toString();
        } catch (NoSuchAlgorithmException e2) {
            e2.printStackTrace();
            return "";
        }
    }

    public static void M(String str) {
        char[] charArray = str.toCharArray();
        String valueOf = String.valueOf(charArray);
        for (int i2 = 0; i2 < 1024; i2++) {
            valueOf = N(valueOf, "SHA1");
        }
        if (!valueOf.equals("d8655ddb9b7e6962350cc68a60e02cc3dd910583")) {
            return;
        }
        char[] copyOf = Arrays.copyOf(charArray, charArray.length);
        charArray[0] = (char) ((copyOf[24] * 2) + 1);
        charArray[1] = (char) (((copyOf[23] - 1) / 4) * 3);
        charArray[2] = Character.toLowerCase(copyOf[22]);
        charArray[3] = (char) (copyOf[21] + '&');
        charArray[4] = (char) ((Math.floorDiv((int) copyOf[20], 3) * 5) + 4);
        charArray[5] = (char) (copyOf[19] - 1);
        charArray[6] = (char) (copyOf[18] + '1');
        charArray[7] = (char) (copyOf[17] + 18);
        charArray[8] = (char) ((copyOf[16] + 19) / 3);
        charArray[9] = (char) (copyOf[15] + '%');
        charArray[10] = (char) (copyOf[14] + '2');
        charArray[11] = (char) (((copyOf[13] / 5) + 1) * 3);
        charArray[12] = (char) ((Math.floorDiv((int) copyOf[12], 9) + 5) * 9);
        charArray[13] = (char) (copyOf[11] + 21);
        charArray[14] = (char) ((copyOf[10] / 2) - 6);
        charArray[15] = (char) (copyOf[9] + 2);
        charArray[16] = (char) (copyOf[8] - 24);
        charArray[17] = (char) (copyOf[7] + Math.pow(4.0d, 2.0d));
        charArray[18] = (char) ((copyOf[6] - '\t') / 2);
        charArray[19] = (char) (copyOf[5] + '\b');
        charArray[20] = copyOf[4];
        charArray[21] = (char) (copyOf[3] - '\"');
        charArray[22] = (char) ((copyOf[2] * 2) - 20);
        charArray[23] = (char) ((copyOf[1] / 2) + 8);
        charArray[24] = (char) ((copyOf[0] + 1) / 2);
        System.out.println("The secret you want is TISC{" + String.valueOf(charArray) + "}");
    }
}
1
2
3
$ javac Flag.java 
$ java Flag              
The secret you want is TISC{C0ngr@tS!us0lv3dIT,KaPpA!}

Flag

TISC{C0ngr@tS!us0lv3dIT,KaPpA!}

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