Mobile / Sea of Secrets CTF 2026

Ghost in the Storage

01 Nov 2026 · 2 min read

The zip holds an evidence tree from a Pixel 6a (Android 13) running org.blackflag.keeper: the app's databases, shared_prefs, an image cache, logcat/dmesg, and an…

Flag: FLAG{d34d_m3n_t3ll_n0_t4l3s_but_fr33l1sts_d0_7a9f}

The zip holds an evidence tree from a Pixel 6a (Android 13) running org.blackflag.keeper: the app's databases, shared_prefs, an image cache, logcat/dmesg, and an android_backup/backup.ab. Everything under evidence/app_data/ is duplicated in the flattened evidence/ folders.

Key observation

logcat spells out the recipe: AES key = PBKDF2(<Chart_Passphrase>:<Waypoint_77_Sector_Token>, salt = master_salt_hex from crypto_state.xml, 100000 iterations), with a 16-byte IV stored in the message row. It also says the local crypto_state.xml is sanitized, so the salt has to come from the ADB backup, and that DELETE FROM transmissions WHERE id = 4 ran while VACUUM was skipped. So the ciphertext is the deleted row 4, and each key part lives in a different place.

Ciphertext: deleted row 4

keeper_vault.db still has rows 1, 2, 3 and 5. secure_delete is off, so row 4's cell is still in the file (the audit_log shows Flint's COURIER_DISPATCH_MASTER_VAULT_TOKEN message being purged). Grepping for that subject string finds it:

bash
grep -abo 'COURIER_DISPATCH_MASTER_VAULT_TOKEN' keeper_vault.db

The cell reads: sender Capt_Flint, recipient The Keeper, subject, then 512 hex chars of ciphertext immediately followed by a 32-hex-char IV (e8f1c0d4a7b329581640dfecba849123), then CONFIDENTIAL_PURGED. The full blob is in the script below.

Salt: the backup

backup.ab is an unencrypted Android backup: a text header ending in none\n, then a zlib stream containing a tar.

bash
python3 - <<'PY'
import zlib
d = open('backup.ab','rb').read()
open('backup.tar','wb').write(zlib.decompress(d[d.find(b'none\n')+5:]))
PY
tar xf backup.tar

apps/org.blackflag.keeper/sp/crypto_state.xml still has master_salt_hex = 50697261746553616c74323032345661756c7421, which is PirateSalt2024Vault!. The on-device copy says PURGED_ON_WIPE_ALERT.

Passphrase halves

  • Chart passphrase: the image cache journal has CLEAN 7c94b79b9a6744319ff422f28328c6e7, and that entry's .1 file holds the cached HTTP headers, including X-Cipher-Passphrase: DeadMenTellNoTales. The other entry (8d4f9b2c...) is an unrelated sloop-rigging chart.
  • Sector token: nav_cache.db, waypoint 77 (San Salvador Deep Reef) has SAN_SALVADOR_REEF_1729.

Decrypt

AES-256-CBC with PKCS#7 padding, key from PBKDF2-HMAC-SHA256. The derived key is 68668626c48262898396ee1df94befa614543d3df7a3c2809280313a5bcae50c.

python
import hashlib
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

blob = ("ad0125ed98fc196034c67e64cdc157ac94a69669b0964a8a038ce69f71f72afa5"
        "dbc2d2a31d498867b39f3179e26aabb3ef04e2526a9e53a642c3615dfdd0dd3f"
        "0167d6fcf6d67ebbf7bdc62b1c228b7cd19c0d14cd997bb71c7c3ba85d507a7d"
        "7b75c35740f2983a6a0e27f06e4b8512f07e937ba508ed4601862aafb918f82b"
        "f1ca24e1e515974b6ec174ef311c03f098af993403f2041696e7364bd6d026a7b"
        "156bc08d8d84a89efc212f7b6f3ec67c169c6151a373ba1e35961066d9fa532f"
        "3852366f0df0390c99d74531b4ad74c7c3b6ec1da832032331b98ceb1ee99db31"
        "0c358ea98e622fb2740c9076245c956d3e4f02c2ad833d51cb03b7c96a11d"
        "e8f1c0d4a7b329581640dfecba849123")
ct, iv = bytes.fromhex(blob[:512]), bytes.fromhex(blob[512:])

key = hashlib.pbkdf2_hmac("sha256", b"DeadMenTellNoTales:SAN_SALVADOR_REEF_1729",
                          b"PirateSalt2024Vault!", 100000, 32)
print(unpad(AES.new(key, AES.MODE_CBC, iv).decrypt(ct), 16).decode())

The plaintext is JSON (vault_id VAULT_BLACK_BEARD_ARCHIVE, transmission_type MASTER_SECRET_DISPATCH) whose flag field is the flag above.

Decoys

BlackBeardIsDead123! (decoy_vault_passcode in the preferences), DeadDropKey2024! (Invalid) (Gunner_Ned's roster hint), and the Waypoint 66 ISLA_MUERTA_REEF entry. None of them go into the key derivation.