Farfour Post Quantom
17 minutes to read
We are given the Python source code of a server:
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
Moreover, there are two external files we are not given (Secret.txt
and flag.txt
):
secret=open("Secret.txt",'rb').read()
assert len(secret)==16
flag=open("flag.txt","rb").read()
Source code analysis
First of all, there are some definitions:
random=SystemRandom()
shuffle=random.shuffle
randint=random.randint
randrange=random.randrange
uniform = lambda: randrange(257//2) - 257//2
P=GF(257)
Notice that uniform
is a function that returns random values within the interval $[-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
Moreover, we will be working in $\mathbb{F}_{257}$ (GF(257)
), so uniform
values will be actually within $[129, 256]$:
>>> min(set(uniform() % 257 for _ in range(10000)))
129
>>> max(set(uniform() % 257 for _ in range(10000)))
256
The server generates these cryptographic objects:
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
The value $g \in [2, 257]$ is used to generate Pub_matrix
. Notice that Secret_matrix
is a $16 \times 16$ matrix generated using uniform
(so all entries will be within $[129, 256]$). Then, the entries of Pub_matrix
are the entries of Secret_matrix
, each of them multiplied by $g^r$, where $r$ is a random integer between $2$ and $256$ (different for each entry).
Options
The server allows us to use four options:
get_flag
:
elif choice["option"]=="get_flag":
print(encrypt_flag(secret))
This just uses the 16-byte secret inside Secret.txt
as an AES key to encrypt the flag (stored at 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))
This one multiplies Secret_matrix
times the 16-byte secret as a column vector. We are given the result:
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))
Here we can send arbitrary columns vectors to multiply them against Pub_matrix
. Unfortunately, the result vector (res
) is shuffled…
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)
This one allows us to regenerate the value of $g$ (and also update both Secret_matrix
and Pub_matrix
).
Attack strategy
Let’s see how we can use all the options to find the Pub_matrix
, then the Secret_matrix
and finally the secret
(Secret.txt
). Once we have this last value, we will be able to decrypt the flag with AES.
Finding Pub_matrix
It is clear that we will need to use get_hint
option, because it allows us to multiply Pub_matrix
by an arbitrary vector. For instance, we can multiply the Pub_matrix
($(p_{i,j})_{16 \times 16}$) by a vector which has a $1$ at the first coordinate and the rest is $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} $$
The problem is that the result vector will not be sorted, so we won’t know the row positions.
The only thing we can do is get full rows with the following strategy:
- Send vector $(1, 0, \dots, 0)$. We will receive the values of the first column of
Pub_matrix
. - Send vector $(0, 1, 0, \dots, 0)$. We will receive the values of the second column of
Pub_matrix
. - Send vector $(1, 1, 0, \dots, 0)$. We will receive the sum of the values from the first and second columns that are in the same row:
$$ \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} $$
- We can do a similar query with the 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} $$
The idea is to take the values from step 1. and 2. and then find pairs of values whose sum is in the set received in step 3. and whose difference is in the set from step 4. As a result, we will get the first two elements of each row of Pub_matrix
.
In Python, we can use a wrapper function:
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')
Then, we can perform the above procedure as follows:
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 [], []
Once we have this, we can start looking for the third element of every row by using vector $(0, 0, 1, 0, \dots, 0)$. And for sanity check, we can also employ vectors $(1, 1, 1, 0, \dots, 0)$ and $(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 [], []
We continue with the fourth, the fifth, and so on until the last one. For each of them, one more check is added.
Although the queries and checks can be automated, I found it simpler to write all checks by hand, just to keep control on what we are doing. As an example, these are the queries and checks for the last element of each row:
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 [], []
It looks awful, but believe me, it is easier to implement and to understand (at least for me).
At this point, we have the rows of Pub_matrix
(but we still don’t know their position).
Finding Secret_matrix
We need to use the fact that Pub_matrix
depends on Secret_matrix
and $g$. Since we can use any value of $g$ (we will need to use reset_connection
several times until we get the value we want), we will force $g$ to be $g = 256 \equiv -1 \pmod{257}$. With this, we get that $g^r \mod{257} = \pm 1$, for any integer $r$.
As a result, having the rows of Pub_matrix
will instantly give us the rows of Secret_matrix
, since these were generated by uniform
($[129, 256]$). So, if one element of Pub_matrix
is less than $129$, it has been multiplied by $-1$ and we only need to compute an offset.
In Python, we can do the following:
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]
# ...
But we have the same problem as before… We don’t know the position of the rows.
Also, we must consider an issue when there is a repeated element in the first column. The presented algorithm only works when all entries in the first column are different. If not, we will end up with repeated rows. Therefore, we can check if the rank of the matrix is maximum or not.
Different approaches
Now, we must use get_secret
to somehow find the secret
vector having the rows of Secret_matrix
.
Brute force
We could think of doing brute force on the possible permutations of rows. However, there are a total of $16!$ possibilities ($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
Also, trying to apply brute force the secret
vector is also bad since we will need to try around $256^{16} = 2^{128}$ possible vectors.
Lattice reduction techniques
There is no clear way to find the position of each row. No matter how you arrange the vector in get_hint
, since the result is shuffled, there is no certainty on the row number.
There is still something we know from Secret_matrix
($(a_{i,j})_{16 \times 16}$) and the secret
vector ($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} $$
Let $A_j = \displaystyle\sum_j a_{?,j}$ and $b = \displaystyle\sum_i b_{i}$. Now we have:
$$ \begin{pmatrix} A_{1} & A_{2} & \dots & A_{16} \end{pmatrix} \cdot \begin{pmatrix} s_1 \\\\ s_2 \\\\ \vdots \\\\ s_{16} \end{pmatrix} = b $$
With this, we have got rid of the shuffling. The above equation is valid for the secret
vector. In Python, these are the last instructions of the get_data
function (we check the rank here as well):
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 [], []
The formula reminded me to a knapsack problem, so I tried to create a lattice and apply LLL in order to find a short vector, which was presumably the secret
vector (something similar to Infinite Knapsack). But it seems that the conditions where not suitable to solve the Shortest Vector Problem with this approach.
System of equations
So, it is clear that we need more precision. This is something directly related to the amount of information we have. Notice that we can continue using reset_connection
until getting $g = 256$, then find Pub_matrix
and Secret_matrix
and use get_secret
again. The result will be another equation as the above one.
If we iterate the process until we have $16$ equations, we will be able to solve a system of equations on the entries of secret
vector (notice that Secret.txt
is always the same).
With this, we have found a winning strategy:
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()}')
Once we have the secret
vector, we can simply decrypt the 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()}')
We can try to solve it locally:
$ 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
If we run it remotely, we will get the flag (after a lot of time):
$ 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
The full script can be found in here: solve.py
.