Mobile / Sea of Secrets CTF 2026
Ghost in the Storage
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:
grep -abo 'COURIER_DISPATCH_MASTER_VAULT_TOKEN' keeper_vault.dbThe 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.
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.tarapps/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.1file holds the cached HTTP headers, includingX-Cipher-Passphrase: DeadMenTellNoTales. The other entry (8d4f9b2c...) is an unrelated sloop-rigging chart. - Sector token:
nav_cache.db, waypoint 77 (San Salvador Deep Reef) hasSAN_SALVADOR_REEF_1729.
Decrypt
AES-256-CBC with PKCS#7 padding, key from PBKDF2-HMAC-SHA256. The derived key is 68668626c48262898396ee1df94befa614543d3df7a3c2809280313a5bcae50c.
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.