模运算不用解释。模逆元就是模世界里的除法,pow(a, -1, m) 一行搞定,前提是 gcd(a,m)=1。字节和十六进制来回转是日常:bytes.fromhex()、int.to_bytes()、pycryptodome 的 long_to_bytes / bytes_to_long。异或记住 a ^ b ^ b == a,流密码的命根子。
chall.txt 的解析有个固定套路——前面带中文说明行,直接 exec 会炸,逐行抠:
1 2 3 4 5
ns = {} for line inopen("chall.txt"): if" = "in line: k, v = line.strip().split(" = ", 1) ns[k] = v
FLAG 第 i 个字节当第 i 个质数的指数,全乘起来得 N。N = 2^b1 * 3^b2 * 5^b3 * ...,b1 是第一个字节的 ASCII 值。
还原靠算术基本定理,分解 N 读指数就行。N 四千多位看着吓人,但质因数全是小质数(大的都在指数上),sympy factorint 秒出,SageMath 里 factor(N) 一样。
1 2 3 4 5 6 7 8 9 10 11 12
# exp05.py import re, sys sys.set_int_max_str_digits(100000) # Python 3.11 默认限 4300 位,先放开 from sympy import factorint
lines = open("chall.txt").read().splitlines() i = next(k for k, l inenumerate(lines) if l.startswith("N = ")) N = int(re.sub(r"\D", "", "".join(lines[i:]))) # N 跨行,拼起来
fac = factorint(N) primes = [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,101,103,107,109,113,127] print(bytes(fac[p] for p in primes if p in fac))
FLAG:flag{s4ge_zz_f4ct0r_{41c2c039}}
三个坑:Python 3.11 起整数转字符串默认限 4300 位不放开直接报错;N 跨行要拼;说明文字里也出现过 “N”,定位要从以 N = 开头那行开始。大整数能不能分解看质因子结构,其次才是位数。
06 异界投影
和 02 一个模子,模 95 换成模素数 p。加密 c = (a*t + b) mod p,解密 (c - b) * a^-1 mod p,没新东西。这题主要是引入 GF(p)——模素数 p 的算术世界里加减乘除全通,叫”域”。SageMath 里写 GF(p)((x-b)/a),除号自动就是乘逆元。
1 2 3 4 5 6 7 8 9 10 11
# exp06.py ns = {} for line inopen("chall.txt"): if" = "in line: k, v = line.strip().split(" = ", 1) ns[k] = v
p, a, b = int(ns["p"]), int(ns["a"]), int(ns["b"]) ct = eval(ns["ct"]) ainv = pow(a, -1, p) print(bytes(((x - b) * ainv) % p for x in ct))
FLAG:flag{s4ge_gf16_pr0j3ct_{2a0297a4}}
以后看到模运算语境里的 /,就是 pow(除数, -1, p)。
07 三角之钥
密文 b = A * x (mod p),x 每个分量一个明文字节。解方程组就行,中学高斯消元搬过来,就两处要改:除法换乘逆元,所有加减乘取模。
# exp07.py ns = {} for line inopen("chall.txt"): if" = "in line: k, v = line.strip().split(" = ", 1) ns[k] = v
p, A, b = int(ns["p"]), eval(ns["A"]), eval(ns["b"])
defsolve_mod(A, b, p): n = len(A) M = [row[:] for row in A] v = b[:] for i inrange(n): for k inrange(i, n): # 选第 i 列非零行当主元 if M[k][i] % p: M[i], M[k] = M[k], M[i] v[i], v[k] = v[k], v[i] break inv = pow(M[i][i], -1, p) M[i] = [(x * inv) % p for x in M[i]] v[i] = (v[i] * inv) % p for k inrange(n): if k != i and M[k][i]: f = M[k][i] M[k] = [(M[k][j] - f * M[i][j]) % p for j inrange(n)] v[k] = (v[k] - f * v[i]) % p return v
raw = open("chall.txt").read() m1, m2, m3 = [int(x) for x in re.search(r"^m1, m2, m3 = (.+)$", raw, re.M).group(1).split(",")] a1, a2, a3 = [int(x) for x in re.search(r"^a1, a2, a3 = (.+)$", raw, re.M).group(1).split(",")]
ns = {} for line inopen("chall.txt"): if" = "in line: k, v = line.strip().split(" = ", 1) ns[k] = v
key, iv, ct = bytes.fromhex(ns["key"]), bytes.fromhex(ns["iv"]), bytes.fromhex(ns["ct"])
ecb = AES.new(key, AES.MODE_ECB) ks, pt = iv, b"" for i inrange(0, len(ct), 16): ks = ecb.encrypt(ks) pt += bytes(a ^ b for a, b inzip(ct[i:i+16], ks)) print(pt.decode())
FLAG:flag{0fb_sync_l0ss_{f7b9789b}}
flag 里 sync_loss 是说 OFB 传输出错几字节后必须重新同步。21–25 做完四种模式都手写了一遍,再看库函数的 mode 参数就不是黑盒了。
raw = open("chall.txt").read() seq = [int(b) for b in re.search(r"= ([01]+)", raw).group(1)] ct = bytes.fromhex(re.search(r"ct_flag_hex = ([0-9a-f]+)", raw).group(1))
defberlekamp_massey(s): C, B = [1], [1] L, m, b = 0, 1, 1 for N inrange(len(s)): d = s[N] for i inrange(1, L + 1): d ^= C[i] & s[N - i] if d == 0: m += 1 else: T = C[:] coef = d * pow(b, -1, 2) % 2 iflen(C) < len(B) + m: C += [0] * (len(B) + m - len(C)) for j inrange(len(B)): C[j + m] ^= coef & B[j] if2 * L <= N: L, B, b, m = N + 1 - L, T, d, 1 else: m += 1 return L, C
L, C = berlekamp_massey(seq) print("递推长度 L =", L)
bits = seq[:] whilelen(bits) < len(seq) + len(ct) * 8: nxt = 0 for i inrange(1, L + 1): nxt ^= C[i] & bits[-i] bits.append(nxt)
ksbits = bits[len(seq):len(seq) + len(ct) * 8] ks = bytes(int("".join(map(str, ksbits[i*8:(i+1)*8])), 2) for i inrange(len(ct))) print(bytes(a ^ b for a, b inzip(ct, ks)))
LCG 参数恢复。X_{n+1} = (a*X_n + c) mod m,C 语言 rand() 就是这个家族。完全线性,三个连续输出解出全部参数:
1 2 3
X2 - X1 ≡ a * (X1 - X0) (mod m) a = (X2 - X1) * (X1 - X0)^-1 (mod m) c = X1 - a*X0 (mod m)
拿到 (a, c, m) 从最后一个已知输出继续递推,后面”随机数”全是已知的。本题模数 2^31,实测 (X1-X0) 和 m 互素逆元直接求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
# exp31.py import re
raw = open("chall.txt").read() m = int(re.search(r"modulus_m = (\d+)", raw).group(1)) X = [int(x) for x in re.search(r"outputs = (.+)", raw).group(1).split(",")] M = [int(x) for x in re.search(r"more_outputs = (.+)", raw).group(1).split(",")] ct = bytes.fromhex(re.search(r"ct_flag_hex = ([0-9a-f]+)", raw).group(1))
a = (X[2] - X[1]) * pow(X[1] - X[0], -1, m) % m c = (X[1] - a * X[0]) % m
state = M[-1] ks = b"" for _ inrange(len(ct)): state = (a * state + c) % m ks += bytes([state & 0xff])
print(bytes(x ^ k for x, k inzip(ct, ks)))
FLAG:flag{lcg_p4r4m_r3c0v3r_1in34r_f8b4a2}
(X1-X0) 和 m 不互素要先约公因子再解。名字带”线性”的生成器(LCG、LFSR、矩阵法)都默认可被代数攻击,抽样模拟没问题,密钥生成等于裸奔。
# exp37.py from math import gcd from Crypto.Util.number import inverse, long_to_bytes
ns = {} for line inopen("chall.txt"): if" = "in line: k, v = line.strip().split(" = ", 1) ns[k] = v
n, e = int(ns["n"]), int(ns["e"]) ct = [int(x) for x in ns["ct_bytes"].split(",")]
defpollard_rho(n): if n % 2 == 0: return2 x = y = 2 c = 1 d = 1 while d == 1: x = (x * x + c) % n y = (y * y + c) % n y = (y * y + c) % n d = gcd(abs(x - y), n) if d == n: x = y = 2 c += 1 d = 1 return d
p = pollard_rho(n) q = n // p d = inverse(e, (p - 1) * (q - 1)) print(b"".join(long_to_bytes(pow(c, d, n)) for c in ct))
defcrt(rs, ms): M = 1 for m in ms: M *= m x = 0 for r, m inzip(rs, ms): x += r * (M // m) * pow(M // m, -1, m) return x % M
x = crt([c1, c2, c3], [n1, n2, n3]) m = sympy.integer_nthroot(x, 3)[0] print(long_to_bytes(m))
FLAG:flag{rsa_broadc4st_1o3_3xp0n3nt_7d4a8c}
k 个接收者时需要 m^e < n1*...*nk。防御就是 OAEP 填充。
40 同态的欺骗
RSA 乘法同态:E(m1) * E(m2) = E(m1*m2) mod n。教科书 RSA 无填充时这是个可利用的代数结构。这题 n 只有 128 位直接分解,FLAG 分三段加密逐段解密拼接。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
# exp40.py import sympy from Crypto.Util.number import inverse
ns = {} for line inopen("chall.txt"): if" = "in line: k, v = line.strip().split(" = ", 1) ns[k] = v
n, e = int(ns["n"]), int(ns["e"]) chunks = eval(ns["ct_chunks"])
p, q = sympy.factorint(n).keys() d = inverse(e, (p - 1) * (q - 1)) flag = b"".join(pow(c, d, n).to_bytes(15, "big").lstrip(b"\x00") for c in chunks) print(flag)
# exp44.py import math from Crypto.Util.number import inverse, long_to_bytes
ns = {} for line inopen("chall.txt"): if" = "in line: k, v = line.strip().split(" = ", 1) ns[k] = v
n, e, c = int(ns["n"]), int(ns["e"]), int(ns["c"])
cf = [] a, b = e, n while b: cf.append(a // b) a, b = b, a % b
p0, q0 = 1, 0 p1, q1 = cf[0], 1 for a in cf[1:]: p0, q0, p1, q1 = p1, q1, a * p1 + p0, a * q1 + q0 k, d = p1, q1 if k == 0: continue if (e * d - 1) % k: continue phi = (e * d - 1) // k s = n - phi + 1 t = math.isqrt(s * s - 4 * n) if t * t == s * s - 4 * n: p = (s + t) // 2 q = (s - t) // 2 if p * q == n: print(long_to_bytes(pow(c, d, n))) break
FLAG:flag{w13n3r_l0w_d_c0nt1nu3d_frac_8d1b4e}
拿到 RSA 题先看 d 有没有给、e 是不是巨大。e 接近 n 而 n 不太大时多半在诱你做 Wiener。
45 小根的秘密
c = m^e mod n。明文 m 非常小、m^e 根本不超过 n 时,取模是多余的,c 就是 m^e 的真值。直接开 e 次根,不用分解 n 不用私钥。特征:c 的长度明显短于 n 一大截。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
# exp45.py import sympy from Crypto.Util.number import long_to_bytes
ns = {} for line inopen("chall.txt"): if" = "in line: k, v = line.strip().split(" = ", 1) ns[k] = v
n, e, c = int(ns["n"]), int(ns["e"]), int(ns["c"])
m = sympy.integer_nthroot(c, e)[0] print(long_to_bytes(m))
FLAG:flag{sm411_3_m3ss4ge_cub3_r00t_4c19d7}
开根必须用整数版 integer_nthroot,浮点 c ** (1/e) 精度不够大数会错。
46 私钥的残片
给了 p 的高位(hex 字符串),只缺低 16 位。低位枚举 2^16=65536 种逐个拼起来试整除 n,整除的就是 p。
# exp46.py from Crypto.Util.number import inverse, long_to_bytes
ns = {} for line inopen("chall.txt"): if" = "in line: k, v = line.strip().split(" = ", 1) ns[k] = v
n, e, c = int(ns["n"]), int(ns["e"]), int(ns["c"]) p_high_hex = ns["p_high_hex"]
lead = int(p_high_hex, 16) for lo inrange(65536): cand = (lead << 16) + lo if n % cand == 0: p = cand break
q = n // p d = inverse(e, (p - 1) * (q - 1)) print(long_to_bytes(pow(c, d, n)))
FLAG:flag{p4rt14l_k3y_l34k_h1gh_b1ts_1f4a63}
知道高位能枚举低位,知道低位当然也能枚举高位。中间缺一段就要上 Coppersmith 格攻击了。
47 故障的讯息
RSA-CRT 故障注入。CRT 加速时签名分别算 S_p = m^dp mod p 和 S_q = m^dq mod q 再合并。某一半出故障,错误签名 S’ 和正确签名 S 在 mod p 下一致、mod q 下不一致。gcd(S - S', n) 直接把出错的素因子分离出来。chall.txt 同时给了 s_correct 和 s_faulty。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
# exp47.py from math import gcd from Crypto.Util.number import inverse, long_to_bytes
ns = {} for line inopen("chall.txt"): if" = "in line: k, v = line.strip().split(" = ", 1) ns[k.strip()] = v.strip() # 有些行 = 前有多个空格,再 strip
n, e, c = int(ns["n"]), int(ns["e"]), int(ns["c"]) s_ok = int(ns["s_correct"]) s_bad = int(ns["s_faulty"])
defecc_add(p, a, P, Q): if P isNone: return Q if Q isNone: return P x1, y1 = P x2, y2 = Q if x1 == x2 and (y1 + y2) % p == 0: returnNone if P == Q: lam = (3 * x1 * x1 + a) * pow(2 * y1, -1, p) % p else: lam = (y2 - y1) * pow((x2 - x1) % p, -1, p) % p x3 = (lam * lam - x1 - x2) % p y3 = (lam * (x1 - x3) - y1) % p return (x3, y3)
defecc_mul(p, a, k, P): R = None while k: if k & 1: R = ecc_add(p, a, R, P) P = ecc_add(p, a, P, P) k >>= 1 return R
defbsgs(p, a, G, Q, r): m = math.isqrt(r) + 1 table = {} cur = None for j inrange(m): table[cur] = j cur = ecc_add(p, a, cur, G) mG = ecc_mul(p, a, m, G) neg = Noneif mG isNoneelse (mG[0], (-mG[1]) % p) cur = Q for i inrange(m): if cur in table: return (i * m + table[cur]) % r cur = ecc_add(p, a, cur, neg) returnNone
BSGS 思想和 14 题完全一致,只是”加法”从模乘换成了曲线点加法。
50 光滑的阶
Pohlig-Hellman。群阶 N 是光滑数,分解后全是小素因子幂。把大问题拆成小问题:对每个素因子幂 q^e,把 P 和 Q 都乘以 N/q^e 投影到阶为 q^e 的子群,BSGS 解出 d mod q^e,CRT 合并成完整 d。
# exp50.py import math from Crypto.Util.number import long_to_bytes
ns = {} for line inopen("chall.txt"): if" = "in line: k, v = line.strip().split(" = ", 1) ns[k] = v p = int(ns["p"]); a = int(ns["a"]) P = eval(ns["P"]); Q = eval(ns["Q"]) N = int(ns["group_order"]) facts = eval(ns["order_factorization"])
# --- ecc_add / ecc_mul / bsgs 粘贴上方 ---
defcrt(rs, ms): M = 1 for m in ms: M *= m x = 0 for r, m inzip(rs, ms): x += r * (M // m) * pow(M // m, -1, m) return x % M
res, mods = [], [] for q, e in facts.items(): mod = q ** e G = ecc_mul(p, a, N // mod, P) H = ecc_mul(p, a, N // mod, Q) x = bsgs(p, a, G, H, mod) res.append(x) mods.append(mod)
ns = {} for line inopen("chall.txt"): if" = "in line: k, v = line.strip().split(" = ", 1) ns[k] = v p = int(ns["p"]) g = eval(ns["g"]); h = eval(ns["h"]) order = int(ns["order(g)"]) if"order(g)"in ns elseeval(ns["order_g"])
deff2_mul(A, B): a, b = A c, d = B return ((a * c - b * d) % p, (a * d + b * c) % p)
deff2_pow(A, k): R = (1, 0) while k: if k & 1: R = f2_mul(R, A) A = f2_mul(A, A) k >>= 1 return R
defbsgs_f2(g, h, r): m = math.isqrt(r) + 1 table = {} cur = (1, 0) for j inrange(m): table[cur] = j cur = f2_mul(cur, g) gm = f2_pow(g, m) a0, b0 = gm inv = pow((a0 * a0 + b0 * b0) % p, -1, p) ginv = (a0 * inv % p, (-b0) * inv % p) cur = h for i inrange(m): if cur in table: return (i * m + table[cur]) % r cur = f2_mul(cur, ginv) returnNone
无效曲线攻击。点加法公式只用到 a 完全不碰 b,所以 a 一样的两条曲线(y²=x³+2 和 y²=x³+3)加法规则一样。服务端只校验坐标存在不校验点在原曲线上,提交小阶曲线上的点(G1 阶 8191、G2 阶 16381),让服务端做标量乘法,返回的就是 d mod 8191 和 d mod 16381。两个小答案 BSGS 秒解 CRT 合并。
ns = {} for line inopen("chall.txt"): if" = "in line: k, v = line.strip().split(" = ", 1) ns[k] = v p = int(ns["p"]); a = int(ns["a"]) P = eval(ns["P"]); Q = eval(ns["Q"])
# --- ecc_add 粘贴上方 ---
cur = None x = None for k inrange(p): if cur == Q: x = k break cur = ecc_add(p, a, cur, P) print("x =", x)
definv_keyschedule(k10): words = [None] * 44 for i inrange(4): words[40 + i] = list(k10[i*4:i*4+4]) for i inrange(43, 3, -1): if i % 4 == 0: t = words[i - 1][:] t = t[1:] + [t[0]] t = [SBOX[b] for b in t] t[0] ^= Rcon[i // 4 - 1] words[i - 4] = [words[i][j] ^ t[j] for j inrange(4)] else: words[i - 4] = [words[i][j] ^ words[i - 1][j] for j inrange(4)] returnb"".join(bytes(w) for w in words[:4])
found = None for combo in product(*[cds for _, cds in cands_pos]): k10 = bytearray(16) for (out, _), K inzip(cands_pos, combo): k10[out] = K master = inv_keyschedule(bytes(k10)) if AES.new(master, AES.MODE_ECB).encrypt(pt) == c: found = master break
assert found isnotNone, "未找到匹配密钥" print("主密钥:", found.hex()) print(AES.new(found, AES.MODE_ECB).decrypt(ct_flag))
bits = [] pat = re.compile(r"bit(\d+):\s*(.+)") rows = [(int(m.group(1)), [float(x) for x in m.group(2).split()]) for m in pat.finditer(raw)] rows.sort()
for idx, vals in rows: avg = sum(vals) / len(vals) bits.append(1if avg > 101.5else0)
d = 0 for b in bits: d = (d << 1) | b print("d =", hex(d))