uiuctf-2024 - Without a Trace
This challenge was solved by my teammate Andrei for the UIUCTF 2024 CTF competition.
The challenge’s description is:
Gone with the wind, can you find my flag?
ncat --ssl without-a-trace.chal.uiuc.tf 1337
import numpy as np
from Crypto.Util.number import bytes_to_long
from itertools import permutations
from SECRET import FLAG
def inputs():
print("[WAT] Define diag(u1, u2, u3. u4, u5)")
M = [
[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):
try:
M[i][i] = int(input(f"[WAT] u{i + 1} = "))
except:
return None
return M
def handler(signum, frame):
raise Exception("[WAT] You're trying too hard, try something simpler")
def check(M):
def sign(sigma):
l = 0
for i in range(5):
for j in range(i + 1, 5):
if sigma[i] > sigma[j]:
l += 1
return (-1)**l
res = 0
for sigma in permutations([0,1,2,3,4]):
curr = 1
for i in range(5):
curr *= M[sigma[i]][i]
res += sign(sigma) * curr
return res
def fun(M):
f = [bytes_to_long(bytes(FLAG[5*i:5*(i+1)], 'utf-8')) for i in range(5)]
F = [
[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):
F[i][i] = f[i]
try:
R = np.matmul(F, M)
return np.trace(R)
except:
print("[WAT] You're trying too hard, try something simpler")
return None
def main():
print("[WAT] Welcome")
M = inputs()
if M is None:
print("[WAT] You tried something weird...")
return
elif check(M) == 0:
print("[WAT] It's not going to be that easy...")
return
res = fun(M)
if res == None:
print("[WAT] You tried something weird...")
return
print(f"[WAT] Have fun: {res}")
if __name__ == "__main__":
main()
Analisys
If we enter the command that is given to us in the description, we’ll get prompted for every element of the diagonal of a matrix filled with 0’s.
After we define the diagonal, our matrix will get verified by getting it’s determinant calculated using the check function in the server script.
def main():
print("[WAT] Welcome")
M = inputs()
if M is None:
print("[WAT] You tried something weird...")
return
elif check(M) == 0:
print("[WAT] It's not going to be that easy...")
return
We can see that in the main function, if the determinant is 0, we’ll get kicked out from the server.
Now, the determinant will be 0 if any of the values that we input is 0. So we’ll avoid doing that :)
def fun(M):
f = [bytes_to_long(bytes(FLAG[5*i:5*(i+1)], 'utf-8')) for i in range(5)]
F = [
[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):
F[i][i] = f[i]
try:
R = np.matmul(F, M)
return np.trace(R)
except:
print("[WAT] You're trying too hard, try something simpler")
return None
Looking at the fun function, we see that the flag is being split into 5 chuncks of bytes and then, every chunk is being transformed into a large number. After that, the FLAG matrix is multiplied with the matrix defined by us, returning the sum of the elements that are on the first diagonal, so that gives us some playground.
If we type only 1’s to the server, we’ll get the sum of the chunks of the flag.
Now, using the same thought process as in the picture above, but this time using one 1 and the rest of values being -1’s, we can create a system of 2 ecuations, resulting in us getting one chunk’s value.
Using the thought process presented above, we can get every chunks value. Then we will use this script in order to get the flag:
from Crypto.Util.number import long_to_bytes
a = 504280474484
b = 440157893172
c = 426031081311
d = 163852545397
e = 465806107005
strA = long_to_bytes(a).decode()
strB = long_to_bytes(b).decode()
strC = long_to_bytes(c).decode()
strD = long_to_bytes(d).decode()
strE = long_to_bytes(e).decode()
str = strA + strB + strC + strD + strE
print(str)
Flag
uiuctf{tr4c1ng_&&_mult5!}