2026PolarCTF春季赛密码全解

屏幕截图 2026-03-21 212944
尽力了 拿了第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)分别代入关系式中得到两个同余式

\[19a + b \equiv 23 \pmod{26} \]

\[5a + b \equiv 9 \pmod{26} \]

化简一下可得

\[(19a - 5a) \equiv (23 - 9) \pmod{26} \\ 14a \equiv 14 \pmod{26} \]

也就是说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分解 但是比赛过程中我确实是分解出来了的

屏幕截图 2026-03-23 201230

可能是非预期解 还是学习一下这位师傅的做法叭

看一下题目的加密逻辑

p=2** 79 * r+1 q=4 * r + 3 n=p*q

r 很小,用 Coppersmith 找出 r ,从而恢复 p

把flag分为五部分分别进行加密

\[c=y^{m}x^{2^{79}}(modn),n=pq \]

观察一下可发现p-1光滑 且p−1=2^79⋅r

根据费马小定理:

\[x^{p-1} \equiv 1 \pmod p \quad (x \not\equiv 0) \]

我们在模 p 下

\[c \equiv y^m \cdot x^{2^{79}} \pmod p \]

可以将x{2{79}}这一项完全消掉 得到离散对数

\[c^r \equiv (y^r)^m \pmod p \]

而且 g 的阶是 2^{79}。

所以可以把 m 看成 79 位二进制:

\[m = b_0 + 2b_1 + 2^2b_2 + \cdots + 2^{78}b_{78} \]

然后一位一位求。

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) 的坐标。这些点都在同一条曲线上,且满足

\[Y_i^2 \equiv X_i^3 + aX_i + b \pmod p \]

我们可以通过构造差分方程来消去未知数。令

\[E_i = Y_i^2 - X_i^3,则有 E_i \equiv aX_i + b \pmod p \]

将相邻的点相减,可以消去 b:

\[E_i - E_{i+1} \equiv a(X_i - X_{i+1}) \pmod p \]

为了进一步消去 a 并求出 p,我们可以取三个点 (i, i+1, i+2) 进行交叉相乘。可以利用了以下行列式关系构造了一个必定能被 p 整除的数

\[D = X_i(E_{i+1} - E_{i+2}) - X_{i+1}(E_i - E_{i+2}) + X_{i+2}(E_i - E_{i+1}) \]

由于在实数环上 D 不为 0,但在模 p下

\[D \equiv 0 \pmod p \]

所以 D 必然是 p 的倍数。

操作步骤:

计算出所有的 D_i 后,对它们求最大公约数,并剔除掉小的素因子(如 2 和 3),即可精准恢复出模数 p。随后通过逆元运算即可求出 a 和 b

2. Smart Attack

\[Q_i = m_i \cdot R + nonce_i \cdot E + sh_i \cdot C \]

应该是异常曲线,可以利用 Smart Attack 将椭圆曲线群同构映射到有限域加法群上

3.HNP

经过 Smart Attack 后,我们得到了 k个线性方程,但每个方程中包含了随机的 nonce 和 Hash 值 sh,这意味着普通的格规约很难直接奏效。我们需要消除 phi(R), phi(E), phi(C) 这三个基准未知数的影响。

我们构造一个 k *k 的矩阵 M,试图寻找一组整数系数 c_i,使得:

\[c_0 h_0 + c_1 h_1 + \dots + c_{k-1} h_{k-1} \equiv 0 \pmod p \]

构造的矩阵形式如下

\[\begin{pmatrix} 1 & 0 & \dots & 0 & -h_0 \cdot h_{k-1}^{-1} \pmod p \\ 0 & 1 & \dots & 0 & -h_1 \cdot h_{k-1}^{-1} \pmod p \\ \vdots & \vdots & \ddots & \vdots & \vdots \\ 0 & 0 & \dots & 1 & -h_{k-2} \cdot h_{k-1}^{-1} \pmod p \\ 0 & 0 & \dots & 0 & p \end{pmatrix} \]

对该矩阵打 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打出来的 复现到最后可能还是一知半解 正交格本来就不太会 放到一起考直接绝望了 对我来说除了这道题难一些 其他题目还是偏简单的 以上

posted @ 2026-03-23 23:25  A1g3rn0n  阅读(49)  评论(0)    收藏  举报