solved by tomato
Solving the DLP in matrices over a finite field is no trivial task. What are your thoughts on this GLNQ belief?
Note: flag = MAPNA{m}, Don’t convert m to bytes.
DISCLAIMER: My solution to this challenge involves heavy trolling and is much more complex than required.
1 | #!/usr/bin/env sage from Crypto.Util.number import * from flag import flag F, k = GF(2**8), 14 while True: G = random_matrix(F, k) if G.is_invertible(): break flag = flag.lstrip(b'MAPNA{').rstrip(b'}') m = bytes_to_long(flag) H = G ** m print(f'G = {G}') print(f'H = {H}') |
We are given a code snippet which generates a random 14 by 14 (invertible) matrix G
under GF(2^8). Then, it gives us both G
and G^m
, and we have to recover m. Essentially, DLP on matrices under GF(2^8).
Jordan normal form
When you think of raising a matrix to a power, you may think of one of the basic topics of linear algebra, diagonalization. If a diagonalizable matrix has eigenvalues , then one such diagonalization could be:
and equivalently
This is useful, as if you wanted to raise the matrix to the th power, you could do it by raising to the th power, which is much easier as it is diagonal:
(this would make discrete log simpler as you can consider the discrete log of the elements of the matrix instead of the entire matrix). But, not all matrices are diagonalizable. We can check this:
1 | G.is_diagonalizable() |
So, we instead use the matrix’s Jordan normal form. The Jordan normal form is an alternative for non-diagonalizable matrices, which is almost diagonal:
where each is a jordan block of the form:
So, raising to the th power would now require raising the Jordan blocks to the th power, which are much easier to deal with than the entire matrix. Sage has a builtin method to calculate Jordan normal, so we can just use it:
1 | G.jordan_form() |
Right.. the Jordan normal form still requires all the eigenvalues to exist (but not necessarily be distinct). In order to force the eigenvalues to exist, we need to extend the field we are working on, which is GF(2^8). To find the field we need to extend to (with sage API), we can first consider the characteristic polynomial of , which is a monic polynomial whose roots are the eigenvalues of . Then, we find the splitting field of this polynomial, which is the smallest field extension over which such a polynomial completely factorizes into linear factors, meaning that it would have 14 real roots aka 14 real eigenvalues in our case.
With this, we are able to find the Jordan normal form of :
1 | chG = G.charpoly() |
Now, we can try doing discrete log on the Jordan blocks of J
. We compute and do discrete log: (since J happens to be diagonal we can just index it).
1 | JH = ~P*H*P |
gives [7393434767644474031, 7393434767644474031, 7393434767644474031, 7393434767644474031, 7393434767644474031, 235852996149746, 235852996149746, 235852996149746, 235852996149746, 7393434767644474031, 235852996149746, 7393434767644474031, 7393434767644474031, 235852996149746]
.
Notice that not all of these values are the same. This is because the Jordan blocks do not necessarily have the same multiplicative order as the original matrix, and may have lower orders. If are different powers we obtain from taking the discrete log of Jordan blocks, we would have:
which implies that
meaning that we can still recover with the Chinese remainder theorem, provided that . Lucky for us, is actually equal to the lcm, otherwise we would have to do a bit of guesswork.
The reason that finding the Jordan normal form happens to work well here is that our Jordan blocks have multiplicative orders that are much lower than the original multiplicative order, allowing for the discrete log to be much easier to compute now. (this does not always happen)
Full sage implementation
1 | GGG, HHH = open("./glnq_9c3935a6c97ee38b4ba28e28da342b26ac13b45a/glnq/output.txt").read().lstrip("G = ").split("H = ") |
giving the flag MAPNA{6424379811053277573417442136}
.
Simpler solution
I was a complete bozo when solving this, and there is a miles simpler solution which I found out about in discord. Although discrete_log(H, G)
seems to error out,
1 | discrete_log(H, G, algorithm="lambda") |
solves the entire challenge in a few seconds. Simply a misstep on my end to have not tried all the discrete_log
algorithms at the start, because seeing the solve count I didn’t think to try. Sage API is too powerful… at least I learned a bit about linear algebra because of this challenge though
Note: the name of the challenge GLNQ seems to lead to this paper, although understanding the algorithm described there was really difficult and I sank too much time into this route.