Contents

BreizhCTF 2026 - Keys, Keys, Keys

Keys, Keys, Keys

  • Difficulty: Easy
  • Category: Forensic
  • Author: Zlippy

Description

A white living room console from the mid-2000s, famous for its motion-controller, was briefly “borrowed” from its owner. A partial dump of its external storage is provided.

Your mission: identify the name of the game being played, and recover a second flag fragment hidden in the artifacts.

Flag format: BZHCTF{GameName_artifact}

Files:

  • image.dd

Solve

Step 1 — Identify the image

file image.dd
# image.dd: DOS/MBR boot sector, code offset 0x58+2, OEM-ID "mkfs.fat", FAT (32 bit)

A FAT32 partition image. Mount it to explore its contents:

sudo mount -o loop image.dd /mnt/case/
find /mnt/case/ -type f
/mnt/case/19E4-3310/private/uii/title/BZPP/data.bin

A single file at a very characteristic path: /private/uii/title/BZPP/data.bin.

Step 2 — Identify the console: Nintendo Wii

The path /private/wii/title/<TITLEID>/data.bin is the standard format used by the Nintendo Wii to store save files on SD cards (since firmware 3.0 in 2007). Here, uii replaces wii — a cosmetic obfuscation by the challenge author.

The TITLEID in the path (BZPP) normally identifies the game, but it has been modified to mislead.

Step 3 — Decrypt the data.bin

The Wii data.bin is partially encrypted with AES-128-CBC:

SectionEncryptionContent
0x000xF0BFAES-128-CBCWIBN banner section (icons, title)
0xF0C0 → endPlaintextBk section (file table)
  • Key (SD Key): AB01B9D8E1622B08AFBAD84DBFC2A55D (public key known to the homebrew community)
  • IV: 16 null bytes
from Crypto.Cipher import AES

SD_KEY = bytes.fromhex("AB01B9D8E1622B08AFBAD84DBFC2A55D")

with open("data.bin", "rb") as f:
    data = f.read()

cipher = AES.new(SD_KEY, AES.MODE_CBC, iv=b'\x00' * 16)
decrypted = cipher.decrypt(data)

with open("data_decrypted.bin", "wb") as f:
    f.write(decrypted)

print(decrypted[0x20:0x24]) # → b'WIBN'

the WIBN magic should appear at offset 0x20.

WIBN confirms the Wii Banner format. The structure after decryption:

OffsetSizeContent
0x204Magic WIBN
0x4064Game title (UTF-16BE)
0xC07 × 0x12007 icon frames, 48×48 px (RGB5A3)
0xEAC00x6000Banner, 192×64 px (RGB5A3)

Step 4 — Extract the banner image

The TITLEID in the path (BZPP) and the one in the metadata section (RSPP, found at offset 0xF122) are both decoys. The real game name is visible in the embedded banner image.

Nintendo Wii textures use the RGB5A3 format: tiles of 4×4 pixels, 2 bytes per pixel, where bit 15 selects between RGB555 and RGB4A3 encoding.

from PIL import Image
import struct

def rgb5a3_to_rgba(val):
    if val & 0x8000:          # RGB555 mode
        r = ((val >> 10) & 0x1F) * 255 // 31
        g = ((val >> 5)  & 0x1F) * 255 // 31
        b = (val & 0x1F) * 255 // 31
        a = 255
    else:                     # RGB4A3 mode
        a = ((val >> 12) & 0x07) * 255 // 7
        r = ((val >> 8)  & 0x0F) * 255 // 15
        g = ((val >> 4)  & 0x0F) * 255 // 15
        b = (val & 0x0F) * 255 // 15
    return (r, g, b, a)

def decode_rgb5a3(raw, width, height):
    img = Image.new('RGBA', (width, height))
    pixels = img.load()
    offset = 0
    for ty in range(0, height, 4):
        for tx in range(0, width, 4):
            for y in range(4):
                for x in range(4):
                    val = struct.unpack_from('>H', raw, offset)[0]
                    offset += 2
                    if tx + x < width and ty + y < height:
                        pixels[tx + x, ty + y] = rgb5a3_to_rgba(val)
    return img

# 7 icon frames (48×48 px)
frames = []
for i in range(7):
    off = 0xC0 + i * 0x1200
    frame = decode_rgb5a3(decrypted[off:off + 0x1200], 48, 48)
    frames.append(frame)

frames[0].save("icon_animated.gif", save_all=True,
               append_images=frames[1:], loop=0, duration=150)

# Banner (192×64 px)
banner = decode_rgb5a3(decrypted[0xEAC0:0xEAC0 + 0x6000], 192, 64)
banner.save("banner.png")

Icon animation:

/breizhctf-2026/keys-keys-keys/icon_animated.gif

The icon animation clearly shows the game name: BREIZHSports.

Step 5 — Extract the hidden fragment from the title field

The WIBN title field is at offset 0x40 (64 bytes), normally UTF-16BE. The challenge author embedded raw ASCII bytes inside it to hide a message.

title_raw = decrypted[0x40:0x80]

# Filter out null bytes to read the raw ASCII
non_null = bytes(b for b in title_raw if b != 0)
print(non_null)
# b'LastPArt:_W11_1z_fun}'

Fragment extracted: _W11_1z_fun}

Step 6 — Build the flag

ElementValueSource
Game nameBREIZHSportsBanner image (RGB5A3)
Fragment_W11_1z_fun}WIBN title field, offset 0x40

Flag : BZHCTF{BREIZHSports_W11_1z_fun}