Urgencia Espacial - NNCTF2025
Reto basado en la decodificación mediante un análisis de frecuencia sobre Reed Solomon.
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!!}
