0%

2025-CryptoCTF-wp-crypto

夺冠!

image-20250716103759017

@Aqua Cat和@Q7实在太猛,赛中好多题都没来得及看就被秒了,所以不少题复现的时候都要从零开始看,流程比较长,所以这篇wp自然就有点拖。

不过也确实很摸 (=´ω`=)

输出文件就都不放了,需要的师傅应该还可以在CRYPTO CTF下载到,或者直接找我拿也行。

Warm-up

Welcome! 👋(361 solves , 27 pts)

题目

题目描述:

1
2
3
4
Welcome!
We just learned Base64, so we hid half the flag with it. Decoding is optional... if you like flags!

CCTF{w3lc0m3_T0_7h3_m4dh0u5e_aDR2M180X1M0bjE3eV81QW5kdzFjaCF9

思路

后半段base64解掉:

1
flag{w3lc0m3_T0_7h3_m4dh0u5e_h4v3_4_S4n17y_5Andw1ch!}



Easy-Peasy

Vinad(211 solves , 34 pts)

题目

题目描述:

1
Vinad's ‘random’ keys are as unpredictable as a cat. Poke its weak spots, steal the flag—chaos included!

题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#!/usr/bin/env python3

from Crypto.Util.number import *
from flag import flag

def parinad(n):
return bin(n)[2:].count('1') % 2

def vinad(x, R):
return int(''.join(str(parinad(x ^ r)) for r in R), 2)

def genkey(nbit):
while True:
R = [getRandomNBitInteger(nbit) for _ in range(nbit)]
r = getRandomNBitInteger(nbit)
p, q = vinad(r, R), getPrime(nbit)
if isPrime(p):
e = vinad(r + 0x10001, R)
if GCD(e, (p - 1) * (q - 1)) == 1:
return (e, R, p * q), (p, q)

def encrypt(message, pubkey):
e, R, n = pubkey
return pow(message + sum(R), e, n)

nbit = 512
pubkey, _ = genkey(nbit)
m = bytes_to_long(flag)
assert m < pubkey[2]
c = encrypt(m, pubkey)

print(f'R = {pubkey[1]}')
print(f'n = {pubkey[2]}')
print(f'c = {c}')

题目流程如下:

  • 生成一个长度为512,其中每个数字均为512bit随机数的列表R以及一个512bit的随机数r
  • 计算p = vinad(r, R)以及e = vinad(r + 0x10001, R)
  • 给出R及公钥n,要求分解n,解RSA得到flag

思路

对于vinad函数,其输出的每一比特其实可以看作是x与对应位置的ri在$GF(2)$下向量相加,得到的向量再与全1向量在$GF(2)$下相乘的结果。照这个思路可以把该函数写成一个$GF(2)$下的矩阵方程如下:

把这个矩阵方程拆开就知道最后输出的结果只会与输入x的1的数量有关系,所以对于确定的R来说,其vinad的输出只会有两种可能,因此ep均在这两种可能中,就可以很轻松的解掉RSA。

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from Crypto.Util.number import *
from output import *

def parinad(n):
return bin(n)[2:].count('1') % 2

def vinad(x, R):
return int(''.join(str(parinad(x ^ r)) for r in R), 2)

p = vinad(1, R)
q = n // p
e = vinad(1, R)
m = pow(c, inverse(e, (p-1)*(q-1)), n)
m = (m - sum(R)) % n
print(long_to_bytes(m))


#CCTF{s0lV1n9_4_Syst3m_0f_L1n3Ar_3qUaTi0n5_0vEr_7H3_F!3lD_F(2)!}


Interpol(129 solves , 45 pts)

题目

题目描述:

1
Only brute force won't crack Interpol! Its massive polynomial guards the flag fiercely.

题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/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()

题目用很奇怪的randpos函数生成了若干个$Q[x]$上的点对,这些点对中部分点的纵坐标和flag字符相关,最后将这些点对插值成一个$Q[x]$上的多项式$f(x)$,要求还原flag。

思路

看上去randpos略有点复杂, 但实际上,其中有用的仅有randint(0,1)=1的那个分支的点对,而这些点的坐标总是绝对值不超过flag长度的负数,因此爆破flag长度,并在$f(x)$上逐个代入这些横坐标,得到的纵坐标是ASCII码值时就可以得到n以及对应的flag字符和索引。

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from Crypto.Util.number import *

PR.<x> = PolynomialRing(QQ)
with open("output.raw", "rb") as f:
c = f.read()
fx = loads(c)

for l in range(20, 100):
if(GCD(l, 19) != 1):
continue
flag = ["*" for _ in range(l)]
for i in range(-l-1, 0):
t = fx(i)
if(32 <= t < 128):
n = (-i + 13) * inverse(19, l) % l
flag[(63*n-40) % l] = chr(t)
if("CCTF" in "".join(flag)):
print("".join(flag))


#CCTF{7h3_!nTeRn4t10naL_Cr!Min41_pOlIc3_0r9An!Zati0n!}


Mechanic(109 solves , 50 pts)

题目

题目描述:

1
Mechanic’s ‘post-quantum’ lock is so easy, even Schrödinger’s cat could crack it, alive and dead.

题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/usr/bin/env python3

from quantcrypt.kem import MLKEM_1024
from quantcrypt.cipher import KryptonKEM
from random import randint
from pathlib import *
from os import urandom
from flag import flag

kem = MLKEM_1024()
kry = KryptonKEM(MLKEM_1024)
pt = Path('/Mechanic/flag.png')
f = open('output.raw', 'wb')
m = randint(2 ** 39, 2 ** 40)
B, c = bin(m)[2:], 0
for b in B:
if b == '1':
pkey, skey = kem.keygen()
ct = Path(f'/flag_{c}.enc')
kry.encrypt(pkey, pt, ct)
pt = ct
c += 1
else:
pkey, skey = urandom(kem.param_sizes.pk_size), urandom(kem.param_sizes.sk_size)
f.write(skey)
f.close()

题目基于MLKEM_1024,随机生成了一个二进制长度为40的比特串,并根据每一比特具体是0还是1进行如下操作:

  • 当前bit为1,生成MLKEM_1024的一对公私钥pkeyskey,将当前明文文件进行加密,并将密文文件作为新的明文文件用于下一次加密
  • 当前bit为0,生成和合法公私钥相同长度的随机串pkeyskey

并且每一次都将skey写入到output.raw里,要求还原最初的明文文件flag.png

思路

要想每一轮都能正常解密,主要就是要区分bit为0生成的随机的skey和真正合法的skey的区别,问一下Gemini会知道skey中会有pkeyh(pkey)的部分,因此可以对每个skey做这样的检验就知道是不是合法skey了。

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from quantcrypt.kem import MLKEM_1024
from quantcrypt.cipher import KryptonKEM
from pathlib import *
import hashlib

def check_skey(skey):
pk = skey[1536 : 1536 + 1568]
h_pk = skey[1536 + 1568 : 1536 + 1568 + 32]
calculated_h_pk = hashlib.sha3_256(pk).digest()
return calculated_h_pk == h_pk

with open(r"1\Mechanic\output.raw", "rb") as f:
skeys = f.read()
skeys = [skeys[3168*i:3168*(i+1)] for i in range(len(skeys)//3168)]

kem = MLKEM_1024()
kry = KryptonKEM(MLKEM_1024)
c = 22
idx = -1
while(c >= 0):
pt = Path(f'1/Mechanic/flag_{c-1}.enc')
ct = Path(f'1/Mechanic/flag_{c}.enc')
if(check_skey(skeys[idx])):
kry.decrypt_to_file(skeys[idx], ct, pt)
c -= 1
idx -= 1


#CCTF{k3y_3NcAp5uL4t!0n_M3cH4n1Sms!}



Getting There

Mancity(98 solves , 54 pts)

题目

题目描述:

1
Decipher Mancity by exploiting RSA modulus secrets, bit by bit, relation by relation.

题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/usr/bin/env python3

from Crypto.Util.number import *
from flag import flag

def man(n):
B = bin(n)[2:]
M = ''
for b in B:
if b == '0':
M += '01'
else:
M += '11'
return int(M, 2)

def keygen(nbit):
while True:
p = getPrime(nbit)
r = man(p)
B = bin(p)[2:] + '1' * nbit
q = int(B, 2)
if isPrime(q) and isPrime(r):
return q, r

nbit = 256
p, q = keygen(nbit)
m = bytes_to_long(flag)
assert m < n
e, n = 1234567891, p * q
c = pow(m, e, n)

print(f'n = {n}')
print(f'c = {c}')

题目要分解RSA的公钥n,而组成n的两个素数生成方式满足:

  • 先生成256bit的随机素数$p$
  • 将$p$的每一bit扩展成两位组成$r$,扩展方式为每一bit后面加一个1
  • 将$p$后面加上256个1,得到$q$
  • $n = qr$

思路

很显然是一个剪枝问题,由于低位有$q$的256个1干扰,所以从高位搜索$p$的比特位即可。

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from Crypto.Util.number import *

nbit = 256
e = 1234567891
n = 147170819334030469053514652921356515888015711942553338463409772437981228515273287953989706666936875524451626901247038180594875568558137526484665015890594045767912340169965961750130156341999306808017498374501001042628249176543370525803456692022546235595791111819909503496986338431136130272043196908119165239297
c = 77151713996168344370880352082934801122524956107256445231326053049976568087412199358725058612262271922128984783428798480191211811217854076875727477848490840660333035334309193217618178091153472265093622822195960145852562781183839474868269109313543427082414220136748700364027714272845969723750108397300867408537

def dfs(h, qh, rh):
r_max = int(rh + "1"*(2*nbit-len(rh)), 2)
r_min = int(rh + "0"*(2*nbit-len(rh)), 2)
q_max = int(qh + "1"*(nbit-len(qh)) + "1"*nbit, 2)
q_min = int(qh + "0"*(nbit-len(qh)) + "1"*nbit, 2)
if(q_max * r_max < n or q_min * r_min > n):
return
if(h == 256):
q, r = q_max, r_max
print(long_to_bytes(pow(c, inverse(e, (q-1)*(r-1)), n)))
exit()
dfs(h+1, qh+"0", rh+"01")
dfs(h+1, qh+"1", rh+"11")

dfs(1, "1", "11")


#CCTF{M4nch3sReR_c0D!ng_wI7H_RSA}


Vainrat(67 solves , 72 pts)

题目

题目描述:

1
Precision is key, Vainrat escapes sluggish calculators effortlessly.

题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#!/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)
y = R((x * y) ** 0.5)
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()

记flag长度为$t$,其对应大整数值为$m$,题目主要流程如下:

  • 生成精度为440的实数域$R$

  • $x_0 = 10^{-t}m$,$y_0$为$R$上的随机元素取绝对值,给出$y_0$

  • 可无限次进行如下迭代:

当迭代次数超过12次时,就有机会获得当前的$y_i$,当超过19次时则一定能获得当前$y_i$,要求还原flag。

思路

由于当超过19次后,我们一定能连续获得每一轮的$y_i$,而关于$x_i$则没有什么信息,因此要考虑的是消去$x_i$,从而把$y_i$关联起来向前回推。

不妨拿$y_1,y_2,y_3$做例子,可以做如下推导:

所以也就有$y_1 = \frac{y_2^3}{2y_3^2 - y_2^2}$成立,因此可以从得到的连续的$y$开始,反复迭代回去,直到求出所有的$y_i$来。

求完所有$y_i$后自然有$y_1$,结合本身就有的$y_0$,可以利用$y_1 = (\frac{x_{0}+y_{0}}{2}y_{0})^{\frac{1}{2}}$求出$x_0$,也就得到了flag。需要注意的是这里m的长度是flag转为大整数后再转为字符串的长度,所以爆破范围需要大一些。

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
from Crypto.Util.number import *
from pwn import *

context.log_level = "critical"
sh = remote("91.107.252.0", 11117)

nbit = 110
prec = 4 * nbit
R = RealField(prec)

sh.recvuntil(b"We know y0 = ")
y0 = R(sh.recvline().strip().decode())
nums = 20
Y = []
for i in range(nums):
sh.sendline(b"c")
for i in range(2):
sh.sendline(b"c")
sh.recvuntil(b"y = ")
Y.append(R(sh.recvline().strip().decode()))

for i in range(nums+20):
y2, y3 = Y[-2], Y[-1]
y1 = (y2^3) / (2*y3^2 - y2^2)
Y = [y1, y2]
if(abs(y1 - y0) < 0.0000000000000000000000000000000001):
print("Find")
break

y0, y1 = Y
x0 = (2*y1^2 - y0^2) / y0

for l in range(20, 500):
m = int(x0 / 10 ** (-l))
if(m < 0):
continue
flag = long_to_bytes(m)
if(b"CCTF" in flag):
print(flag)


#CCTF{h3Ur1s7!c5_anD_iNv4rIanTs_iN_CryptoCTF_2025!}


Matemith(59 solves , 80 pts)

题目

题目描述:

1
Solving Matemith’s polynomial equations is easier than pronouncing ‘Matemith’, but that’s not saying much!

题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#!/usr/bin/env sage

from Crypto.Util.number import *
from flag import flag

l, flag = 14, flag.lstrip(b'CCTF{').rstrip(b'}')
FLAG = [flag[l * i:l * (i + 1)] for i in range(len(flag) // l)]
M = [bytes_to_long(_) for _ in FLAG]
p = getPrime(313)

R.<u, v, w, x, y, z> = PolynomialRing(QQ)

COEFS = [getRandomRange(1, p - 1) for _ in range(21)]

f = COEFS[0] * u * v + COEFS[1] * u + COEFS[2] * v
g = COEFS[3] * u * x * y + COEFS[3] * x + COEFS[5] * y + COEFS[6] * v
h = COEFS[7] * u * w + COEFS[8] * w + COEFS[9] * u
i = COEFS[10] * v * y * z + COEFS[11] * y + COEFS[12] * z + COEFS[13] * w
j = COEFS[14] * v * w + COEFS[15] * v + COEFS[16] * w
k = COEFS[17] * w * z * x + COEFS[18] * z + COEFS[19] * x + COEFS[20] * u

f, g, h, i, j, k = R(f), R(g), R(h), R(i), R(j), R(k)
CNST = [_(M[0], M[1], M[2], M[3], M[4], M[5]) for _ in [f, g, h, i, j, k]]
f, g, h, i, j, k = [[f, g, h, i, j, k][_] + (p - CNST[_]) % p for _ in range(6)]

print(f'{p = }')
print(f'{f = }')
print(f'{g = }')
print(f'{h = }')
print(f'{i = }')
print(f'{j = }')
print(f'{k = }')

题目定义了六个变量$u,v,w,x,y,z$以及六个关于变量的方程$f,g,h,i,j,k$,将flag分为六个14字节的部分并代入六个变量中,得到$GF(p)$下$f,g,h,i,j,k$对应的六个等式,要求解出六个变量值从而还原完整flag。

思路

六个变量六个方程,但是方程不是线性的,所以第一思路就是用groebner试一试:

1
2
3
4
5
6
7
8
9
10
11
12
R.<u, v, w, x, y, z> = PolynomialRing(GF(p))

f =
g =
h =
i =
j =
k =

res = Ideal([f, g, h, i, j, k]).groebner_basis()
for i in res:
print(i.monomials())

可以发现并不能直接得到解,但可以得到下面几个单项构成的新等式:

1
2
3
4
5
6
7
8
9
[y*z^2, y*z, z^2, x, y, z, 1]
[z^3, y*z, z^2, x, y, z, 1]
[x^2, y*z, z^2, x, y, z, 1]
[x*y, y*z, z^2, x, y, z, 1]
[y^2, y*z, z^2, x, y, z, 1]
[x*z, y*z, z^2, x, y, z, 1]
[u, x, y, z, 1]
[v, x, y, z, 1]
[w, x, y, z, 1]

而最后三个等式是线性的,而由于flag的每个部分只有14x8=112bit,在$GF(p)$下是小量,所以可以造格求解一波。

格出来的东西和目标向量$(x,y,z,1,u,v,w)$有一定区别,前三个值做过约简,但可以有$u,v,w$的准确值。

有了这个准确值后,理想中再代回去求一次groebner就行,或者也可以直接解线性方程。但试了一下代回去groebner发现虽然解不出来,但是简化后的多项式的单项变成了:

1
2
3
4
5
6
7
[y^2, y, z, 1]
[y*z, y, z, 1]
[z^2, y, z, 1]
[u, 1]
[v, 1]
[w, 1]
[x, y, z, 1]

由于模数$p$是素数,所以稍微手动一下resultant求根就可以求出$x,y,z$。

exp

1
2
3
4
5
6
7
8
9
10
11
12
R.<u, v, w, x, y, z> = PolynomialRing(GF(p))

f =
g =
h =
i =
j =
k =

res = Ideal([f, g, h, i, j, k]).groebner_basis()
for i in res:
print(i.monomials(), i.coefficients())
1
2
3
4
5
6
7
8
9
10
11
v1 = [1, 9278059260927794625321822521445150709925989348199550227539018734257217333155253421906997535184, 8448761634028956410528911781238729498451220906568524310219824841324137446537586214296021618303, 8832923846876892468664467862366245924256914356941763689573886659181473460260016336177890742462, 6222026266229455098581548668905780547435323354175322166684186804438675930293185506378599302869]
v2 = [1, 2856406640129254927027476906283620669647760161242188164647875301297863984616179666389588786040, 7547885649598994473453601797483929388994770687549010756576201311304175193232991465676229350225, 7063931180022227535019566532096486842628351110756319183931492995289585551986752686035100980252, 8693815681325957620821232151875903538834208184880857286627457793946833993287358258510903277766]
v3 = [1, 1717231562191321157334629945811010410099853821434770073134153869445378776340235294384281516760, 1141764791981958828834229106092156218986720487172049623865150175898307587627569023296679328078, 9284787453738801484991918405952556972015979041247864886372137642820906132986432684976717807617, 1742833796903100907615534838805446594930171572727059706374413348359928730539465306111678325918]
M = Matrix(ZZ, v1[1:]).stack(Matrix(ZZ, v2[1:]).stack(Matrix(ZZ, v3[1:])))
L = block_matrix(ZZ, [
[1, -M.T],
[0, -p]
])
L[3, 3] *= 2^(14*8-1)
L = L.LLL()
U, V, W = list(map(abs, L[3][4:]))
1
2
3
res = Ideal([f, g, h, i, j, k, u-U, v-V, w-W]).groebner_basis()
for i in res:
print(i.monomials())
1
2
3
4
5
6
ff1 = res[0]
ff2 = res[1]
zs = ff1.sylvester_matrix(ff2, y).det().univariate_polynomial().roots(multiplicities=False)
Z = int(min(zs))
ys = ff1.sylvester_matrix(ff2, z).det().univariate_polynomial().roots(multiplicities=False)
Y = int(min(ys))
1
2
res = Ideal([f, g, h, i, j, k, u-U, v-V, w-W, y-Y, z-Z]).groebner_basis()
print(res)
1
2
3
4
5
X = p - 9892984422801315119260311427714389408772405421306235794826916608345176797419211841055778587142
print(long_to_bytes(U) + long_to_bytes(V) + long_to_bytes(W) + long_to_bytes(X) + long_to_bytes(Y) + long_to_bytes(Z))


#CCTF{50lv!n6_7H3_H1dD3n__num8Ers_Pr08l3m_f0r_C51dH_4nd_C5uRf_v14_4uT0m473d_C0pp3r5m17h!!?}

其他

为什么直接groebner会出不来呢?后来想了一下原因,觉得和后面的Goliver_II这个题原因也许一样:这个形式的方程一定有多解——也就是resultant之后的那个高次多项式的另外的根,也都是这个多项式系统的解之一,所以是不可能groebner出一个一次多项式直接得到所有变量的值的。

然后就是flag说的CSIDH-HNP,才想起昨年帮@yolbby验题的时候看过的这篇,可能是因为这题模仿了这篇里构造的等式结构。

不过@yolbby昨年想出的CIHNP恰好在WMCTF赛前不久被idekCTF出掉了XD


Sobata(44 solves , 102 pts)

题目

题目描述:

1
Master Sobata by dissecting ECC secrets, then tame its walk function’s hidden path.

题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#!/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()

def gen_params(nbit):
while True:
p = getPrime(nbit)
if p % 6 == 1:
F = GF(p)
R = [F.random_element() for _ in '01']
a, b = [R[_] ** ((p - 1) // (3 - _)) for _ in [0, 1]]
if a != 1 and b != 1:
c, d = [F.random_element() for _ in '01']
E = EllipticCurve(GF(p), [0, d])
return (p, E, a, b, c)

def walk(P, parameters):
p, E, a, b, c = parameters
x, y = P.xy()
Q = (a * x, b * y)
assert Q in E
return int(c) * E(Q)

def jump(P, n, parameters):
_parameters = list(parameters)
_parameters[-1] = pow(int(_parameters[-1]), n, _parameters[1].order())
return walk(P, _parameters)

def main():
border = "┃"
pr( "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓")
pr(border, ".:: Welcome to the Sobata challenge! ::. ", border)
pr(border, " You should analyze this weird oracle and break it to get the flag", border)
pr( "┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛")
nbit = 512
parameters = gen_params(nbit)
E = parameters[1]
m = bytes_to_long(FLAG)
assert m < parameters[0]
while True:
try:
P = E.lift_x(m)
break
except:
m += 1
while True:
pr("| Options: \n|\t[E]ncrypted FLAG \n|\t[W]alking with P \n|\t[J]umping over P \n|\t[Q]uit")
ans = sc().decode().strip().lower()
if ans == 'e':
_P = walk(P, parameters)
pr(border, f'The encrypted flag is: {_P.xy()}')
elif ans == 'w':
pr(border, 'Please send your desired point over E: ')
Q = sc().decode().strip().split(',')
try:
Q = [int(_) for _ in Q]
except:
die(border, 'Your input is not valid!!')
if Q in E:
pr(border, f'The result of the walk is: {walk(E(Q), parameters).xy()}')
else:
die(border, 'Your point is not on the curve E! Bye!!')
elif ans == 'j':
pr(border, 'Send your desired point over E: ')
Q = sc().decode().strip().split(',')
pr(border, 'Let me know how many times you would like to jump over the given point: ')
n = sc().decode().strip()
try:
Q = [int(_) for _ in Q]
n = int(n)
except:
die(border, 'Your input is not valid!!')
if Q in E:
pr(border, f'The result of the jump is: {jump(E(Q), n, parameters).xy()}')
else:
die(border, 'Your point is not on the curve E! Bye!!')
elif ans == 'q': die(border, "Quitting...")
else: die(border, "Bye...")

if __name__ == '__main__':
main()

题目基于ECC,其基本信息有:

  • 生成参数$p,a,b,c,d$,其中$p$为512bit的素数,$a,b$分别为$Z_p^*$下的三阶和二阶元素,$c,d$均为$Z_p$下的随机值
  • 生成曲线E = EllipticCurve(GF(p), [0, d])
  • 将flag的大整数值$m$做lift到曲线$E$上,记该点为$P$
  • 定义一个walk函数,其接受曲线$E$上的一点$(x,y)$作为输入,输出$c\cdot (ax,by)$
  • 定义一个jump函数,其接受曲线$E$上的一点$(x,y)$以及一个数字$n$作为输入,输出

在此基础上,我们有无限次的交互机会:

  • 输入e,可以获得walk(P)的值
  • 输入w,可以给定曲线$E$上的一点$Q$,靶机回复walk(Q)的值
  • 输入j,可以给定曲线$E$上的一点$Q$以及一个数字$n$,靶机回复jump(Q, n)的值

要求还原flag。

思路

由于题目在交互前啥都不给,所以要先求出曲线参数才行。最开始我们输入e能获得曲线$E$上的一点,对该点反复向后walk就可以获得曲线上的很多点,所以恢复参数不是问题。

而由于$a,b$阶都很小,所以基本可以算是知道,因此要从walk(P)获得flag的话,大概可以有两个出发点:

  • 直接把$c$想办法求出来
  • 想办法利用walkjump把$c$消掉

这里可以把walk看作是两个映射的复合:第一个映射是$\phi:(x,y)\rightarrow (ax,by)$,这是曲线$E$上的自同态;第二个映射则就是普通的$[c]$,只是$c$未知。由于$a,b$的阶分别为3,2,所以$\phi^6$是一个恒等变换,这两种映射是可交换的。

可以看出第二条路非常容易走通,我们只需要令$n$为-1,就可以直接jump回初始点,然后小爆一下$a,b$就可以得到其横坐标了。

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
from Crypto.Util.number import *
from ast import literal_eval
from pwn import *

################################################################### part1 get parameters
sh = process(["sage", "sobata.sage"], level='critical')
sh.recvuntil(b"[Q]uit")
sh.sendline(b'e')
sh.recvuntil(b"The encrypted flag is: ")
P_enc = literal_eval(sh.recvline().strip().decode())

sh.recvuntil(b"[Q]uit")
sh.sendline(b'w')
sh.sendline((str(P_enc[0])+","+str(P_enc[1])).encode())
sh.recvuntil(b"The result of the walk is: ")
P1 = literal_eval(sh.recvline().strip().decode())

sh.recvuntil(b"[Q]uit")
sh.sendline(b'w')
sh.sendline((str(P1[0])+","+str(P1[1])).encode())
sh.recvuntil(b"The result of the walk is: ")
P2 = literal_eval(sh.recvline().strip().decode())

p = GCD((P2[1]^2-P2[0]^3 - (P1[1]^2-P1[0]^3)), (P1[1]^2-P1[0]^3 - (P_enc[1]^2-P_enc[0]^3)))
for i in range(2, 2000):
while(p % i == 0):
p //= i
d = (P2[1]^2-P2[0]^3) % p
E = EllipticCurve(GF(p), [0, d])

################################################################### part2 get P
sh.recvuntil(b"[Q]uit")
sh.sendline(b'j')
sh.sendline((str(P_enc[0])+","+str(P_enc[1])).encode())
sh.sendline(str(-1).encode())
sh.recvuntil(b"The result of the jump is: ")
PP = E(literal_eval(sh.recvline().strip().decode()))

for i in range(10):
a = pow(randint(0, p), (p-1)//3, p)
m = int(PP.x()) * inverse(a^2, p) % p
if(b'CCTF' in long_to_bytes(int(m))):
print(long_to_bytes(int(m)))
break

sh.close()


ASIS Primes(40 solves , 110 pts)

题目

题目描述:

1
ASIS Primes is a cryptography challenge requiring primes with printable, meaningful bytes; can you generate such primes effectively?

题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#!/usr/bin/env python3

import sys
from Crypto.Util.number import *
from random import randint
import string
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()

def is_valid(msg):
msg, charset = msg.decode(), string.printable[:63] + '_{-}'
return all(_ in charset for _ in msg)

def rand_str(l):
charset = string.printable[:63] + '_'
return ''.join([charset[randint(0, 63)] for _ in range(l)])

def main():
border = "┃"
pr( "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓")
pr(border, ".::: Welcome to the ASIS Primes cryptography task! ::.", border)
pr(border, ".: Your mission is to find flag by analyzing the crypto-system :.", border)
pr( "┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛")
global flag
nbit = 512
p, q = [getPrime(nbit) for _ in range(2)]
e = 65537
while True:
pr(f"{border} Options: \n{border}\t[E]ncrypted the flag! \n{border}\t[S]ubmit primes! \n{border}\t[Q]uit")
ans = sc().decode().strip().lower()
if ans == 'e':
m = bytes_to_long(flag)
c = pow(m, e ^ 1, p * q)
pr(f'{c = }')
elif ans == 's':
pinit = f'CCTF{{7H!S_iZ_th3_f1RSt_pRim3__P_f0R_oUr_{nbit}-bit_m0DulU5_{rand_str(randint(5, 40))}'.encode()
qinit = f'CCTF{{7H!S_iZ_th3_s3c0Nd_pRim3_Q_f0R_oUr_{nbit}-bit_m0DulU5_{rand_str(randint(5, 40))}'.encode()
pr(border, f'the condition for the first prime is: {pinit}')
pr(border, f'the condition for the second prime is: {qinit}')
pr(border, f'Please submit the primes p, q: ')
inp = sc().decode().strip()
try:
_p, _q = [int(_) for _ in inp.split(',')]
_pbytes, _qbytes = [long_to_bytes(_) for _ in (_p, _q)]
if (
isPrime(_p) and isPrime(_q)
and _pbytes.startswith(pinit) and _qbytes.startswith(qinit)
and _pbytes.endswith(b'}') and _qbytes.endswith(b'}')
and is_valid(_pbytes) and is_valid(_qbytes)
and (9 * _p * _q).bit_length() == 2 * nbit
):
p, q = _p, _q
except:
pr(border, f'The input you provided is not valid! Try again!!')
nbit += 1
elif ans == 'q':
die(border, "Quitting...")
else:
die(border, "Bye...")

if __name__ == '__main__':
main()

题目大意可以理解为:

  • 生成两组前缀
  • 需要给出素数$p,q$,这两个素数转为字节串后需满足:
    • 前缀分别为给定前缀
    • 均以}结尾
    • 所有字符均需要在指定字符集string.printable[:63] + '_{-}'
    • 比特长度要求(9 * _p * _q).bit_length() == 2 * nbit
  • 给出符合要求的$p,q$后,题目就会用65536当作加密指数,输入的$p,q$当作私钥对flag加密并给出密文。

思路

每失败提交一次,nbit就会对应增大1,所以可以先把nbit刷大一点。

之后似乎除了硬爆好像也没啥好办法TT,这个比特长度还有点小麻烦,所以偷懒直接贴@Q7当时比赛的数据好了。65536的指数用nth_root就行

exp

1
2
3
4
5
6
7
8
9
10
11
12
from Crypto.Util.number import *

p=26733320120995238856304839895484678194426704898905896925993838403995581968792440358015763888549341981307986249115922264098444925695381931461986566249905428980992302236647805499531987048620116008143258580088685332315018711660146220617341
q=1198763533819339555149477327548369647696977533041482983341065069330368735993310543678252306488065219013727082110723383136184073050182776752526905130365004965512519145928550501089821709866911101
c = 754773098769182379406131958221325701451825166766201191377356228568373335373963941180944315885911267976656985457150825900070011990531646115392980205967658689470993643936251892119268312623512199306300234182426179106597500546889158531066728193363625516335675993434663676238609514561095951694157577547288623650389543520215117944436030790067350353975490358783899320232982529707706456517034185959070835747434942013227226242239753879

t = Zmod(p)(c).nth_root(65536, all=True)
for i in t:
print(long_to_bytes(int(i)))


#CCTF{3AzY_RSA_cH4l13n9E_!n_ASIS_CTF!!}


Ikkyu San(31 solves , 134 pts)

题目

题目描述:

1
Ikkyu San, a sage child, developed a unique crypto-system that helps young CTF players identify and choose talented individuals.

题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#!/usr/bin/env sage

from Crypto.Util.number import *
from time 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()

def Ikkyu(nbit):
p = getPrime(nbit)
while True:
a, b = [randint(1, p - 1) for _ in range(2)]
E = EllipticCurve(GF(p), [a, b])
G, H = [E.random_point() for _ in range(2)]
try:
I = E.lift_x(1)
except:
if legendre_symbol(b - a - 1, p) < 0:
return p, E, G, H

def fongi(G, H, P):
try:
xG, xP, yP = G.xy()[0], P.xy()[0], P.xy()[1]
except:
xP = 1337
return int(xP) * G + int(yP) * H + int(xG) * P

def main():
border = "┃"
pr( "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓")
pr(border, "Welcome to the Ikkyu-san challenge!! Your mission is to find the ", border)
pr(border, "flag with given information, have fun and good luck :) ", border)
pr( "┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛")
nbit = 256
pr(border, f'Generating parameters, please wait... ')
p, E, G, H = Ikkyu(nbit)
F = GF(p)
while True:
pr(f"{border} Options: \n{border}\t[E]ncrypted flag!\n{border}\t[R]andom point\n{border}\t[G]et Ikkyu-san point!\n{border}\t[Q]uit")
ans = sc().decode().strip().lower()
if ans == 'g':
pr(border, f"Please provide your desired point `P` on elliptic curve E like x, y: ")
xy = sc().decode()
try:
x, y = [F(int(_)) for _ in xy.split(',')]
P = E(x, y)
except:
pr(border, f"The input you provided is not valid!")
P = E.random_point()
pr(border, f'{fongi(G, H, P) = }')
elif ans == 'r':
pr(border, f'{E.random_point() = }')
elif ans == 'e':
m = bytes_to_long(flag)
assert m < p
pr(border, f'{m * G.xy()[0] * H.xy()[1] = }')
elif ans == 'q':
die(border, "Quitting...")
else:
die(border, "Bye...")

if __name__ == '__main__':
main()

依然是ECC的题目,流程为:

  • 生成256bit的素数$q$,以及$GF(p)$下的随机值$a,b$,并用$a,b$生成曲线$E$

  • 生成曲线$E$上的两个随机点$G,H$

    题目对$E$有些奇怪的要求:

    • 横坐标1不能在曲线上
    • $b-a-1$不能是二次剩余

    但是我的做法似乎都没用到

  • 此外还定义了一个fongi函数,不妨记为$f$,对于作为输入的$E$上的点$P$,返回$f(P) = x_PG + y_PH + x_GP$

参数生成完后,我们有无限次交互机会:

  • 输入g,可以输入$E$上的点$P$,得到$f(P)$
  • 输入r,可以得到$E$上的随机点
  • 输入e,可以得到$GF(p)$下的$m \cdot x_G \cdot y_H$,其中$m$是flag对应的整数值

思路

既然可以随便拿$E$上的点,那么恢复曲线方程是不成问题的,而要解得flag就是要把$G,H$求出来,为此要想办法利用g

而我的思路很简单,只要找$E$上的低阶点作为$P$,那么当$x_GP = O$时,$f(P)= x_PG + y_PH$,此时搜集两个方程就可以解出$G,H$来。

或者也可以构造$P$和$-P$发过去,总之能消掉$P$就可以了

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
from Crypto.Util.number import *
from pwn import *

context.log_level = "critical"

################################################################# recover E
def calc(points):
(x1,y1),(x2,y2),(x3,y3),(x4,y4) = points
t1 = (y2**2-y1**2-x2**3+x1**3)
t2 = (y3**2-y2**2-x3**3+x2**3)
t3 = (y4**2-y3**2-x4**3+x3**3)
k1p = t1*(x3-x2) - t2*(x2-x1)
k2p = t2*(x4-x3) - t3*(x3-x2)
k3p = t1*(x4-x3) - t3*(x2-x1)
p = GCD(k1p,k2p)
p = GCD(p,k3p)

for i in range(2,1000):
while(p % i == 0):
p //= i
a = inverse(x2-x1,p)*t1 % p
b = (y1**2-x1**3-a*x1) % p

return a,b,p

while(1):
sh = remote("91.107.252.0", 37773)
#sh = process(["sage", "Ikkyu_san.sage"])
sh.recvuntil(b"[Q]uit")
points = []
for i in range(4):
sh.sendline(b'r')
sh.recvuntil(b"E.random_point() = (")
points.append([int(i) for i in sh.recvline().strip().decode().split(":")[:2]])

a,b,p = calc(points)
E = EllipticCurve(GF(p), [a,b])
P_list = E(0).division_points(3)
if(len(P_list) == 3):
P_list = [i for i in P_list[1:]]
n = E.order()

xGyH = []
for i in range(2):
sh.sendline(b'g')
msg = str(P_list[i].x()) + "," + str(P_list[i].y())
sh.sendline(msg.encode())
sh.recvuntil(b"fongi(G, H, P) = (")
xGyH.append(E([int(i) for i in sh.recvline().strip().decode().split(":")[:2]]))

xs = [int(i.x()) for i in P_list]
ys = [int(i.y()) for i in P_list]
try:
H = (xs[1] * xGyH[0] - xs[0] * xGyH[1]) * inverse(ys[0]*xs[1] - ys[1]*xs[0], n)
G = (xGyH[0] - ys[0]*H) * inverse(xs[0], n)
except:
print("Next")
sh.close()
continue

sh.recvuntil(b"[Q]uit\n")
sh.sendline(b'e')
sh.recvuntil(b" = ")
c = int(sh.recvline().strip().decode())
flag = c * inverse(int(G.xy()[0] * H.xy()[1]), p) % p

print(long_to_bytes(flag))
sh.close()
#break

sh.close()


#CCTF{prOm!nEn7_Z3n_Ikkyu_p03t?!}


Nahan(8 solves , 316 pts)

题目

题目描述:

1
Nahan hides better than a cat at bath time; do the right thing at the right moment, and it pops out with the flag!

题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#!/usr/bin/env python3

import sys
from Crypto.Util.number import *
from random 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()

def next_prime(n):
while True:
if isPrime(n): return n
else: n += 1

def main():
border = "┃"
pr( "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓")
pr(border, ".::: Welcome to the Nahan Maskara cryptography task! ::.", border)
pr(border, ".: Your mission is to find flag by analysing the Nahan Maskara :.", border)
pr( "┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛")
secret = getPrime(len(flag) << 3)
l, c, step = secret.bit_length(), 0, secret.bit_length() >> 1
while True:
pr(f"{border} Options: \n{border}\t[G]et Nahan value! \n{border}\t[S]end secret! \n{border}\t[Q]uit")
R, _b = [], False
ans = sc().decode().strip().lower()
if ans == 'g':
pr(border, 'Now please provide two integers s, t: ')
inp = sc().decode().strip()
try:
s, t = [int(_) for _ in inp.split(',')]
if all(3 * l > 6 * _.bit_length() > 2 * l for _ in (s, t)):
_b = True
except:
die(border, f"The input you provided is not valid!")
if _b:
r = next_prime(s * t ^ 2 ** l)
if r in R:
die(border, 'You cannot use repeated integers! Bye!!')
else:
R.append(r)
u = list(bin(secret ^ r)[2:])
shuffle(u)
pr(border, f'n = {r * int("".join(u), 2)}')
if c >= step:
die(border, f'You can get Nahan value at most {step} times! Bye!!')
c += 1
else:
die(border, f"Your input does not meet the requirements!!!")
elif ans == 's':
pr(border, "Please send secret: ")
_secret = sc().decode()
try:
_secret = int(_secret)
except:
die(border, "The secret is incorrect! Quitting...")
if _secret == secret:
die(border, f"Congrats, you got the flag: {flag}")
else:
die(border, "The secret is incorrect! Quitting...")
elif ans == 'q':
die(border, "Quitting...")
else:
die(border, "Bye...")

if __name__ == '__main__':
main()

题目首先生成比特长度为$l$的secret,记为$d$,并且给了其$\frac{l}{2}$次交互次数,每次交互内容为:

  • 输入$s,t$,其比特长度$k$均需满足$83 < k < 124$
  • 计算$(st) \oplus 2^l$,并取nextprime作为$r$,每次的$r$不能重复
  • 给出$d \oplus r$的汉明重量

这里可以先测出来secret长度是248,交互次数是124

我们要在$\frac{l}{2}$次交互内恢复$d$,并提交给靶机得到flag。

思路

由于每次交互是给$d \oplus r$的汉明重量,并且$r$已知,因此我想到了SEETF的Neutrality这一题,也许能将0、1映射到1、-1然后LLL求解。但实际测试会发现,在这个问题下,方程仅有变量数的一半的时候恢复效果很一般,所以这个思路走不通。

测了一下如果再多30次左右交互,这种方法效果才好起来

而与Neutrality一题不同的点在于,这个题目的$r$并非完全随机,而是可以自己构造的,而@hashhash正好之前看过@Aqua Cat给他分享的一道算法题和这个有关:

https://www.luogu.com/article/1dzympve

@hashhash测试了一下,发现这个可以做到64次交互恢复192bit,而剩下的bit可以用多出来的很多交互次数一个个点就行,唯一的问题在于这64次发送的数据都必须是给定的,低位和高位可以有一些扰动。

想了一下这个还是可以做到,由于$r - \delta = (st) \oplus 2^l$,就有$(r - \delta) \oplus 2^l = st $,所以可以爆破$i$,尝试分解多个$(r+i) \oplus 2^l$,来得到满足要求的$s,t$即可。

因为低位可以扰动

我这题也就完成了这一小部分,给@hashhash打打杂把每一轮要输入的$s,t$给他,因为算法的部分看着比较头大(´・ω・`)



Silky(40 solves , 110 pts)

题目

题目描述:

1
Silky’s ‘noisy’ equations are like static on a radio—annoying, but solvable if you tune just right.

题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#!/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()

def randroad(B):
return vector(ZZ,[randint(-B, B) for _ in range(n)])

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

def main():
border = "┃"
pr( "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓")
pr(border, ".::: Welcome to the Silky cryptography oracle task! :::.", border)
pr(border, "Your mission is to find flag by analyzing this weird oracle! :-) ", border)
pr( "┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛")
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)
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...")

if __name__ == '__main__':
main()

题目流程为:

  • 生成一个长为19的向量key,记为$\mathbf{k}$,其中每个分量均为$[-5,5]$中的随机整数
  • 有最多10次交互机会,可以输入m,拿到1088组silky(key)
  • 如果可以提交正确的$\mathbf{k}$,就可以拿到flag

其中,silky(key)生成的是满足如下两点要求长为19的向量$\mathbf{r}_i$:

  • 所有分量均在$[-555, 555]$中随机取值
  • $\mathbf{r}_i - \mathbf{k}$的每个分量需落在$[-550, 550]$中

思路

将$\mathbf{k}$的每个分量分开来看的话,10次交互机会等于是给了10880次关于该分量的不等式约束,比较简单的思路是对$[-5,5]$的整数逐个检验是否符合所有不等式约束即可。

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
from pwn import *
from ast import literal_eval

context.log_level = "critical"

sh = process(["sage", "Silky.sage"])

B, n = 5, 19
D, t = 110, 128
l = int(4 * D * B / t)
R = []
for _ in range(10):
sh.recvuntil(b"[Q]uit\n")
sh.sendline(b"m")
lR = int(l * t // 2)
for i in range(lR // 16):
R += literal_eval(sh.recvline().strip().decode().replace(" ", ",")[2:])

def check(ki, li):
Y = 1
for _ in li:
if(_ - ki < - B * D or _ - ki > B * D):
Y = 0
if(Y == 1):
return True

key = []
for i in range(n):
li = [r[i] for r in R]
for ki in range(-B, B+1):
if(check(ki, li)):
key.append(ki)
break

sh.recvuntil(b"[Q]uit\n")
sh.sendline(b"g")
sh.sendline(str(key)[1:-1].encode())
sh.recvuntil(b"Congrats! You got the flag: ")
print(sh.recvline())
sh.close()


Toffee(36 solves , 119 pts)

题目

题目描述:

1
Sticky situation: Toffee’s crypto seems sweet, until you’re stuck chewing on its unsolvable core.

题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#!/usr/bin/env sage

import sys
from Crypto.Util.number import *
from hashlib import sha512
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()

def sign(msg, skey):
global k
h = bytes_to_long(sha512(msg).digest())
k = toffee(u, v, k)
P = k * G
r = int(P.xy()[0]) % _n
s = inverse(k, _n) * (h + r * skey) % _n
return (r, s)

def toffee(u, v, k):
return (u * k + v) % _n

def main():
border = "┃"
pr( "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓")
pr(border, ".::: Welcome to the Toffee chocolate cryptography task! ::.", border)
pr(border, ".: Your mission is to find flag by analyzing the signatures! :.", border)
pr( "┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛")
global flag, u, v, k, _n, G
skey = bytes_to_long(flag)
p = 0xaeaf714c13bfbff63dd6c4f07dd366674ebe93f6ec6ea51ac8584d9982c41882ebea6f6e7b0e959d2c36ba5e27705daffacd9a49b39d5beedc74976b30a260c9
a, b = -7, 0xd3f1356a42265cb4aec98a80b713fb724f44e747fe73d907bdc598557e0d96c5
_n = 0xaeaf714c13bfbff63dd6c4f07dd366674ebe93f6ec6ea51ac8584d9982c41881d942f0dddae61b0641e2a2cf144534c42bf8a9c3cb7bdc2a4392fcb2cc01ef87
x = 0xa0e29c8968e02582d98219ce07dd043270b27e06568cb309131701b3b61c5c374d0dda5ad341baa9d533c17c8a8227df3f7e613447f01e17abbc2645fe5465b0
y = 0x5ee57d33874773dd18f22f9a81b615976a9687222c392801ed9ad96aa6ed364e973edda16c6a3b64760ca74390bb44088bf7156595f5b39bfee3c5cef31c45e1
F = FiniteField(p)
E = EllipticCurve(F, [a, b])
G = E(x, y)
u, v, k = [randint(1, _n) for _ in ';-)']
while True:
pr(f"{border} Options: \n{border}\t[G]et toffee! \n{border}\t[S]ign message! \n{border}\t[Q]uit")
ans = sc().decode().strip().lower()
if ans == 'g':
pr(border, f'Please let me know your seed: ')
_k = sc().decode().strip()
try:
_k = int(_k)
except:
die(border, 'Your seed is not valid! Bye!!')
pr(f'{toffee(u, v, _k) = }')
elif ans == 's':
pr(border, f'Please send your message: ')
msg = sc().strip()
r, s = sign(msg, skey)
pr(border, f'{r = }')
pr(border, f'{s = }')
elif ans == 'q':
die(border, "Quitting...")
else:
die(border, "Bye...")

if __name__ == '__main__':
main()

题目给定曲线$E$,随机生成三个数$u, v, k$,在此基础上进行ECDSA签名,每次签名的$k$会在模曲线阶$n$下线性递推,即$k_{i+1} = uk_i + v$。依然有无限次交互机会:

  • 输入g,可以再输入一个数$x$,返回$(ux+v) \% n$
  • 输入s,可以再输入一个消息,并得到对该消息的签名$(r,s)$

我们需要获得作为私钥的flag。

思路

思路实在太直白了,首先输入两次g把$u,v$拿到,之后再签名两次,利用线性$k$的关系式把$k$消掉就能拿到flag。

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
from Crypto.Util.number import *
from hashlib import sha512

p = 0xaeaf714c13bfbff63dd6c4f07dd366674ebe93f6ec6ea51ac8584d9982c41882ebea6f6e7b0e959d2c36ba5e27705daffacd9a49b39d5beedc74976b30a260c9
a, b = -7, 0xd3f1356a42265cb4aec98a80b713fb724f44e747fe73d907bdc598557e0d96c5
_n = 0xaeaf714c13bfbff63dd6c4f07dd366674ebe93f6ec6ea51ac8584d9982c41881d942f0dddae61b0641e2a2cf144534c42bf8a9c3cb7bdc2a4392fcb2cc01ef87
x = 0xa0e29c8968e02582d98219ce07dd043270b27e06568cb309131701b3b61c5c374d0dda5ad341baa9d533c17c8a8227df3f7e613447f01e17abbc2645fe5465b0
y = 0x5ee57d33874773dd18f22f9a81b615976a9687222c392801ed9ad96aa6ed364e973edda16c6a3b64760ca74390bb44088bf7156595f5b39bfee3c5cef31c45e1
F = FiniteField(p)
E = EllipticCurve(F, [a, b])
G = E(x, y)

from pwn import *

context.log_level = "critical"
sh = process(["sage", "Toffee.sage"])

################################################################################# part1 get u, v
sh.recvuntil(b"[Q]uit")
sh.sendline(b"g")
sh.sendline(b"1")
sh.recvuntil(b"toffee(u, v, _k) = ")
u_v = int(sh.recvline().strip().decode())

sh.recvuntil(b"[Q]uit")
sh.sendline(b"g")
sh.sendline(b"2")
sh.recvuntil(b"toffee(u, v, _k) = ")
u2_v = int(sh.recvline().strip().decode())
u = (u2_v - u_v) % _n
v = (u_v - u) % _n


################################################################################# part2 sign 2 msg
sh.recvuntil(b"[Q]uit")
sh.sendline(b"s")
sh.sendline(b"1")
sh.recvuntil(b"r = ")
r1 = int(sh.recvline().strip().decode())
sh.recvuntil(b"s = ")
s1 = int(sh.recvline().strip().decode())
h1 = bytes_to_long(sha512(b"1").digest())

sh.recvuntil(b"[Q]uit")
sh.sendline(b"s")
sh.sendline(b"2")
sh.recvuntil(b"r = ")
r2 = int(sh.recvline().strip().decode())
sh.recvuntil(b"s = ")
s2 = int(sh.recvline().strip().decode())
h2 = bytes_to_long(sha512(b"2").digest())


################################################################################# part3 get flag
secret = (s1*h2 - s1*s2*v - h1*s2*u) * inverse(r1*s2*u - s1*r2, _n) % _n
print(long_to_bytes(int(secret)))


Lowdown(30 solves , 138 pts)

题目

题目描述:

1
These 0s and 1s have the digital handwriting of a drunk Ph.D., trace their signature, sign your own name, and leak their crayon-scrawled Lowdown.

题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
#!/usr/bin/env sage

import sys, string
from Crypto.Util.number import *
from hashlib import sha1
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()

def h(a):
if a == 0:
return 0
else:
g = F.gen()
for _ in range(1, 256):
if g ** _ == a:
return _

def H(M):
assert M.nrows() == M.ncols()
k, _H = M.nrows(), []
for i in range(k):
for j in range(k):
_h = h(M[i, j])
_H.append(bin(_h)[2:].zfill(8))
return ''.join(_H)

def Hinv(m, k):
B = bin(m)[2:].zfill(8 * k**2)
g = F.gen()
_H = [int(B[8*i:8*i + 8], 2) for i in range(k**2)]
_M = [0 if _h == 0 else g ** _h for _h in _H]
M = Matrix(F, [[a for a in _M[k*i:k*i + k]] for i in range(k)])
return M

def M2i(M):
_H = H(M)
return int(_H, 2)

def random_oracle(msg):
_h = sha1(msg).digest()
return bytes_to_long(_h)

def makey(k):
while True:
g = random_matrix(F, k)
if g.is_invertible():
ng = 1 << 192 # g.order()
break
r, a = [randint(2, ng - 2) for _ in '01']
gg = g ** r
pkey, skey = (g, gg ** a), r
return(pkey, skey)

def sign(pkey, skey, msg):
g, ga = pkey
ng = 1 << 192 # g.order()
_h = random_oracle(msg)
assert _h <= ng
_g = g ** skey
n = randint(2, ng - 2)
s, t = ga * (_g.inverse()) ** (n * _h), _g ** n
return (s, t)

def verify(sgn, pkey, msg):
_, ga = pkey
s, t = sgn
_h = random_oracle(msg)
return s * t ** _h == ga

def main():
border = "┃"
pr("┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓")
pr(border, "Hi all, now it's time to sign a given message in a strange signature", border)
pr(border, "schema. You will receive the flag if you are able to sign a message.", border)
pr("┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛")

global F, k
F = GF(256)
k = 10
pkey, skey = makey(k)

msg = ''.join([string.printable[randint(0, 85)] for _ in range(40)]).encode()

while True:
pr("| Options: \n|\t[G]et the flag \n|\t[P]ublic Key \n|\t[S]ign a message \n|\t[V]erify signature \n|\t[Q]uit")
ans = sc().decode().lower().strip()
if ans == 'g':
pr(border, 'You should send the valid signature for my given message!')
pr(border, f'Message = {msg}')
pr(border, 'Send the signature of the above message: ')
_sgn = sc().split(b',')
try:
_s, _t = [int(_) for _ in _sgn]
sgn = (Hinv(_s, k), Hinv(_t, k))
if verify(sgn, pkey, msg) and str(_s).startswith('13') and str(_t).startswith('37'):
pr(border, f'Congratulation! You got the flag!')
die(border, f'flag = {flag}')
else:
pr(border, 'Your signature is not correct!')
except:
die(border, 'Exiting...')
elif ans == 's':
pr(border, 'Send your message to sign: ')
_msg = sc().strip()
if len(_msg) >= 10:
die(border, 'Sorry, I sign only short messages! :/')
_s, _t = sign(pkey, skey, _msg)
pr(border, f's = {M2i(_s)}')
pr(border, f't = {M2i(_t)}')
elif ans == 'v':
pr(border, 'Send your signature to verify: ')
_sgn = sc().split(b',')
try:
_s, _t = [int(_) for _ in _sgn]
_sgn = (Hinv(_s, k), Hinv(_t, k))
pr(border, 'Send your message: ')
_msg = sc().strip()
if verify(_sgn, pkey, _msg):
pr(border, 'Your message successfully verified :)')
else:
pr(border, 'Verification failed :(')
except:
pr(border, 'Try to send valid signature!')
continue
elif ans == 'p':
_g, _ga = pkey
pr(border, f'g = {M2i(_g)}')
pr(border, f'ga = {M2i(_ga)}')
elif ans == 'q':
die(border, 'Quitting...')
else:
die(border, 'You should select valid choice!')

if __name__ == '__main__':
main()

这个题看上去似乎比较麻烦,但是实际上并不是。总的来说题目基于一个奇怪的签名,我们可以获取任意长度小于10的消息签名值,要伪造一个长度为40的随机msg的签名,并且该签名对应的整数字符串前缀要满足分别为1337,从而获得flag。其中签名的操作如下:

  • 首先生成一组密钥$(g,g^{ar},r)$,其中$(g,g^{ar})$为公钥,$r$为私钥,$g$为$GF(2^8)$下的随机十阶可逆矩阵,$a,r$为$[2,2^{192}-2]$的随机值

  • 对消息$m$,其签名值$s,t$满足:

    其中$n$就是nonce,每次签名在$[2,2^{192}-2]$中随机取值

验签就验证$s\cdot t^{h(m)}$和$g^{ar}$相等即可,这很显然是成立的。

思路

再仔细看下他的验签函数:

1
2
3
4
5
def verify(sgn, pkey, msg):
_, ga = pkey
s, t = sgn
_h = random_oracle(msg)
return s * t ** _h == ga

可以看出这个验签函数实在过于简单了,没有对输入做任何可能的检查,因此伪造方式也非常容易,只需要先随意输入消息$m_1$,并获取到对他的签名$(s_1,t_1)$,此时就有:

直接随意选$t_2$,先使其满足前缀37的要求,然后计算:

然后令$s_2 = \frac{s_1}{k}$,多爆破几组使得其满足前缀13的要求就可以了。

公钥都不需要拿

这里有一个坑点在于他的msg本身就.encode()过,所以再从靶机发送过来会套两层b"",收到之后不能简单.decode()就结束。

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from pwn import *
from ast import literal_eval

context.log_level = "critical"
sh = process(["sage", "lowdown.sage"])

#################################################################################### part1 get valid signature
sh.recvuntil(b"[Q]uit")
sh.sendline(b's')

sh.sendline(b"1")
sh.recvuntil(b"s = ")
s1 = int(sh.recvline().strip().decode())
sh.recvuntil(b"t = ")
t1 = int(sh.recvline().strip().decode())
h1 = random_oracle(b"1")
s1 = Hinv(s1, k)
t1 = Hinv(t1, k)


#################################################################################### part2 forge signature
sh.recvuntil(b"[Q]uit")
sh.sendline(b'g')
sh.recvuntil(b"Message = ")
msg = literal_eval(sh.recvline().strip().decode())
h2 = random_oracle(msg)
while(1):
t2 = random_matrix(F, k)
if t2.is_invertible() and str(M2i(t2)).startswith('37'):
kk = (t2**h2) / (t1**h1)
s2 = s1 / kk
if(str(M2i(s2)).startswith('13')):
break
sh.sendline((str(M2i(s2)) + "," + str(M2i(t2))).encode())
sh.recvuntil(b"flag = ")
print(sh.recvline())

sh.close()


Mechanic II(28 solves , 146 pts)

题目

题目描述:

1
Mechanic II cranks PQC to hilarious levels, bring a quantum wrench and a PhD in pain.

题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#!/usr/bin/env python3

from quantcrypt.kem import MLKEM_1024
import sys, os, string
from random import randint
import hashlib
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()

def rand_str(l):
charset = string.printable[:63] + '_'
return ''.join([charset[randint(0, 63)] for _ in range(l)]).encode()

def pow():
head = rand_str(15)
tail = rand_str(4)
h = hashlib.sha3_256(head + tail).hexdigest()
return head, h

def main():
border = "┃"
pr( "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓")
pr(border, ".::: Welcome to the Mechanic II cryptography task! ::.", border)
pr(border, "Your mission is to find flag by analyzing this amazing Oracle! :)", border)
pr( "┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛")
head, h = pow()
pr(border, f'Please pass the proof of work first: {head, h}')
_tail = sc().strip()
if hashlib.sha3_256(head + _tail).hexdigest() == h:
_b = True
else:
die(border, 'Your should pass the POW! Bye!!')
kem = MLKEM_1024()
c, n = 0, 1337
KEY_PAIR = [kem.keygen() for _ in range(n)]
SKEYS = [KEY_PAIR[_][1] for _ in range(n)]
r = randint(0, n - 1)
cipher, shasec = kem.encaps(KEY_PAIR[r][0])
secret = hashlib.sha3_256(shasec + hashlib.sha3_256(shasec + str(r).encode()).digest()).hexdigest()
while _b:
if c > 3 * n:
die(border, 'The server is need to rest :/')
pr(f"{border} Options: \n{border}\t[D]ecrypt cipher \n{border}\t[R]andomize a secret key! \n{border}\t[S]ubmit the secret \n{border}\t[Q]uit")
ans = sc().decode().strip().lower()
c += 1
if ans == 'd':
pr(border, 'Please select an ID: ')
_id = sc().decode().strip()
try:
_id = int(_id)
_shasec = kem.decaps(SKEYS[_id], cipher)
except:
die(border, 'Your input ID is invalid! Bye!!')
pr(border, f'{_shasec = }')
elif ans == 'r':
pr(border, 'Please select an ID: ')
_id = sc().decode().strip()
try:
_id = int(_id)
_skey = SKEYS[_id][:-32] + os.urandom(32)
except:
die(border, 'Your input ID is invalid! Bye!!')
SKEYS.append(_skey)
elif ans == 's':
pr(border, 'Please send the secret: ')
_secret = sc().decode().strip()
if _secret == secret:
die(border, f'Congrats, you got the flag: {flag}')
else:
die(border, 'Your secret is incorrect! Bye!!')
elif ans == 'q':
die(border, "Quitting...")
else:
die(border, "Bye...")

if __name__ == '__main__':
main()

题目和前置题Mechanic一样,依然基于MLKEM_1024,只不过变成了交互题,看上去就长了很多。其流程为:

  • 通过一个简单的哈希爆破pow
  • 生成$n=1337$对密钥,并把私钥按顺序存放在列表SKEYS
  • 生成$[0,1336]$的随机索引$r$,封装该密钥得到cipher, shasec = kem.encaps(KEY_PAIR[r][0])
  • 计算secret = hashlib.sha3_256(shasec + hashlib.sha3_256(shasec + str(r).encode()).digest()).hexdigest()

我们有$3n+1$次交互机会:

  • 输入d,可以再输入一个ID,靶机用该位置的私钥对cipher解封装,并给出解封装得到的shasec
  • 输入r,可以再输入一个ID,靶机将该位置的私钥的末32字节改成随机字节,并添加到列表SKEYS末尾
  • 输入s提交secret,正确就拿到flag

思路

由于交互部分能做的事情很有限,手动试一试就能试出来一个事实:对于正确的索引$r$来说,其私钥末32字节无论如何变动,解封装得到的shasec都是一样的,而索引错误时shasec则会变动,因此最坏的情况$3n$次交互也刚好可以获得正确的shasec和$r$了。

至于原理为什么是这样,问了一下Gemini说这似乎是Kyber的Key Mismatch Attack,但是具体是不是以及为什么是我就不太懂了,以后有机会再研究。

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
from pwn import *
from itertools import *
import string
from hashlib import sha3_256
from ast import literal_eval
from tqdm import *

context.log_level = "critical"
charset = string.printable[:63] + '_'

sh = process(["python3", "mechanic_ii.py"])
sh.recvuntil(b"Please pass the proof of work first: ")
head, h = literal_eval(sh.recvline().strip().decode())
for i in product(charset, repeat=4):
tail = "".join(i)
if(sha3_256(head + tail.encode()).hexdigest() == h):

sh.sendline(tail.encode())

for i in trange(1337):
sh.recvuntil(b"[Q]uit\n")
sh.sendline(b"d")
sh.sendline(str(i).encode())
sh.recvuntil(b"_shasec = ")
ss1 = sh.recvline().strip().decode()

sh.recvuntil(b"[Q]uit\n")
sh.sendline(b"r")
sh.sendline(str(i).encode())

sh.recvuntil(b"[Q]uit\n")
sh.sendline(b"d")
sh.sendline(str(i+1337).encode())
sh.recvuntil(b"_shasec = ")
ss2 = sh.recvline().strip().decode()

if(ss1 == ss2):
shasec = literal_eval(ss1)
secret = hashlib.sha3_256(shasec + hashlib.sha3_256(shasec + str(i).encode()).digest()).hexdigest()
sh.recvuntil(b"[Q]uit\n")
sh.sendline(b"s")
sh.sendline(secret.encode())
sh.recvuntil(b"Congrats, you got the flag: ")
print(sh.recvline())
break

sh.close()


Mitram(24 solves , 164 pts)

题目

题目描述:

1
Solving Mitram’s equations? Time to row reduce your expectations... and your sanity.

题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#!/usr/bin/env sage

from flag import flag

q, v, m = 256, 40, 14
_F = GF(q)

def makeup(M, n):
for i in range(n):
for j in range(i, n):
M[i, j] += M[j, i]
M[j, i] = 0
return M

def mitramap():
_M = []
for s in range(m):
M = zero_matrix(_F, v + m, v + m)
for i in range(0, v):
M[i, (i + s + 1) % v] = _F.random_element()
M[i, (i + s) % m + v] = _F.random_element()
M = makeup(M, v + m)
_M.append(M)
return _M

def n2F(n):
x = _F.gen()
e = sum(((n >> _) & 1) * x ** _ for _ in range(8))
return e

def embed_secret(msg, v, m):
M = random_matrix(_F, v, m)
for _ in range(v):
M[_, 0] = n2F(msg[_])
return M

def maketrig():
return block_matrix([[identity_matrix(_F, v), embed_secret(flag, v, m)], [zero_matrix(_F, m, v), identity_matrix(_F, m)]])

def makepub(F, S):
S = S.submatrix(0, v, v, m)
Z = zero_matrix(_F, m, v)
return [
block_matrix([
[G := M.submatrix(0, 0, v, v), (G + G.transpose()) * S + (H := M.submatrix(0, v, v, m))],
[Z, makeup(S.transpose() * G * S + S.transpose() * H, m)]
])
for M in F[:m]
]

F, S = mitramap(), maketrig()
P = makepub(F, S)

print(f'{dumps(F) = }')
print(f'{dumps(P) = }')

题目流程为:

  • 在$GF(2^8)$下生成$m$个$(v+m,v+m)$的矩阵,存放在列表F

  • 生成一个$(v+m,v+m)$的$S$矩阵,其结构为:

    其中子矩阵$S_{v, m}$的第一列嵌入了flag

  • 计算公钥矩阵列表P,其中每个矩阵结构满足:

    其中$G,H$都是列表F中对应索引的矩阵的子矩阵,makeup是如下函数:

    1
    2
    3
    4
    5
    6
    def makeup(M, n):
    for i in range(n):
    for j in range(i, n):
    M[i, j] += M[j, i]
    M[j, i] = 0
    return M

给出FP,要解出$S_{v, m}$,然后用其第一列的值还原flag。

思路

又是一个看上去有点复杂,但是实际上做法特别简单的题目,注意到公钥矩阵列表P中,每一个矩阵的右上角分块为:

而$G,H$都是可以从F中提取的,所以直接找$G+G^T$可逆的组,然后就可以直接解出$S_{v, m}$了。

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
dF = 
dP =

q, v, m = 256, 40, 14
_F = GF(q)

F = loads(dF)
P = loads(dP)

for i in range(m):
G = F[i].submatrix(0, 0, v, v)
H = F[i].submatrix(0, v, v, m)
K = P[i].submatrix(0, v, v, m)

if((G+G.T).rank() == v):
S = (G+G.T)^(-1)*(K-H)
s = S[:, :1].list()
flag = ""
for j in s:
flag += chr(j.to_integer())
print(flag)
break


#CCTF{Un8reAk4Bl3_MQ-Sign_crYpT0_ma9iC!!}


Asemoon(10 solves , 285 pts)

题目

题目描述:

1
No entry until Asemoon’s token breaks! Can you reverse its secrets?

题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#!/usr/bin/env sage

from Crypto.Util.number import *
import time, sys
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()

def gen_CP(nbit):
R.<x> = PolynomialRing(GF(2))
while True:
c = ''.join([hex(randint(0, 15))[2:] for _ in range(nbit >> 2)])
G = R(Integer(int(c, 16)).bits()[::-1]) + x ^ nbit
if G.is_irreducible():
return int(c, 16)

def recheck(msg, v):
for m in msg:
v = v ^^ m
v = (v >> 8) ^^ CT[v & 0xFF]
return v & 0xFFFFFFFFFFFFFFFF

def verify(v, t):
return recheck(long_to_bytes(skey ^^ t), v) == v

def main():
border = "┃"
pr( "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓")
pr(border, ".:: Welcome to the Asemoon challenge! ::. ", border)
pr(border, " You should analyze this login oracle and break it to get the flag", border)
pr( "┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛")
global nbit, skey, CT
nbit = 64
skey = getRandomNBitInteger(nbit)
CP, CT = gen_CP(nbit), []
for _ in range(256):
cb = _
for _ in range(8):
if cb & 1:
cb = (cb >> 1) ^^ CP
else:
cb >>= 1
CT.append(cb & 0xFFFFFFFFFFFFFFFF)
while True:
pr(f"{border} Options: \n{border}\t[L]ogin \n{border}\t[P]ublic information! \n{border}\t[Q]uit")
ans = sc().decode().strip().lower()
if ans == 'l':
pr(border, 'Please send your hex token here:')
token = sc().decode().strip()
try:
token = int(token, 16)
except:
die(border, "Your token is invalid! Bye!!")
if 0 <= token < 2**nbit:
t = int(time.time())
t -= t % 10
if verify(token, t):
die(border, f"Congrats, you got the flag: {flag}")
die(border, "Your token is wrong!")
elif ans == 'p':
t = int(time.time())
t -= t % 10
pub_1 = recheck(b"CCTF", recheck(long_to_bytes(skey ^^ t), 0))
while True:
pub_2 = getPrime(nbit)
if pub_2 > CP:
break
pub_3 = pow(5, CP, pub_2)
pr(border, f'The first public information is: {pub_1}')
pr(border, f'The second public information is: {pub_2}')
pr(border, f'The third public information is: {pub_3}')
elif ans == 'q': die(border, "Quitting...")
else: die(border, "Bye...")

if __name__ == '__main__':
main()

连接上靶机后,靶机先做下面的事情:

  • 生成64bit的skey,记为$s$,并随机生成一个度为64的不可约多项式$G$,记其整数值为CP

  • 定义一个recheck函数,接受输入msgv,实际上就是对msg做初始值为v的CRC64,所以后面直接把recheck直接记为CRC

    这里预计算的CT应该就是为了加快计算

之后有无限次交互机会:

  • 输入l,可以再输入一个token,记其值为$v$,如果满足$CRC(s \oplus t, v) == v$,就能拿到flag,其中$t$是当前时间的整数值去掉个位,可以看作是已知的
  • 输入g,可以得到:
    • $y = CRC(m, CRC(s \oplus t, 0))$的值,其中m = b"CCTF"是固定的
    • 64bit的随机素数$p$以及$5^{CP} \% p$

思路

首先要拿到CRC的模多项式$G$,也就是要拿到CP,这里不知道为什么不直接给,还要搞个花里胡哨的pub_2pub_3出来,总之简单DLP一下就能拿到$G$。

拿到$G$之后,在模$G$的商环下,参考这篇能知道,题目的recheck(msg,v)可以写成如下等式:

其中$M$是输入msg对应的多项式,$I$是输入v对应的多项式,$b$是8倍输入msg的字节长度

所以对于我们输入g拿到的值$y$对应的多项式$Y$,记$s \oplus t$对应的多项式为$M1$,m = b"CCTF"对应的多项式为$M_2$,有:

而我们需要的token对应的多项式$J$则满足:

因此利用$Y$将$M_1x^{64}$求出来即可求出$J$,转回整数再发送给靶机就能拿到flag。

exp

不知道为啥是概率成功的,CRC部分可能写的有点小bug:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
from Crypto.Util.number import *

##################################################################### chall's util
def gen_CP(nbit):
R.<x> = PolynomialRing(GF(2))
while True:
c = ''.join([hex(randint(0, 15))[2:] for _ in range(nbit >> 2)])
G = R(Integer(int(c, 16)).bits()[::-1]) + x ^ nbit
if G.is_irreducible():
return int(c, 16)

def recheck(msg, v):
for m in msg:
v = v ^^ m
v = (v >> 8) ^^ CT[v & 0xFF]
return v & 0xFFFFFFFFFFFFFFFF

##################################################################### tools
b = 64
n = 64
CP, CT = gen_CP(n), []
for _ in range(256):
cb = _
for _ in range(8):
if cb & 1:
cb = (cb >> 1) ^^ CP
else:
cb >>= 1
CT.append(cb & 0xFFFFFFFFFFFFFFFF)

PR.<x> = PolynomialRing(GF(2))
G = PR(Integer(CP).bits()[::-1]) + x ^ n
R = PR.quo(G)

def i2p(p):
return R(Integer(p).bits())

def rev(p, n):
p = (p.list() + [0] * n)[:n]
return R(p[::-1])

def ri2p(p, n):
return rev(i2p(p), n)

def p2ir(p, n):
return Integer((rev(p, n)).list(), 2)

##################################################################### test
m1 = b"1"*8
m2 = b"2"*8

M1 = ri2p(int.from_bytes(m1, 'little'), b)
M2 = ri2p(int.from_bytes(m2, 'little'), b)
c1 = ri2p(recheck(m1, 1), n)
c2 = ri2p(recheck(m2, 1), n)
I = ri2p(1, n)

print(c1 - (M1*x^n + I*x^b))
print(c2 - (M2*x^n + I*x^b))

print(p2ir(M1, n))
print(int.from_bytes(m1, 'little'))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
from Crypto.Util.number import *
from pwn import *
import time

sh = process(["sage", "asemoon.sage"])

###################################################################### part1 get CP and Mx^n
sh.recvuntil(b"[Q]uit")
sh.sendline(b"p")
t = int(time.time())
t -= t % 10
sh.recvuntil(b"The first public information is: ")
pub_1 = int(sh.recvline().strip().decode())
sh.recvuntil(b"The second public information is: ")
pub_2 = int(sh.recvline().strip().decode())
sh.recvuntil(b"The third public information is: ")
pub_3 = int(sh.recvline().strip().decode())

Fp = GF(pub_2)
CP = int(Fp(pub_3).log(Fp(5)))
CT = []
for _ in range(256):
cb = _
for _ in range(8):
if cb & 1:
cb = (cb >> 1) ^^ CP
else:
cb >>= 1
CT.append(cb & 0xFFFFFFFFFFFFFFFF)
PR.<x> = PolynomialRing(GF(2))
G = PR(Integer(CP).bits()[::-1]) + x ^ n
R = PR.quo(G)


###################################################################### part2 get J
c1 = ri2p(pub_1, n)
m = b"CCTF"
M2 = ri2p(int.from_bytes(m, 'little'), 32)
Mxn = (c1 - M2*x^n) / x^32


J = (-Mxn) / (x^b-1)
sh.recvuntil(b"[Q]uit\n")
sh.sendline(b"l")
sh.sendline(hex(p2ir(J, n))[2:].encode())
print(sh.recvline())
print(sh.recvline())

sh.close()


Sobata II(8 solves , 316 pts)

题目

题目描述:

1
Sobata_II is a hardened variant of Sobata, optimized for secure operations in large finite fields with enhanced resistance to attacks.

题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
#!/usr/bin/env sage

import sys, re
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()

def sanitize_string(inp):
pattern = r'[^0-9g*+,]|[a-fh-zA-FH-Z]'
return re.sub(pattern, '', inp)

def gen_params(nbit):
while True:
p = getPrime(nbit)
R.<x> = PolynomialRing(GF(p))
f = x^2 + 13 * x + 37
f = R(f)
if f.is_irreducible():
F.<g> = GF(p^2, modulus = f)
while True:
a, b = [__ ** (__.multiplicative_order() // (3 - _)) for _, __ in enumerate(F.random_element() for _ in ':)')]
if a.multiplicative_order() - 3 == b.multiplicative_order() - 2 == 0:
c, d = [randint(1, p) for _ in ':)']
E = EllipticCurve(F, [0, d])
return (p, F, E, a, b, c)

def walk(P, parameters):
p, F, E, a, b, c = parameters
x, y = P.xy()
Q = (a * x, b * y)
assert Q in E
return int(c) * E(Q)

def jump(P, n, parameters):
_parameters = list(parameters)
_parameters[-1] = pow(int(_parameters[-1]), n, _parameters[1].order())
return walk(P, _parameters)

def main():
border = "┃"
pr( "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓")
pr(border, ".:: Welcome to the Sobata II challenge! ::. ", border)
pr(border, " You should analyze this weird oracle and break it to get the flag", border)
pr( "┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛")
nbit = 196
parameters = gen_params(nbit)
p, F, E = parameters[0], parameters[1], parameters[2]
g = F.gen()
m = bytes_to_long(FLAG)
assert m < p
while True:
try:
P = E.lift_x(m + 1404 * g)
break
except:
m += 1
while True:
pr("| Options: \n|\t[E]ncrypted FLAG \n|\t[W]alking with P \n|\t[J]umping over P \n|\t[Q]uit")
ans = sc().decode().strip().lower()
if ans == 'e':
_P = walk(P, parameters)
pr(border, f'The encrypted flag is: {_P.xy()}')
elif ans == 'w':
pr(border, 'Please send your desired point over E: ')
Q = sc().decode().strip()
Q = sanitize_string(Q).split(',')
try:
allowed_vars = {'g': g}
Q = [eval(_, {'__builtins__': None}, allowed_vars) for _ in Q]
except:
die(border, 'Your input is not valid!!')
if Q in E:
pr(border, f'The result of the walk is: {walk(E(Q), parameters).xy()}')
else:
die(border, 'Your point is not on the curve E! Bye!!')
elif ans == 'j':
pr(border, 'Send your desired point over E: ')
Q = sc().decode().strip()
Q = sanitize_string(Q).split(',')
pr(border, 'Let me know how many times you would like to jump over the given point: ')
n = sc().decode().strip()
try:
allowed_vars = {'g': g}
Q = [eval(_, {'__builtins__': None}, allowed_vars) for _ in Q]
n = int(n)
except:
die(border, 'Your input is not valid!!')
if Q in E:
pr(border, f'The result of the jump is: {jump(E(Q), n, parameters).xy()}')
else:
die(border, 'Your point is not on the curve E! Bye!!')
elif ans == 'q': die(border, "Quitting...")
else: die(border, "Bye...")

if __name__ == '__main__':
main()

与前置题Sobata相比,基本函数是差不多的,几个小区别在于:

  • 这题域换成了$GF(p^2)$,不过与之对应的$p$从512bit变成了196bit
  • jump函数中的模数从曲线阶变成了$GF(p^2)$的乘法群阶$p^2-1$

思路

这题最开始被@Q7用[sc for sc in g.__class__.__mro__[-1].__subclasses__() if sc.__name__ == 'catch_warnings'][0]()._module.__builtins__['__import__']('os').system('cat flag.py')直接RCE掉了XD,不过后来@factoreal似乎发现了端倪,修了这个让重做。

由于jump的阶压根不是曲线阶,所以没太好的办法让他像Sobata一样直接跳回来,但是由于$y^2= x^3 + d$这样的曲线在$GF(p^2)$下很容易是超奇异的,阶为$(p+1)^2$,所以就反复重连一直到这个光滑然后ECDLP就好了。

exp

这题偷懒就不写了吧:)


Juliax(7 solves , 334 pts)

题目

题目描述:

1
Juliax’s partial RSA key is like a half-baked cookie crumbling, but still oddly satisfying to crack!

题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#!/usr/bin/env sage

from Crypto.Util.number import *
from flag import flag

nbit, ebit, xbit = 512, 72, 313
Q = 2 ** (nbit - xbit)

def keygen(nbit):
while True:
p, q = [getPrime(nbit) for _ in ':)']
n, phi = p * q, (p - 1) * (q - 1)
if n.bit_length() == 2 * nbit:
e = getPrime(ebit)
if GCD(e, phi) > 1:
continue
u, v = [inverse(e, _ - 1) for _ in [p, q]]
k = (e * u - 1) // (p - 1)
l = (e * v - 1) // (q - 1)
if GCD(2 * e, k) == 1:
break
U, V = u % Q, v % Q
return n, e, U, V

n, e, U, V = keygen(nbit)
m = bytes_to_long(flag)
assert m < n
c = pow(m, e, n)

print(f'{e = }')
print(f'{n = }')
print(f'{U = }')
print(f'{V = }')
print(f'{c = }')

非常纯粹的题目,$d_p,d_q$均泄露了低512-313=199​位,$e$为72位,要求分解$n$解RSA得到flag。

思路

可以说是见过不少次的论文了,直接抄个板子也能很快做出来,简单描述一下思路。

首先有:

得到:

未知的$d_{ph},d_{qh}$可以被模掉,也就是:

由于$e$只有72bit,那么$k,l$自然也很小,因此把两个等式稍作变形,把未知$p,q$合成已知的$n$,就能得到仅以$k,l$为小根的多项式:

然后二元copper就能解出来$k,l$。

之后再回看前面的等式,就比如选带$k$的等式:

这个等式在模$kp$下有一个313bit的小根$d_{ph}$,所以可以单元copper一下试试。

实际测的话sage自带的small_roots应该打300bit就差不多了,还差13bit就只能爆一爆。差的这点上界可以照论文改shift多项式解决,当然最快的还是直接抄

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
from Crypto.Util.number import *
import itertools

def small_roots(f, bounds, m=1, d=None):
if not d:
d = f.degree()

R = f.base_ring()
N = R.cardinality()

f = f.change_ring(ZZ)

G = Sequence([], f.parent())
for i in range(m+1):
base = N^(m-i) * f^i
for shifts in itertools.product(range(d), repeat=f.nvariables()):
g = base * prod(map(power, f.variables(), shifts))
G.append(g)

B, monomials = G.coefficients_monomials()
monomials = vector(monomials)

factors = [monomial(*bounds) for monomial in monomials]
for i, factor in enumerate(factors):
B.rescale_col(i, factor)

B = B.dense_matrix().LLL()

B = B.change_ring(QQ)
for i, factor in enumerate(factors):
B.rescale_col(i, 1/factor)

H = Sequence([], f.parent().change_ring(QQ))
for h in filter(None, B*monomials):
H.append(h)
I = H.ideal()
if I.dimension() == -1:
H.pop()
elif I.dimension() == 0:
roots = []
for root in I.variety(ring=ZZ):
root = tuple(R(root[var]) for var in f.variables())
roots.append(root)
return roots

return []
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
nbit, ebit, xbit = 512, 72, 313
i = nbit - xbit
e = 4680013789992958764661
n = 113512878655961571626610562291692317083167898593072246908072509473338669866931624486434843922077792562235492835323939380660867587409311081240029070350808655984402585845023288249807250489084430773691893497493957878187939757801622886103893275017257035278212160216032814012251157961899906789943525036078018769313
dpl = 1931999207628789396725122770203483408911326042952326921451
dql = 799504796180001663308018451701479236857150404193865300422493
c = 94105129348907954980205351665290609913865320383526984688577432708537003146181471259880907643772804194349299707552600926808992628679380768658711570812064692302538521952981150231103309549852666196113547591789678339722493939214907786911484309585843582998176263433226474196365066591224488571958002184788519619403

################################################################# part1 get k,l
PR.<x, y> = PolynomialRing(Zmod(e*2^i))
f = (e*dpl - 1 + x) * (e*dql - 1 + y) - x*y*n
res = small_roots(f, (2^72, 2^72), m=3, d=3)[0]
k, l = list(map(int, res))

################################################################# part2 get dph
if(0):
PR.<x> = PolynomialRing(Zmod(k*n))
f = e*(2^i*x + dpl) - 1 + k
f = f.monic()
res = f.small_roots(X=2^xbit, beta=(72+512)/(1024+72)-0.001, epsilon=0.03)
print(res)

m = 20
t = 10
X = 2^xbit

poly = []
monomials=set()

PR.<x> = PolynomialRing(ZZ)
x = PR.gens()[0]
f = x + ((e*dpl - 1 + k) * inverse(e*2^i, k*n) % (k*n))

for _ in range(m+1):
g = f^_ * k^(m-_) * n^(max(0,t-_))
poly.append(g)
for mono in g.monomials():
monomials.add(mono)

L = Matrix(ZZ, len(poly), len(monomials))

monomials = sorted(monomials)
for row,shift in enumerate(poly):
for col,monomial in enumerate(monomials):
L[row,col] = shift.monomial_coefficient(monomial)*monomial(X)

L = L.LLL()
f = 0
for idx,monomial in enumerate(monomials):
f += (L[0][idx] // monomial(X)) * monomial
f = f.change_ring(ZZ)
dph = int(f.monic().roots(multiplicities = False)[0])

dp = dph*2^i + dpl
p = (e*dp - 1) // k + 1
q = n // p

d = pow(e, -1, (p-1)*(q-1))
flag = int(pow(c, d, n))
print(long_to_bytes(flag))


#CCTF{f4c70r1ng_w!7h_0nlY_a_thIrD_0F_7H3_53creT_CR7-3xp0n3n75!}



Pearls(6 solves , 354 pts)

题目

题目描述:

1
Unlock Pearls by hunting primes, each one brings you closer to the hidden flag!

题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#!/usr/bin/env python3

import sys, time
from Crypto.Util.number import *
from secret import decrypt, 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()

def keygen(nbit):
p, q = [getPrime(nbit) for _ in '01']
pkey = p * q
skey = (p, q)
return pkey, skey

def burnish(skey, l):
nbit = skey[0].bit_length()
IRE = [[getRandomRange(0, 2), getRandomNBitInteger(nbit + getRandomRange(-3, 3)), getRandomNBitInteger(int(nbit * 0.74))] for _ in range(l)]
PLS = [skey[IRE[_][0]] * IRE[_][1] - IRE[_][2] for _ in range(l)]
return PLS

def kouichi(r, l, n, e):
nbit = n.bit_length()
B = bin(r)[2:].zfill(nbit)[-(l + 1):]
return pow(int(B, 2), e, n)

def encrypt(m, pubkey):
n, e = pubkey, 1234567891
r = getRandomRange(1, n)
m1, m2 = (1 - r) * m % n, inverse(r, n) * m % n
s = m1 * m2 % n
u = (s + inverse(s, n)) * inverse(2, n) % n
a = (inverse(s, n) - u) * inverse(m2, n) % n
t = (u - a * m2) % n
v = getRandomRange(1, n)
l = n.bit_length() >> 1
c0 = pow(v, e, n)
c1 = (kouichi(v, l, n, e) + t * c0) % n
c2 = (a + v**2) % n
return c0, c1, c2

def main():
border = "┃"
pr( "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓")
pr(border, ".:: Welcome to the Pearls challenge! ::. ", border)
pr(border, " You should analyze this cryptosystem and break it to get the flag", border)
pr( "┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛")
nbit = 1024
pkey, skey = keygen(nbit)
m = bytes_to_long(FLAG)
enc = encrypt(m, pkey)
while True:
pr("| Options: \n|\t[B]urnish the keys \n|\t[E]ncrypt the message \n|\t[P]ublic parameters \n|\t[Q]uit")
ans = sc().decode().strip().lower()
if ans == 'e':
pr(border, 'please send your message to encrypt: ')
_m = sc().decode().strip()
try:
_m = int(_m)
except:
die(border, 'Your input is not correct! Bye!')
_m = _m % pkey
_enc = encrypt(_m, pkey)
pr(border, f'enc = {_enc}')
elif ans == 'p':
pr(border, f'pkey = {pkey}')
pr(border, f'encrypted_flag = {enc}')
elif ans == 'b':
pr(border, 'Please let me know how many times you want to burnish and burnish the key: ')
l = sc().decode().strip()
try:
l = int(l) % 5
except:
die(border, 'Please be polite! Bye!!')
PLS = burnish(skey, l)
i = 0
for pls in PLS:
pr(border, f'PLS[{i}] = {PLS[i]}')
i += 1
elif ans == 'q': die(border, "Quitting...")
else: die(border, "Bye...")

if __name__ == '__main__':
main()

题目的encrypt看上去相当奇怪,不过先不管他,总的来说题目依然基于RSA分解$n$,为此提供了无限次交互:

  • 输入e,可以调用他的encrypt,对任意消息进行加密

    说实话没看出这有啥用,公钥都知道TT

  • 输入p,可以获得公钥$n$以及对flag使用encrypt加密的结果

  • 输入b,可以输入次数$l$,从而获得burnish(skey, l)的结果,burnish(skey, l)函数内容为:

    1
    2
    3
    4
    5
    def burnish(skey, l):
    nbit = skey[0].bit_length()
    IRE = [[getRandomRange(0, 2), getRandomNBitInteger(nbit + getRandomRange(-3, 3)), getRandomNBitInteger(int(nbit * 0.74))] for _ in range(l)]
    PLS = [skey[IRE[_][0]] * IRE[_][1] - IRE[_][2] for _ in range(l)]
    return PLS

思路

encrypt虽然奇怪,但是仔细看下会发现,只要有私钥$p,q$,要反向写个decrypt是很容易的,因此重心是在分解$n$的步骤上,也就是如何利用b选项的输出。

burnish函数其实也很明显是个AGCD问题,测试一下会发现五组就可以格出来。问题在于每一次的样本不仅可能是关于$p$的近似公约数,也可能是关于$q$的,所以还要多拿一些数据然后本地简单爆一下。

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
from Crypto.Util.number import *

def kouichi(r, l, n, e):
nbit = n.bit_length()
B = bin(r)[2:].zfill(nbit)[-(l + 1):]
return pow(int(B, 2), e, n)

def decrypt(c, skey):
c0, c1, c2 = c
p, q = skey
n = p*q
e = 1234567891
d = inverse(e, (p-1)*(q-1))

l = n.bit_length() >> 1
v = pow(c0, d, n)
a = (c2 - v^2) % n
t = (c1 - kouichi(v, l, n, e)) * inverse(c0, n) % n
s = t
u = (s + inverse(s, n)) * inverse(2, n) % n

m2 = (u-t) * inverse(a, n) % n
m1 = s * inverse(m2, n) % n

PR.<x> = PolynomialRing(Zmod(p))
f = m2 * (x-m1) - x^2
resp = f.roots(multiplicities=False)

PR.<x> = PolynomialRing(Zmod(q))
f = m2 * (x-m1) - x^2
resq = f.roots(multiplicities=False)

for i in resp:
for j in resq:
m = crt([int(i), int(j)], [p, q])
flag = long_to_bytes(int(m))
if(b"flag" in flag):
print(flag)


from ast import literal_eval
from pwn import *
from random import sample
from tqdm import *

context.log_level = "critical"

sh = process(["python3", "pearls.py"])

######################################################################## part1 get n, enc
sh.recvuntil(b"[Q]uit")
sh.sendline(b"p")
sh.recvuntil(b"pkey = ")
n = int(sh.recvline().strip().decode())
sh.recvuntil(b"encrypted_flag = ")
enc = literal_eval(sh.recvline().strip().decode())


######################################################################## part2 agcd and decrypt
PLS = []
for i in range(5):
sh.recvuntil(b"[Q]uit")
sh.sendline(b"b")
sh.sendline(b"4")
for j in range(4):
sh.recvuntil(("PLS[" + str(j) + "] = ").encode())
PLS.append(int(sh.recvline().strip().decode()))
sh.close()

while(1):
l = 5
nbit = 1024
s = sample(PLS, l)
L = Matrix(ZZ, l, l)
L[0,0] = 2^int(nbit * 0.74)
for i in range(1, l):
L[0,i] = s[i]
L[i,i] = -s[0]
L = L.LLL()
q = s[0] // (abs(L[0,0]) // 2^int(nbit * 0.74)) + 1
if(n % q == 0):
p = n // q
skey = (p, q)
decrypt(enc, skey)
break


Goliver II(5 solves , 375 pts)

题目

题目描述:

1
Goliver_II is an innovative elliptic curve cryptosystem designed to present unique challenges for the new era of cryptographic security.

题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
#!/usr/bin/env python3

from hashlib import sha256
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()

def ADD(A, B):
s = (B[1] - A[1]) * inverse(B[0] - A[0], p) % p
x = (s**2 - A[0] - B[0]) % p
y = (s * (A[0] - x) - A[1]) % p
return (x, y)

def DOUBLE(A):
s = ((3 * A[0] ** 2 + a) * inverse(2 * A[1], p)) % p
x = (s**2 - 2 * A[0]) % p
y = (s * (A[0] - x) - A[1]) % p
return (x, y)

def MUL(A, d):
_B = bin(d)[2:]
_Q = A
for i in range(1, len(_B)):
_Q = DOUBLE(_Q)
if _B[i] == "1":
_Q = ADD(_Q, A)
return _Q

def GENKEY():
skey = getRandomRange(1, p)
pubkey = MUL(G, skey)
return (pubkey, skey)

def is_valid_k(k, used_k=set()):
if k in used_k:
return False
used_k.add(k)
return True

def _prepare(sign_id, msg):
k = sign_id + int.from_bytes(msg, "big")
if not is_valid_k(k):
return None
r, _ = MUL(G, k)
hmsg = int.from_bytes(sha256(msg).digest(), "big")
return k, r, hmsg

def sign(sign_id, msg):
k, r, hmsg = _prepare(sign_id, msg)
s = (inverse(k, n) * (hmsg + r * skey)) % n
return s

def verify(sign_id, msg, s):
_, r, hmsg = _prepare(sign_id, msg)
u1 = (hmsg * inverse(s, n)) % n
u2 = (r * inverse(s, n)) % n
x1, _ = ADD(MUL(G, u1), MUL(pubkey, u2))
return (x1 % n) == (r % n)

def main():
border = "┃"
pr("┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓")
pr(
border,
"Welcome! Our signature is half the size of traditional ECDSA, yet super",
border,
)
pr(
border,
"secure with the BTC curve. Try the demo! ",
border,
)
pr("┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛")
global p, a, b, G, n, pubkey, skey
p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
a, b = 0, 7
n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
x = 0x4F22E22228BD75086D77AE65174C000F132BFD4EF3E28BEF20AC476997D4444F
y = 0x3456B224247A4F73BF187AC25864F8F694C078380E6BDDF51379AC33F18BD829
G = (x, y)
pubkey, skey = GENKEY()
level, STEP, _b = 0, 3, False
while True:
pr(
"| Options: \n|\t[S]ign flag \n|\t[V]erify sign \n|\t[G]et the flag \n|\t[P]ublic key \n|\t[Q]uit"
)
ans = sc().decode().strip().lower()
if ans == "s":
pr(border, f"Please provide sign_id:")
sign_id = sc().decode()
try:
sign_id = int(sign_id)
_b = sign_id > 0
except:
die(border, f"The input sign_id you provided is not valid!")
if _b:
s = sign(sign_id, msg = flag[:len(flag) >> 1])
if s is None:
die(border, f"Double use of the same sign_id is not possible!")
pr(border, f"s = {s}")
if level == STEP:
die(border, f"You have only {STEP} rounds to check.")
else:
level += 1
else:
die(border, f"sign_id is a positive value! Bye!!")
elif ans == "v":
pr(border, "Please send the sign_id, message, and signature: ")
inp = sc().decode()
try:
sign_id, msg, s = [int(_) for _ in inp.split(",")]
except:
die(border, f"The input you provided is not valid!")
if verify(sign_id, msg, s):
die(border, f"The signature is correct")
else:
die(border, f"The signature is incorrect")
elif ans == "g":
pr(border, "Please send the private key: ")
_skey = sc().decode()
try:
_skey = int(_skey)
except:
die(border, "The private key is incorrect! Quitting...")
if _skey == skey:
die(border, f"Congrats, you got the flag: {flag}")
else:
die(border, f"The private key is incorrect! Quitting...")
elif ans == "p":
pr(border, f"pubkey = {pubkey}")
elif ans == "q":
die(border, "Quitting...")
else:
die(border, "Bye...")

if __name__ == "__main__":
main()

这个题目的前置题是昨年ASIS的这道Goliver,不过两者除了都使用Secp256k1以外没有什么相同之处,所以还是要理一下流程:

  • 首先生成ECDSA的私钥$d$,以及公钥$dG$

  • 每次连接靶机可以签三次名,签名式子看上去平平无奇:

    不过对于题目来说有几个特色:

    • 每次输入的不是要签名的msg,而是输入一个整数sign_id,靶机会用这个计算$k_i = id + m$,其中$m$是flag的前半段对应的整数值
    • $h_i$永远不会变动,都是对flag的前半段的hash

    • 签名仅返回$s_i$,不返回$r_i$

除了这个操作外,还可以做的交互有:

  • 输入v,可以验签
  • 输入g,可以提交私钥$d$,正确则拿到flag
  • 输入p,可以拿到公钥$dG$

思路

一个非常大的问题在于,对于反复重连靶机,他只有私钥$d$会随着重连变动,而$k,r$这两个值只要输入的sign_id一样,就也会一直保持相同。

$h$更是永远都不会变

所以对于一次连接,多出来的变量仅有这次连接的私钥,但是却可以在这次连接中拿到三个等式,因此三次连接之后,我们就可以拿到大于变量数的等式。而由于方程中含有$r_id$这样的二次项,所以可以考虑用groebner解这个系统。

然后grorbner似乎不能直接解出来所有变量的值,加重连次数也没有太大作用。不过观察groebner出来的多项式结构可以发现有一个仅含$h$的二次多项式,所以可以求出其两个根再分别回代,就可以解出本次的$d$并提交了。

不能直接groebner出来的原因可能就是因为一定有多解

除此之外我还试了一下把所有等式乘$r_i^{-1}$,这样等式结构会变成:

这样做的好处在于,把$k_ir_i^{-1}$以及$hr_i^{-1}$看作新变量的话,这样的方程都是线性的,而线性系统求解显然比含二次项的多变量系统求解简单。

然而试了一下会发现矩阵的kernel总是有二维,所以多解确实是无论如何避免不了的

exp

1
2
3
4
5
6
7
8
9
10
11
from Crypto.Util.number import *
from hashlib import sha256

p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
a, b = 0, 7
n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
x = 0x4F22E22228BD75086D77AE65174C000F132BFD4EF3E28BEF20AC476997D4444F
y = 0x3456B224247A4F73BF187AC25864F8F694C078380E6BDDF51379AC33F18BD829
E = EllipticCurve(GF(p), [a, b])
G = E(x, y)
o = E.order()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *

context.log_level = "critical"

S = []
nums = 3
for _ in range(nums):
sh = process(["python3", "goliver_ii.py"])

Si = []
for i in range(3):
sh.recvuntil(b"[Q]uit")
sh.sendline(b"s")
sh.sendline(str(i+1).encode())
sh.recvuntil(b"s = ")
Si.append(int(sh.recvline().strip().decode()))
S.append(Si)
1
2
3
4
5
6
7
8
9
10
11
12
13
Vs = GF(o)["k, " + "h, " + "".join(["r"+str(i)+", " for i in range(3)]) + "".join(["d"+str(i)+", " for i in range(nums)])[:-2]]
k, h, r, d = Vs.gens()[0], Vs.gens()[1], vector(Vs.gens()[2:5]), vector(Vs.gens()[5:])

F = []
for i in range(nums):
for j in range(3):
f = S[i][j] * (k+j) - h - r[j]*d[i]
F.append(f)
print(len(F))
print(len(Vs.gens()))
res = Ideal(F).groebner_basis()
for i in res:
print(i)
1
2
3
4
5
6
7
8
PR.<x> = PolynomialRing(GF(o))
f = x^2 + 11783364047569629358951535274920935565912110112299635162681431622986325880887*x + 109451085191253862640675572395812770676333853497240573026528462502720448035258
res = f.roots(multiplicities = False)
for i in res:
basis = Ideal(F + [h-i]).groebner_basis()
for j in basis:
print(j)
print()
1
2
3
4
5
6
7
8
9
10
k = o - 115792089237316195423570985008687907852837564279074780560359191442700976971211
r = int(((k+2)*G).x())
d = (o - 64529825684417237416509006489377924726196240508977127218827406097872802968877) * inverse(r, o) % o

sh.recvuntil(b"[Q]uit")
sh.sendline(b"g")
sh.sendline(str(d).encode())
print(sh.recvline())
print(sh.recvline())
print(sh.recvline())



Brain Buster

Phoney(25 solves , 159 pts)

题目

题目描述:

1
Phoney laughs! Until you unpick its multi-prime RSA and random message lock.

题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#!/usr/bin/env python3

import sys, os
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()

def keygen(nbit):
p, q, r = [getPrime(nbit + (nbit >> 3) * _) for _ in range(3)]
return p, q, r

def main():
border = "┃"
pr( "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓")
pr(border, " Welcome to the Phoney crypto-system task, a nice cryptosystem ", border)
pr(border, " that's so good, it's theoretically unbreakable because it exists", border)
pr(border, " only in the realm of imagination!! Try the get the long flag :-)", border)
pr( "┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛")
global flag
m = bytes_to_long(os.urandom(len(flag)) + flag + os.urandom(len(flag)))
nbit = 512
p, q, r = keygen(nbit)
n, s, e = p * q * r, inverse(p, q * r) + p, 1234567891
while True:
pr(f"{border} Options: \n{border}\t[E]ncrypt the flag! \n{border}\t[P]ublic information \n{border}\t[Q]uit")
ans = sc().decode().strip().lower()
if ans == 'e':
assert m < n
c = pow(m, e, n)
pr(f'{c = }')
elif ans == 'p':
pr(border, f'{n = }')
pr(border, f'{s = }')
pr(border, f'{q % p = }')
elif ans == 'q':
die(border, "Quitting...")
else:
die(border, "Bye...")

if __name__ == '__main__':
main()

也是很直白的基于RSA的题目:

  • $n = pqr$,三个因子分别是512,576,640bit
  • 给出两个额外信息:
    • $s = (p^{-1} \% qr) + p$
    • $t = q \% p$

要求分解$n$来解RSA得到flag。

思路

首先,从$s$可以建出一个三次方程:

这个方程以$p$为根,而由于$p$在模$n$下很小所以可以copper出来。

直接small_roots会返回0,把$x$换成$2x+1$即可,因为$p$的最低位一定是1

得到$p$之后可以把$q$写成$q = kp + t$,此时$k$只是一个64bit的小量,因此可以再copper一次拿到$q$。

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from Crypto.Util.number import *

n = 10655414108570071712342871629822322336052276260848921995692517934306048881480845778521473048635594109687093769615043853785629027252935155852678604090818892054779932437371829806959975259781301401037920800140432871594815692959623722591958331010098935592498558375481451032720241561581722519448780385128797141926268784117189153984334792436425916704791599835929134043023222228811266926072622810606806514985809830312134014034789087871536056885702923232374033995730216047148728943247466750782377374582090667136150361030649087939
s = 87097781153879218737644776922089706089324452996056918604476854964044978860885659967051160050015147075110573526703448597964645473413522817698372010440238498683644745662351859109986178768840569965093817635030281121135670009909294558965054901209214898480783463127123307003368822223444206220966165889336251711810347424883164891318164535069639519669665033299042957932890
t = 6096838214966504321035284656137980136493773522607688353026421454794983078333230276304065022746834844779325959061085991763580096161850112545878479486077132
c = 288623575970075250750197758754884375584171834626509918518721509685031764019357012160701423446085137202859797306422538687992693537137452442508841868175963407053673447491634645326232467665746996327478326467148081195101758855037784666708969059555532637752822802988379738952668128152173824777876694840910120372553438920587808532289330036583443582906599782696250540171882674531462779938189448505635137263271868942201487396357737753026607385736028786694815358693793539243049825102812645310656091155289734061415020712424831229
e = 1234567891

PR.<x> = PolynomialRing(Zmod(n))
f = (2*x+1)^3 - s*(2*x+1)^2 + (2*x+1)
f = f.monic()

res = f.small_roots(X=2^512, beta=1, epsilon=0.05)
p = int(res[0])*2 + 1
assert n % p == 0

PR.<x> = PolynomialRing(Zmod(n//p))
g = x * p + t
g = g.monic()
res = g.small_roots(X=2^64, beta=576/(576+640)-0.001, epsilon=0.05)
q = int(res[0]) * p + t
assert n % q == 0

r = n // (p*q)
print(long_to_bytes(int(pow(c, inverse(e, (p-1)*(q-1)*(r-1)), n))))


#CCTF{c0UlD_b3_ReCoVEr3d_v!4_Coppersmiths_m3ThOd?}


Snails(14 solves , 236 pts)

题目

题目描述:

1
Breaking Snails’ sigs? Even a decrypted flag moves at glacial pace!

题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#!/usr/bin/env sage

import sys
from Crypto.Util.number import *
from hashlib import sha512
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()

def sign(msg, skey):
h = bytes_to_long(sha512(msg).digest())
k = getRandomNBitInteger(h.bit_length())
P = k * G
r = int(P.xy()[0]) % n
s = pow(k, -1, n) * (h + r * skey) % n
return (r, s)

def main():
border = "┃"
pr( "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓")
pr(border, " Hey! To solve the Snails cryptography challenges one often needs", border)
pr(border, " to perform meticulous bit by bit analysis to uncover loopholes ", border)
pr(border, " and ultimately extract the high value flag! Good luck ;) 🐌🐌🐌 ", border)
pr( "┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛")
global flag, G, n
skey = bytes_to_long(flag)
p = 0x013835f64744f5f06c88c8d7ebfb55e127d790e5a7a58b7172f033db4afad4aca1ae1cdb891338cf963b30ff08d6af71327770d00c472c52290a60fb43f1d070025b
a = 0x0109ec0177a5a57e7b7890993e11ba1bc7ba63c1f2afd904a1df35d1fda7363ea8e83f3291e25b69dac26d046dc5ba9a42ff74cd7e52c9df5dbe8d4d02755d26b111
b = 0x0037c84047a6cc14e36d180f9b688fe9959cb63f4ac37b22eb24559e83cfc658ff0ab753540b8ab8d85a62dd67aa92f79dec20d28e453d4663ef2882c7b031ddc0b9
n = 0x013835f64744f5f06c88c8d7ebfb55e127d790e5a7a58b7172f033db4afad4aca1aad8763fe2401b5189d1c449547a6b5295586ce30c94852845d468d52445548739
x = 0x00339495fdbeba9a9f695d6e93effeb937609ce2e628958cd59ba307eb3a43c4c3a54b9b951cd593c876df93a9b0ed7d64df641af94668cb594b6a636ae386e1ac1b
y = 0x00038389f29ad8c87e79a8b854e78310b72febb6b1840e360b0a43733933529ee6a04f6d7ea0d91104eb83d1162d55c410eca1c7b45829925fb2a9bf9c1232c32972
E = EllipticCurve(GF(p), [a, b])
G = E(x, y)
m = '✔✔✔ My signature is the priority'.encode()
while True:
pr(f"{border} Options: \n{border}\t[S]ign message! \n{border}\t[Q]uit")
ans = sc().decode().strip().lower()
if ans == 's':
pr(border, f'Please send your message: ')
msg = sc().strip()
if m in msg and len(msg) == 40:
r, s = sign(msg, skey)
pr(border, f'{r = }')
pr(border, f'{s = }')
else:
die(border, 'Not valid message! Bye!!')
elif ans == 'q':
die(border, "Quitting...")
else:
die(border, "Bye...")

if __name__ == '__main__':
main()

题意非常简单,基于模数为521bit素数的ECDSA,私钥是flag,我们可以对任意长为40且包含特定子串的msg做ECDSA签名,要求还原私钥。

思路

题意简单,漏洞也十分明显,注意到每次$k$的比特长度和$h(m_i)$的比特长度一样,而hash用的是sha512,所以每次都相当于泄露了$k$的高9bit,因此当HNP问题做就可以拿到私钥。此外要是限制次数,还可以刻意选一些哈希值较小的msg发送过去签名,这样子泄露的比特更多。

这其实是昨年上半年的一个CVE(CVE-2024-31497),当时看到@石卡库写的公众号还去做了下φ(゜▽゜*)♪,不过这次既然可以用无限组那就偷懒不做消d、balance、BKZ之类的优化了,私钥长度也是随便弄的

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
p = 0x013835f64744f5f06c88c8d7ebfb55e127d790e5a7a58b7172f033db4afad4aca1ae1cdb891338cf963b30ff08d6af71327770d00c472c52290a60fb43f1d070025b
a = 0x0109ec0177a5a57e7b7890993e11ba1bc7ba63c1f2afd904a1df35d1fda7363ea8e83f3291e25b69dac26d046dc5ba9a42ff74cd7e52c9df5dbe8d4d02755d26b111
b = 0x0037c84047a6cc14e36d180f9b688fe9959cb63f4ac37b22eb24559e83cfc658ff0ab753540b8ab8d85a62dd67aa92f79dec20d28e453d4663ef2882c7b031ddc0b9
n = 0x013835f64744f5f06c88c8d7ebfb55e127d790e5a7a58b7172f033db4afad4aca1aad8763fe2401b5189d1c449547a6b5295586ce30c94852845d468d52445548739
x = 0x00339495fdbeba9a9f695d6e93effeb937609ce2e628958cd59ba307eb3a43c4c3a54b9b951cd593c876df93a9b0ed7d64df641af94668cb594b6a636ae386e1ac1b
y = 0x00038389f29ad8c87e79a8b854e78310b72febb6b1840e360b0a43733933529ee6a04f6d7ea0d91104eb83d1162d55c410eca1c7b45829925fb2a9bf9c1232c32972
E = EllipticCurve(GF(p), [a, b])
G = E(x, y)
m = '✔✔✔ My signature is the priority'.encode()

from Crypto.Util.number import *
from hashlib import sha512
from tqdm import *
from pwn import *
import string

context.log_level = "critical"
nums = 100

sh = process(["sage", "snails.sage"])

sig = []
h = []
for i in trange(nums):
sh.sendline(b"s")
msg = m + "".join([choice(string.ascii_letters+string.digits) for _ in range(2)]).encode()
sh.sendline(msg)
sh.recvuntil(b"r = ")
r = int(sh.recvline().strip().decode())
sh.recvuntil(b"s = ")
s = int(sh.recvline().strip().decode())
sig.append([r, s])
h.append(bytes_to_long(sha512(msg).digest()))
sh.close()

r = [sig[i][0] for i in range(nums)]
s = [sig[i][1] for i in range(nums)]

L = Matrix(ZZ, 2+nums, 2+nums)
L[0, 0] = 1
L[1, 1] = 1
for i in range(nums):
L[0, i+2] = r[i] * inverse(s[i], n) % n
L[1, i+2] = h[i] * inverse(s[i], n) % n
L[i+2, i+2] = n

Q = diagonal_matrix(ZZ, [2^(512-22*8), 2^512] + [1]*nums)
L = L * Q
L = flatter(L)
L = L / Q
d = abs(L[0, 0])
print(long_to_bytes(int(d)))


Multi shooti(12 solves , 259 pts)

题目

题目描述:

1
The Multi-shooti code left clues out in the open. Spot them, steal the key, grab the flag, and prove it’s not so tough after all.

题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
#!/usr/bin/env python3

from random import *
from flag import flag
import sys
from os import urandom

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()

class Shooti:
def __init__(self, key, vec):
self.SETTIMER = 160
self.NEXT = [0 for _ in range(80)]
self.BEST = [0 for _ in range(80)]
self.SBOX = [
1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0,
1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0,
0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1,
0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1,
1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0,
1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1,
0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0,
1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0,
0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1,
1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1,
0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1,
0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0,
0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1,
1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0,
1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0,
1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0,
1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1,
1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1,
0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1,
1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0,
0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0,
0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0,
1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0,
1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 1,
0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1,
1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1,
0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0,
1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1,
0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0,
0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0,
1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1,
0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0
]
self.LOGIC = [1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1]
self.klen, self.vlen = 80, 64

key = [key[_:_ + 2] for _ in range(0, len(key), 2)]
self.key = [int(_, 16) for _ in key]
vec = [vec[_:_ + 2] for _ in range(0, len(vec), 2)]
self.vec = [int(_, 16) for _ in vec]

for i in range(self.vlen // 8):
for j in range(8):
self.NEXT[i * 8 + j] = (self.key[i] >> j) & 1
self.BEST[i * 8 + j] = self.NEXT[i * 8 + j]

for i in range(self.vlen // 8, self.klen // 8):
for j in range(8):
self.NEXT[i * 8 + j] = (self.key[i] >> j) & 1
self.BEST[i * 8 + j] = 1

for _ in range(self.SETTIMER):
RESB = self.BITSEQ()
self.BEST[79] ^= RESB
self.NEXT[79] ^= RESB
def N(self, c):
return self.NEXT[80 - c]
def B(self, c):
return self.BEST[80 - c]

def BITSEQ(self):
B0, B1, B2, B3 = self.BEST[3], self.BEST[25], self.BEST[46], self.BEST[64]
N0 = self.NEXT[63]
RESB = self.N(79) ^ self.N(78) ^ self.N(76) ^ self.N(70) ^ self.N(49) ^ self.N(37) ^ self.N(24) ^ self.LOGIC[(N0 << 4) | (B3 << 3) | (B2 << 2) | (B1 << 1) | B0]
NBit = self.B(80) ^ self.N(18) ^ self.N(66) ^ self.N(80) ^ self.SBOX[sum(self.N(i) << (9 - j) for j, i in enumerate([17, 20, 28, 35, 43, 47, 52, 59, 65, 71]))]
LBit = self.B(18) ^ self.B(29) ^ self.B(42) ^ self.B(57) ^ self.B(67) ^ self.B(80)
for i in range(1, self.klen):
self.NEXT[i - 1] = self.NEXT[i]
self.BEST[i - 1] = self.BEST[i]
self.NEXT[self.klen - 1] = NBit
self.BEST[self.klen - 1] = LBit
return RESB
def BYTESEQ(self):
while True:
BITSEQ = 0
for j in range(8):
RESB = self.BITSEQ()
RESB <<= j
BITSEQ = BITSEQ | RESB
yield BITSEQ

def encrypt(self, MSG):
enc = [M ^ B for M, B in zip(MSG, self.BYTESEQ())]
return enc

class MultiShooti:
def __init__(self, keys, vec):
self._encs = [Shooti(key, vec) for key in keys]
def encrypt(self, MSG):
res = MSG
for enc in self._encs:
res = enc.encrypt(res)
return res

def main():
global flag
border = "┃"
pr("┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓")
pr(
border,
"Welcome, challenger! We've crafted an ultra-secure encryption system",
border,
)
pr(
border,
"with multiple layers of encryption. But... we kinda forgot the keys!",
border,
)
pr(
border,
"Can you help us recover them? Good luck! ",
border,
)
pr("┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛")

p = list(map(ord, "give me the flag!"))
keys = [urandom(10).hex() for _ in range(8)]
vec = urandom(8).hex()
c = MultiShooti(keys, vec).encrypt(p)
while True:
pr(
border, f"Options: \n{border}\t[D]ecrypt \n{border}\t[C]ipher \n{border}\t[Q]uit"
)
ans = sc().decode().strip().lower()
if ans == "d":
pr(border, "Please send keys:")
_keys = sc().decode()
try:
_keys = list(map(str.strip,_keys.split(",")))
_b = all(int(_, 16).bit_length() == 80 for _ in _keys)
except:
die(border, "Invalid input! Quitting...")
if _b:
tmp = MultiShooti(_keys, vec).encrypt(c)
pr(border, f"plaintext = {tmp}")
if tmp == p:
die(border, f"Congrats, you got the flag: {flag}")
else:
die(border, "Invalid input! Quitting...")
elif ans == "c":
pr(border, f"challenge cipher = {c}")
elif ans == "q":
die(border, "Quitting...")
else:
die(border, "Bye...")

if __name__ == "__main__":
main()

带S盒的题看着都头大,先放一下,以后再说(,,・ω・,,)

对称和流密码这种现在我已经退化成看到就直接润的程度了


Leptocole(2 solves , 450 pts)

题目

题目描述:

1
Leptocole’s equation is so last-year, yet here you are, still solving for ‘U’ like a nerd!

题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#!/usr/bin/env sage

import sys
from flag import flag

def ranpermat(n, q):
P = zero_matrix(n, n)
I = list(range(n))
shuffle(I)
F = GF(q)
for i in range(n):
while True:
r = F.random_element()
if r != 0:
P[i, I[i]] = r
break
return P

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()

def main():
border = "┃"
pr( "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓")
pr(border, ".::: Welcome to the Leotocole cryptography task! :::.", border)
pr(border, ".: Your mission is to find flag by analyzing the given oracle! :.", border)
pr( "┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛")
global flag, q, n, k
q, n, k = 127, 26, 14
F = GF(q)
G = random_matrix(F, k, n)
Q = ranpermat(n, q)
H = (G * Q).echelon_form()
while True:
pr(f"{border} Options: \n{border}\t[G]et the G and H! \n{border}\t[S]olve the Leotocole! \n{border}\t[Q]uit")
ans = sc().decode().strip().lower()
if ans == 'g':
pr(f'{G = }')
pr(f'{H = }')
elif ans == 's':
pr(border, f'Please send the matrix U row by row: ')
_U = []
for _ in range(k):
_r = sc().decode().strip()
try:
_r = [int(_) for _ in _r.split(',')]
_U.append(_r)
except:
die(border, "Your input is not valid! Bye!!")
pr(border, f'Now, please send the matrix P row by row: ')
_P = []
for _ in range(n):
_r = sc().decode().strip()
try:
_r = [int(_) for _ in _r.split(',')]
if _r.count(0) == n - 1:
_P.append(_r)
except:
die(border, "Your input is not valid! Bye!!")
try:
_U = matrix(GF(q), _U)
_P = matrix(GF(q), _P)
if _U * G * _P == H and _U.is_invertible() and _P.is_invertible():
_b = True
except:
die(border, "Something went wrong with your input :| Quitting!")
if _b:
die(border, f"Congrats, you got the flag: {flag}")
elif ans == 'q':
die(border, "Quitting...")
else:
die(border, "Bye...")

if __name__ == '__main__':
main()

这个是这次撑的最久的题目,到比赛结束两小时左右才被大伙搞定。虽然题目不太好做,但是题意反而很容易理解,其流程为(计算均定义在$GF(q)$下):

  • 生成随机矩阵$G_{k,n}$

  • 生成一个26x26的随机置换矩阵,并在置换矩阵每个非0位置乘上一个非零系数,得到矩阵$Q_{n,n}$

  • 计算$GQ$的行阶梯形矩阵$H_{k, n}$,也就是存在可逆矩阵$K_{k,k}$使得$KGQ = H$

    这个结构的$H$就很类似编码里生成矩阵的结构

然后就是交互部分:

  • 输入g,可以拿到$G$和$H$

  • 输入s,可以逐行输入一个矩阵$U$和矩阵$P$,如果满足:

    • $UGP = H$
    • $U$和$P$均可逆

    就可以拿到flag。

思路

比较明显的是这是一个编码方向的问题,虽然我对编码了解的非常有限,不过队伍里@hashhash,@yolbby,@Aqua Cat和@Q7都懂编码,所以从大伙的对话我大概了解到这是一个码等价问题(Code Equivalence Problem),更具体的说,这是一个线性码等价问题(Linear Code Equivalence Problem),也就是题目标题的LEP。

这个等价很好理解,如果我们输入的$U = K$,$P=Q$的话,显然就满足题目要求,而如果能找到其他的输入也满足这个要求,那么$U,P$和$K,Q$在这个意义下就是等价的。

然而由于懂的实在太少,所以大伙最后集中讨论这题的时候我基本等于看天书,而且比赛第二天上午家里还有急事必须要走,就全靠队友carry(;´д`)ゞ。

不过@hashhash昨年的时候就预言了一波:前年CCTF有个同源的板子,昨年有个多变量的板子,那么今年说不定就有个编码的板子——事实证明确实如此,@Q7最后找到了针对这一道题目、并且还带有代码实现的这篇论文,能一定概率解决题目设置参数的码等价问题,然后@Aqua Cat超级快手秒写交互就出了。

仓库在这里

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from ast import literal_eval
from pwn import process, context

q = 127
n = 26
k = 14

def recv_Mat(k, n):
M = []
for i in range(k):
t = literal_eval(sh.recvline().strip().decode().replace(" ", " ").replace(" ", " ").replace(" ", ",").replace("[,", "["))
M.append(t)
return Matrix(GF(q), M)

context.log_level = "critical"

sh = process(["sage", "leptocole.sage"])

sh.sendline(b"g")
sh.recvuntil(b"G = ")
G = recv_Mat(k, n)
sh.recvuntil(b"H = ")
H = recv_Mat(k, n)
1
2
3
4
5
6
7
8
load("lep_solver.sage")

Fq = GF(q)

result = lepCollSearch(G,H)
if result != None:
U, P = result
print("lepCollSearch succesfully recovered solution.")
1
2
3
4
5
6
7
8
sh.recvuntil(b"[Q]uit")
sh.sendline(b"s")
for i in range(k):
sh.sendline(str(U[i].list())[1:-1].encode())
for i in range(n):
sh.sendline(str(P[i].list())[1:-1].encode())
sh.recvuntil(b"Congrats, you got the flag: ")
print(sh.recvline())



Head-Scratcher

Hoshi(6 solves , 354 pts)

题目

题目描述:

1
Hoshi: a cryptic puzzle demanding brilliance, where only the sharpest minds unravel its celestial secrets.

题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#!/usr/bin/env sage


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()

def gen_curve(p, l, n):
while True:
a = randint(1, p - 1)
b = randint(1, p - 1)
E = EllipticCurve(Zmod(p ^ l), [a, b])
try:
PTS = [E.lift_x(_ + 1) for _ in range(n)]
return PTS
except:
continue

def main():
border = "┃"
pr( "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓")
pr(border, ".::: Welcome to the Hoshi cryptography task! :::.", border)
pr(border, ".:: Your mission is to find flag by analysing the Hoshi system ::.", border)
pr( "┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛")

nbit = 256
l, p, n = 12, getPrime(nbit), 5
PTS = gen_curve(p, l, n)
SCV = [randint(1, p ^ (l - 1)) for _ in range(n)]
EPT = [SCV[_] * PTS[_] for _ in range(n)]
ORD = [
(1, "first"),
(2, "second"),
(3, "third"),
(4, "forth"),
(5, "fifth")
]
while True:
pr(f"{border} Options: \n{border}\t[B]ase Points \n{border}\t[E]ncrypted Points \n{border}\t[S]olve the Hoshi! \n{border}\t[Q]uit")
ans = sc().decode().strip().lower()
if ans == 'b':
for _ in range(n):
pr(border, f'BPT_{_ + 1} = {PTS[_][1]}')
elif ans == 'e':
for _ in range(n):
pr(border, f'EPT_{_ + 1} = {EPT[_][0]}')
elif ans == 's':
for _ in range(n):
_b = False
pr(border, f"Please provide the {ORD[_][1]} integer:")
inp = sc().decode()
try:
s = int(inp)
if s * PTS[_] == EPT[_]:
_b = True
except:
die(border, f"The input you provided is not valid!")
if _b:
if _ == n - 1:
die(border, f'Congratulations! You got the flag: {flag}')
else:
pr(border, f'Great job, now try the {ORD[_][1]} level :)')
else:
die(border, f"The input you provided is not correct!")
elif ans == 'q':
die(border, "Quitting...")
else:
die(border, "Bye...")

if __name__ == '__main__':
main()

这个题目题意也很简单,定义一条在$Zmod(p^{12})$下的曲线,给出上面的五个点$P_i$以及他们的倍点$Q_i=s_iP_i$,其中$s_i$均小于等于$p^{11}$,求解出所有的$s_i$并发回靶机就可以拿到flag。

思路

对于这种$Zmod(p^{k})$下的DLP问题,求解$Zmod(p^{k-1})$下的部分都是简单的,而对于ECDLP来说也就是Smart’s Attack,由于上次CodegateCTF正好写了一版,所以直接拿来用,却发现存在一个问题:对于$Zmod(p^4)$下能成功求解出来,但是$Zmod(p^5)$及以上就不行了。

问了一下Gemini说是精度问题,并且超过$p^4$的部分似乎需要迭代求解,让他写了个exp再稍微改了下发现确实就行了

赛后看discord的讨论发现$Zmod(p^4)$之后之所以不行似乎还有更深层次的原因:

image-20250723120624230

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def smarts_attack_dlog(P, Q, l):
E_Qp = E.change_ring(Qp(p, prec=l+5, print_mode='series'))
P_Qp = E_Qp(P)
Q_Qp = E_Qp(Q)

E_p = EllipticCurve(Zmod(p), [a, b])
order_p = E_p.order()
P_ = order_p * P_Qp
Q_ = order_p * Q_Qp
zP = -P_[0] / P_[1]
zQ = -Q_[0] / Q_[1]

k0 = Mod(zQ / zP, p)
known_k = Integer(k0)
for i in range(1, l - 1):
Q_i = Q_Qp - known_k * P_Qp
P_i = (p^i) * P_Qp

Q_i_ = order_p * Q_i
P_i_ = order_p * P_i
z_Qi = -Q_i_[0] / Q_i_[1]
z_Pi = -P_i_[0] / P_i_[1]

ki = Mod(z_Qi / z_Pi, p)
known_k += Integer(ki) * (p**i)

return known_k
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from Crypto.Util.number import *
from pwn import *

context.log_level = "critical"

sh = remote("91.107.132.34", 31117)
#sh = process(["sage", "hoshi.sage"])
sh.recvuntil(b"[Q]uit\n")
sh.sendline(b"b")
points = []
for x in range(1, 6):
sh.recvuntil(b" = ")
points.append((ZZ(x), ZZ(sh.recvline().decode())))

sh.recvuntil(b"[Q]uit\n")
sh.sendline(b"e")
target = []
for _ in range(5):
sh.recvuntil(b" = ")
target.append(ZZ(sh.recvline().decode()))

l = [(x, y^2 - x^3) for x, y in points]
l = [(l[i+1][0] - l[i][0], l[i+1][1] - l[i][1]) for i in range(len(l)-1)]
mod = gcd([l[i+1][1] * l[i][0] - l[i+1][0] * l[i][1] for i in range(len(l)-1)])

for j in range(2, 2000):
while(mod % j == 0):
mod //= j
a, = set([y * pow(x, -1, mod) % mod for x, y in l])
b, = set([(y^2 - x^3 - a*x) % mod for x, y in points])
p, perfect = mod.nth_root(12, truncate_mode=True)
assert perfect and 1 <= a < p and 1 <= b < p

E = EllipticCurve(Zmod(p^12), [a, b])
1
2
3
4
5
6
7
8
9
10
11
l, n = 12, 5
PTS = [E.lift_x(_ + 1) for _ in range(n)]
EPT = [E.lift_x(target[_]) for _ in range(n)]
dlogs = []
for i in range(n):
P, Q = PTS[i], EPT[i]
dlog = smarts_attack_dlog(P, Q, l)
if(dlog*PTS[i] == EPT[i]):
dlogs.append(dlog)
else:
dlogs.append(smarts_attack_dlog(P, -Q, l))
1
2
3
4
5
6
7
8
9
sh.recvuntil(b"[Q]uit\n")
sh.sendline(b"s")
for i in range(n):
sh.sendline(str(dlogs[i]).encode())
print(i, sh.recvline())
print(sh.recvline())
print(sh.recvline())
print(sh.recvline())
print(sh.recvline())
1
2
3
print(sh.recvline())

#CCTF{Hosh_em8od!e5_a_pr0fouNd_!n7ellig3ncE_thaT_shIneS_thrOu9h_1n_ev3rY_in5iGh7fuL_d3ci5ion!}


Alice Sig Slip(3 solves , 423 pts)

题目

题目描述:

1
I messed up and a function wiped some of Alice's data. Can you help me restore it before she notices?

题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
#!/usr/bin/env python3

import sys
from Crypto.PublicKey import ECC
from Crypto.Signature import eddsa
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()

def init_data():
global alice_data

alice_data = []
alice_key = ECC.generate(curve = "ed25519")
signer = eddsa.new(alice_key, "rfc8032")
public_key = alice_key.public_key().export_key(format="raw")
for msg in [
b"Alice cracks codes in her sleep.",
b"Alice never leaves a cipher unsolved.",
b"No flag for those who give up too soon, says Alice.",
b"Alice never gives up; that's why she always gets the flag.",
b"Alice loves solving ciphers, especially when they're tricky.",
]:
signature = signer.sign(msg)
alice_data.append((public_key, signature[:32], signature[32:], msg))

def erase():
global alice_data
for i, row in enumerate(alice_data):
alice_data[i] = (b"",) * i + row[i:]

def alice_check():
global alice_data
for public_key, r, s, msg in alice_data:
if min(map(len, [public_key, r, s, msg])) == 0:
return False
public_key = eddsa.import_public_key(encoded=public_key)
if public_key.pointQ.x * public_key.pointQ.y == 0:
return False
verifier = eddsa.new(public_key, "rfc8032")
try:
verifier.verify(msg, r + s)
except ValueError:
print("Wrong sign")
return False
return True

def main():
border = "┃"
pr("┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓")
pr(border, "I accidentally triggered a function that erased part of Alice's data. ", border)
pr(border, "Can you help me recover it before she finds out? ", border)
pr("┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛")

global alice_data

init_data()
erase()

while True:
pr(
f"{border} Options: \n{border}\t[G]et data \n{border}\t[U]pdate data \n{border}\t[A]lice check \n{border}\t[Q]uit"
)
ans = sc().decode().strip().lower()
if ans == "g":
for row_inx, row_val in enumerate(alice_data):
pr(border, row_inx, ":", ", ".join(map(bytes.hex, row_val)))
elif ans == "u":
pr(border, "row_inx, public_key, r, s, msg:")
_new_data = sc().decode()
try:
_new_data = _new_data.split(",")
row_inx = int(_new_data[0])
public_key, r, s, msg = map(bytes.fromhex, _new_data[1:])
except:
die(border, "Bad input! Quitting...")

new_row = [public_key, r, s, msg]
if 0 <= row_inx < len(alice_data) and all(
len(old_val) == 0 or new_val == old_val
for old_val, new_val in zip(alice_data[row_inx], new_row)
):
alice_data[row_inx] = new_row
else:
die(border, "Bad input values! Quitting...")
elif ans == "a":
if alice_check():
die(border, f"Hey Alice, everything seems fine here! Here's the {flag: }!")
else:
die(border, "Uh-oh, Alice! Looks like the data went on vacation without telling anyone.")
elif ans == "q":
die(border, "Quitting...")
else:
die(border, "Bye...")

if __name__ == "__main__":
main()

这个题也是卡了大伙比较久。题目基于Ed25519的签名,流程为:

  • 在连上靶机后,靶机生成密钥对,然后对下面五个消息进行签名:

    1
    2
    3
    4
    5
    b"Alice cracks codes in her sleep."
    b"Alice never leaves a cipher unsolved."
    b"No flag for those who give up too soon, says Alice."
    b"Alice never gives up; that's why she always gets the flag."
    b"Alice loves solving ciphers, especially when they're tricky."

    签名值存放格式为[public_key, r, s, msg]

  • 之后,靶机将五个消息的签名分别进行一定数量的”擦除”,然后发送给我们。也就是实际上我们拿到的五次消息对应的签名值为:

    1
    2
    3
    4
    5
    [public_key, r, s, msg],
    ["", r, s, msg],
    ["", "", s, msg],
    ["", "", "", msg],
    ["", "", "", ""],
  • 我们需要往这些被擦除的签名里填入值,使得五个消息均通过Ed25519的验签就能拿到flag。

    需要注意的是对签名值内已有的部分不能进行篡改

思路

前置知识

由于题目用的是库函数,没有把签名具体对应的等式写出来,所以这里先简单介绍一波。

首先,Ed25519并不是常见的Weierstrass型的椭圆曲线,而是一条扭曲爱德华曲线(Twisted Edwards Curve),照该曲线的常用记法:

  • 记该曲线阶为$l$,基点为$B$,
  • 我们的私钥为$k$,计算$\text{SHA512}(k)$,得到一个512bit的数字,记前半部分为$s$,后半部分为$t$
  • 公钥为$A = sB$

那么对于该曲线上的签名,其式子为:

其中$r = h(t || M)$,此处注意签名值为$(R, S)$而非$(r,s)$。

验签即验证下式成立:

Standard curve database贴心的给全了Ed25519的参数以及其与Weierstrass型椭圆曲线的相互转换。

做法

回看一下我们实际拿到的五条内容:

1
2
3
4
5
[public_key, r, s, msg],
["", r, s, msg],
["", "", s, msg],
["", "", "", msg],
["", "", "", ""],

第一、第二、第四、第五条都没有问题:第一条压根不需要填,第二条填个public_key就行,第四、第五条自己生成个私钥去签名就可以了。因此关键在于第三条——我们需要在给定$S$的条件下,伪造出指定消息的一个签名,而可以操作的是公钥$A$以及$R$的值。

漏洞点在于Ed25519有一个8阶的子群,因此我们取该子群中的一点作为公钥$A$,那么验签的时候$8h(R || A || M)\cdot A$这一部分就消去了,剩下的就是$8S\cdot B = 8R$,此时用$R = S \cdot B$就可以通过验签了。

要注意一下解析数据时的大小端序

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
p = 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed
K = GF(p)
a = K(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffec)
d = K(0x52036cee2b6ffe738cc740797779e89800700a4d4141d8ab75eb4dca135978a3)
E = EllipticCurve(K, (K(-1/48) * (a^2 + 14*a*d + d^2),K(1/864) * (a + d) * (-a^2 + 34*a*d - d^2)))
def to_weierstrass(a, d, x, y):
return ((5*a + a*y - 5*d*y - d)/(12 - 12*y), (a + a*y - d*y -d)/(4*x - 4*x*y))
def to_twistededwards(a, d, u, v):
y = (5*a - 12*u - d)/(-12*u - a + 5*d)
x = (a + a*y - d*y -d)/(4*v - 4*v*y)
return (x, y)
G = E(*to_weierstrass(a, d, K(0x216936D3CD6E53FEC0A4E231FDD6DC5C692CC7609525A7B2C9562D608F25D51A), K(0x6666666666666666666666666666666666666666666666666666666666666658)))
E.set_order(0x1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3ed * 0x08)
n = 0x1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3ed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def hex_to_E(hexdata):
field_bytes = bytes.fromhex(hexdata)

sign_bit = (field_bytes[31] >> 7) & 1
y_bytes = bytearray(field_bytes)
y_bytes[31] &= 0x7F
y = K(int.from_bytes(y_bytes, 'little'))

numerator = y^2 - 1
denominator = d*y^2 + 1
x_squared = numerator / denominator
x = x_squared^((p+3)//8)

if x^2 != x_squared:
i = K(2)^((p-1)//4)
x = x * i

if (int(x) % 2) != sign_bit:
x = -x

point_on_E = E(*to_weierstrass(a, d, x, y))

return point_on_E

def E_to_hex(point_on_E):
if point_on_E.is_zero():
point_on_edwards = (K(0), K(1))
else:
u, v = point_on_E.xy()
point_on_edwards = to_twistededwards(a, d, u, v)

x, y = point_on_edwards
sign_bit = int(x) % 2
y_bytes = bytearray(int(y).to_bytes(32, 'little'))
y_bytes[31] |= (sign_bit << 7)

return y_bytes.hex()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from Crypto.PublicKey import ECC
from Crypto.Signature import eddsa
from pwn import *

context.log_level = "critical"

#sh = remote("91.107.133.165", 13777)
sh = process(["python3", "AliceSigSlip.py"])
sh.recvuntil(b"[Q]uit\n")
sh.sendline(b"g")
Alice_data = []
for i in range(5):
sh.recvuntil(b":")
temp = sh.recvline().strip().decode()
Alice_data.append([j.strip() for j in temp.split(",")])

############################################################## 0
sh.recvuntil(b"[Q]uit\n")
sh.sendline(b"u")
msg = "0" + "," + Alice_data[0][0] + "," + Alice_data[0][1] + "," + Alice_data[0][2] + "," + Alice_data[0][3]
sh.sendline(msg.encode())

############################################################## 1
sh.recvuntil(b"[Q]uit\n")
sh.sendline(b"u")
msg = "1" + "," + Alice_data[0][0] + "," + Alice_data[1][1] + "," + Alice_data[1][2] + "," + Alice_data[1][3]
sh.sendline(msg.encode())

############################################################## 3
alice_key = ECC.generate(curve = "ed25519")
signer = eddsa.new(alice_key, "rfc8032")
public_key = alice_key.public_key().export_key(format="raw")
msg = bytes.fromhex(Alice_data[3][3])
signature = signer.sign(msg)
sh.recvuntil(b"[Q]uit\n")
sh.sendline(b"u")
msg = "3" + "," + public_key.hex() + "," + signature[:32].hex() + "," + signature[32:].hex() + "," + Alice_data[3][3]
sh.sendline(msg.encode())

############################################################## 4
sh.recvuntil(b"[Q]uit\n")
sh.sendline(b"u")
msg = "4" + "," + Alice_data[0][0] + "," + Alice_data[0][1] + "," + Alice_data[0][2] + "," + Alice_data[0][3]
sh.sendline(msg.encode())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
while(1):
A = E.random_element() * n
if(A != E(0)):
break

B = G
S = int.from_bytes(bytes.fromhex(Alice_data[2][2]), 'little')
R = S*B


############################################################## 2
sh.recvuntil(b"[Q]uit\n")
sh.sendline(b"u")
msg = "2" + "," + E_to_hex(A) + "," + E_to_hex(R) + "," + Alice_data[2][2] + "," + Alice_data[2][3]
sh.sendline(msg.encode())

sh.recvuntil(b"[Q]uit\n")
sh.sendline(b"a")
print(sh.recvline())


SingleRow(3 solves , 423 pts)

题目

题目描述:

1
SingleRow’s crypto is one line of defense, turns out it's also one line of nope! ;)

题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#!/usr/bin/env sage

from Crypto.Util.number import *
from flag import flag

def makecore(B, E):
_B = list(B)
while len(_B) < E.dimension():
b = E.random_element()
if not _B and b.is_zero():
continue
if b not in E.span(_B):
_B.append(b)
return _B

def genkey(q, k, v):
n = k + v
FF = GF(q)
M = matrix(FF, makecore([], FF ** n))
F, pkey = [], []
for _ in range(k):
while True:
A, B = matrix(FF, k, k), random_matrix(FF, k, v)
C, D = random_matrix(FF, v, k), random_matrix(FF, v, v)
E = block_matrix([[A, B], [C, D]])
if E.is_invertible():
break
if FF.characteristic() != 2:
E = FF(2) ** (-1) * (E + E.transpose())
G = M.transpose() * E * M
F.append(E)
pkey.append(G)
skey = (M, F)
return skey, pkey

def sign(skey, G):
while True:
M, F = skey
q = M.base_ring().cardinality()
k = len(F)
n = F[0].dimensions()[0]
v = n - k
_M = M.inverse()
FF = GF(q)
RR = PolynomialRing(FF, 'x', k)
X = RR.gens()
Y = vector(RR, list(X) + [FF.random_element() for _ in range(v)])
E = [Y * F[e] * Y for e in range(k)]
C = [[E[i].coefficient(X[j]) for j in range(k)] for i in range(k)]
S = matrix(FF, C)
B = vector([eq([0] * k) for eq in E])
T = G - B
try:
_S = S.solve_right(T)
V = vector(FF, list(_S) + list(Y[k:]))
return _M * V
except:
continue

def vecsub(skey):
A, F = skey
V = span(A.inverse().columns()[:len(F)])
return V.random_element()

q, m, v = 256, 40, 64
skey, pkey = genkey(q, m, v)
A, F = skey
M = bytes_to_long(flag)
l = M.bit_length()
FLAG_BITS = [int(_) for _ in list(bin(M)[2:])]

SIGNS = []
for i in range(len(FLAG_BITS)) :
if FLAG_BITS[i] :
SIGNS.append(vecsub(skey))
else :
SIGNS.append(sign(skey, vector([randint(0, 1) for _ in range(m)])))

f = open('signatures.txt', 'w')
f.write(str(SIGNS))
f.close()

这个题基于UOV,梳理一遍题意头很大,不过好就好在可以站在巨人队友的肩膀上,换个角度理解这个题就简单很多。

思路

就我们的想法而言,整个题目浓缩一下其实就剩这样一小部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from Crypto.Util.number import *
from flag import flag

def vecsub(skey):
A, F = skey
V = span(A.inverse().columns()[:len(F)])
return V.random_element()

q, m, v = 256, 40, 64
skey, pkey = genkey(q, m, v)
A, F = skey
M = bytes_to_long(flag)
l = M.bit_length()
FLAG_BITS = [int(_) for _ in list(bin(M)[2:])]

SIGNS = []
for i in range(len(FLAG_BITS)) :
if FLAG_BITS[i] :
SIGNS.append(vecsub(skey))
else :
SIGNS.append(sign(skey, vector([randint(0, 1) for _ in range(m)])))

f = open('signatures.txt', 'w')
f.write(str(SIGNS))
f.close()

核心点是这样:当flag当前bit为1时,输出的是一个$m \times (m+v)$的子空间中的向量,而当前bit为0时,直接不管他的sign具体是什么操作,总之输出的向量几乎不可能在这个子空间中。

而flag我们知道是以CCTF{}包裹住的,这就天然提供了24个1,因此在剩余的26个flag字符中,我们只需要找到17个1,和这24个1拼在一起,其矩阵的秩会是40而不是41,然后再将剩余向量挨个与这个矩阵拼起来检查秩即可逐bit做出判断。

@Q7给出了两个非常聪明的想法:

  • 观察前面题目的flag,以!}结尾的概率非常高
  • 几乎每个flag都有好几个下划线,而下划线的二进制表示有足足六个1

因此爆下划线的位置是最方便的做法。

exp

1
2
3
4
5
6
7
8
9
10
11
from itertools import combinations
from Crypto.Util.number import *
from math import comb
from tqdm import *
import copy

q, m, v = 256, 40, 64
n = m+v
FF = GF(q, names="z", modulus=x^8+x^6+x^4+x^3+x^2+x+1)
z8 = FF.gens()[0]
data = eval(open("signatures.txt", "r").read())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
flag_ = bytes_to_long(b"CCTF{" + b"\x00"*26 + b"}")
flag_bin = bin(flag_)[2:].zfill(255)

#################################################################################### init
S = Matrix(FF, 0, m+v)
for i in range(255):
if(flag_bin[i] == "1"):
S = S.stack(vector(FF, data[i]))

#################################################################################### brute force
flag__ = [0 for _ in range(26)]
for i in tqdm(combinations(range(0, 26), 3) ,total=int(comb(26, 3))):
temp = copy.deepcopy(flag__)
for j in i:
temp[j] = ord("_")
temp = bin(bytes_to_long(bytes(temp)))[2:].zfill(26*8)

SS = S
for j in range(26*8):
if(temp[j] == "1"):
SS = SS.stack(vector(FF, data[j+5*8-1]))

if(SS.rank() == 40):
print(i)
break
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#################################################################################### check bit by bit
flag_ = list(b"CCTF{" + b"\x00"*26 + b"}")
for i in (3, 10, 13):
flag_[i + 5] = ord("_")
flag_bin = bin(bytes_to_long(bytes(flag_)))[2:].zfill(255)
S = Matrix(FF, 0, m+v)
for i in range(255):
if(flag_bin[i] == "1"):
S = S.stack(vector(FF, data[i]))

flag_2 = ""
for i in range(255):
if(S.stack(vector(FF, data[i])).rank() == 40):
flag_2 += "1"
else:
flag_2 += "0"
print(long_to_bytes(int(flag_2, 2)))


##CCTF{On3_vEc7or_!n_UOV_5ch3meS!}

其他

其实这个题在第一天晚上@hashhash就想出做法了,不过无论怎么调代码或者优化,flag就是出不来。

甚至@hashhash搞了一个究极优化的对1特别稀疏、没有下划线的flag都能秒出的做法,我们当时还醒着的几个全都本地生成数据测了一下,但对题目数据还是搞不出来flag,而且当时半夜四点多,@factoreal还要睡觉了:

img

于是我们产生了各种各样的怀疑:

  • 附件数据放错了

  • redownload之后,附件数据还是放错了TT

  • flag不是以CCTF开头的,也许是flag

    还猜过是ASIS,毕竟昨年有个题目就是XD,不过试了下确实也不行

然后我想到了之前给SUCTF出题的时候利用了sage 10.5版本之后与更早的版本生成随机数的区别,但是没想到能怎么影响,不过还是简单提了一嘴,然后@Aqua Cat想到了一个问题:

image-20250723153535147

但是@Aqua Cat测了下在9.5和10.4都测试了下发现@hashhash的做法都可以出,所以又感觉不是这个问题,问了下Gemini也说确实是这样:

img

然而又过了一会儿@Aqua Cat把flag搞出来了,应该是爆破了模多项式,发现题目用的实际是:

然后赛后在discord上才看到@factoreal用的版本居然是:

image-20250723153926933

好家伙,这确实太新了,要不是@hashhash弄了个几乎百分百本地能秒出的exp,还真难怀疑是这个问题。

很早很早很早之前,也就是昨年出NKCTF的这道题目的时候@rec和我提过这一点,说他很早之前也被sage不同版本的modulus坑过,看来以后还要仔细留意下这个问题



总结

队友实在太强了,今年比赛对我来说真的算是混个老五冠军= ̄ω ̄=,赛后复现发觉审计代码的速度和写attack的速度还有很大进步空间,要慢慢努力了。

不过不管怎么说这次比赛结果很圆满,cyberCryer明年继续加油!