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
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:
- A password is received and it performs 1024 iterations of SHA1 hashing on it.
- 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!}