tl;dr
- Reverse bytearray to recover matrix
cflag
. - Use first element of matrix to recover e (bruteforce &iroot)
- Reduce the flag to finite field of a 32-bit prime, solve for each character.
Challenge Points: 249
Solves: 31
Solved By: xxMajinxx, v4d3r
Challenge Description
While Laura was looking for her brother, she found a program that seems to scramble a password and save the result. Could you help her find the original password so that she can find and save her brother before it’s too late?
Challenge Files:-
| -> encode.py
| -> enc
First Impressions
- The challenge description suggests that the challenge involves unscrambling the flag text.
- The starting bytes of the flag and the seed are revealed. This implies randomization is reversible.
- There are 2 unknown variables e (int - most likely), flag (string).
- The scrambled flag is stored as bytes in
enc
. - The enc file is
17.5 Mb
, implying that either the length of the flag is really large or each element in the flag matrix is really large.
The Challenge
The challenge can be mainly explained in 3 parts:
Order
1 | n = int(sqrt(len(flag))) + 2 |
- Initially, the challenge uses the $n = \sqrt{l}+2$ (l is the length of the flag), to create a list of tuples (order) with each tuple containing 2 elements (each element range from 0 to n).
- This list is shuffled and then sorted based on the sign of the difference between the variables in each element.
Note:- After some testing, we can conclude that this was done so that for any flag of length l, the $n*n$ matrix will contain the scrambled flag in its lower triangle.
Matrix class
1 | class Matrix: |
- The class maintains a matrix and allows a few operations on the matrix.
- The Matrix class uses a tuple with 2 variables to get and set values in their (X, Y) positions.
- Matrix is capable of matrix multiplication & exponentiation.
Main
1 | m = Matrix() |
- Based on
order
the flag bytes is mapped to a matrixcflag
and exponentiated w\ the variablee
. - The integers are converted to a string and padded to the size of the largest integer in the matrix.
- The padded strings are joined together and converted to bytes & are stored in the
enc
file.
Solution
The solution was a 4 step process. It is possible to recover the flag from the cflag matrix if reduced to e = 1
, but for this, we need two objects.
- The cflag matrix.
- The e value
Step 1 - Retrieve cflag
This is the easy step. Since we know how the bytes are stored in the enc
file we can easily undo the step to get the cflag as a string.
1 | cip = ''.join([str(i).zfill(2) for i in open('enc','rb').read()]) |
Since we don’t know the length of the flag, we don’t know the n
value (ie the order of the matrix). Thus it might be difficult to decide how the string is to be split.
Hint: All elements are of equal length.
But if we look at the factors of length of the flag, it becomes more obvious.
1 | sage: ecm.factor(len(cip)) |
Since 2 and 3 matrices are too small to contain atleast 7 bytes, The most likely matrix would be 7.
This was the correct choice when the string was split based on the word length $wrdln = cipln/7^2$, the paddings aligned correctly to each element.
1 | a = [int(cip[i:i+wrdln]) for i in range(0, ln, wrdln)] |
Step 2 - Get e
The first character of the matrix $a[0,0]$ when exponentiated will be just $a^e$. Therefore this value can be reduced to find the e (lost e) value. We checked with all possible primes to reduce the value to 20-128 range. We got a return $48, [2, 2, 85381]$ after a long bruteforce. Implying that the first character code 48, and the exponent is 341524.
1 | def get_e(val, k= 2, ret = []): |
Step 3 - Recover the Matrix Values
Each element in the Matrix was at least 1907399 bits, we couldn’t do any operations on the matrix(much less reduce it). So one idea was to reduce the size of the matrix with modular arithmetics.
1 | ct = Matrix(GF(p), 7, 7, a) |
Now that we have a simple matrix and the exponent e
, we can start finding which values will result in the ct
matrix.
Note:- In exponentiation of matrices that only occupy the lower triangle, each element is only affected by the elements that are above it or to its right.
Since the elements in the diagonal are not effected by any other value those are bruteforced first $(mat^e)[i,i] = x^e$
Next the elements that are below the diagonal elements are bruteforced $(mat^e)[i+1,i] = x^e + c$
This process is repeated until all elements are recovered.
1 | def brute(pt, ct, loc): |
Step 4 - Unscamble
Now that we have recovered all bytes we can unscamble the matrix with the known order to get the flag.
1 | flag = ''.join([chr(mat[loc]) for loc in order if mat[loc]]) |
Conclusion
Large numbers are hard to work with, therefore most CPU heavy tasks can be reduced to a few bytes with modular arithematics. And never reveal the seed.Exploit Code
: sol.sageFlag
: CTF-BR{s0M3_0F_m47r1X_106}