Este blog es solo para fines educativos y de análisis técnico en CTFs. No promueve actividades maliciosas ni el uso indebido de herramientas descritas.
Entrada

Urgencia Espacial - NNCTF2025

Reto basado en la decodificación mediante un análisis de frecuencia sobre Reed Solomon.

Urgencia Espacial - NNCTF2025

Nombre del reto: Urgencia Espacial

Autor del reto: Kesero (Creado por mí)

Dificultad: Fácil

Enunciado

1
2
3
4
5
6
7
Hace unas horas, el equipo de comunicaciones terrestres detectó una transmisión urgente desde la Estación Espacial Internacional (ISS). Sin embargo, el mensaje llega corrupto e ilegible.

El sistema de la ISS tiene un fallo conocido: el mensaje se envía con ráfagas de ruido que corrompen partes de la información. Cada vez que intentamos recibirlo, obtenemos una versión diferente y distorsionada.

Tienes acceso a la consola de recepción y solo a un número limitado de intentos antes de que el canal se bloquee.

Necesitamos reconstruir el mensaje que la tripulación intenta enviarnos. Puede tratarse de una advertencia vital para la seguridad de la Tierra.

Archivos

1
server.py

Archivos utilizados aquí.

Analizando el reto

Al abrir el archivo nos encontramos lo siguiente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
#!/usr/bin/env python3
import os
import sys
import binascii
import secrets
from reedsolo import RSCodec

def banner():

    print(r'''                                                                                                                                               
                     .                                    
                   :#+                      =*.           
                  =@= .:.                 . .*%-          
                 +@: -%*.                 *#: -@=         
                -@- -@= .+=            :- .+@- -@-        
               .%* :@= .##:            :%*. +@: *%.       
               -@: *%  *%.    :*%%*-    .%*  %* :@-       
               =@. %* .@+    .%@@@@@:    =@. +@..@+       
               -@: ##  ##    .*@@@@#.    =@. +%..@+       
               .@= -@: -@=    .#%#%.    :@+ .%* :@-       
                +@. *%: -%=   -@:.@=   -%*  +@. *%.       
                .%#. *%= ..  .%*  +@.  :: .*%: =@-        
                 .##: -+.    +@:=*#@*     **. =@-         
                  .+%=      :@@*+-.-@:      :*%-          
                    :-      *@%+.   #%.     -+.           
                           -@-.+%*: .@=                   
                          .%*   .+%*:+@.                  
                          =@-.    .=##@*                  
                         .@**#+-.   .+@@:                 
                         *%  .=*%*=***+##.                
                        -@: .-+*##*%#=.:@=                
                       .%@+##*=:.  .-+#*%@.               
                       =@+-:          .:+@*               
                      .#=                -%.                                                                                              
    ''')

def print_confidential(remaining):
    print()
    print("╔══════════════════════════════════════════════════════════════╗")
    print("║                    C O M U N I C A C I Ó N                   ║")
    print("║                    C O N F I D E N C I A L                   ║")
    print("╠══════════════════════════════════════════════════════════════╣")
    print("║  Canal: ISS — Enlace Seguro                                  ║")
    print("║  Restricciones: Lectura Única — Protocolo Establecido        ║")
    print("║  Esta transmisión es CLASIFICADA.                            ║")
    print("╠══════════════════════════════════════════════════════════════╣")
    print("║                        I N T E R F A Z                       ║")
    print("╠══════════════════════════════════════════════════════════════╣")
    print("║  [1] Iniciar comunicación                                    ║")
    print("║  [2] Cerrar la comunicación                                  ║")
    print("╠══════════════════════════════════════════════════════════════╣")
    print(f"║  Intentos restantes: {remaining:<3}")
    print("╚══════════════════════════════════════════════════════════════╝")
    print("\n[!] Seleccione una opción: ", end="")

def print_main_menu(remaining):
    print()
    print("╔══════════════════════════════════════════════════════════════╗")
    print("║                        I N T E R F A Z                       ║")
    print("╠══════════════════════════════════════════════════════════════╣")
    print("║  [1] Reintentar comunicación                                 ║")
    print("║  [2] Cerrar la comunicación                                  ║")
    print("╠══════════════════════════════════════════════════════════════╣")
    print(f"║  Intentos restantes: {remaining:<3}")
    print("╚══════════════════════════════════════════════════════════════╝")
    print("\n[!] Seleccione una opción: ", end="")

FLAG = (
    b"REDACTED"
)

def apply_bursts(cw: bytes, bursts: int = 1) -> bytes:
    arr = bytearray(cw)
    for _ in range(bursts):
        start = secrets.randbelow(N)
        blen = secrets.randbelow(BURST_MAX - BURST_MIN + 1) + BURST_MIN
        end = min(N, start + blen)

        for i in range(start, end):
            arr[i] = secrets.randbelow(256)
    
    return bytes(arr)

N = 256
K = 254
NSYM = N - K
MAX_QUERIES = 32
BURST_MIN = 3
BURST_MAX = 6
BURST_PER_QUERY = 50

message = FLAG.ljust(K, b'\x00')
rsc = RSCodec(NSYM)
codeword = rsc.encode(message)

def main():

    queries = 0

    banner()
    print_confidential(MAX_QUERIES - queries)

    while True:
        cmd = input().strip().lower()

        if cmd == "2":
            print("\n[!] Cerrando canal...\n")
            break

        elif cmd == "1":
            if queries >= MAX_QUERIES:
                print("[!] No se permiten más intentos de comunicación, canal saturado.\n")
                break

            queries += 1
            corrupted = apply_bursts(codeword, BURST_PER_QUERY)

            print("\n--- TRANSMISION CORRUPTA RECIBIDA ---")
            print(binascii.hexlify(corrupted).decode())
            print(f"\n[!] Intentos restantes: {MAX_QUERIES - queries}")

            if queries < MAX_QUERIES:
                print_main_menu(MAX_QUERIES - queries)
            else:
                print("[!] No se permiten más intentos de comunicación, canal saturado.\n")
                break

        else:
            print("\n[!] Fallo crítico en el sistema.\n")
            exit()

if __name__ == "__main__":
    main()

El archivo server.py simula un sistema de comunicación con la ISS que transmite un mensaje secreto protegido con Reed-Solomon para añadir redundancia. Cada vez que el usuario solicita una transmisión, el servidor devuelve el mensaje corrupto aleatoriamente por ráfagas de ruido que sustituyen varios bytes en posiciones distintas, lo que hace que cada intento sea diferente.

Solución

Para resolver este ejercicio, tenemos que conectarnos al servidor y recibir todos los mensajes corruptos para posteriormente aplicar “votación por mayoría” en cada byte, obteniendo de esta manera el valor más frecuente entre todas las muestras.

Por último, debemos decodificar la cadena resultante con Reed-Solomon para obtener el mensaje en claro.

Una implementación en Python puede ser la siguiente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
from pwn import remote
import binascii
import collections
from reedsolo import RSCodec

def connect_to_server(host, port):
    io = remote(host, port)
    io.recvuntil("opción: ".encode())
    return io

def collect_samples(io, max_queries):
    samples = []
    for i in range(max_queries):
        io.sendline(b"1")
        io.recvuntil(b"--- TRANSMISION CORRUPTA RECIBIDA ---")
        io.recvline()
        hexline = io.recvline().strip().decode()

        cw = binascii.unhexlify(hexline)
        samples.append(cw)
        print(f"[+] Muestra {i+1}/{max_queries} recibida")

        if i < max_queries - 1:
            io.recvuntil("opción: ".encode())
    return samples

def majority_vote(samples, n):
    """Aplica votación por mayoria byte a byte para reconstruir la cadena original."""
    consensus = bytearray()
    for pos in range(n):
        counter = collections.Counter(s[pos] for s in samples)
        byte_common, count = counter.most_common(1)[0]
        consensus.append(byte_common)
        print(f"Pos {pos:3d}: Byte {byte_common:02x} (frecuencia: {count}/{len(samples)})")
    return consensus

def decode_reed_solomon(consensus, nsym):
    rsc = RSCodec(nsym)
    decoded = rsc.decode(consensus)
    message = decoded[0] if isinstance(decoded, tuple) else decoded
    return message.rstrip(b"\x00").decode(errors="ignore")

def main():

    N = 256
    K = 254
    NSYM = N - K
    MAX_QUERIES = 32
    HOST = "localhost"
    PORT = 5000

    io = connect_to_server(HOST, PORT)
    samples = collect_samples(io, MAX_QUERIES)
    io.close()
    print(f"\n[+] Recogidas {len(samples)} muestras")

    consensus = majority_vote(samples, N)
    print("\n[+] Cadena original reconstruida.")

    flag = decode_reed_solomon(consensus, NSYM)
    print("\n[!] Flag recuperada:", flag)

if __name__ == "__main__":
    main()

Al ejecutarlo obtenemos lo siguiente:

1
2
3
4
5
[+] Recogidas 32 muestras

[+] Cadena original reconstruida.

[!] Flag recuperada: Beeep beep bep... Sabemos que NavajaNegra2025 acaba de comenzar. Por motivos obvios, nuestro equipo no podra asistir en esta edicion. De todas formas, queremos enviaros un regalo muy espacial: nnctf{M4nd4dn0s_Un_p4r_d3_M1gu3l1tOs_3n_l4_Pr0x1ma_M1sioN!!}

Flag

nnctf{M4nd4dn0s_Un_p4r_d3_M1gu3l1tOs_3n_l4_Pr0x1ma_M1sioN!!}

Esta entrada está licenciada bajo CC BY 4.0 por el autor.