We are given the following Python code being run on the remote server:
import numpy as npfrom Crypto.Util.number import bytes_to_longfrom itertools import permutationsfrom SECRET import FLAGdefinputs():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 inrange(5):try: M[i][i] =int(input(f"[WAT] u{i +1} = "))except:returnNonereturn Mdefhandler(signum,frame):raiseException("[WAT] You're trying too hard, try something simpler")defcheck(M):defsign(sigma): l =0for i inrange(5):for j inrange(i +1, 5):if sigma[i]> sigma[j]: l +=1return (-1)**l res =0for sigma inpermutations([0,1,2,3,4]): curr =1for i inrange(5): curr *= M[sigma[i]][i] res +=sign(sigma)* currreturn resdeffun(M): f = [bytes_to_long(bytes(FLAG[5*i:5*(i+1)], 'utf-8'))for i inrange(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 inrange(5): F[i][i] = f[i]try: R = np.matmul(F, M)return np.trace(R)# return sum along the diagonalsexcept:print("[WAT] You're trying too hard, try something simpler")returnNonedefmain():print("[WAT] Welcome") M =inputs()# generates a diagonal matrix with entries of our choiceif M isNone:print("[WAT] You tried something weird...")returnelifcheck(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...")returnprint(f"[WAT] Have fun: {res}")if__name__=="__main__":main()
main() first calls inputs(), which prompts us for inputs. Our input is then used as diagonal entries for a matrix M: M[i][i] = int(input(f"[WAT] u{i + 1} = "))
Then our matrix is checked using check(M). If check(M) == 0, our inputs are rejected. Looking at check(), it is using a permutation algorithm to calculate the determinant of the matrix and returns it. Since our matrix is diagonal, its determinant is the product of the diagonal entries. Hence to pass the check, none of our inputs can be 0.
After the check, the server prints fun(M). The line
f = [bytes_to_long(bytes(FLAG[5*i:5*(i+1)], 'utf-8'))for i inrange(5)]
first breaks the flag into 5 groups of 5 bytes each. Then, each group is converted to a long, giving us an array of 5 longs. Following this, we have:
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 inrange(5): F[i][i] = f[i]
which takes the longs in f and uses them as diagonal entries for F.
fun() then does
R = np.matmul(F, M)return np.trace(R)# return sum along the diagonals
So it multiplies the 5x5 diagonal matrix F (representing the flag) with our given 5x5 diagonal matrix M, obtaining another 5x5 diagonal matrix R. Then we are given the sum of R's diagonal entries.
Therefore,
from pwn import*from Crypto.Util.number import long_to_bytescontext.log_level ="debug"arr = [1,1,1,1,1]elems = []for i inrange(5): p =remote("without-a-trace.chal.uiuc.tf", 1337, ssl=True) new_arr = [1,1,1,1,1] new_arr[i]+=1for j inrange(5): p.sendlineafter(b"= ", str(new_arr[j]).encode()) res = p.recvline() res =int(res[16:-1].decode()) p.close() p =remote("without-a-trace.chal.uiuc.tf", 1337, ssl=True)for j inrange(5): p.sendlineafter(b"= ", str(arr[j]).encode()) res2 = p.recvline() res2 =int(res2[16:-1].decode()) p.close() elems.append(res - res2)print(elems)flag =b""for x in elems: flag +=long_to_bytes(x)print(flag)# uiuctf{tr4c1ng_&&_mult5!}
Even if none of our m's can be 0, we can still obtain the different values of f! In one connection we can set m0=2,m1=m2=m3=m4=1, which would give us res0=2f0+f1+f2+f3+f4 and in another connection we can set m0=m1=m2=m3=m4=1, giving us res1=f0+f1+f2+f3+f4. We can obtain f0 by calculating res0−res1! And repeat this process to obtain f1,f2,f3 and f4. Then, we can use long_to_bytes from Crypto.Util.number to retrieve the bytes within each group.