私钥的序列化格式是带 2 字节大端长度前缀的 (p, q, d, u),AES-ECB 整体加密后存储:
1 2 3 4 5 6
defformat_rsa_privkey(self): data = self.format_number(self.share_key.p) data += self.format_number(self.share_key.q) data += self.format_number(self.share_key.d) data += self.format_number(self.share_key.u) return pad(data, 16)
Megalomaniac 1
裁坏 CRT 系数 + 二分
利用点清单
#
源码位置
弱点
A
Challenge.__init__ 的 material
注册材料(含加密私钥)全公开
B
Client / Client_new_login 的 gen_keys
同 PASS/SALT,PBKDF2 确定性,C_.enc_key == C.enc_key
C
AES.new(self.master_key, AES.MODE_ECB)
ECB 无认证:密文块可裁剪重排,客户端无感知
D
parse_rsa_privkey 的切片 + assert len==4
越界静默截断,裁剪后照样恰好解析出 4 个元素
E
wait_login 分支没有current_step 检查
状态机可无限重置,预言机无限次使用
F
RSA_CRT_decrypt 的 m = h*p + mp
mod p 恒等于 mp,与 u 对错无关
G
SID = SID[:-16]
砍掉低 16 字节:错误解密也能以数据形式回显(观察窗口)
H
get_encrypted_flag 用 SHA256(p‖q)
flag 密钥只依赖 p、q
其中 B 是重放的前提:banner 里 C 的 master_key_enc 原样重放给 C_,由于 enc_key 相同,客户端解出的就是 C 的真 master_key;再用它解 share_key_enc,C 的真私钥就被装进了登录实例。
核心数学:CRT 的”半边正确性”
Garner 公式:
1 2 3 4 5
mp = ct^d mod p # = m mod p mq = ct^d mod q # = m mod q t = (mq − mp) mod q h = t·u mod q # u = p⁻¹ mod q m = h·p + mp
盯住 m mod p = mp——p 那一项被模掉了,与 u 对错无关。所以 u 错了,解密结果 mod p 仍然正确、mod q 全是垃圾。u 坏 = “只对 mod p 正确的解密机”。
再用一个关键性质把输出变成判定:发 SID_enc = s^e,则 mp = s mod p。
s < p 时 mp = s,t = (mq − s) mod q = 0(因为 mq ≡ s mod q),h = 0——u 根本没上场,输出精确还原为 s;
#!/usr/bin/env python3 import json, time from pwn import remote, log from Crypto.Util.number import long_to_bytes from Crypto.Hash import SHA256 from Crypto.Cipher import AES from Crypto.Util.Padding import unpad
HOST, PORT = "socket.cryptohack.org", 13408 BLK = 16
#!/usr/bin/env python3 # sage -python mega2.py import json from pwn import remote from pwn import log as pwlog # 起别名, 避免和 sage.all 的符号 log 冲突 from sage.allimport PolynomialRing, Zmod from math import gcd from Crypto.Util.number import long_to_bytes from Crypto.Hash import SHA256 from Crypto.Cipher import AES from Crypto.Util.Padding import unpad
R = int(resp["SID"], 16) s = (1 << 1024) - 1 C0 = R * 2**128 - s
# Coppersmith: f(x) = x + C0 在 mod p 下有 <2^128 的根, p >= n^0.5 PR = PolynomialRing(Zmod(n), 'x') x = PR.gen() r = (x + C0).small_roots(X=2**128, beta=0.45)[0] p = gcd(int(C0 + int(r)), n) assert n % p == 0 q = n // p
flag 不再在 SHA256(p‖q) 下面,而是藏进了”文件”里:文件 → node_key 加密,node_key → master_key 加密。 2. recovered_shared_key 直接给出 (n, e, p)——“Wow what a hacker, you already managed to grab this user share_key…”,默认你已经打完了前两关。 3. get_encrypted_flag 动作没了;登录次数恢复不限。
io.recvuntil(b"crypto material...\n") material = json.loads(io.recvline()) io.recvuntil(b"uploading a file...\n") file_upload = json.loads(io.recvline()) io.recvuntil(b"user share_key...\n") recovered = json.loads(io.recvline())
n, e, p = recovered["share_key"] q = n // p assert p * q == n master_key_enc = bytes.fromhex(material["master_key_enc"]) share_key_enc = bytes.fromhex(material["share_key_enc"]) node_key_enc = bytes.fromhex(file_upload["node_key_enc"]) file_enc = bytes.fromhex(file_upload["file_enc"])
s = 2 * p - 1# s ∈ [p, 2p): mp = s-p, mq = s mod q mp = s - p mq = s % q t = (mq - mp) % q assert t != 0
R = int(resp["SID"], 16) X = R * 2**128 - s + p for h in (X // p, X // p + 1): u2 = h * pow(t, -1, q) % q if u2 >= 2**256: continue node_key = long_to_bytes(u2, 32)[8:24] pt = AES.new(node_key, AES.MODE_ECB).decrypt(file_enc) ifb"Congratulations"in pt: print(unpad(pt, 16).decode()) break