2026PolarCTF春季赛密码全解

尽力了 拿了第23名
2-1 百万赏金
题目:干员,本次的行动任务非常艰巨,我们截获了博士的实验数据,但是这个文件被加密了,请你认真搜索,搞定就撤。
已知实验数据加密规则严格遵循以下顺序执行:
第一轮:对原始 Flag 执行凯撒密码加密(偏移量未知,偏移范围 1~10,仅对字母字符移位)
第二轮:将凯撒加密后的字符串执行栅栏密码加密(密钥未知,密钥范围 2~4,加密方式为按W型读取,且标准型带有两个特殊字符)
密文:DFGNBSZNGNMKFF
题目按照W型读取(Zip-Zag路径)
按原始路径顺序重新提取后,得到凯撒加密状态下的中间字符串: DNGFNBFSMFKZGN
之后再爆破凯撒的key 当偏移量为5时 得出YIBAIWANHAFUBI
最后答案就是flag{YIBAIWANHAFUBI}
也可以写脚本去爆破栅栏密码和凯撒的key 同样可以解出答案
2-2 博士的实验数据
题干
密文: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(对应提示明密文)。
将明文T(19) 密文X(23) 明文F(5) 密文J(9)分别代入关系式中得到两个同余式
化简一下可得
也就是说14a-14是26的倍数,即14(a-1)=26k,同除以2得到:7(a-1) =13k
因为gcd(7,13)=1 , 所以13必须整除(a-1) , 结果显而易见a只能为1,b则为4
x ≡ (y - 4) mod 26偏移量为4的凯撒 得出结果是 MFXTMFBTVWGH
flag{MFXTMFBTVWGH}
2-3 RC4的密钥泄露
某设备采用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)
4. 补充说明:RC4密钥流生成与明文无关,仅由密钥决定;本题无需推导完整密钥,仅需通过已知明文-密文对推导对应长度密钥流,即可解目标密文。
异或运算规则:0⊕0=0、0⊕1=1、1⊕0=1、1⊕1=0,十六进制转十进制后计算,结果再转回十六进制/ASCII。
已知 RC4 满足:P ⊕ K = C
所以:K = P ⊕ C
给的已知明文片段是:TestData_ForRC4_Decrypt
它的前 22 字节十六进制正好是:
54 65 73 74 44 61 74 61 5F 46 6F 72 52 43 34 5F 44 65 63 72 79 70
而给的对应密文片段也正好是同一串:
54 65 73 74 44 61 74 61 5F 46 6F 72 52 43 34 5F 44 65 63 72 79 70
因此前 22 字节密钥流就是:
K = P ⊕ C = 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
也就是前 22 字节全 0。
接着解目标密文 C_flag:
66 6C 61 67 7B 70 6F 6C 61 72 5F 6B 69 6E 67 6B 69 6E 67 7D
因为同一密钥流、且前 20 字节也都是 0,所以:
P_flag = C_flag ⊕ 00...00 = C_flag
把它转成 ASCII:flag{polar_kingking}
ps:给的rsa解出来无意义
2-4冰原上的OTA谜题
小冰狐想用一次性密码本(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(格式:十六进制小写,无空格)。
在标准的计算机处理中,一个字节的 8 个比特通常按 第 7 位(MSB) -> 第 0 位(LSB) 的顺序书写。小冰狐的加密逻辑是基于“第 0 位 -> 第 7 位”去逐位计算异或的,但在最后生成二进制字符串时,却错误地按照标准的高位到低位(7→0)顺序将其拼接了上去。
我们只需要将截获的 16 个字节的错误二进制流逐个进行翻转(0位与7位换,1位与6位换,以此类推),并转换成十六进制
脚本直接解
s = "11010110100101101110100110101100011001010100101110111001110110110111011001011001110011011011010111001011100011010101110111101011"
flag = ""
for i in range(0, len(s), 8):
chunk = s[i:i+8]
flag += f"{int(chunk[::-1], 2):02x}"
print=(flag)
flag{6b699735a6d29ddb6e9ab3add3b1bad7}
2-5 伪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 = 500532925884017190157531654042977388637611201227338971326884172046371105194776392356795147
# y = 213088474978954913521695933149257926315459990908578573756933176330915972508162260163992936
# cs = [57494912618263048538571755953837772127117773898872797680570116373460237301011181142984690, 344186007342959044249362172584754916978318670779607618696087105142714882053499189453591750, 11170932486684627637967687021711067484959106608189352734064089980678923008744240797135422, 73837068555811384284867151570572743386582880055013744261872093001909203963879165023864836, 64356403000986744386743473269071732498867064770469172347340097989063717305436807805878673]
'''
这道题看了叁玖师傅的wp说nn没办法用yafu分解 但是比赛过程中我确实是分解出来了的

可能是非预期解 还是学习一下这位师傅的做法叭
看一下题目的加密逻辑
p=2** 79 * r+1 q=4 * r + 3 n=p*q
r 很小,用 Coppersmith 找出 r ,从而恢复 p
把flag分为五部分分别进行加密
观察一下可发现p-1光滑 且p−1=2^79⋅r
根据费马小定理:
我们在模 p 下
可以将x{2{79}}这一项完全消掉 得到离散对数
而且 g 的阶是 2^{79}。
所以可以把 m 看成 79 位二进制:
然后一位一位求。
from Crypto.Util.number import long_to_bytes
k = 79
P.<x> = PolynomialRing(Zmod(n))
f = (2^k) * x + 1
roots = f.monic().small_roots(X=2^70, beta=0.49, epsilon=0.015)
r_p = Integer(roots[0])
p = Integer((2^k) * r_p + 1)
rp_val = (p - 1) // (2^k)
#P1 = 729686531647380216431209698235054102301315851
#P2 = 685956097824618861007209469433124282167197697
#if (P1 - 1) % (2**k) == 0:
# p, q = P1, P2
#else:
# p, q = P2, P1
y = 213088474978954913521695933149257926315459990908578573756933176330915972508162260163992936
cs = [
57494912618263048538571755953837772127117773898872797680570116373460237301011181142984690,
344186007342959044249362172584754916978318670779607618696087105142714882053499189453591750,
11170932486684627637967687021711067484959106608189352734064089980678923008744240797135422,
73837068555811384284867151570572743386582880055013744261872093001909203963879165023864836,
64356403000986744386743473269071732498867064770469172347340097989063717305436807805878673
]
r = (p - 1) // (2**k)
flag = b''
for i, c in enumerate(cs):
Fp = GF(p)
cp = Fp(c)
yp = Fp(y)
# 消除盲化因子
C_prime = cp^r
Y_prime = yp^r
# Pohlig-Hellman 求离散对数
m = discrete_log(C_prime, Y_prime)
piece = long_to_bytes(int(m))
flag += piece
print(f"[+] Block {i+1} 解密成功: {piece}")
print(f"\n {flag.decode()}")
求解p,q可以分解 也可以打copper 就都贴上了
最终flag{go0_j06!let1sm0v31n_t0_th3renges~>_<}
2-6 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) #填充\x00
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)
1:恢复a,b,p
题目并没有直接给出素数 p 以及曲线参数 a 和 b,而是只给出了 k 个点 (X_i, Y_i) 的坐标。这些点都在同一条曲线上,且满足
我们可以通过构造差分方程来消去未知数。令
将相邻的点相减,可以消去 b:
为了进一步消去 a 并求出 p,我们可以取三个点 (i, i+1, i+2) 进行交叉相乘。可以利用了以下行列式关系构造了一个必定能被 p 整除的数
由于在实数环上 D 不为 0,但在模 p下
所以 D 必然是 p 的倍数。
操作步骤:
计算出所有的 D_i 后,对它们求最大公约数,并剔除掉小的素因子(如 2 和 3),即可精准恢复出模数 p。随后通过逆元运算即可求出 a 和 b
2. Smart Attack
应该是异常曲线,可以利用 Smart Attack 将椭圆曲线群同构映射到有限域加法群上
3.HNP
经过 Smart Attack 后,我们得到了 k个线性方程,但每个方程中包含了随机的 nonce 和 Hash 值 sh,这意味着普通的格规约很难直接奏效。我们需要消除 phi(R), phi(E), phi(C) 这三个基准未知数的影响。
我们构造一个 k *k 的矩阵 M,试图寻找一组整数系数 c_i,使得:
构造的矩阵形式如下
对该矩阵打 LLL。因为有 3 个干扰基底(对应 3 个维度),这 k个向量其实被限制在一个 3 维的子空间中。LLL 规约后,矩阵中前 k-3 行 就构成了这三个基底的正交补空间。
最后对右核的基底矩阵 B 再次进行 LLL 规约,LLL 算法会自动寻找晶格中最短的向量。这个最短向量上的各个元素,在取绝对值并模 256 后,就是原本填充在 m_i 中的 flag 字符。
import re
from sage.all import *
def smart_log(P, p, E_qp):
x_orig, y_orig = ZZ(P[0]), ZZ(P[1])
P_qp = next((pt for pt in E_qp.lift_x(x_orig, all=True) if GF(p)(pt[1]) == GF(p)(y_orig)), None)
if P_qp is None: raise ValueError("未找到匹配的提升点")
p_P = E_qp(0)
Q, k = P_qp, p
while k > 0:
if k & 1: p_P += Q
Q += Q
k >>= 1
x_p, y_p = p_P.xy()
return int(GF(p)(-(x_p / y_p) / p))
def solve(filename='坐标.txt'):
try:
with open(filename, 'r', encoding='utf-8') as f:
matches = re.findall(r'(\d+)\s*,\s*(\d+)', f.read())
Q_pts = [(ZZ(x), ZZ(y)) for x, y in matches]
except FileNotFoundError:
return print(f"[-] 未找到文件: {filename}")
if not Q_pts: return print("[-] 坐标提取失败")
n = len(Q_pts)
print("[*] 阶段一:恢复参数 p, a, b...")
E_vals = [y**2 - x**3 for x, y in Q_pts]
diff_x = [Q_pts[i][0] - Q_pts[i+1][0] for i in range(n-1)]
diff_E = [E_vals[i] - E_vals[i+1] for i in range(n-1)]
p = 0
for i in range(n - 2):
p = gcd(p, diff_E[i] * diff_x[i+1] - diff_E[i+1] * diff_x[i])
for prime in primes(100):
while p % prime == 0 and p > prime:
p //= prime
a = (diff_E[0] * inverse_mod(diff_x[0], p)) % p
b = (E_vals[0] - a * Q_pts[0][0]) % p
print("[*] 阶段二:Smart 攻击映射至整数域...")
Eqp = EllipticCurve(Qp(p, 5), [ZZ(a), ZZ(b)])
q_vals = [smart_log(pt, p, Eqp) for pt in Q_pts]
L = Matrix(ZZ, n, n)
pivot = next((i for i in range(n-1, -1, -1) if q_vals[i] != 0), -1)
if pivot == -1: raise ValueError("[-] 映射值均为 0,攻击失效。")
inv_q_pivot = inverse_mod(q_vals[pivot], p)
row_idx = 0
for i in range(n):
if i == pivot: continue
L[row_idx, i] = 1
L[row_idx, pivot] = (-q_vals[i] * inv_q_pivot) % p
row_idx += 1
L[n-1, pivot] = p
print("[*] 正在执行 LLL 与右零空间计算 (Right Kernel)...")
W_red = L.LLL()[:n-3, :].right_kernel(ZZ).basis_matrix().LLL()
for row in W_red:
for sign in [1, -1]:
chars = [int(val * sign) & 0xFF for val in row]
try:
flag_str = bytes(chars).decode('ascii')
if 'flag{' in flag_str or 'polar{' in flag_str:
print(f"\n[+] 破译成功!Flag: {flag_str}")
return
except UnicodeDecodeError:
continue
print("[-] 未能直接匹配到可见字符串,可能需要手动检查 B_red 输出。")
if __name__ == '__main__':
solve()
唉 还是有点学艺不精 这道题ai打出来的 复现到最后可能还是一知半解 正交格本来就不太会 放到一起考直接绝望了 对我来说除了这道题难一些 其他题目还是偏简单的 以上

浙公网安备 33010602011771号