CTF-Writeups

Music Box v2 - CTF Challenge

Category: Misc / Steganography
Difficulty: Medium-Hard
Flag: KIB{aes_k3y_sl1ngs_christm4s_tun3}

📥 Download Challenge

Download Music Box v2.7z - Try to solve it yourself before reading the writeup!


Challenge Summary

You’re given a 7z archive:

Music Box v2.7z

The solution chain:

  1. Find a hidden hex blob inside the 7z and decode it.
  2. Use that decoded message to identify which song to inspect.
  3. Open that song in a spectrogram and read Morse code to get the passphrase.
  4. Fix XOR-corrupted image files to get valid WebP images.
  5. Carve attached AES ciphertext from those WebP images.
  6. Decrypt both presents with AES-128-CBC + PBKDF2-MD5 using the passphrase.
  7. Identify which decrypted present contains the real flag.

1. Inspecting the 7z Archive

Instead of just extracting, we inspect the archive content as raw bytes:

strings "Music Box v2.7z" | grep -E "[0-9A-Fa-f]{32,}"

Among the output, we find a long hex string, e.g.:

XOR1_HEX:3027242730272c21271d363023212978620430232c2962...

We copy only the hex part (no XOR1_HEX: label).
This looks like an obfuscated message: long, clean hex, not random noise.


2. Decoding the XOR’d Hex Hint

We suspect this hex is XOR-encoded with a single-byte key.
Using Python:

python3 - << 'EOF'
import binascii

hex_str = "3027242730272c21271d363023212978620430232c2962112b2c23363023626f62112b2e272c36620c2b252a366c352334"

data = binascii.unhexlify(hex_str)

key = 0x42  # XOR key used by the elves
decoded = bytes(b ^ key for b in data)

print("As text:", decoded.decode())
EOF

Output (example):

As text: reference_track: <name_of_song>.wav

This tells us exactly which track to analyze for Morse in the next step.


3. Spectrogram & Morse – Getting the Passphrase

Extract the archive and list the contents:

7z x "Music Box v2.7z" -oMusicBox_v2
cd MusicBox_v2
ls

Among the audio files, we locate the reference_track mentioned above.

We open it in Audacity (or any audio editor), switch the track view to Spectrogram, and zoom in.
We see a clear dot–dash pattern: Morse code rendered as tones in the spectrogram.

Decoding the Morse (visually or using a decoder) gives us a passphrase:

holly!jolly!xmas

This will be used later as the AES passphrase.


4. Fixing the Corrupted Images (Global XOR)

The archive also contains two image files representing the presents, for example:

elf.png
santa.png

Initially, they appear broken. When we inspect them:

file elf.png santa.png

After reversing the XOR (see below), they turn out to actually be WebP (RIFF) images with .png extensions.

The challenge used a global XOR with key 0x42 over the entire image files.
We undo this:

python3 - << 'EOF'
key = 0x67
names = ["elf.png", "santa.png"]

for name in names:
    with open(name, "rb") as f:
        data = f.read()
    fixed = bytes(b ^ key for b in data)
    out = name.replace(".png", "_fixed.png")
    with open(out, "wb") as f_out:
        f_out.write(fixed)
    print(f"{name} -> {out}")
EOF

Now we check:

file elf_fixed.png santa_fixed.png

Example result:

elf_fixed.png: RIFF (little-endian) data, Web/P image, ...
santa_fixed.png: RIFF (little-endian) data, Web/P image, ...

Opening them shows valid images (Santa vs Grinch).


5. Carving Attached Presents from WebP (RIFF) Images

The presents are not separate files; they are appended to the image files.
Because they’re raw AES ciphertext with no magic header, binwalk shows nothing useful.

WebP files are wrapped in a RIFF container:

Therefore, the expected length of the WebP file is:

expected_len = size_val + 8

Anything beyond that is extra data → our attached present.

We parse and extract that tail with Python:

python3 - << 'EOF'
import struct

for name in ["elf_fixed.png", "santa_fixed.png"]:
    with open(name, "rb") as f:
        data = f.read()

    if data[:4] != b"RIFF":
        print(f"{name}: not RIFF, magic =", data[:4])
        continue

    size_val = struct.unpack("<I", data[4:8])[0]
    expected_len = size_val + 8
    actual_len = len(data)
    extra_len = actual_len - expected_len

    print(f"{name}: actual={actual_len}, expected={expected_len}, extra={extra_len}")

    if extra_len > 0:
        extra = data[expected_len:]
        out = name.replace(".png", "_present.gift")
        with open(out, "wb") as f_out:
            f_out.write(extra)
        print(f"  -> extracted attached data to {out}")
    else:
        print("  -> no attached data found")
EOF

We end up with:

These are small, high-entropy binary blobs - our encrypted presents.


6. Crypto: AES-128-CBC + PBKDF2-MD5

From the challenge design:

We decrypt both *_present.gift files and see which one contains the real flag.

Using a virtual environment and pycryptodome:

python3 -m venv ctfenv
source ctfenv/bin/activate
pip install pycryptodome

Then:

nano decrypt_presents.py
from Crypto.Cipher import AES
from Crypto.Protocol.KDF import PBKDF2
from Crypto.Hash import MD5

ITERATIONS = 100000   # Must match the encrypt-side setting
KEY_LEN = 16          # AES-128


def pkcs7_unpad(data):
    pad_len = data[-1]
    if pad_len < 1 or pad_len > 16:
        raise ValueError("Invalid padding")
    if data[-pad_len:] != bytes([pad_len]) * pad_len:
        raise ValueError("Invalid padding pattern")
    return data[:-pad_len]


def decrypt_file(fname, password):
    with open(fname, "rb") as f:
        blob = f.read()

    if len(blob) < 32:
        raise ValueError(f"{fname}: blob too small for salt+iv")

    salt = blob[0:16]
    iv = blob[16:32]
    ciphertext = blob[32:]

    # PBKDF2-HMAC-MD5 → 16-byte key for AES-128
    key = PBKDF2(password.encode(), salt,
                 dkLen=KEY_LEN,
                 count=ITERATIONS,
                 hmac_hash_module=MD5)

    cipher = AES.new(key, AES.MODE_CBC, iv)
    plaintext = cipher.decrypt(ciphertext)
    plaintext = pkcs7_unpad(plaintext)
    return plaintext


if __name__ == "__main__":
    password = "holly!jolly!xmas"

    for name in ["elf_fixed_present.gift", "santa_fixed_present.gift"]:
        print(f"=== {name} ===")
        try:
            pt = decrypt_file(name, password)
            try:
                print(pt.decode())
            except UnicodeDecodeError:
                print(repr(pt))
        except Exception as e:
            print(f"Error decrypting {name}: {e}")
        print()

Run it:

python decrypt_presents.py

One present is a decoy (Grinch), the other contains the real flag.


7. Flag

The correct decrypted present yields the final flag:

KIB{aes_k3y_sl1ngs_christm4s_tun3}

Takeaways

This challenge is a nice multi-stage chain involving: