HTB: Cryptohorrific Challenge
Cryptohorrific - HackTheBox Challenge Writeup
Challenge Information
| Property | Value |
|---|---|
| Name | Cryptohorrific |
| Category | Reversing / Mobile |
| Difficulty | Medium |
| Author | d3vn0mi |
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:
unzip -l /out/a12c7373-9020-41fe-b337-fc58e65231e5.zipThe 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:
unzip -o -P hackthebox /out/a12c7373-9020-41fe-b337-fc58e65231e5.zipThat 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-KaPdSgVkYandQfTjWnZq4t7w!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):
# Confirm the source zip has real content despite the 0-byte extracted fileunzip -l /out/a12c7373-9020-41fe-b337-fc58e65231e5.zip
# Re-extract with HTB's default archive passwordunzip -o -P hackthebox /out/a12c7373-9020-41fe-b337-fc58e65231e5.zip2. Identify the target as an iOS app bundle and locate the encrypted flag:
# Confirm it's an iPhone-simulator bundleplutil -p hackthebox.app/Info.plist
# Extract the base64 encrypted flag blobplutil -p hackthebox.app/challenge.plist3. Recover the hardcoded key material from the Mach-O binary:
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 base64from 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 usedFlag
HTB{REDACTED}Tools Used
unzip— password-protected archive recoveryplutil— parsing iOS.plistfiles (Info.plist,challenge.plist)strings— extracting hardcoded key/IV material and method signatures from the Mach-Oobjdump— 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 aniv: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.