played with NullCipher and got 23rd
only solved baby challs cause i was busy lololol
Interpol
source:
#!/usr/bin/env sage
from Crypto.Util.number import *
from flag import flag
def randpos(n):
if randint(0, 1):
return True, [(-(1 + (19*n - 14) % len(flag)), ord(flag[(63 * n - 40) % len(flag)]))]
else:
return False, [(randint(0, 313), (-1) ** randint(0, 1) * Rational(str(getPrime(32)) + '/' + str(getPrime(32))))]
c, n, DATA = 0, 0, []
while True:
_b, _d = randpos(n)
H = [d[0] for d in DATA]
if _b:
n += 1
DATA += _d
else:
if _d[0][0] in H: continue
else:
DATA += _d
c += 1
if n >= len(flag): break
A = [DATA[_][0] for _ in range(len(DATA))]
poly = QQ['x'].lagrange_polynomial(DATA).dumps()
f = open('output.raw', 'wb')
f.write(poly)
f.close()
the randpos
function stands out the most, so lets analyze it
it basically flips a coin and does one of the following:
- a good coord leaking 1 char at random index from our flag
(-some index, ord(flag[another index]))
- a bad coord that we dont need
(+???, ???)
we can differentiate good and bad coords trivially by checking if x is negative!
but first, we need to reverse the lagrange polynomial back to coords
since they’re all small positive integers, we can just brute:
from sage.all import *
with open('output.raw', 'rb') as f:
poly = loads(f.read())
flag = ''
for x in range(0, -100, -1):
y = poly(x)
if y in range(32, 127):
flag += chr(int(y))
print(flag)
which gives
eai_AC{_nCl401TZM39_F30}Oni4n!!hrLTc1!pRtn!nr70aCItn_
next we have to reverse the repermutation logic! …or not. apparently i thought trial and error was a good idea
lets observe the order when flag is abcde
, with their respective index of flag:
x = -2, y = 0
x = -1, y = 3
x = -5, y = 1
x = -4, y = 4
x = -3, y = 2
[(-2, 97), (-1, 100), (-5, 98), (-4, 101), (-3, 99)]
we can see that if we sort by y, and get the char with the corresponding x coord, we will recover abcde
let’s apply that on our ciphertext!
from sage.all import *
with open('output.raw', 'rb') as f:
poly = loads(f.read())
#Note: we know that flag is 53 chars long by amount of valid coords
coords={}
for x in range(0, -100, -1):
y = poly(x)
if y in range(32, 127):
coords[x]=chr(y)
asdf=[]
for j in range(0, 53):
new_x = -(1 + (19*j - 14) % 53)
asdf.append(new_x)
decode_perm=[-1] * 53
for j in range(0, 53):
new_y = (63 * j - 40) % 53
decode_perm[new_y] = asdf[j]
print(''.join(coords[decode_perm[i]] for i in range(53)))
flag: CCTF{7h3_!nTeRn4t10naL_Cr!Min41_pOlIc3_0r9An!Zati0n!}
Vainrat
source:
#!/usr/bin/env sage
import sys
from Crypto.Util.number import *
from flag import flag
def die(*args):
pr(*args)
quit()
def pr(*args):
s = " ".join(map(str, args))
sys.stdout.write(s + "\n")
sys.stdout.flush()
def sc():
return sys.stdin.buffer.readline()
nbit = 110
prec = 4 * nbit
R = RealField(prec)
def rat(x, y):
x = R(x + y) * R(0.5) # x1 = (x0 + y0) / 2
y = R((x * y) ** 0.5) # y1 = sqrt(x1 * y0)
return x, y
def main():
border = "┃"
pr( "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓")
pr(border, ".:: Welcome to Vain Rat challenge! ::. ", border)
pr(border, " You should chase the vain rat and catch it to obtain the flag! ", border)
pr( "┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛")
m = bytes_to_long(flag)
x0 = R(10 ** (-len(str(m))) * m)
while True:
y0 = abs(R.random_element())
if y0 > x0: break
assert len(str(x0)) == len(str(y0))
c = 0
pr(border, f'We know y0 = {y0}')
while True:
pr("| Options: \n|\t[C]atch the rat \n|\t[Q]uit")
ans = sc().decode().strip().lower()
if ans == 'c':
x, y = rat(x0, y0)
x0, y0 = x, y
c += 1
if c <= randint(12, 19):
pr(border, f'Unfortunately, the rat got away :-(')
else: pr(border, f'y = {y}')
elif ans == 'q': die(border, "Quitting...")
else: die(border, "Bye...")
if __name__ == '__main__':
main()
starting from \((x_0, y_0)\), the rat’s coord undergoes the rat(x,y)
transformation and jumps to \((x_1, y_1)\), that is:
next you’ll need divine intervention and come to a conclusion that \( x_1^2 - y_1^2 \) is useful:
$$ \begin{aligned} x_1^2 - y_1^2 &= \frac{x_0^2 + y_0^2}{4} \newline \text{thus}\ x_n^2 - y_n^2 &= \frac{x_0^2 + y_0^2}{4^n} \end{aligned} $$since we have \(y_0\), we can recover \(x_0\)!
$$ \begin{aligned} x_0 = \sqrt{4^n(x_n^2 - y_n^2) - y_0^2} \end{aligned} $$finally we can recover m
by bruting for the flag’s length, and with a little gambling we can recover the flag!
solve script by @killua4564
:
from Crypto.Util.number import long_to_bytes
from pwn import remote
from sage.all import RealField, sqrt
c = 20
prec = 440
R = RealField(prec)
conn = remote("91.107.252.0", "11117")
conn.recvuntil(b"We know y0 = ")
y0 = R(conn.recvuntil(b"\n").strip().decode())
for _ in range(c-1):
conn.sendlineafter(b"[Q]uit", b"C")
conn.sendlineafter(b"[Q]uit", b"C")
conn.recvuntil(b"y = ")
yc0 = R(conn.recvuntil(b"\n").strip().decode())
conn.sendlineafter(b"[Q]uit", b"C")
conn.recvuntil(b"y = ")
yc1 = R(conn.recvuntil(b"\n").strip().decode())
xc0 = R(2) * yc1 ** 2 / yc0 - yc0
x0 = ((xc0 ** 2 - yc0 ** 2) * R(4) ** c + y0 ** 2) ** R(0.5)
# xc1 = yc1 ** 2 / yc0
# x0_squared = ((xc1 ** 2 - yc1 ** 2) * R(4) ** (c + 1) + y0 ** 2) ** R(0.5)
for k in range(1, prec):
flag = long_to_bytes(int(x0 * R(10) ** k))
if flag.startswith(b"CCTF{"):
print(flag.decode())
flag: CCTF{h3Ur1s7!c5_anD_iNv4rIanTs_iN_CryptoCTF_2025!}