# Requiem Cipher

**Categoría:** Esteganografía / Crypto\
**Dificultad:** Insane

### 0. Resumen del reto

El reto entrega un archivo MIDI que, a simple vista, parece una pieza musical normal. Sin embargo, el archivo contiene varias capas ocultas:

1. Una **clave AES** escondida en eventos MIDI concretos.
2. Un conjunto de bits ocultos en otra pista/canal del MIDI.
3. Esos bits están **desordenados** mediante una permutación pseudoaleatoria.
4. Al reordenarlos se reconstruye un **código QR**.
5. El QR no contiene la flag directamente, sino un `IV:ciphertext`.
6. Con la clave AES recuperada se descifra el ciphertext y se obtiene la flag.

El flujo completo es:

```
MIDI → clave AES → bits ocultos → deshacer permutación → QR → IV+ciphertext → AES-CBC → flag
```

### 1. Análisis inicial

Un archivo MIDI no es audio como tal. No almacena una onda sonora, sino instrucciones musicales:

* qué nota se toca;
* cuándo se toca;
* con qué intensidad;
* en qué canal;
* en qué pista.

Esto lo hace muy interesante para esteganografía, porque se pueden ocultar datos en campos que no son evidentes para quien solo escucha la melodía.

En este reto, los datos relevantes no están en el sonido como espectrograma, sino en los **eventos MIDI**.

### 2. Inspección básica del MIDI

Lo primero es leer el archivo y observar sus eventos.

Instalamos la librería necesaria:

```bash
pip install mido
```

Código de inspección:

```python
from mido import MidiFile

mid = MidiFile("requiem_cipher.mid")

for track_index, track in enumerate(mid.tracks):
    print(f"--- Track {track_index} ---")
    for msg in track[:20]:
        print(msg)
```

Esto nos permite ver eventos como:

```
note_on channel=9 note=35 velocity=...
note_on channel=2 note=73 velocity=...
note_on channel=2 note=74 velocity=...
```

Estas repeticiones son sospechosas porque aparecen con patrones muy concretos.

### 3. Hipótesis de ocultación

Después de inspeccionar el MIDI, se observan tres grupos relevantes:

| Canal         |          Nota | Posible uso           |
| ------------- | ------------: | --------------------- |
| Canal 0       |  Varias notas | Música normal / ruido |
| Canal 2       | Notas 73 y 74 | Clave AES             |
| Canal 9       |       Nota 35 | Bits ocultos del QR   |
| Otros canales |  Varias notas | Ruido / distracción   |

La hipótesis es:

* la música principal es decorativa;
* la clave AES está escondida en el canal 2;
* la información visual está escondida en el canal 9;
* el resto de eventos sirven para que el archivo parezca natural.

### 4. Recuperación de la clave AES

#### 4.1. Patrón encontrado

En el canal 2 aparecen eventos `note_on` con notas 73 y 74.

El patrón es:

```
note 73 → nibble alto
note 74 → nibble bajo
```

Cada byte de la clave se divide en dos nibbles:

```
byte = high_nibble || low_nibble
```

Además, cada valor se codifica usando la velocity:

```
nibble = velocity - 40
```

Finalmente, el byte reconstruido está ofuscado con XOR `0x55`:

```
byte_real = byte_codificado XOR 0x55
```

#### 4.2. Código para extraer la clave

```python
from mido import MidiFile

mid = MidiFile("requiem_cipher.mid")

nibbles = []

for track in mid.tracks:
    for msg in track:
        if msg.type == "note_on":
            channel = getattr(msg, "channel", None)
            note = getattr(msg, "note", None)

            if channel == 2 and note in [73, 74]:
                nibbles.append((note, msg.velocity - 40))
```

Aquí filtramos únicamente:

* mensajes `note_on`;
* canal 2;
* notas 73 y 74.

#### 4.3. Reconstrucción byte a byte

```python
key_bytes = []

for i in range(0, len(nibbles), 2):
    pair = nibbles[i:i+2]

    if len(pair) < 2:
        break

    if pair[0][0] == 73 and pair[1][0] == 74:
        high = pair[0][1]
        low = pair[1][1]

        encoded = ((high & 0xF) << 4) | (low & 0xF)
        key_bytes.append(encoded ^ 0x55)

key = bytes(key_bytes[:16])
print(key.hex())
```

La clave AES final tiene 16 bytes, por tanto corresponde a AES-128.

### 5. Extracción de bits ocultos

#### 5.1. Canal oculto del QR

El siguiente patrón aparece en:

```
canal 9
nota 35
```

El campo `velocity` codifica bits:

```
velocity > 80  → bit 1
velocity <= 80 → bit 0
```

Esto es esteganografía musical porque la información está escondida en la intensidad de golpes de percusión.

#### 5.2. Código de extracción

```python
bits = ""

for track in mid.tracks:
    for msg in track:
        if msg.type == "note_on":
            channel = getattr(msg, "channel", None)
            note = getattr(msg, "note", None)

            if channel == 9 and note == 35:
                bits += "1" if msg.velocity > 80 else "0"

print(len(bits))
```

En este punto tenemos una cadena de bits, pero todavía no podemos reconstruir el QR directamente.

### 6. Problema: los bits no están en orden

Si intentamos interpretar estos bits de forma directa, el QR no se reconstruye correctamente.

Esto indica que los bits han sido permutados.

La permutación se genera usando una seed derivada de la clave AES:

```python
seed = int.from_bytes(hashlib.sha256(key).digest()[:4], "big")
```

Esto significa que no podemos ordenar los bits correctamente sin recuperar antes la clave.

Por eso el orden correcto de resolución es:

```
1. recuperar clave AES
2. usar la clave para derivar seed
3. deshacer permutación de bits
4. reconstruir QR
```

### 7. Deshacer la permutación

#### 7.1. Generar la misma permutación

```python
import hashlib
import random

seed = int.from_bytes(hashlib.sha256(key).digest()[:4], "big")
rng = random.Random(seed)

indices = list(range(len(bits)))
rng.shuffle(indices)
```

El reto usó esta permutación para desordenar los bits. Para revertirla hacemos la operación inversa.

#### 7.2. Reordenar los bits

```python
payload_bits = ["0"] * len(bits)

for real_pos, shuffled_pos in enumerate(indices):
    payload_bits[real_pos] = bits[shuffled_pos]

payload_bits = "".join(payload_bits)
```

Ahora `payload_bits` contiene los bits en el orden original.

### 8. Reconstrucción del QR

#### 8.1. Formato interno

Los primeros 8 bits indican el tamaño del QR:

```python
size = int(payload_bits[:8], 2)
```

Después vienen los píxeles:

```python
qr_bits = payload_bits[8:8 + size * size]
```

Cada bit representa un píxel:

```
1 → negro
0 → blanco
```

#### 8.2. Crear la imagen

```python
from PIL import Image

img = Image.new("1", (size, size), 1)
pixels = img.load()

for i, bit in enumerate(qr_bits):
    x = i % size
    y = i // size
    pixels[x, y] = 0 if bit == "1" else 1

img = img.resize((size * 12, size * 12))
img.save("recovered_qr.png")
```

Al abrir `recovered_qr.png`, aparece un código QR válido.

### 9. Lectura del QR

El QR no contiene la flag directamente. Contiene dos valores separados por `:`:

```
iv_hex:ciphertext_hex
```

Ejemplo de formato:

```
aabbccddeeff00112233445566778899:deadbeef...
```

El primer valor es el IV usado en AES-CBC. El segundo es el ciphertext.

Podemos leer el QR manualmente con un lector de QR, o automáticamente con OpenCV.

Instalación:

```bash
pip install opencv-python
```

Lectura automática:

```python
import cv2

qr_img = cv2.imread("recovered_qr.png")
detector = cv2.QRCodeDetector()
qr_payload, points, _ = detector.detectAndDecode(qr_img)

print(qr_payload)
```

### 10. Descifrado AES-CBC

Una vez tenemos:

* `key`, recuperada desde el MIDI;
* `iv`, leído desde el QR;
* `ciphertext`, leído desde el QR;

podemos descifrar.

```python
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

iv_hex, ct_hex = qr_payload.split(":")

iv = bytes.fromhex(iv_hex)
ct = bytes.fromhex(ct_hex)

cipher = AES.new(key, AES.MODE_CBC, iv)
flag = unpad(cipher.decrypt(ct), 16)

print(flag.decode())
```

Resultado:

```
THL{flag}
```

### 11. Solver completo

```python
from mido import MidiFile
from PIL import Image
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import hashlib
import random
import cv2

MIDI_FILE = "requiem_cipher.mid"

mid = MidiFile(MIDI_FILE)

# 1. Extraer clave AES
nibbles = []

for track in mid.tracks:
    for msg in track:
        if msg.type == "note_on":
            channel = getattr(msg, "channel", None)
            note = getattr(msg, "note", None)

            if channel == 2 and note in [73, 74]:
                nibbles.append((note, msg.velocity - 40))

key_bytes = []

for i in range(0, len(nibbles), 2):
    pair = nibbles[i:i+2]

    if len(pair) < 2:
        break

    if pair[0][0] == 73 and pair[1][0] == 74:
        high = pair[0][1]
        low = pair[1][1]

        encoded = ((high & 0xF) << 4) | (low & 0xF)
        key_bytes.append(encoded ^ 0x55)

key = bytes(key_bytes[:16])

if len(key) != 16:
    raise ValueError("No se pudo reconstruir la clave AES")

print("[+] AES key:", key.hex())

# 2. Extraer bits QR
bits = ""

for track in mid.tracks:
    for msg in track:
        if msg.type == "note_on":
            channel = getattr(msg, "channel", None)
            note = getattr(msg, "note", None)

            if channel == 9 and note == 35:
                bits += "1" if msg.velocity > 80 else "0"

print("[+] Bits extraídos:", len(bits))

# 3. Deshacer permutación
seed = int.from_bytes(hashlib.sha256(key).digest()[:4], "big")
rng = random.Random(seed)

indices = list(range(len(bits)))
rng.shuffle(indices)

payload_bits = ["0"] * len(bits)

for real_pos, shuffled_pos in enumerate(indices):
    payload_bits[real_pos] = bits[shuffled_pos]

payload_bits = "".join(payload_bits)

# 4. Reconstruir QR
size = int(payload_bits[:8], 2)

if size <= 0 or size > 100:
    raise ValueError(f"Tamaño QR inválido: {size}")

qr_bits = payload_bits[8:8 + size * size]

if len(qr_bits) != size * size:
    raise ValueError("No hay suficientes bits para reconstruir el QR")

img = Image.new("1", (size, size), 1)
pixels = img.load()

for i, bit in enumerate(qr_bits):
    x = i % size
    y = i // size
    pixels[x, y] = 0 if bit == "1" else 1

img = img.resize((size * 12, size * 12))
img.save("recovered_qr.png")

print("[+] QR reconstruido: recovered_qr.png")

# 5. Leer QR
qr_img = cv2.imread("recovered_qr.png")
detector = cv2.QRCodeDetector()
qr_payload, points, _ = detector.detectAndDecode(qr_img)

if not qr_payload:
    raise ValueError("No se pudo leer el QR automáticamente")

print("[+] QR payload:", qr_payload)

# 6. Descifrar AES
iv_hex, ct_hex = qr_payload.split(":")

iv = bytes.fromhex(iv_hex)
ct = bytes.fromhex(ct_hex)

cipher = AES.new(key, AES.MODE_CBC, iv)
flag = unpad(cipher.decrypt(ct), 16)

print("[+] Flag:")
print(flag.decode())
```

### 12. Flujo mental del jugador

Un jugador debería razonar así:

```
1. El archivo es MIDI, así que analizo eventos.
2. Encuentro patrones repetidos en ciertos canales.
3. El canal 2 parece codificar bytes mediante nibbles.
4. Reconstruyo una clave AES.
5. El canal 9 contiene muchos bits por velocity.
6. Los bits directos no forman nada válido.
7. La clave AES también sirve para deshacer el orden.
8. Reconstruyo una imagen QR.
9. El QR contiene IV y ciphertext.
10. Descifro AES-CBC.
11. Obtengo la flag.
```

### 13. Por qué es esteganografía musical

No se oculta texto dentro de metadatos, ni se añade un archivo al final. La información se distribuye dentro de eventos musicales aparentemente normales:

* intensidades de notas;
* canales MIDI;
* notas concretas;
* orden pseudoaleatorio;
* ruido de otras pistas.

El archivo puede sonar como una pieza normal, pero al analizar su estructura se revela el contenido oculto.

### 14. Conclusión

Este reto combina varias áreas:

* análisis de formato MIDI;
* esteganografía en eventos musicales;
* reconstrucción visual mediante QR;
* permutaciones pseudoaleatorias;
* criptografía simétrica AES-CBC.

La dificultad no está en una sola operación, sino en correlacionar todas las capas:

```
canal 2 → clave
canal 9 → bits
clave → seed de permutación
bits ordenados → QR
QR → IV + ciphertext
AES → flag
```

Esto convierte el reto en una cadena completa de razonamiento, no en una simple extracción directa.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://beafn28.gitbook.io/beafn28/mis-ctfs/requiem-cipher.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
