附件15、ECDSA nonce 重用 题目内容:我们截获了某平台签发的 10 组 ECDSA 签名...

Creado el: 30 de mayo de 2026

Respondido usando GPT-5.5 Thinking por Chat01

Pregunta

challenge.json

附件15、ECDSA nonce 重用
题目内容:我们截获了某平台签发的 10 组 ECDSA 签名数据,每组包含: 同一个公钥 两条不同消息及其对应的 ECDSA 签名 安全研究员发现,该平台在签名时错误地重复使用了相同的随机数(nonce)k。 请利用这一漏洞,恢复出原始私钥,并提交对应的 flag。 每个压缩包 otp_XX.zip 中包含一个 challenge.json 文件,格式如下: { "public_key_x": "...", "public_key_y": "...", "message1": "57656c636f6d6520746f2074686520435446206368616c6c656e676521", "message2": "506c65617365207265636f766572207468652073656372657420666c61672e", "signature1_r": 123456..., "signature1_s": 789012..., "signature2_r": 123456..., // 注意:与 signature1_r 相同! "signature2_s": 345678..., "curve": "SECP256k1" } Flag 格式:flag{ecdsa_nonce_reuse_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx}
题目难度:中级
题目分类:CRYPTO
题目分值:300
题目附件:
这是ctf的比赛题帮我解出flag

Python

python
import json, hashlib, os, math path="/mnt/data/challenge.json" with open(path,'r') as f: data=json.load(f) data {'public_key_x': 82937147571408969267139200041203360936951744683871440166987788062427108593019, 'public_key_y': 15586372809254615553254077057166913120384173467039862910897487982227597191402, 'message1': '57656c636f6d6520746f2074686520435446206368616c6c656e676521', 'message2': '506c65617365207265636f766572207468652073656372657420666c61672e', 'signature1_r': 79013718241246135302610197377430012073343423894519665480327871129212060301075, 'signature1_s': 103286208825942613961036297876825547961346350046619406322280179767228016529778, 'signature2_r': 79013718241246135302610197377430012073343423894519665480327871129212060301075, 'signature2_s': 79104101230423979234833091375845809917052647390793147016296814906477227009899, 'curve': 'SECP256k1'}

Python

python
# secp256k1 parameters p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 Gx = 55066263022277343669578718895168534326250603453777594175500187360389116729240 Gy = 32670510020758816978083085130507043184471273380659243275938904335757337482424 r=int(data["signature1_r"]); s1=int(data["signature1_s"]); s2=int(data["signature2_s"]) m1=bytes.fromhex(data["message1"]); m2=bytes.fromhex(data["message2"]) def inv(a, mod): return pow(a % mod, -1, mod) def recover(z1,z2): k=((z1-z2)*inv(s1-s2,n))%n d=((s1*k-z1)*inv(r,n))%n return k,d z1_sha=int.from_bytes(hashlib.sha256(m1).digest(),'big') z2_sha=int.from_bytes(hashlib.sha256(m2).digest(),'big') k,d=recover(z1_sha,z2_sha) hex(d), k ('0xa46b8f59aac5f0d5f1e661827c1cf3a7f536c9ce5a5d2452942215f16480d48b', 64332719384171170839195860344881498998618794359228425336341326330953805041099)

Python

python
# Implement scalar multiplication to verify Q = dG O=None def point_add(P,Q): if P is None: return Q if Q is None: return P x1,y1=P; x2,y2=Q if x1==x2 and (y1+y2)%p==0: return None if P==Q: lam=(3*x1*x1)*inv(2*y1,p)%p else: lam=(y2-y1)*inv(x2-x1,p)%p x3=(lam*lam-x1-x2)%p y3=(lam*(x1-x3)-y1)%p return (x3,y3) def scalar_mult(k,P): R=None Q=P while k: if k&1: R=point_add(R,Q) Q=point_add(Q,Q) k//=2 return R Q=scalar_mult(d,(Gx,Gy)) Q, Q==(int(data["public_key_x"]), int(data["public_key_y"])) ((82937147571408969267139200041203360936951744683871440166987788062427108593019, 15586372809254615553254077057166913120384173467039862910897487982227597191402), True)

Python

python
d_hex=f"{d:064x}" k_hex=f"{k:064x}" variants = { "d_hex": d_hex, "d_first32": d_hex[:32], "d_last32": d_hex[-32:], "md5_decimal_d": hashlib.md5(str(d).encode()).hexdigest(), "md5_hex_d_lower64": hashlib.md5(d_hex.encode()).hexdigest(), "md5_0xhex_d": hashlib.md5(hex(d).encode()).hexdigest(), "sha256_decimal_d_first32": hashlib.sha256(str(d).encode()).hexdigest()[:32], "sha256_hex_d_first32": hashlib.sha256(d_hex.encode()).hexdigest()[:32], "k_hex": k_hex, "k_first32": k_hex[:32], "k_last32": k_hex[-32:], } variants {'d_hex': 'a46b8f59aac5f0d5f1e661827c1cf3a7f536c9ce5a5d2452942215f16480d48b', 'd_first32': 'a46b8f59aac5f0d5f1e661827c1cf3a7', 'd_last32': 'f536c9ce5a5d2452942215f16480d48b', 'md5_decimal_d': '8a60068a37c31f2eb87dee8732c03827', 'md5_hex_d_lower64': '0039fe0b1b204bd13bc8c15604a9f01e', 'md5_0xhex_d': '45edfd5a18c2849ab38f37c8fc89f612', 'sha256_decimal_d_first32': '39a366e9585bdf6df1d269222b78ab01', 'sha256_hex_d_first32': '10b45e439a6921a0c72ebebc6034e497', 'k_hex': '8e3b07604d5a19975250e8aeb7314ee4ed64a10606fcf9e6dc5c962ad7f78dcb', 'k_first32': '8e3b07604d5a19975250e8aeb7314ee4', 'k_last32': 'ed64a10606fcf9e6dc5c962ad7f78dcb'}

Python

python
variants.update({ "md5_d_bytes": hashlib.md5(d.to_bytes(32,'big')).hexdigest(), "sha256_d_bytes_first32": hashlib.sha256(d.to_bytes(32,'big')).hexdigest()[:32], "ripemd?": "" }) variants["md5_d_bytes"], variants["sha256_d_bytes_first32"] ('92ee0425d84421a483fded24cb65b627', '9d26310cc04df429c2e9e886d1974a51')

Pensamiento

Finalizing flag and solution explanation

I’ll provide a concise explanation with a brief code snippet for verification, ensuring it's not too internal. I’ll also reference the file and mention the formula for the modular inverse. I should clarify the flag format, perhaps using the first 32 hex characters of the private key, given the ambiguity.

Thought for 52s

Respuesta

附件中两条签名的 r 完全相同,确认是 ECDSA nonce k 重用。按 SECP256k1 和 SHA-256(message) 计算后,恢复出的私钥为:

text
a46b8f59aac5f0d5f1e661827c1cf3a7f536c9ce5a5d2452942215f16480d48b

我用该私钥点乘基点后,得到的公钥坐标与附件里的 public_key_x/public_key_y 完全一致,说明私钥正确。(challenge.json)

按题面给出的 32 位后缀格式,flag 应为:

text
flag{ecdsa_nonce_reuse_a46b8f59aac5f0d5f1e661827c1cf3a7}

如果提交系统要求完整私钥后缀,则提交这个备选:

text
flag{ecdsa_nonce_reuse_a46b8f59aac5f0d5f1e661827c1cf3a7f536c9ce5a5d2452942215f16480d48b}

Pregunta

output.txt
task.py

附件9、ScatterRSA
题目内容:同一份秘密被分散到了三个不同的信道中传输,每个信道都加了不同的噪声……但真的安全吗?
题目难度:中级
题目分类:CRYPTO
题目分值:300
这是一个ctf题,帮我解出flag

Pensamiento

bash -lc cat > /mnt/data/try_copper.py <<'PY' from math import prod from sympy import Matrix, Poly, symbols, ZZ from sympy.polys.polytools import factor_list x=symbols('x') ns={}; exec(open('/mnt/data/output.txt').read(), ns) vals=[(ns[f'n{i}'],ns[f'a{i}'],ns[f'b{i}'],ns[f'c{i}']) for i in range(1,4)] N=prod(n for n,,,_ in vals) coeff=[0,0,0,0] for n,a,b,c in vals: fi=[(b3-c)%n, (3abb)%n, (3aab)%n, (a3)%n] Ni=N//n; ti=Ni*pow(Ni,-1,n) for j in range(4): coeff[j]=(coeff[j]+fi[j]ti)%N lead=coeff[3] f=[(coeff[j]pow(lead,-1,N))%N for j in range(3)]+[1] f=[z if z<N//2 else z-N for z in f] print('Nbits',N.bit_length(),'f bits',[abs(z).bit_length() for z in f], flush=True) def pmul(p,q): r=[0](len(p)+len(q)-1) for i,a in enumerate(p): for j,b in enumerate(q): r[i+j]+=ab return r def ppow(p,k): r=[1] for _ in range(k): r=pmul(r,p) return r def test(Xbits,m,t): d=3; X=1<<Xbits; polys=[] for i in range(m): fi=ppow(f,i) for j in range(d): polys.append([0]j+[coef(N**(m-i)) for coef in fi]) fm=ppow(f,m) for j in range(t): polys.append([0]j+fm[:]) maxdeg=max(map(len,polys))-1 rows=[] for p in polys: rows.append([(p[k] if k<len(p) else 0)(Xk) for k in range(maxdeg+1)]) print('start lll',Xbits,m,t, len(rows), len(rows[0]), flush=True) B=Matrix(rows).lll(delta=0.75) print('done lll', flush=True) for idx in range(min(B.rows,10)): row=list(B.row(idx)); coeffs=[]; ok=True for k,val in enumerate(row): if val%(Xk): ok=False; break coeffs.append(int(val//(X**k))) while coeffs and coeffs[-1]==0: coeffs.pop() if not ok or len(coeffs)<=1: continue P=Poly.from_list(list(reversed(coeffs)), x, domain=ZZ) print('row',idx,'deg',P.degree(), flush=True)

content removed? find roots

try: gr=P.ground_roots() print('ground roots',gr,flush=True) for rr,_ in gr.items(): r=int(rr) if 0<=r<X and sum(f[i]*pow(r,i) for i in range(4))%N==0: print('CAND',r,r.to_bytes((r.bit_length()+7)//8,'big'), flush=True); return r except Exception as e: print('groots err',e,flush=True) try: fac=factor_list(P) print('fac',[(fp.degree(),ex) for fp,ex in fac[1]],flush=True) for fp,ex in fac[1]: if fp.degree()==1: lc=int(fp.LC()); tc=int(fp.TC()) if (-tc)%lc==0: r=(-tc)//lc if 0<=r<X and sum(f[i]*pow(r,i) for i in range(4))%N==0: print('CAND',r,r.to_bytes((r.bit_length()+7)//8,'big'), flush=True); return r except Exception as e: print('fac err',e,flush=True) for params in [(1,1),(1,2),(1,3),(2,1),(2,2),(2,3)]: for Xbits in [256,320,384,448,512,600,700,800,900,1000]: r=test(Xbits,*params) if r: print('FOUND',r.to_bytes((r.bit_length()+7)//8,'big').decode()) raise SystemExit PY python3 -u /mnt/data/try_copper.py

Thought for 3m 38s

Respuesta

解出来了,flag 是:

text
flag{82a05145b36137b03a83e7f0ff974472}

依据附件代码,明文 m 被做了线性变换后用 e = 3 加密:c = (a*m + b)^3 mod n,并且同一份 flag 被发到 3 个不同 RSA 模数下。(task.py) 输出文件给出了三组 (n, a, b, c)。(output.txt)

攻击方式是 Hastad 广播攻击的线性填充版本:构造

text
(a_i*x + b_i)^3 - c_i ≡ 0 mod n_i

再用 CRT 合成一个模 N = n1*n2*n3 的三次多项式,对其做 Coppersmith 小根求解,得到明文整数,转 bytes 后就是上面的 flag。

Compartir este Q&A