$ cat writeup.md…
$ cat writeup.md…
HackTheBox
"While examining the device, we discovered that critical evidence or artifacts may have been overlooked. We believe that your expertise in mobile forensics will enable you to uncover the missing piece."
"While examining the device, we discovered that critical evidence or artifacts may have been overlooked. We believe that your expertise in mobile forensics will enable you to uncover the missing piece."
The challenge provides a 1.25 GB ZIP file (served over TCP) containing an Android /data partition dump with ~89,800 entries. All files are encrypted with ZipCrypto.
7z l -slt revealed all entries encrypted with ZipCrypto (weak encryption vulnerable to known-plaintext attacks)/data partition structure with ~78,990 non-empty filescom.thinkyeah.galleryvault) — file-hiding app with custom encryptionorg.thoughtcrime.securesms) — encrypted SQLCipher databasecom.topjohnwu.magisk) — device was rootednotes.notepad.checklist.calendar.todolist.notebook)ZipCrypto with Store (no compression) is vulnerable to known-plaintext attacks when at least 12 bytes of plaintext are known. Android shared_prefs XML files of exactly 65 bytes always contain identical content, providing a perfect known-plaintext source.
Gallery Vault hides files by:
>>tyfs>> / <<tyfs<< markersAndroid shared_prefs XML files of 65 bytes always contain:
<?xml version='1.0' encoding='utf-8' standalone='yes' ?> <map />
Used bkcrack to recover internal encryption keys:
# Create known plaintext file (65 bytes) printf "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n<map />\n" > known_plaintext.bin # Attack using a known shared_prefs file (ZipCrypto Store, no compression) bkcrack -C challenge.zip \ -c "data/user_de/0/com.android.bluetooth/shared_prefs/bluetooth_volume_map.xml" \ -p known_plaintext.bin # Recovered keys: 890ca696 587c6483 cd091757 (~78 seconds) # Create new ZIP with known password bkcrack -C challenge.zip -k 890ca696 587c6483 cd091757 -U decrypted.zip password123
Queried Gallery Vault's SQLite database:
-- data/data/com.thinkyeah.galleryvault/databases/galleryvault.db SELECT _id, uuid, name, folder_id, file_type, mime_type, original_path, encrypt_state, file_size FROM file_v1;
Found flag.png:
b238f4cd-f79f-4e19-be3b-068b7add4d85/storage/emulated/0/Pictures/flag.pngEncrypted file location:
data/media/0/.galleryvault_DoNotDelete_1742398726/files/b2/b238f4cd-f79f-4e19-be3b-068b7add4d85
DES Key Derivation Chain (from decompiled APK with jadx):
| Step | Operation | Input | Output |
|---|---|---|---|
| 1 | Hardcoded hex | 676F6F645F6776 | "good_gv" |
| 2 | DES-ECB decrypt | 5283CBBD2FE1AAF43503D1DDD64DFDD6 with key "good_gv\0" | "tianxiawuzei" |
| 3 | Pad to 16 chars with '0', take first 8 | "tianxiawuzei00000" | DES key = "tianxiaw" |
Key Java classes (obfuscated names → original):
Hf/k.java → GvFileSecurity.java — encryption orchestratorOc/d.java → FileTailOperatorV1.java — V1 tail format + XOR encryptionMc/c.java → ThinkSecurity.java — DES key derivationMc/b.java → StreamSecurity.java — XOR stream cipherFile Tail Structure (V1):
[0 .. orig_len-gveh_len-1] : Unencrypted original data (from offset gveh_len)
[orig_len-gveh_len .. +7] : ">>tyfs>>" head marker
[+8 .. +8+gveh_len-1] : XOR encrypted first gveh_len bytes of original
[tail metadata]:
8 bytes - original file length (big-endian)
8 bytes - partial encrypt length / gveh_len (big-endian)
1 byte - full encrypt flag
N bytes - DES-ECB encrypted XOR key (4 bytes + PKCS5 padding = 8 bytes)
8 bytes - encrypted key length (big-endian)
M bytes - DES-ECB encrypted JSON metadata
8 bytes - metadata length (big-endian)
2 bytes - version (0x01, 0x01)
"<<tyfs<<" tail marker
XOR Stream Cipher:
decrypted[i] = encrypted[i] ^ ((i & 0xFF) ^ key[i % key_len])
#!/usr/bin/env python3 """Gallery Vault File Decryptor for HTB Protected""" import struct from Crypto.Cipher import DES TAIL_MARKER = b"<<tyfs<<" HEAD_MARKER = b">>tyfs>>" def derive_des_key(key_str: str) -> bytes: """Mc.c.e() - derive DES key from string""" if len(key_str) < 16: key_str = key_str + "0" * (16 - len(key_str)) return key_str[:8].encode() def xor_decrypt(data: bytes, key: bytes, start_pos: int = 0) -> bytes: """Mc.b.a.a() - XOR stream cipher decryption""" result = bytearray(len(data)) for i in range(len(data)): pos = start_pos + i result[i] = data[i] ^ ((pos & 0xFF) ^ key[pos % len(key)]) return bytes(result) # Read encrypted file (after stripping GV icon header) with open("flag.png", "rb") as f: encrypted_data = f.read() # DES key: "good_gv" → DES decrypt → "tianxiawuzei" → pad → "tianxiaw" des_key = derive_des_key("tianxiawuzei") # b"tianxiaw" cipher = DES.new(des_key, DES.MODE_ECB) # Parse tail (read backwards from <<tyfs<< marker) pos = len(encrypted_data) - 8 # skip <<tyfs<< pos -= 2 # version pos -= 8 # metadata length meta_len = struct.unpack(">q", encrypted_data[pos:pos+8])[0] pos -= meta_len # skip metadata pos -= 8 # encrypted key length key_len = struct.unpack(">q", encrypted_data[pos:pos+8])[0] pos -= key_len # encrypted XOR key # Decrypt XOR key enc_key = encrypted_data[pos:pos+key_len] xor_key_padded = cipher.decrypt(enc_key) pad = xor_key_padded[-1] xor_key = xor_key_padded[:-pad] # [0x2e, 0xb8, 0xf9, 0x50] pos -= 1 # full encrypt flag full_encrypt = encrypted_data[pos] pos -= 8 # original file length orig_len = struct.unpack(">q", encrypted_data[pos:pos+8])[0] # 787165 pos -= 8 # partial encrypt length (gveh_len) partial_len = struct.unpack(">q", encrypted_data[pos:pos+8])[0] # 2803 # Find head marker and decrypt head_pos = encrypted_data.find(HEAD_MARKER) enc_first = encrypted_data[head_pos + 8 : head_pos + 8 + partial_len] dec_first = xor_decrypt(enc_first, xor_key, start_pos=0) # Reconstruct: decrypted header + unencrypted body unencrypted_rest = encrypted_data[:head_pos] original = dec_first + unencrypted_rest # 787165 bytes, valid PNG with open("flag_decrypted.png", "wb") as f: f.write(original) # Decrypted PNG (564x568) displays: HTB{REDACTED}
bkcrack. Android shared_prefs XML files are ideal candidates due to their predictable content./data/data/*/), then check for vault/locker/hiding apps. Their databases often contain the original file paths and metadata needed to locate and decrypt hidden evidence.$ cat /etc/motd
Liked this one?
Pro unlocks every complete writeup and expanded API access. $9/mo.
$ cat pricing.md$ grep --similar