Challenge Protected
This challenge consists of analyzing an Android /data partition dump from a device running the GalleryVault app to recover a hidden media file and the solution consists of querying SQLite databases to locate the file, statically analyzing the decompiled APK with Jadx, and performing a Known-Plaintext Attack (KPA) to extract the encryption key, break the custom XOR obfuscation, and assemble the final image containing the flag.
The provided files consist of an Android file system dump. A preliminary inspection of the data directory reveals various standard Android folders.
$ cd data
$ ls
adb app-ephemeral bootchart fonts local.prop misc_ce per_boot rollback-history system_ce user_de
anr app-lib cache gsi lost+found misc_de preloads rollback-observer system_de vendor
apex app-private dalvik-cache gsi_persistent_data media nfc property server_configurable_flags tombstones vendor_ce
app app-staging data incremental mediadrm ota resource-cache ss system unencrypted vendor_de
app-asec backup drm local misc ota_package rollback system user
An unencrypted folder is present but contains only keys and references.
$ ls unencrypted
key mode per_boot_ref ref
Searching for installed APKs yields several applications of interest.
$ find -name base.apk 2>/dev/null
./app/~~8Ot3xFnHxIA5G1EBv2XkAA==/com.android.vending-0WnXQTRao1TRfgOYf6N2rw==/base.apk
./app/~~kGGaXfYOgnywjhPpaMVKbQ==/org.thoughtcrime.securesms-fKTywNyaYqPs30rWNVPWtQ==/base.apk
./app/~~xQBJ50iw1oO3bdl3q0vg_Q==/com.google.android.apps.restore-DUZq-5l_c-H-Ehpgh7WTPw==/base.apk
./app/~~EbACqxIPu-027FzlAQAAxw==/com.google.android.contactkeys-hB1aQm1LDeaT148PuUXjzw==/base.apk
./app/~~lDpCJWRbjfx7U3-X05iUCg==/com.google.android.gms-hh6YJN96J7LdG7A22eBKMg==/base.apk
./app/~~a7jcwJHUu9joGQrhYMPkDg==/com.google.ar.core-lhnIYYSmiYFHA2bR0lulxQ==/base.apk
./app/~~YtqSpVnlFnQ-CZBF-Js7HQ==/com.topjohnwu.magisk-S6_izVj57Zo74r4BVN8xbA==/base.apk
./app/~~aSe9Hqb8bP-Dh3DCicv9Qg==/notes.notepad.checklist.calendar.todolist.notebook-Y5KhcAnQ73h6JFGlMYvP1w==/base.apk
./app/~~yGbAQeKpuoLQ_p40HsRbog==/com.google.android.trichromelibrary_677813638-bMi4oXsZdav-Z3Dj_DOmqg==/base.apk
./app/~~2qXqwf1y-IeO0sr8TIHMgw==/com.thinkyeah.galleryvault-lqXfwbOnDTEzGpHYF5CHpA==/base.apk
./app/~~50gTwEyIu6K-QI0D1Snj1Q==/com.google.android.youtube-QnYH4lT8sSigEfVvzUxsVA==/base.apk
./app/~~Bve9RB9NcOrvaXbJeavNWQ==/com.android.chrome-An0s-R6NITvoziJXdBz7uQ==/base.apk
./app/~~txG5PGKmjIicn0p8yjg3MQ==/com.passmark.osfextract-jwbcOR1VUQhPr5C6jxyAjg==/base.apk
./app/~~o_1cVZLkLcKrOUcCdQy5DQ==/com.google.android.apps.youtube.music-CWcfzUMG92jhKEdRyQHBpQ==/base.apk
The presence of Magisk indicates a rooted device, while OSFExtract was likely used to acquire the file system dump. Gallery Vault, an app used for hiding media files, stands out as a prime target for investigation. The application's database file can be explored using SQLite3.
$ sqlite3 data/com.thinkyeah.galleryvault/databases/galleryvault.db
SQLite version 3.53.3 2026-06-26 20:14:12
Enter ".help" for usage hints.
Listing the tables reveals a file_v1 table which holds metadata for hidden files.
sqlite> .tables
android_metadata cloud_transfer_items export_unhidden_history file_v1 recycle_bin_file_view tab_group
break_in_report download_and_encrypt_view file_folder_revision folder_v1 recycle_bin_folder_info tab_group_with_tab_view
browser_history download_task file_system_sync folder_with_cover_file_view recycle_bin_v1 web_url
change_actions encrypt_after_download_v1 file_system_sync_file_add misc_data tab
Querying the file_v1 table uncovers a hidden PNG file named flag.png associated with a specific UUID.
sqlite> select * from file_v1;
โญโโโโฌโโโโโโโโโโฌโโโโโโโโโฌโโโโโโโโโฌโโโโโโโโโฌโโโโโโโโโฌโโโโโโโโโฌโโโโโโโโโโโฌโโโโโโโโโฌโโโโโโโโโฌโโโโโโโโโฌโโโโโโโโโฌโโโโโโโโโฌโโโโโโโโโฌโโโโโโโโโโโฌโโโโโโโโโฌโโโโโโโฌโโโโโโโโโฌโโโโโโโโโฌโโโโโโโโโฎ
โ_idโ uuid โprofile_โ name โfolder_iโfile_typโmime_typโoriginal_pโimage_orโimage_wiโimage_heโvideo_duโencrypt_โadded_tiโfile_last_โfile_sizโsourceโcompleteโstorage_โfile_sorโ
โ โ โ id โ โ d โ e โ e โ ath โientatioโ dth โ ight โ ration โ state โ me_utc โmodifie...โ e โ โ _state โ type โt_index โ
โ โ โ โ โ โ โ โ โ n โ โ โ โ โ โ โ โ โ โ โ โ
โโโโโชโโโโโโโโโโชโโโโโโโโโชโโโโโโโโโชโโโโโโโโโชโโโโโโโโโชโโโโโโโโโชโโโโโโโโโโโชโโโโโโโโโชโโโโโโโโโชโโโโโโโโโชโโโโโโโโโชโโโโโโโโโชโโโโโโโโโชโโโโโโโโโโโชโโโโโโโโโชโโโโโโโชโโโโโโโโโชโโโโโโโโโชโโโโโโโโโก
โ 1โb238f4cd-โ 1โflag.pngโ 11โ 0โimage/ โ/storage/ โ 0โ 564โ 568โ 0โ 0โ17423990โ1742399018โ 787165โNULL โ 0โ 1โ 0โ
โ โf79f-4e19โ โ โ โ โpng โemulated/0โ โ โ โ โ โ 78763โ 000โ โ โ โ โ โ
โ โ-be3b- โ โ โ โ โ โ/Pictures/โ โ โ โ โ โ โ โ โ โ โ โ โ
โ โ068b7add4โ โ โ โ โ โflag.png โ โ โ โ โ โ โ โ โ โ โ โ โ
โ โd85 โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ
โฐโโโโดโโโโโโโโโโดโโโโโโโโโดโโโโโโโโโดโโโโโโโโโดโโโโโโโโโดโโโโโโโโโดโโโโโโโโโโโดโโโโโโโโโดโโโโโโโโโดโโโโโโโโโดโโโโโโโโโดโโโโโโโโโดโโโโโโโโโดโโโโโโโโโโโดโโโโโโโโโดโโโโโโโดโโโโโโโโโดโโโโโโโโโดโโโโโโโโโฏ
Searching the filesystem for this UUID locates the physical file.
$ find -name "*7add4d85" 2>/dev/null
./media/0/.galleryvault_DoNotDelete_1742398726/files/b2/b238f4cd-f79f-4e19-be3b-068b7add4d85
Viewing the image directly fails.
$ kitty +kitten icat ./media/0/.galleryvault_DoNotDelete_1742398726/files/b2/b238f4cd-f79f-4e19-be3b-068b7add4d85
This indicates the file is encrypted or obfuscated, necessitating a deeper analysis of the Gallery Vault application. Decompiling the APK with JADX allows for static analysis of the source code. Inspecting the FileTailOperatorV1 class reveals a custom encryption routine.
public final void a(File file, File file2, JSONObject jSONObject, byte[] bArr, boolean z10, long j10, long j11) throws IOException {
...
a aVar = new a(file2, 0L, j10);
...
for (int i7 = 0; i7 < i3; i7++) {
long j13 = ((long) i7) + j12;
bArr2[i7] = (byte) (bArr2[i7] ^ ((byte) (bArr[(int) (j13 % ((long) bArr.length))] ^ ((byte) j13))));
}
...
}
The application reads a portion of the original file, iterates over its bytes, and applies an XOR cipher using a key, the length of the key, and the byte index. Furthermore, the ThinkSecurity class demonstrates how the encryption key is derived from a PIN.
public static SecretKey e(String str) {
String strSubstring;
...
if (str.length() < 16) {
StringBuilder sb = new StringBuilder();
for (int i3 = 0; i3 < 16 - str.length(); i3++) {
sb.append("0");
}
strSubstring = str + sb.toString();
} else {
strSubstring = str.substring(0, 16);
}
return SecretKeyFactory.getInstance("DES").generateSecret(new DESKeySpec(strSubstring.getBytes()));
} catch (Exception unused) {
return null;
}
}
The application uses the user's PIN to generate a DES key, adding zero-padding if the PIN is under 16 characters or truncating it if it exceeds 16 characters.
With the encryption mechanism understood, a custom script can be crafted to reverse the process. The script needs to parse the file structure, discarding the decoy PNG header, and isolating the unencrypted body up to the >>tyfs>> signature. The original 2803-byte header, encrypted with a 4-byte key and the XOR formula EncryptedByte[i] = PlaintextByte[i] ^ Key[i % 4] ^ (i & 0xFF), is located immediately after this signature. Given that the original file is known to be a PNG, a Known-Plaintext Attack (KPA) can be executed using the PNG magic bytes (89 50 4e 47) on the first 4 bytes of the encrypted header to mathematically extract the 4-byte key.
$ cat exploit.py
#!/usr/bin/env python3
import sys
file_path = './data/media/0/' \
'.galleryvault_DoNotDelete_1742398726/' \
'files/b2/b238f4cd-f79f-4e19-be3b-068b7add4d85'
with open(file_path, 'rb') as f:
data = f.read()
idx = data.find(b'>>tyfs>>')
if idx == -1:
print("Signature not found")
sys.exit(1)
enc_hdr = data[idx + 8 : idx + 8 + 2803]
unenc_body = data[2803 : idx]
png_hdr = bytes.fromhex('89504E47')
key = bytearray()
for i in range(4):
key.append(enc_hdr[i] ^ png_hdr[i] ^ i)
print(f"4 byte key recovered: {key.hex()}")
dec_hdr = bytearray()
for i in range(2803):
dec_hdr.append(enc_hdr[i] ^ key[i % 4] ^ (i & 0xFF))
with open('flag.png', 'wb') as f:
f.write(dec_hdr)
f.write(unenc_body)
print("File saved as flag.png")
Executing the script recovers the encryption key and reconstructs the image.
$ python3 exploit.py
4 byte key recovered: 2eb8f950
File saved as flag.png
Viewing the recovered image reveals the flag.
$ kitty +kitten icat flag.png
HTB{FLAG}