You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: src/content/posts/jeanne-dhack-ctf-2026-mobile-odyssey.md
+308-9Lines changed: 308 additions & 9 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -17,30 +17,329 @@ lang: en
17
17
18
18
## Introduction
19
19
20
-
...
20
+
The first mobile challenge of the Jeanne DHACK CTF 2026 is here! The challenge provides an APK file of a mobile game called Mobile Odyssey, and the goal is to find secret information hidden within the app. Easy, right?
21
+
22
+
This challenge is not particularly hard if you have experience with mobile app reverse engineering and the right tools. This is my first time using certain tools. Not having a rooted phone makes dynamic reverse engineering challenging.
23
+
24
+
I spent much more time setting up the environment than solving the challenge, but I finally got the flag.
25
+
26
+
I used **Waydroid** for emulation and **ADB**, as well as **Frida**. For reverse static analysis, I used **JADX** and **JADX-GUI**, plus some other tools like **apktool** and **uber-apk-signer**.
d2 = {"Lcom/jeannedhackctf/mobileodyssey/GameActivity;", "remoteConfig", "Lcom/google/firebase/remoteconfig/FirebaseRemoteConfig;", "REMOTE_KEY", "fetchRemoteConfig", "tryDecodeBase64", "app_debug"} // The metadata shows us some good leaks
190
+
191
+
privatefinalStringREMOTE_KEY="sEcre7vALu3"; // The key used to fetch the secret value from Firebase Remote Config
Nothingelse is present, and the retrieved flags seem not to be used anywhere else.
223
+
Therefore, we must focus on FirebaseRemoteConfig. The key is **"sEcre7vALu3"** and the value is Base64-encoded. Howdo we get the value?
224
+
225
+
Initially, I believed it was only possible with **REMOTE_KEY**, but since I didn't know which Firebase instance the app was connected to, I couldn't do anything. So the idea was to launch the app and analyze it dynamically with **Frida** to capture the remote value once it was fetched.
34
226
35
227
## Solution
36
228
37
-
...
229
+
HereI downloaded **Waydroid**, an Android emulation environment forLinux, and installed the apk in it. I started adb, installed the application with `waydroid app install`.The command did not give any errors, but the application was not installed. I tried again with `adb install` and got: `adb: failed to install mobile_odyssey_v0.0.1.apk:Failure [INSTALL_FAILED_OLDER_SDK:Requires newer sdk version #35 (current version is #33)]`.So the problem was that Waydroid uses an older version of Android (33) than the one required by the app (35).
230
+
231
+
One way to get around the problem is to change the required SDK in the manifest, so I extracted the apk with `apktool d mobile_odyssey_v0.0.1.apk` and modified the `AndroidManifest.xml` file by changing the line
and I rebuilt it with `apktool b mobile_odyssey_v0.0.1-o mobile_odyssey_modded.apk` and reinstalled it with `adb install mobile_odyssey_modded.apk`.And here too, an error: `adb: failed to install odyssey_edited-2.apk:Failure [INSTALL_PARSE_FAILED_NO_CERTIFICATES:Failed to collect certificates from /data/app/vmdl209082530.tmp/base.apk:Attempt to get length of null array]` The application needed to be signed again, so I used `uber-apk-signer` to sign it again: `uber-apk-signer -a mobile_odyssey_modded.apk` and obtained the file `mobile_odyssey_modded-aligned-signed.apk`, which I reinstalled with `adb install mobile_odyssey_modded-aligned-signed.apk`, and this time the installation was successful.
246
+
247
+
OK, now we just need to launch the application and we'll have the flag, right? Wrong, the application crashes instantly. Looking at the logs with `waydroid logcat | grep -i "AndroidRuntime"`, I find:
38
248
39
-
```python
40
-
# filename: exploit.py
249
+
```
250
+
01-30 15:51:47.980 3326 3326 E AndroidRuntime: java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{com.jeannedhackctf.mobileodyssey/com.jeannedhackctf.mobileodyssey.MainActivity}: java.lang.ClassNotFoundException: Didn't find class "com.jeannedhackctf.mobileodyssey.MainActivity" on path:DexPathList[[zip file "/data/app/~~mF_dXhC8jDmUju1Dx9lK_A==/com.jeannedhackctf.mobileodyssey-Sc7Ypls90RmuTLh9nTJ5Mg==/base.apk"],nativeLibraryDirectories=[/data/app/~~mF_dXhC8jDmUju1Dx9lK_A==/com.jeannedhackctf.mobileodyssey-Sc7Ypls90RmuTLh9nTJ5Mg==/lib/x86_64, /data/app/~~mF_dXhC8jDmUju1Dx9lK_A==/com.jeannedhackctf.mobileodyssey-Sc7Ypls90RmuTLh9nTJ5Mg==/base.apk!/lib/x86_64, /system/lib64, /system/system_ext/lib64, /system/product/lib64]]
251
+
```
41
252
253
+
This means that the MainActivity classis missing, which is strange because it was present in the original apk. So I reviewed the AndroidManifest.xml file and noticed that the main activity is GameActivity and not MainActivity, so I edited the manifest to change the name of the main activity from MainActivity to GameActivity:
console.log("\n[+] Found GameActivity instance in memory!");
310
+
console.log(" Attempting to force fetchRemoteConfig()...");
311
+
try {
312
+
// Since fetchRemoteConfig is private, we should make it accessible or call it via reflection if it fails,
313
+
// but Frida often bypasses privacy. We try calling tryDecodeBase64 directly if we have the string.
314
+
// Or we call Firebase's public method ourselves:
315
+
316
+
var config = instance.remoteConfig.value; // Access to remoteConfig field (Kotlin property)
317
+
if (config) {
318
+
var secret = config.getString("sEcre7vALu3");
319
+
console.log(" [MANUAL TRIGGER] Value read directly: "+ secret);
320
+
} else {
321
+
console.log(" [-] remoteConfig is null in the instance.");
322
+
}
323
+
324
+
} catch (e) {
325
+
console.log(" [-] Error in manual trigger: "+ e.message);
326
+
}
327
+
},
328
+
onComplete: function () {
329
+
console.log("[*] Memory scan completed.");
330
+
}
331
+
});
332
+
333
+
});
334
+
```
335
+
336
+
BINGO!!!
337
+
338
+

339
+
340
+
## Post notes
341
+
342
+
Initially, I installed Frida incorrectly and couldn't use the script, so I started looking through the application files in `/data/data/com.jeannedhackctf.mobileodyssey/files/` and found the file `frc_1:<number>:android: <id>_firebase_activate.json`, which contained the encoded flag, but I still wanted to use Frida for the solve because it's OBJECTIVELY cooler (this is possibile only when the application start correctly).
0 commit comments