Traces - Cyber Apocalypse 2025

Traces - Cyber Apocalypse 2025

Introducción

El reto consistía en explotar una vulnerabilidad de reutilización de clave y nonce en AES-CTR, combinado con el uso de mensajes con prefijos conocidos (!nick <nickname>), lo que nos permitió aplicar la técnica clásica de crib dragging para obtener los mensajes originales y la flag.


Análisis del servicio

El servidor simula un pequeño sistema de chat IRC donde:

  • Cada usuario envía su primer mensaje de la forma !nick <nickname>.
  • Las conversaciones se cifran con AES-256 en modo CTR.
  • El servicio reutiliza el mismo keystream para todos los mensajes.

Dado que:

[\text{C} = \text{P} \oplus K]

Si obtenemos dos mensajes cifrados (C_0) y (C_1) de dos mensajes originales (P_0) y (P_1) cifrados con el mismo keystream (K):

[C_0 \oplus C_1 = P_0 \oplus P_1]

Esto nos permite obtener la XOR directa entre los textos planos.


Explotación - General Channel

Al conectarnos al canal #general observamos múltiples mensajes cifrados.

Sabemos que cada mensaje comienza con !nick <nickname>, por tanto, aplicando crib-dragging:

  1. Suponemos que el mensaje de Runeblight es !nick Runeblight.
  2. Calculamos el keystream parcial.
  3. Recuperamos automáticamente otros mensajes como:
1
2
3
4
!nick Doomfang
!nick Stormbane
... (conversación) ...
Here is the passphrase for our secure channel: %mi2gvHHCV5f_kcb=

Obtuvimos la contraseña para el canal secreto: %mi2gvHHCV5f_kcb=


Explotación - Secret Channel

Repetimos la misma estrategia dentro del canal #secret.

Aplicamos crib-dragging de nuevo para ir recuperando los mensajes hasta encontrar en claro la flag:

1
It is labeled as: HTB{Crib_Dragging_Saves_The_Day}

Técnica empleada

TécnicaDescripción
Reutilización de keystreamMismo keystream utilizado para cifrar múltiples mensajes
Crib draggingAprovechar conocimiento parcial del mensaje para descifrar XORs
AES-CTRModo de cifrado vulnerable a este ataque si se reutiliza el nonce

Nota final

El reto es un ejemplo clásico de por qué no se debe reutilizar nonce y clave en AES-CTR. Basta una pequeña pista conocida para desmoronar por completo la seguridad del sistema.

El script utilizado permite interactuar de forma dinámica con los ciphertexts aplicando crib-dragging hasta revelar todos los mensajes y obtener la flag.


Script completo

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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
#Genreal Channel
import binascii
from pwn import xor
from collections import defaultdict

# =============================================
# Configuración inicial con los mensajes reales
# =============================================

ciphertexts = [
    "598f7a3368a4c9537fd7a22da9a0",
    "598f7a3368a4de487fc8a92ea6a981",
    "598f7a3368a4df497edfa620aea08ccc",
    "2f84342666a4ea53649aa56ca9a293982220b07aafefd36af60c986090e5a44727e70cb55432baa9328796c82a19cb8a102da1007f384b7002160377320f84c6d790",
    "2d8f773571f7f9537fdeea6c8fa697982221a528abadde7ae742cc699bbcf6512ce50ee61574d6b82ec588c83319c9885f37a41b313c03621f5810607b1690c092d2a354bdfe56f75b2efe",
    "368e67707ae1f91030d8b138e78ec3d5762aa83fade6d571e50c9f6798a0f6572bf713b31b7ed6bf2fc78b892d0f80da7924f406373e5a3105531d763e5990c19e9eb542e9b352ff453fe11d9c26a0d0b25747462e10ab238acfbf4b1ce02ca0e55fe0580d0a",
    "2c897a2323e7e55d7ed4a120e7ae97983826b47abdecda7aa24a837ad5a9b94c22a214a7167985e266ec809c660f8e89472ba011377b577e565906777b0997dbc4dfb642e9ac54f74065",
    "3084613523edfe1c64d2a16cb7a697cb2621b23bbde89c79ed5ecc6780b7f65120e115b41f3295a427ce8b8d2d468edf5d2be61529136b522003155a301a878fe88ab47285af54c1676dae27",
    "3f8e67706af0a31c5fd4a835e7b48cd9242ce033baadcb76f644cc6780b7f64f2af114e60e6083bf32c581c82010c2935531fa",
    "2184607e23cbf84e30d6a53fb3e789d7202ce037aff49c77e35a892899a0b05665f612a7197785e266f780c82c09dd8e1020b152293e5168565512773e1f90de9c",
    "31c67e7060ece85f7bd3aa2be7a891ca7625af3dbdadc870a24e892886b0a44765ec0fe60e6097af23808a8e6113db881023b70636344d62564416683a108bc19c",
    "3384762023e9e81c65caa02db3a280967600a67abae5d966a24f8d7c96adf64d2bae40b11f359aa066c8849e245cda951023b7067f3d42620218",
    "31c67f3c23e7e25160dbb629e7b38cdd7625a12eabfec83fe64d9869d5b2bf562da20fb3083294ad25cb9098610cc29b5e6cf4253a7b4e6405425360291896d792dfac5ee9ad52ff436bb610dd27a082a3180f403414eb",
    "3187333575e1ff4564d2ad22a0e78dcb762aac3fafff903ff549cc659ab3b30231ed40b21277d6a223d891c83208cf9d556cf43d2a29037619571f25320ac5c5dbcaaa4ea7fe49fd4c28a95b",
    "308e7f3423ebe31230f3e321e7b481dd3f27a77abdf9ce7eec4b892886acb14c24ee13e61c6099a166cf909c3215ca9f1e6283177f364a761e4253673e5992d3c6ddaa42adf0",
    "2f84333362eaaa4830cea527a2e785d62f69b233bde6cf31a260897cd2b6f64e20e316a35a669ea5358086802012c09f5c62b6173934517456421b60225991c0d3dda907bcad15",
    "3986613566e0a31c5dd5b229e7a688d4763da136a5fe9c6bed0c986090e5a6502cf401b21f3284a329cdcbc81309c09f522ebd15372f0f31065a1664281cc5d1dedba355e9aa53fd0d27ae128e70ad95b45d49",
    "2d8f773571f7f9537fdeea6c8ee089983220b339a1e3d27ae158856692e5b84d32ac408f1c3282a423d9c580200acbda4327b11c7f2e503d56411625360c96c692daab54a8ae4bfd4c39e11c903da094af5913402a08eb",
    "598d763175e1",
    "598d763175e1",
    "598d763175e1"
]

# Pares conocidos (basados en los últimos mensajes idénticos)
known_pairs = [
    ("598d763175e1", b"!leave")  # Los 3 últimos mensajes son idénticos y probablemente son !leave
]

# =============================================
# Reconstrucción inteligente del Keystream
# =============================================

class KeystreamManager:
    def __init__(self):
        self.keystream = bytearray()
        self.known_positions = set()
    
    def add_known_pair(self, ct_hex, plaintext):
        """Añade un par conocido (cifrado, texto plano) al keystream"""
        ct = binascii.unhexlify(ct_hex)
        if len(ct) > len(self.keystream):
            # Extender el keystream con bytes nulos si es necesario
            self.keystream.extend(b'\x00' * (len(ct) - len(self.keystream)))
        
        # Calcular la porción del keystream
        new_ks = xor(ct, plaintext)
        
        # Actualizar el keystream y marcar posiciones conocidas
        for i in range(len(new_ks)):
            if i < len(plaintext):  # Solo para las posiciones donde tenemos texto plano
                self.keystream[i] = new_ks[i]
                self.known_positions.add(i)
    
    def add_crib(self, ct_hex, crib, offset):
        """Añade un crib (texto supuesto) al keystream"""
        ct = binascii.unhexlify(ct_hex)
        if offset + len(crib) > len(ct):
            print(f"Error: Crib demasiado largo (offset {offset}, len {len(crib)}, ciphertext len {len(ct)})")
            return
        
        # Calcular la porción del keystream
        new_ks = xor(ct[offset:offset+len(crib)], crib)
        
        # Extender el keystream si es necesario
        if len(self.keystream) < offset + len(new_ks):
            self.keystream.extend(b'\x00' * (offset + len(new_ks) - len(self.keystream)))
        
        # Actualizar el keystream
        for i in range(len(new_ks)):
            self.keystream[offset + i] = new_ks[i]
            self.known_positions.add(offset + i)
    
    def decrypt(self, ct_hex):
        """Descifra un mensaje usando el keystream actual"""
        ct = binascii.unhexlify(ct_hex)
        decrypted = bytearray()
        
        for i in range(len(ct)):
            if i < len(self.keystream) and i in self.known_positions:
                decrypted.append(ct[i] ^ self.keystream[i])
            else:
                decrypted.append(0x3F)  # '?' para bytes desconocidos
        
        try:
            return decrypted.decode('utf-8', errors='replace')
        except:
            return str(decrypted)
    
    def find_repetitions(self):
        """Busca repeticiones en el keystream para identificar patrones"""
        repetitions = defaultdict(list)
        for i, byte in enumerate(self.keystream):
            if byte != 0:  # Ignorar bytes no descubiertos
                repetitions[byte].append(i)
        return {k: v for k, v in repetitions.items() if len(v) > 1}

# =============================================
# Análisis inicial
# =============================================

ks_manager = KeystreamManager()

# 1. Añadir pares conocidos
for ct, pt in known_pairs:
    ks_manager.add_known_pair(ct, pt)

# 2. Analizar los primeros mensajes (probablemente comandos !nick)
ks_manager.add_crib("598f7a3368a4c9537fd7a22da9a0", b"!nick Doomfang", 0)
ks_manager.add_crib("598f7a3368a4de487fc8a92ea6a981", b"!nick Stormbane", 0)
ks_manager.add_crib("598f7a3368a4df497edfa620aea08ccc", b"!nick Runeblight", 0)
# =============================================
# Visualización mejorada de mensajes
# =============================================

def print_full_messages():
    print("\n=== MENSAJES COMPLETOS ===")
    print("(Los caracteres desconocidos se muestran como ?)\n")
    
    for idx, ct_hex in enumerate(ciphertexts):
        decrypted = ks_manager.decrypt(ct_hex)
        ct_length = len(binascii.unhexlify(ct_hex))
        known_bytes = sum(1 for i in range(ct_length) if i in ks_manager.known_positions)
        
        print(f"\n=== Mensaje {idx} ({known_bytes}/{ct_length} bytes conocidos) ===")
        print(f"Hex: {ct_hex[:12]}...{ct_hex[-12:]}")
        print("Texto completo:")
        print(decrypted)
        print("-" * 80)

# =============================================
# Descifrado interactivo mejorado
# =============================================

def interactive_decryption():
    print("\n=== DESCIFRADO INTERACTIVO MEJORADO ===")
    print("Instrucciones:")
    print("1. Revisa los mensajes completos mostrados")
    print("2. Cuando identifiques un fragmento legible, ingresa:")
    print("   - Número de mensaje")
    print("   - Offset donde comienza el texto conocido")
    print("   - El texto supuesto (crib)")
    print("3. Escribe 'quit' para salir\n")
    
    while True:
        print_full_messages()
        
        user_input = input("\nIngresa (mensaje offset crib) o 'quit': ").strip()
        if user_input.lower() == 'quit':
            break
        
        try:
            parts = user_input.split()
            msg_idx = int(parts[0])
            offset = int(parts[1])
            crib = ' '.join(parts[2:]).encode()
            
            if msg_idx < 0 or msg_idx >= len(ciphertexts):
                print("Error: Índice de mensaje inválido")
                continue
            
            ks_manager.add_crib(ciphertexts[msg_idx], crib, offset)
            print(f"\nCrib añadido: mensaje {msg_idx}, offset {offset}, texto '{crib.decode()}'")
        except Exception as e:
            print(f"\nError: {e}. Formato esperado: 'mensaje offset texto'")

# =============================================
# Ejecución principal
# =============================================

if __name__ == "__main__":
    # Mostrar estado inicial del keystream
    print("\n=== ESTADO INICIAL DEL KEYSTREAM ===")
    print(f"Longitud actual: {len(ks_manager.keystream)} bytes")
    print(f"Bytes conocidos: {len(ks_manager.known_positions)}")
    
    # Iniciar el modo interactivo
    interactive_decryption()
    
    # Mostrar estado final del keystream
    print("\n=== ESTADO FINAL DEL KEYSTREAM ===")
    print(f"Longitud: {len(ks_manager.keystream)} bytes")
    print(f"Bytes conocidos: {len(ks_manager.known_positions)}")
    print(f"Porcentaje conocido: {len(ks_manager.known_positions)/len(ks_manager.keystream)*100:.2f}%")
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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
#Secret Channel
import binascii
from pwn import xor
from collections import defaultdict

# =============================================
# Configuración inicial con los mensajes reales
# =============================================

ciphertexts = [
    "598f7a3368a4c9537fd7a22da9a0",
    "598f7a3368a4de487fc8a92ea6a981",
    "598f7a3368a4df497edfa620aea08ccc",
    "2f8433236bebf850749aaf29a2b7c4d7233be02aa2ecd271eb428b289da0a4476ba234ae1f3299b932c597c8291dc2964362b5003a7b4d7e02160060380c97d79e9ea349adfe4ff7426bac149329e595bf5d14053110b12787cfa4560be037b1e345a9581645d777e81667d5",
    "3986613566e0a31c44d2a16ca2a981d52f6eb37abdeed36af65fcc6f87aaa10228ed12a35a6293be35c9969c2412dad4100bb2522b334668565512713811c5d7c4dbac07a8fe4cf04438b1108f70aa96e65712576615a0378688be4d42e02ca9e352a94c1748d539e015629ebe2200036550263abe6bb38a150562ad27d15eb3acd6072373d69a0d713cd6f8fdf66f50023010449e2732cb7debd036ed1843b76b358dbb504cbb0eb94e721aaa279dd18f9fb1",
    "31c6653523e6e8597e9ab738b2a39dd1382ee02ea6e89c6bf04d8f6d86e5ba4723f640a41f7a9fa2228087916113db881032a61729324c6405161a6b38188bc6d3caab48a7ad17b84c25a5558e3fa895b2500e4b2151a3218a83a31e19b237afe105a9740b569977e80e6394ec28470d6a1e263efb72acd7153a66fe6ad748a9f898072227978508792597e0e6b3724d02221b01993a79ca60bccd66e61851f4703480a04c48f811f1407348e43b87d1df83f9e55bdb24227c9f4b21a44370b5b30b8fd30a37e182ac15c8e7633fa4147f812841f40ab0351d938adf0d4317cbc2ec28f348355e80d1221e54a53e731bf7eb88dace14d3e8d2ec4f1c7d4b5786e2dba266ad611ca6da13601da111200f6164ca61581307d62e507f0c8b1ad5e54594ed1894f8918698750c5e7914d8dc699c89f814f0de1b13c5e098bae7e3b86f98e6a98229579e685fb3b0006b920a0e25d2522decd318d7de",
    "31c67e7062e8ff5971debd6ca4b58bcb2564a332abeed776ec4bcc6780b7f65135e70caa0d7d84a766c182892812dd8e1036bc177f3a4d721f531d717b0b80d1ddcca654e7fe72fe0d3fa91c8e70a795a75b084b6606a437cf9fb14c1ae037a7a64ae71b1148dd7cff5a7195fd2b060c78533020ea32e0ed123e6bad2ccd43a3acc81a383cd1d8415c3e82acfcf5264b56631c52cc3571d066bdc666ea1247bb3f2ec5aa4c0daf00b949661eef7488948f9ef0a742cd696d29a2576eb5457fbcb305cde2423fb2c1a014deee6673a3157f832e46e806b03b5fd4c5da4c580709211734f90c6653c9d12c4b4eec2a7b1ff7b8dfd6dd09d3f6d1f90c1b7c4b548ce2c7b277ff6106e3d6092519ea111c0678748d704348",
    "2f84333362eae353649aa52aa1a896dc7621a529a7f9dd6beb438226d58cb00231ea09b55a7b85ec2780879a241dcd921c62a01a3a3503651e53534d321e8d92f1d1b749aab757bf5e6ba71a8f33a083e655065c6610a9368a8eb4474ea23de1e945a9540b56996dff1b7d97b06322146950753af67be0d758336be12fd759e7e1d11b2332dc93417d2483e0f1b3624d4d2e554e992632c161bfca34e15d53f6722acca64543f645ce442705ff279d949c83f1a347da6963689f0321b8496ff0be438fe8172cb2c3bd19dce5623fa214398a2946f910fe3a50c5808d4f4f068581e032f25c67538898305b58e2",
    "3d99723377e8f41230fbaa28e7a292dd3869a93ceefad93ff04981699cabf6572bf105a3143290a334808b8736508e8d5562ba173a3f03721958076c351e80dcd1c7e257a5bf55eb036b8813dd24ad95e67b08502812ac28cf89bf4c1aa93ea8e358a9520a579974ec1d7d98ff2f47006d4c2727fb6cb388152562ad29cb58abe898043820d2d6007d2893ffe6b3724d02371d44852632d77bb9cc28e3155ffb7b2983ef6642f812fc016f09fc31c9d5df9ffaa641c660227b920323b34b64a3f74ac9a70730e6d0b65ad4ed276bae147f9f355cf102ac2b11d484d9480a0a9881f038fe407058da",
    "2184607c23e6f84830cda16caab297cc763db23faff99c76f60c836699bcf64336a201e6167385b866d2809b2e0edad4100bb252283e037015421a733a0d8092dbcae253a6b11beb4224af59dd27a0d0b451144e6603a0328a8ebc5700a778a8f258a9571147d86de4157ad5be0a1342654d7522ff7ca5c8503627ec399e0d8fd8fa131421de943e5a3997ebf2fa68457d060d51803b7bd06ebfca29ea2267fe6b32f2844754872bf64f640dd5068cc18c89beb8",
    "3f8e7c342da4c35330c8a12fa8b58098392fe033baadd16af158cc6d8daca55665eb0ee60e7a93ec31d28c9c3519c0da442db9172c75035856411a69375980dcc1cbb042e9bf57f40d3fb3149e35b6d0a74a02052303a4378a8bfc1e0fae3ce1ef5fa9481645d575ad14718dfb314700691e263ef175a5ca153d61ad25d448a9e0c146771ad1d615762ed6e9fbf66b5b022603449e747ec16eb9cd35a41256b7762e81ef5548f812f04d6b48e2359fd1df82f0e55dcd672c678f032dbe4b64b3b20b",
    "3986613566e0a31c44d2a16caaa896dd763ea57aaae4cf7cf75f9f289cb1fa0231ea05e61d6093ad32c597c83514cbda422ba719717b666713440a25361688d7dccae250acfe5ffd412ab859dd24ad95e67b08502812ac28cf9ca44c0bae3fb5ee4ee7485e4dcd6aad1e719dfb2d14077f107519fb3eadd1462627ec29d00db4e3d7067731d2900e6c2ed6e3e0e126554b2d114e9b747dc22fa4d336eb0f44e27133d9b6024eb40aea447446",
    "31c67f3c23e7e25160dbb629e7b38cdd7625a12eabfec83fe64d9869d5b2bf562da20fb3083294ad25cb9098610cc29b5e6cf4253a7b4e6405425360291896d792dfac5ee9ad52ff436bb610dd27a082a3180f403414eb",
    "2f8433236bebf850749aa122a3e790d03f3ae037abe8c876ec4bcc699ba1f64f2af405e60e7dd6ad66cd8a9a245cdd9f5337a6177f28427f154206687559acd492caaa42a0ac1bf54c2ca406dd3fb7d0b5480e403551a4368acfb35201b331afe10be0555204cd71e8033496ff3a470b624a303cfd7bb0d0153d72ff6ad342b5e8cb467704d2d60c6b3882acfbfc720256221e44cc207ac57bebc02ee51353f2317ae1aa560dac0df052270aef749ddc9accf3a45ddc242e6c98502fb14f2ab9b905dbef0b2db2d2a31bdeee29",
    "308e7f3423ebe31230f3e321e7b481dd3f27a77abdf9ce7eec4b892886acb14c24ee13e61c6099a166cf909c3215ca9f1e6283177f364a761e4253673e5992d3c6ddaa42adf0",
    "598d763175e1",
    "598d763175e1",
    "598d763175e1",
    "368e67707ae1f91030d8b138e78ec3d5762aa83fade6d571e50c9f6798a0f6572bf713b31b7ed6bf2fc78b892d0f80da7924f406373e5a3105531d763e5990c19e9eb542e9b352ff453fe11d9c26a0d0b25747462e10ab238acfbf4b1ce02ca0e55fe0580d0a"
]
# Pares conocidos (basados en los últimos mensajes idénticos)
known_pairs = [
    ("598d763175e1", b"!leave")  # Los 3 últimos mensajes son idénticos y probablemente son !leave
]

# =============================================
# Reconstrucción inteligente del Keystream
# =============================================

class KeystreamManager:
    def __init__(self):
        self.keystream = bytearray()
        self.known_positions = set()
    
    def add_known_pair(self, ct_hex, plaintext):
        """Añade un par conocido (cifrado, texto plano) al keystream"""
        ct = binascii.unhexlify(ct_hex)
        if len(ct) > len(self.keystream):
            # Extender el keystream con bytes nulos si es necesario
            self.keystream.extend(b'\x00' * (len(ct) - len(self.keystream)))
        
        # Calcular la porción del keystream
        new_ks = xor(ct, plaintext)
        
        # Actualizar el keystream y marcar posiciones conocidas
        for i in range(len(new_ks)):
            if i < len(plaintext):  # Solo para las posiciones donde tenemos texto plano
                self.keystream[i] = new_ks[i]
                self.known_positions.add(i)
    
    def add_crib(self, ct_hex, crib, offset):
        """Añade un crib (texto supuesto) al keystream"""
        ct = binascii.unhexlify(ct_hex)
        if offset + len(crib) > len(ct):
            print(f"Error: Crib demasiado largo (offset {offset}, len {len(crib)}, ciphertext len {len(ct)})")
            return
        
        # Calcular la porción del keystream
        new_ks = xor(ct[offset:offset+len(crib)], crib)
        
        # Extender el keystream si es necesario
        if len(self.keystream) < offset + len(new_ks):
            self.keystream.extend(b'\x00' * (offset + len(new_ks) - len(self.keystream)))
        
        # Actualizar el keystream
        for i in range(len(new_ks)):
            self.keystream[offset + i] = new_ks[i]
            self.known_positions.add(offset + i)
    
    def decrypt(self, ct_hex):
        """Descifra un mensaje usando el keystream actual"""
        ct = binascii.unhexlify(ct_hex)
        decrypted = bytearray()
        
        for i in range(len(ct)):
            if i < len(self.keystream) and i in self.known_positions:
                decrypted.append(ct[i] ^ self.keystream[i])
            else:
                decrypted.append(0x3F)  # '?' para bytes desconocidos
        
        try:
            return decrypted.decode('utf-8', errors='replace')
        except:
            return str(decrypted)
    
    def find_repetitions(self):
        """Busca repeticiones en el keystream para identificar patrones"""
        repetitions = defaultdict(list)
        for i, byte in enumerate(self.keystream):
            if byte != 0:  # Ignorar bytes no descubiertos
                repetitions[byte].append(i)
        return {k: v for k, v in repetitions.items() if len(v) > 1}

# =============================================
# Análisis inicial
# =============================================

ks_manager = KeystreamManager()

# 1. Añadir pares conocidos
for ct, pt in known_pairs:
    ks_manager.add_known_pair(ct, pt)

# 2. Analizar los primeros mensajes (probablemente comandos !nick)
ks_manager.add_crib("598f7a3368a4c9537fd7a22da9a0", b"!nick Doomfang", 0)
ks_manager.add_crib("598f7a3368a4de487fc8a92ea6a981", b"!nick Stormbane", 0)
ks_manager.add_crib("598f7a3368a4df497edfa620aea08ccc", b"!nick Runeblight", 0)
ks_manager.add_crib("368e67707ae1f91030d8b138e78ec3d5762aa83fade6d571e50c9f6798a0f6572bf713b31b7ed6bf2fc78b892d0f80da7924f406373e5a3105531d763e5990c19e9eb542e9b352ff453fe11d9c26a0d0b25747462e10ab238acfbf4b1ce02ca0e55fe0580d0a", b"Not yet, but I'm checking some unusual signals. If they sense us, we might have to change our tactics.", 0)

# =============================================
# Visualización mejorada de mensajes
# =============================================

def print_full_messages():
    print("\n=== MENSAJES COMPLETOS ===")
    print("(Los caracteres desconocidos se muestran como ?)\n")
    
    for idx, ct_hex in enumerate(ciphertexts):
        decrypted = ks_manager.decrypt(ct_hex)
        ct_length = len(binascii.unhexlify(ct_hex))
        known_bytes = sum(1 for i in range(ct_length) if i in ks_manager.known_positions)
        
        print(f"\n=== Mensaje {idx} ({known_bytes}/{ct_length} bytes conocidos) ===")
        print(f"Hex: {ct_hex[:12]}...{ct_hex[-12:]}")
        print("Texto completo:")
        print(decrypted)
        print("-" * 80)

# =============================================
# Descifrado interactivo mejorado
# =============================================

def interactive_decryption():
    print("\n=== DESCIFRADO INTERACTIVO MEJORADO ===")
    print("Instrucciones:")
    print("1. Revisa los mensajes completos mostrados")
    print("2. Cuando identifiques un fragmento legible, ingresa:")
    print("   - Número de mensaje")
    print("   - Offset donde comienza el texto conocido")
    print("   - El texto supuesto (crib)")
    print("3. Escribe 'quit' para salir\n")
    
    while True:
        print_full_messages()
        
        user_input = input("\nIngresa (mensaje offset crib) o 'quit': ").strip()
        if user_input.lower() == 'quit':
            break
        
        try:
            parts = user_input.split()
            msg_idx = int(parts[0])
            offset = int(parts[1])
            crib = ' '.join(parts[2:]).encode()
            
            if msg_idx < 0 or msg_idx >= len(ciphertexts):
                print("Error: Índice de mensaje inválido")
                continue
            
            ks_manager.add_crib(ciphertexts[msg_idx], crib, offset)
            print(f"\nCrib añadido: mensaje {msg_idx}, offset {offset}, texto '{crib.decode()}'")
        except Exception as e:
            print(f"\nError: {e}. Formato esperado: 'mensaje offset texto'")

# =============================================
# Ejecución principal
# =============================================

if __name__ == "__main__":
    # Mostrar estado inicial del keystream
    print("\n=== ESTADO INICIAL DEL KEYSTREAM ===")
    print(f"Longitud actual: {len(ks_manager.keystream)} bytes")
    print(f"Bytes conocidos: {len(ks_manager.known_positions)}")
    
    # Iniciar el modo interactivo
    interactive_decryption()
    
    # Mostrar estado final del keystream
    print("\n=== ESTADO FINAL DEL KEYSTREAM ===")
    print(f"Longitud: {len(ks_manager.keystream)} bytes")
    print(f"Bytes conocidos: {len(ks_manager.known_positions)}")
    print(f"Porcentaje conocido: {len(ks_manager.known_positions)/len(ks_manager.keystream)*100:.2f}%")