En este post la solución a un reto de criptografía que he visto en una competición CTF (del inglés, 'Capture the Flag'), modalidad 'on-line' y formato 'Jeopardy' en la que me entretuve participando.
La competición es 404 CTF y el reto es sobre enigma, la máquina electromecánica de cifrado utilizada por el ejercito alemán durante la Segunda Guerra Mundial y la más famosa de la historia, y a la que, como sabe cualquier lector de este blog, soy muy aficionado.
Además de que el reto no parece fácil, en mi opinión presenta un nivel de dificultad alto (★★★★☆), el hándicap que tuve para resolverlo fue el idioma en el que se desarrolló la competición y, en consecuencia, en el que estaba escrito éste y todos los demás retos, el francés.
Enunciado:
La traducción, con la ayuda de 'Google Translate', sería más o menos la siguiente:
“¡La siguiente misión es de una confidencialidad absoluta! Interceptamos un mensaje enviado por un miembro de Hallebarde, y encontramos la máquina utilizada, una máquina Enigma M3. ¡Descifre este mensaje, encuentre el nombre de su contacto y frustre los planes de nuestros enemigos!
Durante su investigación, descubre el concepto de 'índice de coincidencia', que le intriga particularmente...
Tiene dos réplicas de máquinas Enigma M3 a su disposición, una en Python, la otra en C ++.
La bandera es el nombre del contacto, no olvide agregar 404ctf {} alrededor de su nombre".
Solución: la primera pista que me dan para resolver el reto es que parece que voy a tener que utilizar el 'índice de coincidencia' (IC), es decir, utilizar la fuerza bruta para ir probando configuraciones de la máquina y, tras calcular el índice de coincidencia del resultado de descifrar el criptograma, quedarme con aquella que presente un valor mayor de éste.
Esto me suena al reto que ya puse en este blog, "Reto 43", en el que expliqué el método 'Ciphertext-only cryptanalysis of Enigma' propuesto por James Gillogly. En resumen, este método consiste en tres pasos, los siguientes:
1.- Obtener el IC mayor del descifrado del criptograma para todos los posibles órdenes de los rotores y, dentro de cada uno de ellos, todas las posiciones iniciales de cada rotor, fijando el ajuste de los anillos en A - A - A y con el tablero de conexiones sin ningún conector puesto.
2.- Para el orden de los rotores y a partir de la posición inicial de cada uno de ellos obtenidos en el paso anterior, obtener el IC mayor del descifrado del criptograma para cada ajuste del anillo posible del rotor derecho o rápido (de 'A' a 'Z'), fijando el ajuste de los anillos izquierdo y central (velocidad de giro lenta y media, respectivamente) en A - A y con el tablero de conexiones sin ningún conector puesto.
Este paso se repite, a partir de la mejor configuración del rotor derecho o rápido hallada, para el rotor central o medio, es decir, para el orden de los rotores y a partir de la posición inicial del rotor izquierdo o lento obtenidos en el Paso 1, y con el ajuste del anillo y posición inicial del rotor derecho o rápido hallados en el Paso 2, obtener el IC mayor del descifrado del criptograma para cada ajuste del anillo posible del rotor central o medio (de 'A' a 'Z'), fijando el ajuste del anillo izquierdo o lento en 1 (A) y con el tablero de conexiones sin ningún conector puesto.
3.- Y finalmente, asumiendo que son correctos los parámetros de configuración de la máquina hallados hasta el momento, obtener los mejores resultados (IC's mayores) para el descifrado del criptograma objeto de análisis, de forma separada, con cada uno de los pares de letras posibles que pueden ser conectadas entre sí en el tablero de conexiones. Es decir, primero con: A - B, después con A - C,..., y finalmente con Y - Z.
Decía yo en el reto que puse en este blog y al que he hecho referencia antes, "Reto 43", que los dos factores críticos para que el criptoanálisis de un criptograma mediante este método tenga éxito son, por una parte, que su tamaño sea suficientemente grande para que el IC sea significativo (el del reto tiene una longitud de 3.131 caracteres, algo totalmente inusual entre los enviados por los alemanes en la WWII, en los que la norma imponía un máximo de 300 caracteres) y, por otra parte, que los pares de letras conectados en el tablero de conexiones no sean demasiados.
Inicialmente, consideré que se cumplía la segunda de las condiciones o factores críticos de éxito mencionados en el párrafo anterior, ya que si hay bastantes o muchas conexiones establecidas en el tablero de clavijas (justo antes de la WWII los alemanes disponían de 6 cables con los que conectar entre sí pares de letras y durante la guerra ese número aumentó a 10) me temo que este método no será eficaz.
Dicho lo anterior, aunque desde el punto de vista de las probabilidades de éxito nos viene muy bien que el criptograma sea tan largo, esta circunstancia incrementa sustancialmente la potencia de cálculo necesaria para dar respuesta al primero de los pasos indicados, por lo que para acortar el tiempo de ejecución del primer script de python que voy a emplear limito la longitud del criptograma a 501 caracteres (ver script más adelante).
Bueno, pues todo ello fue la estrategia que pensé para resolver este reto, y digo "fue" porque antes de empezar a programar los scripts necesarios para realizar el criptoanálisis eché un vistazo a las réplicas de máquinas Enigma M3 que se proporcionan: una en Python y la otra en C ++, y vi que se facilita el ajuste de los anillos de cada uno de los 5 rotores que se pueden emplear en la configuración de la máquina:
Con lo que se simplifica un poco el número de pasos a dar y cambio la estrategia de criptoanálisis por la siguiente:
1.- Obtener el IC mayor del descifrado del criptograma (recordar que limité su longitud a 501 caracteres para acortar el tiempo de ejecución de este primer paso) para todos los posibles órdenes de los rotores y, dentro de cada uno de ellos, todas las posiciones iniciales de cada rotor, fijando el ajuste de los anillos que se proporciona en el reto para cada rotor a utilizar en el descifrado y con el tablero de conexiones sin ningún conector puesto.
2.- Con los parámetros de configuración de la máquina hallados en el punto anterior, obtener los mejores resultados (IC's mayores) para el descifrado del criptograma objeto de análisis, de forma separada, con cada uno de los pares de letras posibles que pueden ser conectadas entre sí en el tablero de conexiones. Es decir, primero con: A - B, después con A - C,..., y finalmente con Y - Z.
Para implementar esta estrategia utilizo el mismo excelente simulador de la máquina enigma que usé en el "Reto 43" de este blog:
1.- Script para el primer paso (los ficheros "rotores_izquierdos.txt", "rotores_centrales.txt" y "rotores_derechos.txt" contienen juntando las mismas filas de cada uno de ellos las variaciones sin repetición de los 5 rotores tomados de 3 en 3 o lo que es lo mismo todas las posibilidades de alojar 3 de los 5 rotores en las 3 ranuras dispuestas en la máquina para ello, es decir, 5!/(5-3)! = 60, mientras que el fichero "posiciones_iniciales.txt" contiene todas las variaciones con repetición de las 26 letras del alfabeto tomados de 3 en 3 o lo que es lo mismo todas las posibles posiciones iniciales de los 3 rotores que se utilicen en el cifrado, es decir, 263 = 17.576):
import enigmaM3
alfabeto=['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z']
reflectores=['B','C']
rotores=['I','II','III','IV','V']
ajustes_anillos=['Q','E','V','J','Z']
tablero_conexiones=''
DEBUG = 0
criptograma=open('chiffre.txt').read()[:501]
print('[+] Criptograma:',criptograma)
print('*** FUERZA BRUTA ROTORES UTILIZADOS, SU ORDEN Y POSICIÓN INICIAL DE LOS MISMOS')
for reflector in reflectores:
print('[+] Reflector:',reflector)
f_rotor_izquierdo=open("rotores_izquierdos.txt")
f_rotor_central=open("rotores_centrales.txt")
f_rotor_derecho= open("rotores_derechos.txt")
for rotor_izquierdo in f_rotor_izquierdo:
rotor_central=f_rotor_central.readline()
rotor_derecho=f_rotor_derecho.readline()
print('[+] Rotor izquierdo o lento:',rotor_izquierdo.rstrip('\n'),'; rotor central o medio:',rotor_central.rstrip('\n'),'; rotor derecho o rápido:',rotor_derecho.rstrip('\n'))
ajustes_anillos_rotores=ajustes_anillos[rotores.index(rotor_izquierdo.rstrip('\n'))] + ajustes_anillos[rotores.index(rotor_central.rstrip('\n'))] + ajustes_anillos[rotores.index(rotor_derecho.rstrip('\n'))]
print('[+] Ajustes de los anillos rotores:',ajustes_anillos_rotores)
f_posiciones_iniciales=open("posiciones_iniciales.txt")
for posiciones_iniciales_rotores in f_posiciones_iniciales:
c=enigmaM3.Enigma(reflector,rotor_izquierdo.rstrip('\n'), rotor_central.rstrip('\n'), rotor_derecho.rstrip('\n'), tablero_conexiones, ajustes_anillos_rotores, posiciones_iniciales_rotores.rstrip('\n'), DEBUG)
texto_claro=''
for i in range(len(criptograma)):
t = c.encode(criptograma[i])
texto_claro=texto_claro+t
IC=0
pares_letras_posibles=len(texto_claro)*(len(texto_claro)-1)/2
for i in alfabeto:
IC+=texto_claro.count(i)*(texto_claro.count(i)-1)/2/pares_letras_posibles
if IC>0.05:
print('[+] Posiciones iniciales de los rotores:',posiciones_iniciales_rotores.rstrip('\n'))
print('[+] IC:',IC)
print('[+] Texto_claro=',texto_claro)
f_posiciones_iniciales.close()
f_rotor_derecho.close()
f_rotor_central.close()
f_rotor_izquierdo.close()
Tras ejecutarlo se obtiene:
Con lo que se ve que el mayor IC se corresponde con:
- Reflector: C
- Rotores: V, II, IV
- Ajuste de los anillos de los rotores: Z, E, J
- Posiciones iniciales de los rotores: P, A, X
Y, por tanto, asumo que estos parámetros forma parte de la configuración de la máquina empleada para cifrar el mensaje.
2.- Script para el segundo paso (el fichero "tablero_conexiones.txt" contiene las 325 posibles conexiones de 2 letras en el tablero de clavijas):
import enigmaM3
alfabeto=['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z']
DEBUG = 0
criptograma=open('chiffre.txt').read().strip('\n')
print('[+] Criptograma:',criptograma)
print('[+] Reflector:','C')
print('[+] Rotor izquierdo o lento: V; rotor central o medio: II; rotor derecho o rápido: IV')
print('[+] Ajuste de los anillos los rotores: Z, E, J')
print('[+] Posición inicial de los rotores: P, A, X')
print('*** FUERZA BRUTA TABLERO DE CONEXIONES')
f_tablero_conexiones=open("tablero_conexiones.txt")
for tablero_conexiones in f_tablero_conexiones:
c=enigmaM3.Enigma('C', 'V', 'II', 'IV', tablero_conexiones.rstrip('\n'), 'ZEJ', 'PAX', DEBUG)
texto_claro=''
for i in range(len(criptograma)):
t = c.encode(criptograma[i])
texto_claro=texto_claro+t
IC=0
pares_letras_posibles=len(texto_claro)*(len(texto_claro)-1)/2
for i in alfabeto:
IC+=texto_claro.count(i)*(texto_claro.count(i)-1)/2/pares_letras_posibles
if IC>0.058:
print('[+] Tablero conexiones:',tablero_conexiones.rstrip('\n'))
print('[+] IC =',IC)
print('[+] Texto_claro=',texto_claro)
f_tablero_conexiones.close()
Tras ejecutarlo se obtiene:
Con lo que se ve que el mayor IC se corresponde con los parámetros hallados en el paso anterior más la conexión de las letras "R" y "V" en el tablero de clavijas. Además, si nos fijamos en el texto en claro ya se pueden ver ciertas palabras en francés e incluso ir adivinando otras.
Lamentablemente, con este método no conseguí obtener más conexiones en el tablero de clavijas, por lo que me tuve que conformar con el texto en claro parcialmente descifrado para conseguir la 'flag', y tras la inestimable ayuda de 'Google Translate' y gran cantidad de imaginación llegué a la conclusión de que ésta era: 404CTF{OISEAUDEMALHEUR}, lo que resultó ser correcto.
Comentarios
Publicar un comentario