HTB: Cryptohorrific Challenge

Cryptohorrific - HackTheBox Challenge Writeup

Challenge Information

PropertyValue
NameCryptohorrific
CategoryReversing / Mobile
DifficultyMedium
Authord3vn0mi

Description

Secure coding is the keystone of the application security!

Solution

The challenge archive initially looked broken: the extracted working directory contained a single 0-byte .nib file and nothing else usable. Rather than treat that as a dead end, I went back to the original zip and listed its contents directly:

Terminal window
unzip -l /out/a12c7373-9020-41fe-b337-fc58e65231e5.zip

The listing showed 17 files with real, non-zero sizes — so the archive itself was fine. The 0-byte artifact meant staging had extracted it without the correct password, silently writing empty placeholder files. HackTheBox’s default archive password recovered everything cleanly:

Terminal window
unzip -o -P hackthebox /out/a12c7373-9020-41fe-b337-fc58e65231e5.zip

That produced a full iOS .app bundle, including a 32KB Mach-O binary, Info.plist, challenge.plist, and supporting resources.

Info.plist identified the bundle as ben.hackthebox, an iPhone-simulator app built in 2018 — so despite whatever label the task carried, this was a mobile reversing challenge, not forensics. Combined with the description’s hint about “secure coding,” the obvious target was a hardcoded secret somewhere in the binary.

challenge.plist inside the bundle held a 64-byte base64 blob under a flag key — an encrypted flag. Pulling printable strings out of the Mach-O surfaced the mechanism:

  • An Objective-C method -[ViewController SecretManager:key:iv:data:]
  • An import of _CCCrypt (Apple’s CommonCrypto)
  • Two hardcoded 16-byte strings sitting right next to each other: !A%D*G-KaPdSgVkY and QfTjWnZq4t7w!z%C

That’s a classic hardcoded-key setup: one string is almost certainly the AES key, the other the IV — but with CCCrypt, whether the IV is actually used depends entirely on the mode flag passed at the call site, which isn’t always what the method signature implies.

Rather than reverse the call site by hand, I brute-forced the small decision matrix: (key, iv) × (AES-CBC, AES-ECB). AES-128-ECB with key !A%D*G-KaPdSgVkY decrypted the blob to a result with 12 bytes of valid 0x0c PKCS#7 padding — a decisive, unambiguous confirmation, since garbage output essentially never produces valid padding by chance. The IV string turned out to be a decoy: even though SecretManager:key:iv:data: accepts an iv: parameter, the underlying CCCrypt call actually runs with kCCOptionECBMode, so the IV is never consumed.

objdump on the analysis host wasn’t able to disassemble this particular Mach-O, so the kCCOptionECBMode conclusion rests on the decryption result (valid padding, sensible plaintext) rather than on directly reading the instruction stream at the call site. Behaviorally it’s as strong a confirmation as you can get without disassembly, but it’s inference, not a direct read of the flag being passed.

Key Steps

1. Recover the real archive contents (password-protected zip, not a corrupted staging artifact):

Terminal window
# Confirm the source zip has real content despite the 0-byte extracted file
unzip -l /out/a12c7373-9020-41fe-b337-fc58e65231e5.zip
# Re-extract with HTB's default archive password
unzip -o -P hackthebox /out/a12c7373-9020-41fe-b337-fc58e65231e5.zip

2. Identify the target as an iOS app bundle and locate the encrypted flag:

Terminal window
# Confirm it's an iPhone-simulator bundle
plutil -p hackthebox.app/Info.plist
# Extract the base64 encrypted flag blob
plutil -p hackthebox.app/challenge.plist

3. Recover the hardcoded key material from the Mach-O binary:

Terminal window
strings -a hackthebox.app/hackthebox | sort -u | grep -v '^_\{0,1\}OBJC'
# -> -[ViewController SecretManager:key:iv:data:]
# -> _CCCrypt
# -> !A%D*G-KaPdSgVkY (16 bytes — candidate key)
# -> QfTjWnZq4t7w!z%C (16 bytes — candidate IV)

4. Brute-force the (key, iv) × (mode) matrix and validate via PKCS#7 padding:

import base64
from Crypto.Cipher import AES
ct = base64.b64decode(
"Tq+CWzQS0wYzs2rJ+GNrPLP6qekDbwze6fIeRRwBK2WXHOhba7WR2OGNUFKoAvyW7njTCMlQzlwIRdJvaP2iYQ=="
)
key = b"!A%D*G-KaPdSgVkY"
iv = b"QfTjWnZq4t7w!z%C"
for mode_name, cipher in [
("CBC", AES.new(key, AES.MODE_CBC, iv)),
("ECB", AES.new(key, AES.MODE_ECB)),
]:
pt = cipher.decrypt(ct)
print(mode_name, pt)
# AES-128-ECB produces 12 trailing 0x0c bytes -> valid PKCS#7 padding
# -> flag recovered, IV was never actually used

Flag

HTB{REDACTED}

Tools Used

  • unzip — password-protected archive recovery
  • plutil — parsing iOS .plist files (Info.plist, challenge.plist)
  • strings — extracting hardcoded key/IV material and method signatures from the Mach-O
  • objdump — attempted disassembly of the Mach-O (inconclusive on this binary)
  • Python + pycryptodome (Crypto.Cipher.AES) — brute-forcing the key/IV/mode matrix and validating via PKCS#7 padding

Key Learnings

  • Empty extracted artifacts don’t mean a broken challenge — check the source archive first. A 0-byte file after extraction is often a staging/password mismatch, not corruption. Listing the original zip’s contents (unzip -l) before assuming the challenge is broken saves a lot of wasted time, and HTB’s default archive password is worth trying immediately.
  • Task category labels can be wrong or misleading — trust the artifacts, not the label. What was tagged as forensics-adjacent turned out to be straightforward iOS/mobile reversing once the bundle was extracted.
  • Two hardcoded 16-byte strings near a crypto method is a strong, generalizable pattern: try AES-ECB with the first string as the key, even if the API signature also accepts an IV. CCCrypt’s actual mode is set by an options flag at the call site — the method signature having an iv: parameter doesn’t guarantee it’s used. When disassembly isn’t available, PKCS#7 padding validity after decryption is a reliable, low-cost way to confirm the correct key/mode combination.
  • Brute-forcing a small, well-defined decision matrix (key × iv × mode) is often faster and more reliable than reversing the call site by hand, especially when tooling (like objdump) fails against the target binary format.