Modular matrices - HarekazeCTF 2019
Post
Cancel

# Modular matrices - HarekazeCTF 2019

In this blogpost I will be writing about modular matrices, a topic I came across while doing one of the crypto challenges from HarekazeCTF 2019!

## Some background math

A modular matrix is simply a matrix in which each cell is under the modulo operator. For instance, here are two matrices that are congruent:

\begin{align} \begin{pmatrix} 1 & 8 \
15 & 9 \
\end{pmatrix} \equiv \begin{pmatrix} 1 & 0 \
3 & 1 \
\end{pmatrix} \pmod{4} \end{align}

Now, modular matrices hold some properties. Whereas matrices in classic arithmetic can be multiplied with this formula:

\begin{align} C_{ij} = \sum_{k=0}^n A_{ik}B_{kj} \end{align}

In modular arithmetic we need to take the modulo of each term in the summation:

\begin{align} C_{ij} = \sum_{k=0}^n [A_{ik}B_{kj} \pmod{x}] \end{align}

## The challenge

We are given two files in the challenge:

• problem.py:
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 #!/usr/bin/python3 import random import binascii import re from keys import flag flag = re.findall(r'HarekazeCTF{(.+)}', flag) flag = flag.encode() #print(flag) def pad25(s): if len(s) % 25 == 0: return b'' return b'\x25'*(25 - len(s) % 25) def kinoko(text): text = text + pad25(text) mat = [] for i in range(0, len(text), 25): mat.append([ [text[i], text[i+1], text[i+2], text[i+3], text[i+4]], [text[i+5], text[i+6], text[i+7], text[i+8], text[i+9]], [text[i+10], text[i+11], text[i+12], text[i+13], text[i+14]], [text[i+15], text[i+16], text[i+17], text[i+18], text[i+19]], [text[i+20], text[i+21], text[i+22], text[i+23], text[i+24]], ]) return mat def takenoko(X, Y): W = [[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]] for i in range(5): for j in range(5): for k in range(5): W[i][j] = (W[i][j] + X[i][k] * Y[k][j]) % 251 return W def encrypt(m1, m2): c = b"" for each in m1: g = random.randint(0,1) if g == 0: mk = takenoko(m2, each) else: mk = takenoko(each, m2) for k in mk: c += bytes(k) return c if __name__ == '__main__': m1 = kinoko(flag) m2 = [[1,3,2,9,4], [0,2,7,8,4], [3,4,1,9,4], [6,5,3,-1,4], [1,4,5,3,5]] print("Encrypted Flag:") enc_flag = binascii.hexlify(encrypt(m1, m2)).decode() print(enc_flag) 
• result.txt:
1 2 Encrypted Flag: ea5929e97ef77806bb43ec303f304673de19f7e68eddc347f3373ee4c0b662bc37764f74cbb8bb9219e7b5dbc59ca4a42018 

### Analysis of the Python script

In order to solve the challenge I started by inspecting the script, identifying what each function did:

• pad25(s): returns a string comprised of as many \x25’s as characters needs the s argument to have a length that is a multiple of 25.

• kinoko(text): pads a the text argument to a multiple of 25 and then splits it into lists of 25 characters, which returns in a list themselves.

• takenoko(X, Y): here comes the (tricky) part of the challenge, recognising that this function performs a modular multiplication of the argument matrices.

• encrypt(m1, m2): calls the takenoko function to perform the multiplication on each of the one dimensional matrices of m1. Another tricky part is that it randomly chooses whether to do $$A \cdot B$$ or $$B \cdot A$$. We need to keep in mind that matrix multiplication is not commutative in most of the cases (hint: this is not one), and hence $$A \cdot B \neq B \cdot A$$. Finally, it returns the concatenation of the bytes resulting from each of the multiplications.

### Objective of the challenge

Given the functions, we only need to take a look at the main part of the script to realise what we need to do:

1 2 3 4 5 6 7 8 if __name__ == '__main__': m1 = kinoko(flag) m2 = [[1,3,2,9,4], [0,2,7,8,4], [3,4,1,9,4], [6,5,3,-1,4], [1,4,5,3,5]] print("Encrypted Flag:") enc_flag = binascii.hexlify(encrypt(m1, m2)).decode() print(enc_flag) 

m1 and m2 are both matrices and encrypt(m1, m2) is the concatenation of the bytes contained in a matrix, let’s call it m3, that is the modular multiplication of m1 and m2. Therefore, what we need to do is, given m2 and m3, obtain m1. i.e: obtain $$A$$ from $$A \cdot B = C$$ given $$B$$ and $$C$$.

### Back to math

Having $$A \cdot B = C$$ and given $$B$$, $$C$$, it is possible to recover $$A$$ if $$B$$ is invertible: $$C \cdot B^{-1} = A$$, where $$B^{-1}$$ is the inverse matrix of $$B$$.

In classic arithmetic, the inverse matrix $$B^{-1}$$ of $$B$$ can be obtained the following way:

\begin{align} B^{-1} = \frac{1}{det(B)} \cdot \left(B^{*}\right)^{t} \end{align}

where

$$det(B)$$ is the determinant of $$B$$
$$B^{*}$$ is the adjoint matrix of $$B$$
$$\left(B^{*}\right)^{t}$$ is the traspose of the adjoint matrix

However, in modular arithmetic things are a bit different. Let’s say we want to find the inverse matrix of $$B \pmod{x}$$. We need to follow a series of steps:

• First we need to find $$det(B)$$. If $$det(B)$$ is coprime to $$x$$, then $$B$$ is invertible modulo $$x$$. This is necessary, since only coprime numbers to $$x$$ will have a modular inverse modulo $$x$$.

• Next, we need to find the modular inverse of $$det(B)\pmod{x}$$, which we will denote as $$(det(B))^{-1}$$. Recall that this number satisfies that $$det(B)\cdot(det(B))^{-1} \equiv 1 \pmod{x}$$.

• Then, we calculate the traspose of the adjoint matrix, which is calculated normally. We will name it as before, $$\left(B^{*}\right)^{t}$$.

• Finally, the matrix obtained from $$(det(B))^{-1}\cdot\left(B^{*}\right)^{t}$$ is the inverse of $$B \pmod{x}$$. Lastly, we take the modulo of each cell in order to ensure that the integers range from $$0$$ to $$x$$.

### Solution of the challenge

Now we just need to put this into code to solve the challenge and retrieve the challenge, easy right? (It wasn’t)

First thing I did was put back the result of the multiplications, which was encoded as hex, into the original format:

1 2 3 4 5 6 7 8 9 10 11 encrypted = "ea5929e97ef77806bb43ec303f304673de19f7e68eddc347f3373ee4c0b662bc37764f74cbb8bb9219e7b5dbc59ca4a42018" text = binascii.unhexlify(encrypted) m3 = [] for i in range(0, len(text), 25): submat = [] for x in range(0, 25, 5): submat.append([ord(text[i+x]), ord(text[i+x+1]), ord(text[i+x+2]), ord(text[i+x+3]), ord(text[i+x+4])]) m3.append(submat) 

Then, using numpy to make things easier I calculated the inverse of m2. I needed to use int() to get rid of floating point residues when getting the determinant and also multiply by the determinant, given that np.linalg.inv(m2) divides the traspose by it:

1 2 det = int(np.linalg.det(m2)) Bprime = (np.linalg.inv(m2) * det * modinv(det, 251)) % 251 

After that, we need to guess the order of the multiplication. This wasn’t difficult and could be done manually, since m3 only had two matrices and so there were 4 possibilities. It turned out to be this one:

1 2 m1_1 = np.dot(m3, Bprime) m1_2 = np.dot(m3, Bprime) 

Finally, we extract the numbers from the matrices taking modulo 251 and join them in a string to get the flag. It was necessary to use the round function, as int() rounds down the numbers (1.9 should be 2 and not 1).

1 2 3 4 5 6 7 8 9 10 s = "" for i in m1_1: print i for x in i: s += chr(int(round(x)) % 251) for i in m1_2: for x in i: s += chr(int(round(x)) % 251) 

Flag: Op3n_y0ur_3y3s_1ook_up_t0_th3_ski3s_4nd_s33.

Here is the whole script in case you are interested: full solution.

I hope this article was useful, see you next time!

Diego Bernal Adelantado
This post is licensed under CC BY 4.0 by the author.