Mobile wallpaper 1Mobile wallpaper 2Mobile wallpaper 3Mobile wallpaper 4Mobile wallpaper 5Mobile wallpaper 6
3635 words
18 minutes
PolarCTF网络安全2026春季个人挑战赛 WriteUp
2026-03-31

CRYPTO#

百万赏金#

任务描述.txt

干员,本次的行动任务非常艰巨,我们截获了博士的实验数据,但是这个文件被加密了,请你认真搜索,搞定就撤。 已知实验数据加密规则严格遵循以下顺序执行: 第一轮:对原始 Flag 执行凯撒密码加密(偏移量未知,偏移范围 110,仅对字母字符移位) 第二轮:将凯撒加密后的字符串执行栅栏密码加密(密钥未知,密钥范围 24,加密方式为按W型读取,且标准型带有两个特殊字符) 密文:DFGNBSZNGNMKFF

按任务描述倒推就行了呗。key = 4的 W 型栅栏求解:

轨道编号: 0 1 2 3 2 1 0 1 2 3 2 1 0 1 字符序列: D N G F N B F S M F K Z G N

rail 0: D F G rail 1: N B S Z N rail 2: G N M K rail 3: F F

得到第一轮结束第二轮前的密文:DNGFNBFSMFKZGN。然后凯撒爆破一下在shift = 5时找到flag

flag{YIBAIWANHAFUBI}

博士的实验数据#

题目:

题干 密文:QJBXQJFXZAKL已知为仿射密码加密,26 个大写英文字母映射规则不变:A=0,B=1,C=2,…,Z=25,加密核心公式:y ≡ (a·x + b) mod 26(x = 明文字母值,y = 密文字母值)。无直接密钥,仅给出 2 组明密文对应提示: 明文T 对应 密文X 明文F 对应 密文J 附加要求:仿射密码中a 必须与 26 互质(必要条件,需验证),请先推导密钥a、b,再解出完整明文。

关键提示 解同余方程组:将两组明密文值代入加密公式,得到两个模 26 等式,两式相减消去 b,先求 a,再回代求 b; 互质性验证:gcd (a,26)=1(最大公约数为 1),否则仿射密码无唯一解; 模逆元求解:扩展欧几里得算法,核心要求a·a⁻¹ ≡ 1 mod 26; 解密公式:x ≡ a⁻¹·(y - b) mod 26,若y-b为负数,先加 26 再计算,最终结果仍取模 26; 字母值速查:F=5,T=19,J=9,X=23(对应提示明密文)。

仿射加密,通过已知明密文对,可以求出a = 1, b = 4a = 1其实就是凯撒,直接解就行。

exp.py

c = "QJBXQJFXZAKL"
flag = "flag{"
for i in c:
flag += chr(((ord(i) - ord("A") - 4) % 26) + ord("A"))
flag += "}"
print(flag)
# flag{MFXTMFBTVWGH}

冰原上的OTP谜题#

冰原上的OTP谜题

小冰狐想用一次性密码本(OTP)加密 “winter_polarctf”,但它在生成密钥时犯了个有趣的错误:它把明文每个字符的 ASCII 值拆成二进制后,将第 i 位(从 0 开始,LSB 为第 0 位)与密钥对应位做了异或,却误将所有位的异或结果按 “第 7 位→第 0 位” 的顺序拼接成了密文。 已知: 明文:winter_polarctf(ASCII 编码,共 16 字节) 密钥生成规则:由 “ice” 的 ASCII 值(0x69, 0x63, 0x65)重复拼接至 16 字节 密文二进制串(按错误顺序拼接,共 16×8=128 位): 11010110 10010110 11101001 10101100 01100101 01001011 10111001 11011011 01110110 01011001 11001101 10110101 11001011 10001101 01011101 11101011 请还原正确的加密过程,计算出按正确顺序(第 0 位→第 7 位)拼接的密文二进制对应的十六进制,即为 flag(格式:十六进制小写,无空格)。

文字游戏么,只需要将密文逆转一下正确拼接并转成十六进制就行。

exp.py

cipher_bits = """
11010110 10010110 11101001 10101100 01100101 01001011 10111001 11011011
01110110 01011001 11001101 10110101 11001011 10001101 01011101 11101011
"""
groups = cipher_bits.split()
ans = ''.join(f'{int(b[::-1], 2):02x}' for b in groups)
print(f'flag{{{ans}}}')
# flag{6b699735a6d29ddb6e9ab3add3b1bad7}

RC4的密钥流泄露#

题目.txt

某设备采用RC4流密码加密传输数据,安全测试人员获取到一组“部分明文-密文对”及目标密文(目标密文对应明文为flag)。已知RC4加密为明文与密钥流逐字节异或,且两组数据使用相同密钥加密,可通过已知信息推导密钥流,进而破解目标密文。 已知信息:

  1. 加密算法:RC4流密码(密钥长度为8字节,可打印ASCII字符;加密/解密均为 明文字节 ⊕ 密钥流字节 = 密文字节,⊕表示异或运算);
  2. 核心特性:同一密钥生成的密钥流固定,且密钥流与明文长度一致;异或运算可逆(a⊕b=c → c⊕b=a、c⊕a=b),手动计算即可推导;
  3. 已知信息:
  • 已知明文片段(P):TestData_ForRC4_Decrypt(共22字节,对应密文前22字节);
  • 对应密文片段(C):54 65 73 74 44 61 74 61 5F 46 6F 72 52 43 34 5F 44 65 63 72 79 70(十六进制,空格分隔字节,共22字节);
  • 目标密文(C_flag):66 6C 61 67 7B 70 6F 6C 61 72 5F 6B 69 6E 67 6B 69 6E 67 7D(十六进制,共20字节,对应明文为flag);
  • 公钥指数 e = 17
  • 模数 n = 20099
  • 目标密文 c = 15523(修正为flag加密后的真实密文,替代原错误c=12345)
  1. 补充说明:RC4密钥流生成与明文无关,仅由密钥决定;本题无需推导完整密钥,仅需通过已知明文-密文对推导对应长度密钥流,即可解目标密文。 异或运算规则:0⊕0=0、0⊕1=1、1⊕0=1、1⊕1=0,十六进制转十进制后计算,结果再转回十六进制/ASCII。

。这不是没加密吗,最后 RC4 异或的是0,给的对应密文还少了一个字节。。。

exp.py

P = "TestData_ForRC4_Decrypt"
c = ""
for i in P:
c += hex(ord(i))[2:]
C = "54 65 73 74 44 61 74 61 5F 46 6F 72 52 43 34 5F 44 65 63 72 79 70".replace(" ", "")
assert C == c[:-2].upper()
C_flag = "66 6C 61 67 7B 70 6F 6C 61 72 5F 6B 69 6E 67 6B 69 6E 67 7D"
print(bytes.fromhex(C_flag).decode())
# flag{polar_kingking}

伪ASR#

题目:

from sympy import isprime
from sympy.ntheory import legendre_symbol
import random
from Crypto.Util.number import bytes_to_long
k=79 #<-- i couldn't stress more
def get_p():
global k
while True:
r=random.randint(2**69,2**70)
p=2**k*r+1
if isprime(p):
return p
else:
continue
def get_q():
while True:
r=random.randint(2**147,2**148)
q=4*r+3
if isprime(q):
return q
else:
continue
def get_y():
global n,p,q
while True:
y=random.randint(0,n-1)
if legendre_symbol(y,p)==1:
continue
elif legendre_symbol(y,q)==1:
continue
else:
return y
flag=b'flag{redacted:)}'
flag_pieces=[flag[0:10],flag[11:21],flag[22:32],flag[33:43],flag[44:]]
#assert int(bytes_to_long((flag_pieces[i] for i in range(5)))).bit_length()==k
p=get_p()
q=get_q()
n=p*q
print(f'{n=}')
y=get_y()
print(f'{y=}')
def encode(m):
global y,n,k
x = random.randint(1, n - 1)
c=(pow(y,m,n)*pow(x,pow(2,k),n))%n
return c
cs=[]
for i in range(len(flag_pieces)):
ci=encode(bytes_to_long(flag_pieces[i]))
cs.append(ci)
print(f'{cs=}')
'''
# n = ...
# y = ...
# cs = [...]
'''

加密核心代码如下:

p = 2^k * r + 1
q = 4*r + 3
n = p*q
c = y^m * x^(2^k) mod n

其中我们已知:k = 79,且y满足:

(yp)=(yq)=1\left( \frac{y}{p} \right)=\left( \frac{y}{q} \right)=-1

同时p也是一个特殊的素数满足:

p=279r+1p1=279rp=2^{79}r+1\Rightarrow p-1=2^{79}r

说明模p下的乘法群 Fp\mathbb{F}_p^* 含有一个阶为2^79的子群。

加密形式是一个典型的高次剩余类加密:

cymx279(modn)c\equiv y^m\cdot x^{2^{79}}\pmod{n}

观察n

p1(mod279)qn(mod279)p\equiv 1\pmod{2^{79}}\Rightarrow q\equiv n\pmod{2^{79}}

也就是说:

q=q0+279tq=q_0+2^{79}t

并且位数也不大,用factordb分解就行(CopperSmith 是不是应该也行虽然我没打出来哦莫)。

接着我们取r = (p - 1) // (2 ** 79),在模p下:

crymrx279rc^r\equiv y^{mr}x^{2^{79}r}

而前面已经说过:

279r=p1xp11(modp)2^{79}r=p-1\Rightarrow x^{p-1}\equiv 1\pmod{p}

因此:

crymr(modp)c^{r}\equiv y^{mr}\pmod{p}

随机项x就被消除了。

然后我们设:

g=yr(modp)g=y^r \pmod{p}

而由于ypq下均为非二次剩余,则:

  • p时,y的指数为奇数,可推出y^r的阶恰好为2^79

因此整个表达式都落在该子群中,有:

cr=(yr)m=gmc^r=(y^r)^m=g^{m}

离散对数在该子群中计算,指数按模2^79计算的:

logg(cr)m(mod279)log_g(c^r)\equiv m\pmod{2^{79}}

也就是说对c^r%p的离散对数的结果就是m

exp.py

# SageMath 10.7
from Crypto.Util.number import long_to_bytes
from sage.all import *
n = ...
y = ...
cs = [
...
]
k = 79
p = 685956097824618861007209469433124282167197697
q = 729686531647380216431209698235054102301315851
r = (p - 1) // (2 ** k)
Fp = GF(p)
g = Fp(pow(y, r, p))
flag = b""
for i, c in enumerate(cs):
c1 = Fp(pow(c, r, p))
m = discrete_log(c1, g)
flag += long_to_bytes(m)
print(flag.decode())
# flag{go0_j06!let1sm0v31n_t0_th3renges~>_<}

ECC的攻击模块#

题目:

from Crypto.Util.number import bytes_to_long
from hashlib import sha256
from os import urandom
# from secret import p, a, b, flag
p=getPrime(512)
flag=b'flag{??????????????}'
a=randint(1,p-1)
b=randint(1,p-1)
ECC = EllipticCurve(GF(p), [a, b])
R, E, C = [ECC.random_point() for _ in range(3)]
M=Matrix(ZZ,len(flag),3)
pad = lambda m: urandom(8) + m + b'\x00' * (ZZ(p).nbits() // 8 - len(m) - 8 - 1)
out = list()
for i in range(len(flag)):
m = pad(chr(flag[i]).encode())
nonce = urandom(16)
sh = sha256(nonce + m).digest()
Q = b2l(m)*R + b2l(nonce)*E + b2l(sh)*C
out.append(Q)

原题么。。传送门:【巅峰极客 2023】Rosita

题目先随机生成pab作为曲线ECC的参数,并在曲线上随机取了REC三点,但是参数和坐标均未知。接着自定义了pad方式:

pad = lambda m: urandom(8) + m + b'\x00' * (ZZ(p).nbits() // 8 - len(m) - 8 - 1)

flag的每一个字节进行如上方式的填充,并在每一次加密过程中都生成一个临时字节流nonce,并计算nonce + pad(flag[i])sha256哈希值,最后给出对应的点Qi坐标:

Qi=miR+nonceiE+shiCQ_i=m_i\cdot R+nonce_i\cdot E+sh_i\cdot C

要求我们通过所有Qi坐标还原出m

首先肯定得先还原出曲线ECC的参数pab,曲线方程为:

y2=x3+ax+b(modp)y^2=x^3+ax+b\pmod{p}

我们可知Qi也均为曲线上的点且我们有很多组,故可以Qi写作:

yixi3+axi+b(modp)y_i\equiv x_i^3+ax_i+b\pmod{p}

这里可以仿照无参数的LCG的思路,通过消元的方法最后能够通过gcd打出p,然后再代回原模方程中求解ab。这种方法上面传送门中鸡块师傅的推导过程已经很清晰了,尝试一下梭哈的办法求 Gröbner 基来还原abp参数。

recover_Parameters.sage

from sage.all import *
from itertools import combinations
from math import gcd
ns = {}
with open("坐标.txt", "r", encoding="utf-8") as f:
exec(f.read(), {}, ns)
coords = [tuple(map(Integer, P)) for P in ns["x"]]
kps = []
for idx in combinations(range(len(coords)), 3):
cc = None
(x1, y1), (x2, y2), (x3, y3) = coords[idx[0]], coords[idx[1]], coords[idx[2]]
R = PolynomialRing(ZZ, ['a', 'b', 'p', 'k1', 'k2', 'k3'], order='lex')
a, b, p, k1, k2, k3 = R.gens()
f1 = y1 ** 2 - x1 ** 3 - a * x1 - b - k1 * p
f2 = y2 ** 2 - x2 ** 3 - a * x2 - b - k2 * p
f3 = y3 ** 2 - x3 ** 3 - a * x3 - b - k3 * p
G = Ideal([f1, f2, f3]).groebner_basis()
for g in reversed(G):
cc = abs(Integer(g.constant_coefficient()))
if cc != 0:
break
if cc is not None:
kps.append(cc)
for i in range(len(kps)):
for j in range(i + 1, len(kps)):
g = Integer(gcd(int(kps[i]), int(kps[j])))
if g.bit_length() <= 512:
print("p =", g)
p = g
u1 = (pts[0][1]^2 - pts[0][0]^3) % p
u2 = (pts[1][1]^2 - pts[1][0]^3) % p
a = ((u2 - u1) * inverse_mod(pts[1][0] - pts[0][0], p)) % p
b = (u1 - a*pts[0][0]) % p
print("a =", a)
print("b =", b)
raise SystemExit
"""
p = 11093300438765357787693823122068501933326829181518693650897090781749379503427651954028543076247583697669597230934286751428880673539155279232304301123931419
a = 490963434153515882934487973185142842357175523008183292296815140698999054658777820556076794490414610737654365807063916602037816955706321036900113929329671
b = 7668542654793784988436499086739239442915170287346121645884096222948338279165302213440060079141960679678526016348025029558335977042712382611197995002316466
"""

恢复参数了之后拿到原曲线,但是我们测试后发现这是一个异常椭圆曲线:E.order() = p

p = 11093300438765357787693823122068501933326829181518693650897090781749379503427651954028543076247583697669597230934286751428880673539155279232304301123931419
a = 490963434153515882934487973185142842357175523008183292296815140698999054658777820556076794490414610737654365807063916602037816955706321036900113929329671
b = 7668542654793784988436499086739239442915170287346121645884096222948338279165302213440060079141960679678526016348025029558335977042712382611197995002316466
E = EllipticCurve(GF(p), [a, b])
print(E.order())
# 11093300438765357787693823122068501933326829181518693650897090781749379503427651954028543076247583697669597230934286751428880673539155279232304301123931419

这时候我们就可以通过 Smart Attack 攻击将椭圆曲线上的离散对数问题转成模p上的普通属于问题:

对于任选生成元G,对每个输出点Qi求:

dilogG(Qi)(modp)d_i\equiv log_G(Q_i)\pmod{p}

类比Qi,我们可以令:

R=rG,E=eG,C=cG,Qi=diGR=rG,E=eG,C=cG,Q_i=d_iG

则原椭圆曲线上的式子可变为:

dimir+nonceie+shic(modp)d_i\equiv m_i\cdot r+nonce_i\cdot e+sh_i\cdot c\pmod{p}

接下来注意一下mipad结构:

mi=(8随机字节flag字节)2548m_i=(8随机字节||flag字节)\cdot 2^{54\cdot 8}

下面我们令pad(flag[i]) = mi * 2 ** 432,我们现在可以根据上面的等式找到矩阵的等式:

(m0nonce0sh0m1nonce1sh1m71nonce71sh71m72nonce72sh72)(2432rec)=(d0d1d71d72)\begin{pmatrix} m_0 & nonce_0 & sh_0\\ m_1 & nonce_1 & sh_1\\ &\cdots\\ m_{71} & nonce_{71} & sh_{71}\\ m_{72} & nonce_{72} & sh_{72}\\ \end{pmatrix} \begin{pmatrix} 2^{432}r\\ e\\ c \end{pmatrix} = \begin{pmatrix} d_0\\ d_1\\ \cdots\\ d_{71}\\ d_{72} \end{pmatrix}

我们记上述等式为:

MXD(modp)MX\equiv D\pmod{p}

我们的目标并非直接求出X,而是想办法恢复所有的mi

注意到矩阵M的列数只有 3,因此它的左核空间维度很大。如果取一个向量

v=(v0,v1,,v72)Z73v=(v_0,v_1,\dots,v_{72})\in\mathbb Z^{73}

该向量满足

vTM=0v^TM=0

那么左乘原式可得

vT(MX)vTD(modp)v^T(MX)\equiv v^TD\pmod p

由此可得

vTD0(modp)v^TD\equiv 0\pmod p

也就是说,任何与M的三列都正交的向量,也一定与D在模p意义下正交。反过来说,我们虽然并不知道M,但是可以先从已知的D出发,去构造所有满足

vTD0(modp)v^TD\equiv 0\pmod p

的整数向量。这样的向量空间中会包含真正与M正交的那些关系。接下来再通过核空间把M反推出来。

接下来我们要构造与D正交的格,我们要找的是整数向量v,使得

i=072vidi0(modp)\sum_{i=0}^{72} v_i d_i \equiv 0\pmod p

等价存在整数t使得

i=072vidi+tp=0\sum_{i=0}^{72} v_i d_i + tp = 0

于是可以构造如下格基:

L=(100Kd0010Kd1001Kd72000Kp)L= \begin{pmatrix} 1 & 0 & \cdots & 0 & Kd_0\\ 0 & 1 & \cdots & 0 & Kd_1\\ \vdots & \vdots & \ddots & \vdots & \vdots\\ 0 & 0 & \cdots & 1 & Kd_{72}\\ 0 & 0 & \cdots & 0 & Kp \end{pmatrix}

这里K是一个足够大的权重。我们对格向量:

(v0,,v72,t)L(v0,\dots,v72,t)L

进行格基规约,得到最后的一个坐标就是

K(i=072vidi+tp)K(\sum^{72}_{i=0}v_id_i+tp)

当这个格向量很短是,最后一维也会倾向于比较小;而由于这一维被乘上了很大的K,最优情况下就会逼迫

i=072vidi+tp=0\sum_{i=0}^{72} v_i d_i + tp = 0

成立,即

vTD0(modp)v^TD\equiv 0\pmod p

所以,对这个格做 LLL 之后,得到的短向量前 73 维就会给出一批与Dp正交的整数关系。接着取这些短向量的前 70 行(经验做法)为矩阵R,也就是选取一批较短的正交关系,其中每行都满足

vTD0(modp)v^TD\equiv 0 \pmod{p}

并且我们希望其中包含足够多的来自M左核空间的关系。而前面提到过:真正与M的三列正交的向量,一定也会满足与Dp正交。因此,R的行空间中应当包含M的左核中的许多向量。如果某一向量xM的一列,则对于所有这些左核向量v都满足

vTx=0v^Tx=0

那么M的三列向量都应落在R的右核中,于是我们直接求R的右核获得所有满足

Rx=0Rx=0

的向量空间。然后其中很明显m列的向量是其中最短的一列,我们对右核基再做一次 LLL 就会得到这一列,再取最低有效字节即可获得flag

exp.py

# SageMath 10.7
from Crypto.Util.number import *
from random import randint
def SmartAttack(P, Q, p):
E = P.curve()
Eqp = EllipticCurve(Qp(p, 2), [ZZ(t) + randint(0, p)*p for t in E.a_invariants()])
P_Qps = Eqp.lift_x(ZZ(P.xy()[0]), all=True)
for P_Qp in P_Qps:
if GF(p)(P_Qp.xy()[1]) == P.xy()[1]:
break
Q_Qps = Eqp.lift_x(ZZ(Q.xy()[0]), all=True)
for Q_Qp in Q_Qps:
if GF(p)(Q_Qp.xy()[1]) == Q.xy()[1]:
break
p_times_P = p * P_Qp
p_times_Q = p * Q_Qp
x_P, y_P = p_times_P.xy()
x_Q, y_Q = p_times_Q.xy()
phi_P = -(x_P / y_P)
phi_Q = -(x_Q / y_Q)
k = phi_Q / phi_P
return ZZ(k)
ns = {}
with open("坐标.txt", "r", encoding="utf-8") as f:
exec(f.read(), {}, ns)
coords = [tuple(map(Integer, P)) for P in ns["x"]]
p = 11093300438765357787693823122068501933326829181518693650897090781749379503427651954028543076247583697669597230934286751428880673539155279232304301123931419
a = 490963434153515882934487973185142842357175523008183292296815140698999054658777820556076794490414610737654365807063916602037816955706321036900113929329671
b = 7668542654793784988436499086739239442915170287346121645884096222948338279165302213440060079141960679678526016348025029558335977042712382611197995002316466
E = EllipticCurve(Zmod(p), [a, b])
G = E.gens()[0]
d = []
for Pxy in coords:
Q = E(Pxy)
d.append(int(SmartAttack(G, Q, p)))
n = len(coords)
L = Matrix(ZZ, n + 1, n + 1)
K = 2 ** 100
for i in range(n):
L[i, i] = 1
L[i, -1] = d[i] * K
L[-1, -1] = p * K
res = L.LLL()
R = Matrix(ZZ, 70, n)
for i in range(70):
R[i] = res[i][:-1]
basis = R.right_kernel().basis()
basis = Matrix(ZZ, basis)
v = basis.LLL()[0]
flag = b""
for x in v:
flag += long_to_bytes(int(x & 0xff))
print(flag.decode())
# Congratulations! Your flag is: flag{893d041e-c0a2-3145-5320-cdee7d3c87fb}
PolarCTF网络安全2026春季个人挑战赛 WriteUp
https://q1uju.cc/posts/polarctf2026spring_writeup/
Author
Q1uJu
Published at
2026-03-31
License
CC BY-NC-SA 4.0

Some information may be outdated