Mobile / Sea of Secrets CTF 2026
The Phantom Application
The APK (com.phantom.navigator) has a vault activity that decrypts an AES ledger once you pass an officer/passcode check. Both the check and the key derivation are in the code, so none…
Mobile Security, Hard, 300 pts
FLAG{ph4nt0m_n4v1g4t0r_cr4ck3d_th3_bl4ck_sp0t_7721}
The APK (com.phantom.navigator) has a vault activity that decrypts an AES ledger once you pass an officer/passcode check. Both the check and the key derivation are in the code, so none of it needs the device.
Decompiling with jadx gives a handful of classes. StorageManager seeds SharedPreferences (navigator_prefs) with the hex ledger and a session nonce, D4RK_T1D3S_7721. It also seeds a few fake flags (sh4r3d_pr3fs_...), and QuartermasterValidator.MASTER_KEY is another one. None of them are the flag.
The passcode is stored in QuartermasterValidator as an int table that is XORed with 42 at check time. Undoing that gives Skul1&Cr0ssB0n3s_1718!, and its SHA-256 matches EXPECTED_PASSCODE_HASH, so it's the real one. The officer string is Captain_Edward_Vane.
SecurityBridge.decryptLedger does the rest:
- key =
SHA-256(passcode + ":" + nonce + ":PHANTOM_FLEET_KEY") - AES-256-CBC, PKCS5, fixed IV
CaribbeanSea1718
Redoing it outside the app:
import hashlib
t = [121,65,95,70,27,12,105,88,26,89,89,104,26,68,25,89,117,27,29,27,18,11]
pw = ''.join(chr(i ^ 42) for i in t)
key = hashlib.sha256(f"{pw}:D4RK_T1D3S_7721:PHANTOM_FLEET_KEY".encode()).hexdigest()
# openssl enc -d -aes-256-cbc -K <key> -iv 43617269626265616e53656131373138Piping the ledger's bytes (DEFAULT_LEDGER in StorageManager, hex-decoded) through that openssl command prints the flag. I used the default ledger and nonce from the code and did not check them against a live device's prefs.