1599 words
8 minutes
IERAE CTF 2025 Writeup

这周跟 Project SEKAI 一起打了 IERAE CTF 2025,比赛的时候基本全花在做 Cubic Math 上了,但没做出来。赛后看了作者的解释,发现自己只差一步就能做出来了,很气。

Cubic Math#

secret = randrange(2**256)
primes = []
p = secret
while len(primes) < 10:
p = next_prime(p)
primes.append(p)
for _ in range(50):
a, b = map(int, input("a, b = ").split(","))
if a == secret:
print(FLAG)
exit()
if not (-2**31 <= a < 2**31 and -2**31 <= b < 2**31):
exit(1)
res = []
for p in primes:
R.<x> = PolynomialRing(GF(p))
if (x^3+a*x+b).is_irreducible():
res.append(p-secret)
print(res)

这题的逻辑很简单,给十个连续的素数,每次询问一个三次多项式 x3+ax+bx^3+ax+bpip_i 下是否可约,如果不可约就输出到 secret 的距离。

首先这题看起来跟椭圆曲线很像,实则毫无关系,因为椭圆曲线这个是否可约没啥性质。所以我们直接来考察三次多项式的可约性。

我们可以从两个角度考察三次多项式。首先,可约等价于它在模pp下有一个根,这可以用gcd(x**p-x, ?)来判断。但我们不知道secret,所以行不通。所以我们只能从三次方程求根公式想办法。

对于方程 x3+px+q=0x^3+px+q=0,它的解长这样

u1+u2,u13=q2+q24+p327,u23=q2q24+p327u_1+u_2, u_1^3 = -\frac{q}{2} + \sqrt{\frac{q^2}{4} + \frac{p^3}{27}}, u_2^3 = -\frac{q}{2} - \sqrt{\frac{q^2}{4} + \frac{p^3}{27}}

对称的 ωu1+ω2u2\omega u_1+\omega^2u_2ω2u1+ωu2\omega^2 u_1+\omega u_2 也都是解。

首先,模pp下的三次剩余现在仍然没做完,所以能不惹就先不惹。那我们就先考虑 q24+p327\sqrt{\frac{q^2}{4} + \frac{p^3}{27}} 这个二次剩余的部分。三次方程有一个判别式 Δ=(4p3+27q2)\Delta=-(4p^3+27q^2),当 Δ0\Delta\geq 0 时,方程有三个实根;当 Δ<0\Delta<0 时,方程有一个实根和两个共轭复根。这个式子在模 pp 下也有意义,我们可以认为 Δ0\Delta\geq 0 相当于模 pp 下是二次剩余。而 Δ<0\Delta<0 相当于模 pp 下不是二次剩余,会产生一个扩域。所以 Δ\Delta 不是二次剩余时,在扩域上有一对共轭的根,这个二次项是不可约的,然后剩下的一个根就不得不回退到 FpF_p,故而可约。

所以判别式相当于告诉了我们这样一件事,如果这个多项式不可约,那么 Δ\Delta 是二次剩余,但反之则不成立。这已经提供了足够的信息,如果可以询问无限多次的话,我们可以这样做:

  • 选取小素数qq,枚举出若干个 Δ=k2q\Delta=k^2q,因为可约不代表 Δ\Delta 不是二次剩余,所以需要多次询问,确定每个ppqq下的二次剩余。
  • 通过二次互反律,得知 pp 模这个小素数的二次剩余,根据这些素数的offset来查表得知secret模qq的值。
  • CRT解出secret。

但是这道题只有50次询问,平均每一次询问需要获取 55 bit 的信息。但因为判别式只在不可约的时候可以获取信息,而不可约的概率只有 13\frac{1}{3},所以是不够 55 bit 的。

在判别式寄了之后,剩下的方法只剩下用三次剩余了。好消息是,三次剩余并不是完全没有研究,至少三次互反律是存在的,这给了我们用三次互反律来CRT的可能。

三次剩余需要使用 Z(ω)\mathbb{Z}(\omega) 的高斯整数环来构造。这个环也是唯一分解整环,如果素数 p2(mod3)p\equiv 2\pmod{3},那么它在 Z(ω)\mathbb{Z}(\omega) 中也是不可约的。如果素数 p1(mod3)p\equiv 1\pmod{3},那么它可以分解为共轭的两个素数 p=(a+bω)(a+bω2)p=(a+b\omega)(a+b\omega^2)。然后我们称一个素数是 primary 的如果 a+bωa+b\omegabb33 的倍数。然后 N(a+bω)=a2ab+b2N(a+b\omega)=a^2-ab+b^2 是模长且一定模 3311。跟二次剩余类似,三次剩余也可以定义

(απ)3αN(π)13(modπ)\left(\frac{\alpha}{\pi}\right)_3\equiv\alpha^{\frac{N(\pi)-1}{3}} \pmod{\pi}

取值一定是三次单位根的一个。对任意两个 primary 的素数 α,π\alpha,\pi,有三次互反律 (απ)3=(πα)3\left(\frac{\alpha}{\pi}\right)_3=\left(\frac{\pi}{\alpha}\right)_3 成立。

如果我们想要在三次方程中使用三次互反律,首先要处理的就是在开立方根之前的那个二次剩余。如果我们随意地选数,不同模 pp 的开根会乱成一团,所以我们要直接构造 a,ba,b 使开根的数是平方数。这也意味着判别式跟一个平方数只差一个 3-3,无法给信息了。一个不错的选择是把根设置为 x=53+253x=\sqrt[3]{5}+\sqrt[3]{25}的所有共轭。这样我们只需要研究 55 的三次剩余,而且 553322,所以它在 Z(ω)\mathbb{Z}(\omega) 中也是素数。

看着十分美好,但实操却发现了很大的问题。因为 pp3322 时所有数都有立方根,所以我们需要再一开始赌有很多素数模 3311。而且就算对模 3311 的素数分析,也会遇到它自己又不是 Z(ω)\mathbb{Z}(\omega) 上的素数的问题。如果我们要对这个强行算类似 Jacobi 符号的东西,会直接永远是 11 寄掉。

以上是我在比赛时的思路,一看不太行就就转头开始继续对判别式做概率分析然后一头撞死。

author&#x27;s solution

比赛结束后,作者也发了他的解法,其实没太看懂为什么,但第一步是在单位根域上找一个三次的子域,比如 77 次单位根对应的就是 x321x7=0x^3-21x-7=0,然后转到我的做法是算 7(1+3ω)7(-1+3\omega) 的三次剩余。这里就发现问题了,他实际在开根的时候保留了一个 3\sqrt{-3} 然后直接转换到了整环上。这样就算对模 3311 的素数分析也能得到一个非平凡的 Jacobi 符号。然后就可以直接用三次互反律来做了。

随便写了一个脚本来验证这个思路的正确性,就不完整做了。

from sage.all import *
w = polygen(ZZ, 'w')
K = NumberField(w**2+w+1, 'w')
w = K.gen()
p = random_prime(2**8)
while p % 3 != 1:
p = random_prime(2**8)
a = list(K(p).factor())[-1][0]
def is_primary(a):
# stupid primary ideal check
u, v = a.parts()
v = ZZ(2*v)
return v % 3 == 0
a = next(a for a in [a, a*w, a*w**2] if is_primary(a))
print(a, a.norm())
norm = ZZ(a.norm())
ret = {}
for i in range(norm):
if i == 0:
continue
b = random_prime(2**20)
while b % norm != i:
b = random_prime(2**20)
b = K(b)
assert b.residue_symbol(K.ideal(a), 3) == a.residue_symbol(K.ideal(b), 3)
ret[ZZ(b)%norm] = a.residue_symbol(K.ideal(b), 3)
print(ret)
while True:
b = random_prime(2**40)
b = K(b)
print(b, ZZ(b)%norm, ret[ZZ(b)%norm], b.residue_symbol(K.ideal(a), 3), a.residue_symbol(K.ideal(b), 3))
assert ret[ZZ(b)%norm] == b.residue_symbol(K.ideal(a), 3) and ret[ZZ(b)%norm] == a.residue_symbol(K.ideal(b), 3)