散题(1)
以后一些我觉得比较有趣的题的WP都会放到“散题”栏目里面
NepCTF 2025 | Lattice Bros
#已知α的极小多项式为三次多项式f(x),即f(α)=0,且α≈54236.606188881754809671280151541781895183337725393 |
观察对flag的加密可以得知这是一个HNP,在求解HNP之前我们需要先知道p,由题可知$p=d-a_0$,且$a_0$是$\alpha\approx54236.6061888\cdots$的三次极小多项式的常数项,参考A Gentle Tutorial for Lattice-Based Cryptanalysis的3.6可以知道如果我们要求该极小多项式,可以构造如下格:
上述矩阵中第一列乘上的系数$10^{45}$是依照题目给出的$\alpha$的近似值精度选取的,对于该格基,有:
根据A Gentle Tutorial for Lattice-Based Cryptanalysis之3.6节所述可以知道直接对$\pmb{B}$进行规约即可得到目标向量,从而恢复出$a_0$:alpha = RealField(200)(54236.606188881754809671280151541781895183337725393)
N = 10^45
L = matrix(ZZ, [[floor(N), 1, 0, 0], [floor(N * alpha), 0, 1, 0], [floor(N * alpha^2), 0, 0, 1], [floor(N * alpha^3), 0, 0, 0]])
res = L.LLL()[0]
a0 = res[1]
print(res)
# 重新求根验证
R.<x> = RealField(1000)[]
f = res[1] + res[2] * x + res[3] * x^2 + x^3
eps = f.roots()[0][0] - alpha
求出$p$之后尝试直接构造如下格进行规约求解:
发现无法规约出想要的$\alpha$(与求极小多项式时的$\alpha$不同),可能是$\alpha$比较大,这个格难以直接规约出我们想要的目标向量,可能需要进行一次配平,但是我们不知道$\alpha$的具体大小,无法计算出具体的配平系数,故尝试添加一个未知的配平系数进行爆破,得到如下格:
测试发现当$k=2$的时候可以得到flag:from Crypto.Util.number import *
from tqdm import trange
samples = [...]
alpha = RealField(200)(54236.606188881754809671280151541781895183337725393)
N = 10^45
L = matrix(ZZ, [[floor(N), 1, 0, 0], [floor(N * alpha), 0, 1, 0], [floor(N * alpha^2), 0, 0, 1], [floor(N * alpha^3), 0, 0, 0]])
res = L.LLL()[0]
a0 = res[1]
print(res)
# 重新求根验证
R.<x> = RealField(1000)[]
f = res[1] + res[2] * x + res[3] * x^2 + x^3
eps = f.roots()[0][0] - alpha
assert abs(eps) < 1/N
n = 30
d = 981020902672546902438782010902608140583199504862558032616415
B = 2^30
p = d - a0
def solve(k):
L = matrix(QQ, n+2, n+2)
for i in range(n):
t, a = samples[i]
L[i, i] = p
L[-2, i] = -t
L[-1, i] = a
L[-2, -2] = (B * 2^k) / p
L[-1, -1] = B
res = L.LLL()[1]
betas = res[:-2]
alpha = int(abs(res[-2] * p)) // (B * 2^k)
flag = long_to_bytes(alpha)
if b'NepCTF' in flag:
return flag
return None
for i in trange(1000):
try:
if solve(i):
print(solve(i))
break
except:
pass
补充
后来看了一下SeanDictionary师傅的wp(NepCTF 2025 - SeanDictionary | 折腾日记)才知道原来对格:
直接规约之后得到的$\alpha$只需要在模$p$下取相反数就能得到flag(学到了>ω<):from Crypto.Util.number import *
from tqdm import trange
samples = [...]
alpha = RealField(200)(54236.606188881754809671280151541781895183337725393)
N = 10^45
alpha = RealField(200)(54236.606188881754809671280151541781895183337725393)
N = 10^45
L = matrix(ZZ, [[floor(N), 1, 0, 0], [floor(N * alpha), 0, 1, 0], [floor(N * alpha^2), 0, 0, 1], [floor(N * alpha^3), 0, 0, 0]])
res = L.LLL()[0]
a0 = res[1]
print(res)
# 重新求根验证
R.<x> = RealField(1000)[]
f = res[1] + res[2] * x + res[3] * x^2 + x^3
eps = f.roots()[0][0] - alpha
assert abs(eps) < 1/N
n = 30
d = 981020902672546902438782010902608140583199504862558032616415
B = 2^30
p = d - a0
L = matrix(QQ, n+2, n+2)
for i in range(n):
t, a = samples[i]
L[i, i] = p
L[-2, i] = -t
L[-1, i] = a
L[-2, -2] = B / p
L[-1, -1] = B
res = L.LLL()[1]
betas = res[:-2]
alpha = (p - int(abs(res[-2] * p)) // B) % p
flag = long_to_bytes(alpha)
print(flag)
NepCTF 2025 | ezRSA2
from Crypto.Util.number import getStrongPrime, getRandomNBitInteger, GCD, inverse, long_to_bytes, bytes_to_long, sieve_base |
根据参数生成函数可以知道本题中使用的$d<N^{\frac{1}{3}}$(大约是675 bits),显然私钥比较小,但是还是大于维纳攻击($d<N^{0.25}$)以及Boneh&Durfee攻击的边界($d<N^{0.292}$)的,题中还给出了:
其中$p_i$为第$i$个质数,令$M=\prod_{i=2}^{r+2}p_{i}$,则可以通过CRT还原出$d_0=d\mod{M}$,也就是$d_0=d-lM$($l\ge0$).因为$d<N^{\frac{1}{3}}$还算是比较小的,而且我们已经知道$d$的大小约为$N^{\frac{1}{6}}$的部分了,所以可以考虑用类似于维纳攻击的方法来求解这道题,因为$ed-k\varphi(N)=1$,那么可以得到:
令$s=1-p-q$,则有:
于是构造格:
预期有$(1, l, k)\pmb{B}=(1, l, ks+1)$,由$ed-k\varphi(N)=1$可以知道$k\approx2^{675},l\approx2^{333}$有$\det(\pmb{B})=N$,$||(1, l, ks+1)||\approx 2^{675+1024}=2^{1699}$,显然有:
故需要进行配平,粗略计算可以得到如下格:
此时进行规约就可以得到目标向量$(2^{1699}, 2^{1366}l, ks+1)$,从而可以计算出$d=d_0+lM$进行解密得到flag了:from Crypto.Util.number import *
from sympy import prod
from tqdm import trange
e = ...
N = ...
ct = ...
hints = [1, 3, 0, 3, 9, 16, 10, 14, 5, 11, 21, 18, 30, 30, 38, 2, 20, 62, 66, 1, 22, 56, 41, 13, 78, 59, 51, 6, 57, 117, 73, 75, 96, 112, 50, 93, 158, 97, 146, 8, 65, 96, 186, 161, 90, 131, 46, 32, 140, 133, 50, 43, 151, 234]
mod = [sieve_base[i] for i in range(1, len(hints) + 1)]
M = prod(mod)
d0 = crt(hints, mod)
L = matrix(ZZ, [[2^1699, 0, e*d0], [0, 2^1366, e*M], [0, 0, -N]])
d = ZZ(abs(L.LLL()[0][1] // 2^1366) * M + d0)
assert d.bit_length() == floor(2048 * .33)
m = pow(ct, d, N)
flag = long_to_bytes(m)
print(flag)
DeadSec CTF 2025 | imrul kAES
#!/usr/local/bin/python |
一道比较折磨的题,观察服务端代码可以看到对于每一次连接,代码都会生成一个模数$n=pq$,并计算$d\equiv e^{-1}\pmod{\varphi(n)}$,然后对flag进行RSA加密,之后用户可以进行无数次输入,对于每个输入的数据$i$,程序会将其使用前面计算出的RSA私钥进行解密,然后与$2^{512}-1$相与,最终转换成bytes,再进行AES加密(密钥是随机的16字节,无法爆破).
可以知道,对于输入的数据$i$,计算$i^{d}\mod{n}$之后与$2^{512}-1$相与,实际上就是取$i^{d}\mod{n}$转换得到的字符串的后$512\div8=64$字节,既然存在这种关系,且我们又已知RSA加密flag(设为$pt$)后得到的密文$ct$,那么我们就可以让输入的$i$解密后的内容是flag + k * b'\x00'
,实际上就是让:
那么就可以通过控制$k$的大小,从后向前通过类似Padding Oracle Attack的方式来爆破flag,但是在进行攻击操作之前我们需要知道$n$的具体数值,由输入部分的代码可以知道,如果我们输入的$i$不在$(0, n)$的区间内,尽管它会显示bad input, exiting
,但是程序并不会真的退出,那么我们可以通过二分的方式来爆破出$n$(大概率需要爆破1024次),在获得$n$之后,我们就可以通过类似Padding Oracle Attack的方式来逐字恢复flag了:
from Crypto.Util.number import * |
在我的电脑上大概20分钟左右就可以爆破出flag: