Farfour Post Quantom
17 minutos de lectura
Se nos proporciona el código fuente en Python del servidor:
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import hashlib
from os import urandom
from random import SystemRandom
from sympy import GF
from sympy.polys.matrices import DomainMatrix
import json
from hashlib import md5
random=SystemRandom()
shuffle=random.shuffle
randint=random.randint
randrange=random.randrange
uniform = lambda: randrange(257//2) - 257//2
P=GF(257)
secret=open("Secret.txt",'rb').read()
assert len(secret)==16
flag=open("flag.txt","rb").read()
def encrypt_flag(secret):
key = hashlib.sha256(secret).digest()[-16:]
iv = urandom(16)
cipher = AES.new(key, AES.MODE_CBC, iv)
enc_flag=cipher.encrypt(pad(flag,16))
return json.dumps({"encrypted_flag":enc_flag.hex(),"IV":iv.hex()})
def get_hint(vec,mat):
res=(Pub_matrix*vec).to_Matrix()
res=[int(res[i,0])%257 for i in range(16)]
shuffle(res)
return json.dumps({"hint":[i for i in res]})
def get_secret(secret,Secret_matrix):
secret_vetor=DomainMatrix([[P(int(i))] for i in secret],(16,1),P)
public_vector=(Secret_matrix*secret_vetor).to_Matrix()
return json.dumps({"secret":[int(public_vector[i,0]) for i in range(16)]})
while True:
g=randint(2,257)
print(json.dumps({"G":int(g)}))
Secret_matrix=[[uniform() for i in range(16)] for j in range(16)]
Pub_matrix=DomainMatrix([[P((pow(g,randint(2,256),257))*i) for i in j] for j in Secret_matrix],(16,16),P)
Secret_matrix=DomainMatrix(Secret_matrix,(16,16),P) # to make it easier for you <3
while True:
try:
choice=json.loads(input("Choose an option\n"))
if("option" not in choice):
print("You need to choose an option")
elif choice["option"]=="get_flag":
print(encrypt_flag(secret))
elif(choice["option"]=="get_hint") and "vector" in choice:
assert len(choice["vector"])==16
vec=[[P(int(i))] for i in choice["vector"]]
input_vector=DomainMatrix(vec,(16,1),P)
print(get_hint(input_vector,Pub_matrix))
elif choice["option"]=="get_secret":
print(get_secret(secret,Secret_matrix))
elif choice["option"]=="reset_connection":
g=randint(2,257)
print(json.dumps({"G":int(g)}))
Secret_matrix=[[uniform() for i in range(16)] for j in range(16)]
Pub_matrix=DomainMatrix([[P((pow(g,randint(2,256),257))*i) for i in j] for j in Secret_matrix],(16,16),P)
Secret_matrix=DomainMatrix(Secret_matrix,(16,16),P)
else:
print("Nothing that we have Right now ")
except:
print("dont try something stupid")
exit(1)
break
Por otro lado, hay dos archivos externos que no tenemos (Secret.txt
y flag.txt
):
secret=open("Secret.txt",'rb').read()
assert len(secret)==16
flag=open("flag.txt","rb").read()
Análisis del código fuente
En primer lugar, hay algunas definiciones:
random=SystemRandom()
shuffle=random.shuffle
randint=random.randint
randrange=random.randrange
uniform = lambda: randrange(257//2) - 257//2
P=GF(257)
Nótese queuniform
es una función que devuelve valores aleatorios dentro del intervalo $[-128, -1]$:
$ python3 -q
>>> from random import SystemRandom
>>>
>>> random=SystemRandom()
>>> randrange=random.randrange
>>> uniform = lambda: randrange(257//2) - 257//2
>>>
>>> min(set(uniform() for _ in range(10000)))
-128
>>> max(set(uniform() for _ in range(10000)))
-1
Además, estaremos trabajando en $\mathbb{F}_{257}$ (GF(257)
), por lo que los valores de uniform
estarán en realidad en $[129, 256]$:
>>> min(set(uniform() % 257 for _ in range(10000)))
129
>>> max(set(uniform() % 257 for _ in range(10000)))
256
El servidor genera estos objetos criptográficos:
while True:
g=randint(2,257)
print(json.dumps({"G":int(g)}))
Secret_matrix=[[uniform() for i in range(16)] for j in range(16)]
Pub_matrix=DomainMatrix([[P((pow(g,randint(2,256),257))*i) for i in j] for j in Secret_matrix],(16,16),P)
Secret_matrix=DomainMatrix(Secret_matrix,(16,16),P) # to make it easier for you <3
El valor $g \in [2, 257]$ se usa para generar Pub_matrix
. Nótese que Secret_matrix
es una matriz de $16 \times 16$ generada mediante uniform
(entonces todas las entradas estarán dentro de $[129, 256]$). Luego, las entradas de Pub_matrix
son las entradas de Secret_matrix
, cada una de ellas multiplicada por $g^r$, donde $r$ es un entero aleatorio entre $2$ y $256$ (Diferente para cada entrada).
Opciones
El servidor nos permite usar cuatro opciones:
get_flag
:
elif choice["option"]=="get_flag":
print(encrypt_flag(secret))
Esta usa el secreto de 16 bytes en Secret.txt
como clave de AES para cifrar la flag (almacenada en flag.txt
):
def encrypt_flag(secret):
key = hashlib.sha256(secret).digest()[-16:]
iv = urandom(16)
cipher = AES.new(key, AES.MODE_CBC, iv)
enc_flag=cipher.encrypt(pad(flag,16))
return json.dumps({"encrypted_flag":enc_flag.hex(),"IV":iv.hex()})
get_secret
:
elif choice["option"]=="get_secret":
print(get_secret(secret,Secret_matrix))
En esta se multiplica Secret_matrix
por el secreto de 16 bytes como vector columna. Se nos devuelve el resultado:
def get_secret(secret,Secret_matrix):
secret_vetor=DomainMatrix([[P(int(i))] for i in secret],(16,1),P)
public_vector=(Secret_matrix*secret_vetor).to_Matrix()
return json.dumps({"secret":[int(public_vector[i,0]) for i in range(16)]})
get_hint
:
elif(choice["option"]=="get_hint") and "vector" in choice:
assert len(choice["vector"])==16
vec=[[P(int(i))] for i in choice["vector"]]
input_vector=DomainMatrix(vec,(16,1),P)
print(get_hint(input_vector,Pub_matrix))
Aquí podemos enviar vectores columna arbitrarios para multiplicarlos con Pub_matrix
. Desafortunadamente, el vector de resultado (res
) está desordenado…
def get_hint(vec,mat):
res=(Pub_matrix*vec).to_Matrix()
res=[int(res[i,0])%257 for i in range(16)]
shuffle(res)
return json.dumps({"hint":[i for i in res]})
reset_connection
:
elif choice["option"]=="reset_connection":
g=randint(2,257)
print(json.dumps({"G":int(g)}))
Secret_matrix=[[uniform() for i in range(16)] for j in range(16)]
Pub_matrix=DomainMatrix([[P((pow(g,randint(2,256),257))*i) for i in j] for j in Secret_matrix],(16,16),P)
Secret_matrix=DomainMatrix(Secret_matrix,(16,16),P)
Este nos permite regenerar el valor de $g$ (y también actualizar Secret_matrix
y Pub_matrix
).
Estrategia de ataque
Veamos cómo podemos usar todas las opciones para encontrar Pub_matrix
, luego Secret_matrix
y finalmente secret
(Secret.txt
). Una vez que tengamos este último valor, podremos descifrar la flag con AES.
Hallando Pub_matrix
Está claro que necesitaremos usar la opción get_hint
, porque nos permite multiplicar Pub_matrix
por un vector arbitrario. Por ejemplo, podemos multiplicar Pub_matrix
($(p_{i,j})_{16 \times 16}$) por un vector que tenga un $1$ en la primera coordenada y el resto sea $0$:
$$ \begin{pmatrix} \vdots & \vdots & \cdots & \vdots \\\\ p_{i,1} & p_{i,2} & \dots & p_{i,16} \\\\ \vdots & \vdots & \cdots & \vdots \end{pmatrix} \cdot \begin{pmatrix} 1 \\\\ 0 \\\\ 0 \\\\ \vdots \\\\ 0 \end{pmatrix} = \begin{pmatrix} p_{1,1} \\\\ p_{2,1} \\\\ \vdots \\\\ p_{16,1} \end{pmatrix} \overset{\text{shuffle}}{\longrightarrow} \begin{pmatrix} p_{?,1} \\\\ p_{?,1} \\\\ \vdots \\\\ p_{?,1} \end{pmatrix} $$
El problema es que el vector de resultado no estará ordenado, por lo que no sabremos las posiciones de las filas.
Lo único que podemos hacer es obtener filas completas con la siguiente estrategia:
- Enviar el vector $(1, 0, \dots, 0)$. Recibiremos los valores de la primera columna de
Pub_matrix
. - Enviar el vector $(0, 1, 0, \dots, 0)$. Recibiremos los valores de la segunda columna de
Pub_matrix
. - Enviar el vector $(1, 1, 0, \dots, 0)$. Recibiremos la suma de los valores de la primera y segunda columna que estén en la misma fila:
$$ \begin{pmatrix} \vdots & \vdots & \cdots & \vdots \\\\ p_{i,1} & p_{i,2} & \dots & p_{i,16} \\\\ \vdots & \vdots & \cdots & \vdots \end{pmatrix} \cdot \begin{pmatrix} 1 \\\\ 1 \\\\ 0 \\\\ \vdots \\\\ 0 \end{pmatrix} = \begin{pmatrix} p_{1,1} + p_{1,2} \\\\ p_{2,1} + p_{2,2} \\\\ \vdots \\\\ p_{16,1} + p_{16,2} \end{pmatrix} \overset{\text{shuffle}}{\longrightarrow} \begin{pmatrix} p_{?,1} + p_{?,2} \\\\ p_{?,1} + p_{?,2} \\\\ \vdots \\\\ p_{?,1} + p_{?,2} \end{pmatrix} $$
- Podemos hacer una consulta similar con el vector $(1, -1, 0, \dots, 0)$:
$$ \begin{pmatrix} \vdots & \vdots & \cdots & \vdots \\\\ p_{i,1} & p_{i,2} & \dots & p_{i,16} \\\\ \vdots & \vdots & \cdots & \vdots \end{pmatrix} \cdot \begin{pmatrix} 1 \\\\ -1 \\\\ 0 \\\\ \vdots \\\\ 0 \end{pmatrix} = \begin{pmatrix} p_{1,1} - p_{1,2} \\\\ p_{2,1} - p_{2,2} \\\\ \vdots \\\\ p_{16,1} - p_{16,2} \end{pmatrix} \overset{\text{shuffle}}{\longrightarrow} \begin{pmatrix} p_{?,1} - p_{?,2} \\\\ p_{?,1} - p_{?,2} \\\\ \vdots \\\\ p_{?,1} - p_{?,2} \end{pmatrix} $$
La idea es tomar los valores del paso 1. y 2. y luego encontrar pares de valores cuya suma está en el conjunto recibido en el paso 3. y cuya diferencia está en el conjunto del paso 4. Como resultado, obtendremos los primeros dos elementos de cada fila de Pub_matrix
.
En Python, podemos usar esta función auxiliar:
def get_hint(vec):
io.sendlineafter(b'Choose an option\n', json.dumps({'option': 'get_hint', 'vector': vec}).encode())
return json.loads(io.recvline().decode()).get('hint')
Luego, podemos realizar el procedimiento anterior de la siguiente manera:
Pub_matrix_prog.status('Querying...')
hint10 = get_hint([1, 0] + [0] * 14)
hint01 = get_hint([0, 1] + [0] * 14)
hint11 = get_hint([1, 1] + [0] * 14)
hint1_1 = get_hint([1, -1] + [0] * 14)
rows = [[] for _ in range(16)]
i = 0
for a in hint10:
for b in hint01:
if (a + b) % 257 in hint11 and (a - b) % 257 in hint1_1:
rows[i].append(a)
rows[i].append(b)
i += 1
break
if not all(len(row) == 2 for row in rows):
Pub_matrix_prog.status('Error')
return [], []
Una vez que tenemos esto, podemos comenzar a buscar el tercer elemento de cada fila usando el vector $(0, 0, 1, 0, \dots, 0)$. Y para asegurar, también podemos emplear los vectores $(1, 1, 1, 0, \dots, 0)$ y $(0, 1, 1, 0, \dots, 0)$:
hint001 = get_hint([0, 0, 1] + [0] * 13)
hint111 = get_hint([1, 1, 1] + [0] * 13)
hint011 = get_hint([0, 1, 1] + [0] * 13)
for row in rows:
for b in hint001:
if (sum(row) + b) % 257 in hint111 and \
(sum(row[1:]) + b) % 257 in hint011:
row.append(b)
break
if not all(len(row) == 3 for row in rows):
Pub_matrix_prog.status('Error')
return [], []
Continuamos con la cuarta, la quinta, y así sucesivamente hasta la última. Para cada uno de ellos, se agrega una verificación más.
Aunque las consultas y las comprobaciones se pueden automatizar, me pareció más sencillo escribir todas las verificaciones a mano, solo para mantener el control sobre lo que estamos haciendo. Como ejemplo, estas son las consultas y verificaciones para el último elemento de cada fila:
hint0000000000000001 = get_hint([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1])
hint1111111111111111 = get_hint([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
hint0111111111111111 = get_hint([0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
hint0011111111111111 = get_hint([0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
hint0001111111111111 = get_hint([0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
hint0000111111111111 = get_hint([0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
hint0000011111111111 = get_hint([0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
hint0000001111111111 = get_hint([0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
hint0000000111111111 = get_hint([0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1])
hint0000000011111111 = get_hint([0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1])
hint0000000001111111 = get_hint([0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1])
hint0000000000111111 = get_hint([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1])
hint0000000000011111 = get_hint([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1])
hint0000000000001111 = get_hint([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1])
hint0000000000000111 = get_hint([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1])
hint0000000000000011 = get_hint([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1])
for row in rows:
for b in hint0000000000000001:
if (sum(row) + b) % 257 in hint1111111111111111 and \
(sum(row[1:]) + b) % 257 in hint0111111111111111 and \
(sum(row[2:]) + b) % 257 in hint0011111111111111 and \
(sum(row[3:]) + b) % 257 in hint0001111111111111 and \
(sum(row[4:]) + b) % 257 in hint0000111111111111 and \
(sum(row[5:]) + b) % 257 in hint0000011111111111 and \
(sum(row[6:]) + b) % 257 in hint0000001111111111 and \
(sum(row[7:]) + b) % 257 in hint0000000111111111 and \
(sum(row[8:]) + b) % 257 in hint0000000011111111 and \
(sum(row[9:]) + b) % 257 in hint0000000001111111 and \
(sum(row[10:]) + b) % 257 in hint0000000000111111 and \
(sum(row[11:]) + b) % 257 in hint0000000000011111 and \
(sum(row[12:]) + b) % 257 in hint0000000000001111 and \
(sum(row[13:]) + b) % 257 in hint0000000000000111 and \
(sum(row[14:]) + b) % 257 in hint0000000000000011:
row.append(b)
break
if not all(len(row) == 16 for row in rows):
Pub_matrix_prog.status('Error')
return [], []
Parece horrible, pero créeme, es más fácil de implementar y de entender (al menos para mí).
En este punto, tenemos las filas de Pub_matrix
(pero todavía no sabemos su posición).
Hallando Secret_matrix
Necesitamos usar el hecho de que Pub_matrix
depende de Secret_matrix
y de $g$. Dado que podemos usar cualquier valor de $g$ (necesitaremos usar reset_connection
varias veces hasta que obtengamos el valor que queremos), Forzaremos que $g$ sea $g = 256 \equiv -1 \pmod{257}$. Con esto, tenemos que $g^r \mod{257} = \pm 1$, para cualquier entero $r$.
Por consiguiente, tener las filas de Pub_matrix
nos dará instantáneamente las filas de Secret_matrix
, dado que estas fueron generados por uniform
($[129, 256]$). Entonces, si un elemento de Pub_matrix
es menor que $129$, habrá sido multiplicado por $-1$ y solo necesitamos calcular un offset.
En Python, podemos hacer lo siguiente:
def get_data():
g = 0
while g != 256:
io.sendlineafter(b'Choose an option\n', b'{"option":"reset_connection"}')
g = json.loads(io.recvline().decode()).get('G')
g_prog.status(str(g))
Pub_matrix_prog.status('Querying...')
# ...
secret_rows = [[i if i >= 129 else 257 - i for i in j] for j in rows]
# ...
Pero tenemos el mismo problema que antes… no sabemos la posición de las filas.
Además, debemos considerar un problema cuando hay un elemento repetido en la primera columna. El algoritmo presentado solo funciona cuando todas las entradas en la primera columna son diferentes. Si no, terminaremos con filas repetidas. Por lo tanto, podemos verificar si el rango de la matriz es máximo o no.
Diferentes enfoques
Ahora debemos usar get_secret
para encontrar de alguna manera el vector secret
teniendo las filas de Secret_matrix
.
Fuerza bruta
Podríamos pensar en hacer fuerza bruta sobre las posibles permutaciones de las filas. Sin embargo, hay un total de $16!$ posibilidades ($2^{44} < 16! < 2^{45}$):
$ python3 -q
>>> from math import log2, prod
>>> prod(range(1, 16 + 1))
20922789888000
>>> log2(prod(range(1, 16 + 1)))
44.25014046988262
>>> 2 ** 44 < prod(range(1, 16 + 1)) < 2 ** 45
True
También, aplicar fuerza bruta al vector secret
también es malo ya que tendremos que probar $256^{16} = 2^{128}$ vectores posibles.
Técnicas de reducción de retículo
No hay una forma clara de encontrar la posición de cada fila. No importa cómo se organice el vector en get_hint
, Dado que el resultado se baraja, no hay certeza en el número de fila.
Todavía hay algo que sabemos de Secret_matrix
($(a_{i,j})_{16 \times 16}$) y el vector secret
($s_{i}$):
$$ \begin{pmatrix} \vdots & \vdots & \cdots & \vdots \\\\ a_{?,1} & a_{?,2} & \dots & a_{?,16} \\\\ \vdots & \vdots & \cdots & \vdots \end{pmatrix} \cdot \begin{pmatrix} s_1 \\\\ s_2 \\\\ \vdots \\\\ s_{16} \end{pmatrix} = \begin{pmatrix} b_1 \\\\ b_2 \\\\ \vdots \\\\ b_{16} \end{pmatrix} $$
Sea $A_j = \displaystyle\sum_j a_{?,j}$ y $b = \displaystyle\sum_i b_{i}$. Con esto tenemos:
$$ \begin{pmatrix} A_{1} & A_{2} & \dots & A_{16} \end{pmatrix} \cdot \begin{pmatrix} s_1 \\\\ s_2 \\\\ \vdots \\\\ s_{16} \end{pmatrix} = b $$
Así, nos hemos librado de los desordenamientos. La ecuación anterior es válida para el vector secret
. En Python, estas son las últimas instrucciones de la función get_data
(verificamos el rango aquí también):
io.sendlineafter(b'Choose an option\n', b'{"option":"get_secret"}')
secret_vector = json.loads(io.recvline().decode()).get('secret')
A = Matrix(F, secret_rows)
if A.rank() == 16:
return [sum(c) % 257 for c in A.columns()], sum(secret_vector) % 257
Pub_matrix_prog.status('Rank error')
return [], []
La fórmula me recordó a un problema de knapsack, por lo que intenté crear un retículo y aplicar LLL para encontrar un vector corto, que sería presumiblemente el vector secret
(similar a Infinite Knapsack). Pero parece que las condiciones no son adecuadas para resolver el Shortest Vector Problem con este enfoque.
Sistema de ecuaciones
Entonces, está claro que necesitamos más precisión. Esto es algo directamente proporcional a la cantidad de información que tenemos. Obsérvese que podemos continuar usando reset_connection
hasta que $g = 256$, luego hallar Pub_matrix
y Secret_matrix
y usar get_secret
de nuevo. El resultado será otra ecuación como la anterior.
Si iteramos el proceso hasta que tengamos $16$ ecuaciones, podremos resolver un sistema de ecuaciones en las entradas del vector secret
(nótese que Secret.txt
es siempre el mismo).
Con esto, hemos encontrado una estrategia ganadora:
def main():
io.sendlineafter(b'Choose an option\n', b'{"option":"get_flag"}')
data = json.loads(io.recvline().decode())
enc_flag, iv = map(bytes.fromhex, [data.get('encrypted_flag'), data.get('IV')])
A, b = [], []
while len(b) < 16:
A_i, b_i = get_data()
if A_i and b_i:
A.append(A_i)
b.append(b_i)
log.success(f'{len(b)} -> {A_i = }; {b_i = }')
g_prog.success()
Pub_matrix_prog.success()
secret_vector = Matrix(F, A).solve_right(vector(F, b))
secret = bytes(secret_vector)
log.info(f'Secret: {secret.decode()}')
Una vez que tenemos el vector secret
, podemos simplemente descifrar la flag:
key = sha256(secret).digest()[-16:]
cipher = AES.new(key, AES.MODE_CBC, iv)
flag = unpad(cipher.decrypt(enc_flag), AES.block_size)
log.success(f'Flag: {flag.decode()}')
Podemos intentar resolverlo en local:
$ echo 'Securinets{f4k3_fl4g_f0r_t3st1ng}' > flag.txt
$ echo -n '0123456789abcdef' > Secret.txt
$ python3 solve.py
[+] g: Done
[+] Pub_matrix: Done
[+] Starting local process '/usr/bin/python3': pid 600334
[+] 1 -> A_i = [84, 158, 178, 47, 78, 152, 184, 238, 189, 148, 111, 197, 99, 131, 86, 217]; b_i = 141
[+] 2 -> A_i = [21, 139, 244, 66, 179, 150, 128, 13, 14, 49, 194, 158, 97, 169, 84, 197]; b_i = 137
[+] 3 -> A_i = [76, 57, 163, 92, 169, 40, 4, 204, 61, 51, 91, 113, 87, 122, 83, 247]; b_i = 166
[+] 4 -> A_i = [121, 116, 9, 158, 102, 198, 116, 30, 97, 178, 243, 29, 226, 60, 35, 142]; b_i = 255
[+] 5 -> A_i = [125, 198, 162, 176, 105, 0, 10, 144, 178, 226, 32, 172, 196, 203, 173, 23]; b_i = 228
[+] 6 -> A_i = [124, 225, 47, 126, 141, 152, 68, 48, 74, 193, 109, 10, 101, 55, 111, 183]; b_i = 22
[+] 7 -> A_i = [241, 246, 236, 55, 156, 152, 93, 250, 244, 144, 256, 227, 102, 78, 242, 54]; b_i = 42
[+] 8 -> A_i = [135, 25, 96, 246, 128, 55, 128, 171, 15, 21, 227, 39, 131, 26, 88, 196]; b_i = 163
[+] 9 -> A_i = [180, 163, 162, 27, 131, 207, 110, 139, 230, 225, 222, 20, 65, 10, 55, 176]; b_i = 118
[+] 10 -> A_i = [127, 16, 131, 29, 105, 26, 1, 53, 25, 154, 49, 240, 84, 246, 136, 225]; b_i = 157
[+] 11 -> A_i = [186, 181, 103, 95, 105, 81, 159, 132, 15, 25, 189, 204, 76, 110, 20, 71]; b_i = 206
[+] 12 -> A_i = [196, 213, 240, 209, 62, 249, 218, 44, 182, 177, 170, 51, 184, 131, 82, 131]; b_i = 25
[+] 13 -> A_i = [87, 215, 228, 181, 12, 133, 110, 22, 204, 46, 173, 60, 224, 217, 252, 79]; b_i = 35
[+] 14 -> A_i = [163, 136, 7, 236, 176, 109, 126, 66, 240, 196, 188, 60, 176, 244, 51, 173]; b_i = 78
[+] 15 -> A_i = [185, 145, 9, 141, 153, 186, 187, 48, 249, 142, 30, 234, 195, 22, 136, 140]; b_i = 206
[+] 16 -> A_i = [11, 206, 59, 217, 145, 15, 7, 105, 170, 95, 250, 92, 204, 209, 232, 15]; b_i = 214
[*] Secret: 0123456789abcdef
[+] Flag: Securinets{f4k3_fl4g_f0r_t3st1ng}
[*] Stopped process '/usr/bin/python3' (pid 600334)
Flag
Si lo ejecutamos de forma remota, obtendremos la flag (después de mucho tiempo):
$ time python3 solve.py crypto.securinets.tn 8888
[+] g: Done
[+] Pub_matrix: Done
[+] Opening connection to crypto.securinets.tn on port 8888: Done
[+] 1 -> A_i = [129, 145, 183, 209, 150, 70, 217, 127, 68, 65, 63, 178, 4, 28, 256, 43]; b_i = 7
[+] 2 -> A_i = [49, 255, 224, 114, 156, 46, 185, 54, 26, 46, 109, 234, 155, 99, 123, 68]; b_i = 81
[+] 3 -> A_i = [252, 100, 232, 10, 103, 38, 235, 176, 161, 99, 46, 182, 137, 177, 59, 28]; b_i = 231
[+] 4 -> A_i = [211, 79, 70, 52, 95, 121, 89, 185, 17, 78, 155, 119, 84, 223, 92, 52]; b_i = 154
[+] 5 -> A_i = [110, 233, 113, 69, 234, 232, 183, 247, 140, 114, 239, 118, 4, 169, 167, 52]; b_i = 177
[+] 6 -> A_i = [252, 49, 38, 130, 12, 50, 225, 186, 147, 25, 31, 133, 76, 111, 248, 74]; b_i = 192
[+] 7 -> A_i = [248, 233, 208, 64, 41, 177, 42, 22, 221, 228, 66, 256, 45, 118, 204, 232]; b_i = 210
[+] 8 -> A_i = [95, 3, 73, 45, 36, 72, 247, 200, 152, 106, 237, 100, 153, 178, 170, 49]; b_i = 69
[+] 9 -> A_i = [32, 173, 20, 118, 150, 51, 97, 189, 95, 236, 237, 202, 251, 188, 59, 95]; b_i = 40
[+] 10 -> A_i = [184, 35, 78, 172, 2, 126, 66, 202, 147, 32, 92, 45, 106, 241, 0, 11]; b_i = 226
[+] 11 -> A_i = [222, 248, 42, 33, 237, 16, 121, 242, 11, 96, 190, 112, 80, 104, 243, 209]; b_i = 36
[+] 12 -> A_i = [19, 209, 137, 254, 95, 102, 11, 126, 183, 16, 235, 195, 33, 100, 208, 2]; b_i = 239
[+] 13 -> A_i = [144, 68, 190, 100, 121, 168, 16, 61, 114, 126, 105, 202, 33, 233, 165, 87]; b_i = 204
[+] 14 -> A_i = [134, 233, 198, 225, 139, 129, 239, 154, 97, 205, 171, 36, 208, 226, 228, 221]; b_i = 53
[+] 15 -> A_i = [134, 15, 123, 43, 156, 218, 184, 7, 19, 179, 7, 148, 77, 68, 220, 115]; b_i = 158
[+] 16 -> A_i = [138, 229, 69, 199, 50, 63, 119, 7, 0, 99, 27, 76, 26, 207, 2, 100]; b_i = 236
[*] Secret: 80gak_7AJ1N_k1dO
[+] Flag: Secruinets{ShuFflIn9_thrOu9h_my_3quAtIoN_1SN7_3Nou9h7_to_s7op_Fr0m_Me}
[*] Closed connection to crypto.securinets.tn port 8888
python3 solve.py crypto.securinets.tn 8888 50,11s user 8,70s system 1% cpu 1:28:58,32 total
El script completo se puede encontrar aquí: solve.py
.