Protected
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."
$ ls tags/ techniques/
Protected — HackTheBox
Description
"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.
Analysis
Initial Reconnaissance
- ZIP analysis with
7z l -sltrevealed all entries encrypted with ZipCrypto (weak encryption vulnerable to known-plaintext attacks) - Android
/datapartition structure with ~78,990 non-empty files - Key apps identified after extraction:
- Gallery Vault (
com.thinkyeah.galleryvault) — file-hiding app with custom encryption - Signal Messenger (
org.thoughtcrime.securesms) — encrypted SQLCipher database - Magisk (
com.topjohnwu.magisk) — device was rooted - Notepad (
notes.notepad.checklist.calendar.todolist.notebook)
- Gallery Vault (
ZipCrypto Weakness
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 Encryption Scheme
Gallery Vault hides files by:
- Prepending a GV icon PNG header (2803 bytes) to disguise files in file managers
- XOR-encrypting the first N bytes of the original file (partial encryption mode)
- Storing encrypted header + metadata in a tail section delimited by
>>tyfs>>/<<tyfs<<markers - Using DES-ECB to encrypt the XOR key and JSON metadata in the tail
Solution
Step 1: Crack ZipCrypto with Known-Plaintext Attack
Android 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
Step 2: Locate Hidden Files in Gallery Vault
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:
- UUID:
b238f4cd-f79f-4e19-be3b-068b7add4d85 - Original path:
/storage/emulated/0/Pictures/flag.png - File size: 787,165 bytes
- Encrypt state: 0 (partial encryption)
Encrypted file location:
data/media/0/.galleryvault_DoNotDelete_1742398726/files/b2/b238f4cd-f79f-4e19-be3b-068b7add4d85
Step 3: Reverse Engineer Gallery Vault Encryption
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 cipher
File 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])
Step 4: Decrypt and Recover Flag
#!/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{G3113ry_D3cr3bt0r}
Lessons Learned
- ZipCrypto is broken — Any ZIP with ZipCrypto encryption and a known-plaintext file ≥12 bytes can be cracked in under 2 minutes with
bkcrack. Androidshared_prefsXML files are ideal candidates due to their predictable content. - Gallery Vault's encryption is security-through-obscurity — The DES key is derived from hardcoded values in the APK, making it universally recoverable. The XOR stream cipher with a 4-byte key provides minimal protection.
- Mobile forensics workflow — Always enumerate installed apps first (
/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 writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar
Similar writeups
- [mobile][free]Jigsaw— HackTheBox
- [forensics][free]TrueSecrets— hackthebox
- [reverse][free]SAW— hackthebox
- [mobile][free]APKey— HackTheBox
- [reverse][free]Challenge Scenario (rev_gameloader)— HackTheBox