### Author : `voiv0de`

The ESAIP CTF was overall a pretty cool event. Usually I take care of the Reverse Engineering challenges but on this one I returned to my first love : Cryptanalysis.

During the CTF, I managed to obtain two first bloods in this category 🩸.

Today, I present to you : Find the Matrix.

``````The flag was encrypted by this algorithm, find a way to reverse it to get the flag!
``````

Attached to it were two files :

``````# chall.py
import numpy as np
from math import sqrt
from sympy import Matrix
from random import choice

BLOCK_SIZE = 4

I = [
[1, 2],
[3, 4]
]

def to_hex(x):
res = ""
for i in [hex(k)[2:] for k in sum(x, [])]:
if len(i) == 1:
res += f"0{i}"
else:
res += i
return res

def to_plain(x):
res = ""
for i in [chr(k) for k in sum(x, [])]:
res += i
return res

while len(b)%size != 0:
b += b"\x03"
return b

def to_m(b:bytes):
return [[j for j in b[i:i+int(sqrt(BLOCK_SIZE))]] for i in range(0,len(b), int(sqrt(BLOCK_SIZE)))]

def block(a:bytes):
return [a[i:i+BLOCK_SIZE] for i in range(0,len(a), BLOCK_SIZE)]

def xor(a, b):
return [[int(i)^int(j) for i,j in zip(a[t], b[t])] for t in range(len(a))]

def encrypt_block(block:bytes, key:list):
m_key = np.array(key)
m_block = np.array(to_m(block))
m_cipher = np.matmul(m_block, m_key)%255
return m_cipher.tolist()

def decrypt_block(block:bytes, key:list):
""" Nothing here """

def gen_keys(b_keys:list, length:int, d=False):
k = []
for b_k in b_keys:
m_b_k = Matrix(to_m(b_k))
while 1:
try:
m_b_k_inv = m_b_k.inv_mod(255)
if d:
else:
break
except ValueError:
continue

for i in range(length-len(k)):
k.append(k[i])
return k

print(k)
b_flag = block(flag)
b_keys = block(k)

iv = "_IV_"
m_iv = to_m(iv.encode())

cipher = ""

for b, k in zip(b_flag, gen_keys(b_keys, len(b_flag))):
m_b = to_m(b)
b_xored = xor(m_iv, m_b)
c = encrypt_block(b_xored, k)
cipher += to_hex(c)
m_iv = c

print(dict(cipher=cipher, iv=iv, key=""))
``````
``````output.txt
{'cipher': '470002d269574fc2705cfd6f5f05b4b2b53c6f9d1432d78d5277b7d83dea85f56f92c506c136ea71b844f8baf09c50a20ba62a06b47e7e119a4876aa6784f711d8bb33a2be36848808b3faf8bc003079', 'iv': '_IV_', 'key': ''}
``````

## Analyzing the code

So what do we have ? The algorithm is dividing the flag into blocks of for bytes, which are then transformed into 2x2 matrixes and xored with the previous block (the first block will be xored with the IV “`_IV_`”).

Moreover, we see a function named `encrypt_block()` which takes a key as an argument. This key is a random line from `rockyou.txt` (which is 14344391 lines long so bruteforcing could take some time). The chosen key is padded with “\x03̀” bytes until it is 16 bytes long before being divided into blocks of 4 bytes each. The `encrypt_block()` function then turn those bytes into another 2x2 matrix. The ciphertext will be the product of the cleartext matrix and the key matrix (modulo 255).

## Reversing the algorithm

The first thing I like to do on those kind of challenges is encrypting a known string with the algorithm. Personnally, since the key is 16 bytes long I chose the string “`0123456789abcdef`”. We will also choose a random line of the `rockyou.txt` as the key to use for our tests.

It gives us the following parameters :

``````flag = b"0123456789abcdef"
key = b'REFUGIO\x03\x03\x03\x03\x03\x03\x03\x03\x03'
``````

By running the algorithm we obtain the following : Now we can work !

The reversing of the algorithm is actually pretty straightforward. The conversion between blocks and matrixes can easily reverted. The xor with the previous block is also trivial to invert thanks to the properties of `xor`. However, the encrypt_block function is a little trickier.

``````def encrypt_block(block:bytes, key:list):
m_key = np.array(key)
m_block = np.array(to_m(block))
m_cipher = np.matmul(m_block, m_key)%255
return m_cipher.tolist()
``````

The function takes the current block (xored with the previous one) and the current key block. Those are turned to matrixes and the output is the product of those two matrixes.

Is it possible to revert it ? Yeah, but not with a simple division because those are not possible with matrixes. But what is a division anyway ?

Let’s say `x`, `y` and `z` are reals and `x * y = z`. Then `z / y = x` or put differently `z * y^-1 = x`.

For matrixes, division does not exist but you can invert matrixes. So, based on the output matrix and the key, `output * key^-1 = input`.

And, luckily for us, the creator of the programm already gave us the means to invert matrixes in the `gen_keys()` function by just setting the parameter `d` to `True`.

``````def gen_keys(b_keys:list, length:int, d=False):
k = []
for b_k in b_keys:
m_b_k = Matrix(to_m(b_k))
while 1:
try:
m_b_k_inv = m_b_k.inv_mod(255)
if d:
else:
break
except ValueError:
continue

for i in range(length-len(k)):
k.append(k[i])
return k
``````

We now have the means to decrypt our homemade ciphertext !

``````# Yeah, this function is basically the same of the encrypt_block function
# It could be cleaner but CTF are what they are ;)
def decrypt_block(block:bytes, key:list):
m_key = np.array(key)
m_block = np.array(to_m(block))
m_decrypt = np.matmul(m_block, m_key) % 255
return m_decrypt.tolist()

cipher = "7b c9 77 89 11 94 f5 d7 b6 8d 94 de d2 92 1c c6".split(" ")
K = b'REFUGIO\x03\x03\x03\x03\x03\x03\x03\x03\x03'
b_keys = block(K)

iv = "_IV_"
m_iv = to_m(iv.encode())

flag = ""

for i in range(0, len(cipher), 4):
k = gen_keys(b_keys, len(flag) // BLOCK_SIZE, d=True)
c = [[0, 0], [0, 0]]
c = int(cipher[i], 16)
c = int(cipher[i+1], 16)
c = int(cipher[i+2], 16)
c = int(cipher[i+3], 16)
b_xored = decrypt_block(c, k)
b_xored = b_xored[(i//4) % 4]
m_b = xor(m_iv, b_xored)
m_iv = c
for i in m_b:
for j in i:
flag += chr(j)

print(flag)
`````` Our algorithm is working, put we still have one tiny problem before we can flag. If you remember the code from the challenge, the password is selected this way :

``````k = pad(choice(open("./rockyou.txt", "r", errors='ignore', encoding='utf-8').readlines()).strip("\n").encode(), 16)
``````

The password is selected from `rockyou.txt` using the function `choice()` from the `random` library. And the seed is unknown to us. And bruteforcing the 14344391 passwords would take too long. However, we know the first later of the flag (in our case `ECTF\{`) so we can reduce the number of passwords to try out by finding the 4 first bytes of the key with a known plaintext attack.

We know the IV is `_IV_` and won’t move so it will not be of any problem to us. Now we could solve an equation to find the 4 bytes we are searching for. Or we could be lazy and just bruteforce with all the password and print the ones that give us a plaintext starting with ECTF ;).

``````cipher = "47 00 02 d2 69 57 4f c2 70 5c fd 6f 5f 05 b4 b2 b5 3c 6f 9d 14 32 d7 8d 52 77 b7 d8 3d ea 85 f5 6f 92 c5 06 c1 36 ea 71 b8 44 f8 ba f0 9c 50 a2 0b a6 2a 06 b4 7e 7e 11 9a 48 76 aa 67 84 f7 11 d8 bb 33 a2 be 36 84 88 08 b3 fa f8 bc 00 30 79".split(" ")

f = open("dump", "w") # Logging the results in case we miss something

iv = "_IV_"
m_iv = to_m(iv.encode())

flag = ""
passwd = open("./rockyou.txt", "r", errors='ignore', encoding='utf-8').readlines()

for line in passwd:
iv = "_IV_"
m_iv = to_m(iv.encode())
flag = ""
if line.strip("\n").encode() == b"":
continue
b_keys = block(K)
for i in range(0, len(cipher), 4):
k = gen_keys(b_keys, len(flag) // BLOCK_SIZE, d=True)
c = [[0, 0], [0, 0]]
c = int(cipher[i], 16)
c = int(cipher[i+1], 16)
c = int(cipher[i+2], 16)
c = int(cipher[i+3], 16)
b_xored = decrypt_block(c, k)
b_xored = b_xored[(i//4) % 4]
m_b = xor(m_iv, b_xored)
m_iv = c
for i in m_b:
for j in i:
flag += chr(j)
if "ECTF" in flag:
print(flag, line)
f.write(flag)
f.write(line)

f.close()
`````` I’m starting to see a pattern don’t you ? Let’s create a file which will be a little less hefty. Okay, now what can we do ? We know the next char of the flag (`{`) but we have no clue about what the 3 following bytes will look like. So we bruteforce again but this time with our newly created file.

``````cipher = "47 00 02 d2 69 57 4f c2 70 5c fd 6f 5f 05 b4 b2 b5 3c 6f 9d 14 32 d7 8d 52 77 b7 d8 3d ea 85 f5 6f 92 c5 06 c1 36 ea 71 b8 44 f8 ba f0 9c 50 a2 0b a6 2a 06 b4 7e 7e 11 9a 48 76 aa 67 84 f7 11 d8 bb 33 a2 be 36 84 88 08 b3 fa f8 bc 00 30 79".split(" ")

f = open("dump", "w") # Logging the results in case we miss something

iv = "_IV_"
m_iv = to_m(iv.encode())

flag = ""
passwd = open("./list.txt", "r", errors='ignore', encoding='utf-8').readlines()

for line in passwd:
iv = "_IV_"
m_iv = to_m(iv.encode())
flag = ""
if line.strip("\n").encode() == b"":
continue
b_keys = block(K)
for i in range(0, len(cipher), 4):
k = gen_keys(b_keys, len(flag) // BLOCK_SIZE, d=True)
c = [[0, 0], [0, 0]]
c = int(cipher[i], 16)
c = int(cipher[i+1], 16)
c = int(cipher[i+2], 16)
c = int(cipher[i+3], 16)
b_xored = decrypt_block(c, k)
b_xored = b_xored[(i//4) % 4]
m_b = xor(m_iv, b_xored)
m_iv = c
for i in m_b:
for j in i:
flag += chr(j)
if "ECTF{" in flag:
print(flag, line)
f.write(flag)
f.write(line)

f.close()
`````` Looks like a flag to me ;). It means the password we were looking for is ilove2shop.

Thanks to all the organizers of the ESAIP CTF and to Ruulian for this challenge which was a lot of fun to solve 🥳. See you next year !