Silky - CryptoCTF
In this challenge, we are given the file Silky.sage. It starts by generating some parameters:
def randroad(B):
return vector(ZZ,[randint(-B, B) for _ in range(n)])
def main():
global flag, B, n, D, t
B, n = 5, 19
D, t = 110, 128
l = int(4 * D * B / t)
c, key = 0, randroad(B)
In particular, it generates a key that is a vector of length 19 with integer values in $[-5, 5]$.
Then, this is the main loop of the challenge:
while True:
c += 1
if c >= 12:
die(border, "My brain is fried, quitting...")
pr(f"{border} Options: \n{border}\t[G]et flag! \n{border}\t[M]ake Silky! \n{border}\t[Q]uit")
ans = sc().decode().strip().lower()
if ans == 'm':
R = [silky(key) for _ in range(int(l * t // 2))]
for i in range(len(R) // 16):
pr(border, f"{str(R[16 * i:16 * (i + 1)]).replace(',', '')}")
elif ans == 'g':
pr(border, f'Please submit the secret key: ')
inp = sc().decode().strip()
try:
_key = vector(ZZ, [int(_) for _ in inp.split(',')])
except:
die(border, f'The input you provided is not valid! Bye!!')
if _key == key:
die(border, f'Congrats! You got the flag: {flag}')
else:
die(border, f'Your key is incorrect!')
elif ans == 'q':
die(border, "Quitting...")
else:
die(border, "Bye...")
In order to get the flag, we need to recover the key from 11 rounds at most. By entering the “[M]ake Silly” option, we are given an array (R) consisting of the output of 1088 runs of the silky function on the key. THis is the silky function:
def roadband():
return randroad(B * (D + 1))
def silky(key):
while True:
R = roadband()
_R = R - key
if min(_R) >= - B * D and max(_R) <= B * D:
return R
Let $K = [K_0, K_1, K_2, \dots, K_{18}]$ be the secret key. The silky function returns a random array $R = [R_0, R_1, \dots, R_{18}]$ that satisfies this property:
$$-550 \leq R_i - K_i \leq 550 ~ \forall i \in {0, 1, \dots, 18}$$
which is equivalent to:
$$R_i - 550 \leq K_i \leq R_i + 550 ~ \forall i \in {0, 1, \dots, 18}$$
These inequalities need to be satisfied for every $R$ givenin the array that the “M” option provides. In total, we will have $1088\times11 = 11968$ inequalities. This should be restrictive enough so that the individual components of the secret key can be recovered. This can be achieved with this piece of code (parsing the R array is pretty tedious):
def parse_rows(R):
parsed = []
for row in R:
row = row.strip()
tuples = row.split(b') (')
tuples[0] = tuples[0][2:]
tuples[-1] = tuples[-1][:-1]
parsed_row = []
for t in tuples:
nums = list(map(int, t.split()))
parsed_row.append(vector(ZZ, nums))
parsed.append(parsed_row)
all_Rs = []
for row in parsed:
all_Rs.extend(row)
return all_Rs
key = [-10] * n
while True:
r.sendlineafter(b"[Q]uit\n", b"M")
R = r.recvuntil(b" Options:", drop=True).strip().replace(b"\xe2\x94\x83", b"").replace(b"[", b"").replace(b"]", b"").splitlines()
parsed_R = parse_rows(R)
for j in range(len(key)):
list_left = []
list_right = []
for i in range(len(parsed_R)):
try:
list_left.append(parsed_R[i][j] - 550)
list_right.append(parsed_R[i][j] + 550)
except:
pass
max_left = max(list_left)
min_right = min(list_right)
if min_right == max_left:
key[j] = min_right
if -10 not in key:
break
Once the key is recovered, it is sent with the “G” option and we get the flag:
r.sendlineafter(b"[Q]uit\n", b"G")
r.sendlineafter(b"key: \n", ",".join(map(str, key)).encode())
r.interactive()