# 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

```bash
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:

```bash
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:

| Section | Encryption | Content |
|---------|------------|---------|
| `0x00` → `0xF0BF` | AES-128-CBC | WIBN banner section (icons, title) |
| `0xF0C0` → end | Plaintext | `Bk` section (file table) |

- **Key (SD Key):** `AB01B9D8E1622B08AFBAD84DBFC2A55D` (public key known to the homebrew community)
- **IV:** 16 null bytes

```python
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:

| Offset | Size | Content |
|--------|------|---------|
| `0x20` | 4 | Magic `WIBN` |
| `0x40` | 64 | **Game title (UTF-16BE)** |
| `0xC0` | 7 × `0x1200` | 7 icon frames, 48×48 px (RGB5A3) |
| `0xEAC0` | `0x6000` | **Banner, 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.

```python
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:

![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.

```python
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

| Element | Value | Source |
|---------|-------|--------|
| Game name | `BREIZHSports` | Banner image (RGB5A3) |
| Fragment | `_W11_1z_fun}` | WIBN title field, offset `0x40` |

**Flag : `BZHCTF{BREIZHSports_W11_1z_fun}`**

