mobilefreemedium

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/
zipcrypto_known_plaintext_attackgallery_vault_decryptiondes_key_derivation_chainxor_stream_cipherandroid_data_partition_analysishidden_app_artifact_recovery

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

  1. ZIP analysis with 7z l -slt revealed all entries encrypted with ZipCrypto (weak encryption vulnerable to known-plaintext attacks)
  2. Android /data partition structure with ~78,990 non-empty files
  3. 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)

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:

  1. Prepending a GV icon PNG header (2803 bytes) to disguise files in file managers
  2. XOR-encrypting the first N bytes of the original file (partial encryption mode)
  3. Storing encrypted header + metadata in a tail section delimited by >>tyfs>> / <<tyfs<< markers
  4. 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):

StepOperationInputOutput
1Hardcoded hex676F6F645F6776"good_gv"
2DES-ECB decrypt5283CBBD2FE1AAF43503D1DDD64DFDD6 with key "good_gv\0""tianxiawuzei"
3Pad to 16 chars with '0', take first 8"tianxiawuzei00000"DES key = "tianxiaw"

Key Java classes (obfuscated names → original):

  • Hf/k.javaGvFileSecurity.java — encryption orchestrator
  • Oc/d.javaFileTailOperatorV1.java — V1 tail format + XOR encryption
  • Mc/c.javaThinkSecurity.java — DES key derivation
  • Mc/b.javaStreamSecurity.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

  1. ZipCrypto is broken — Any ZIP with ZipCrypto encryption and a known-plaintext file ≥12 bytes can be cracked in under 2 minutes with bkcrack. Android shared_prefs XML files are ideal candidates due to their predictable content.
  2. 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.
  3. 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