一道ctf的密码题 解出flag

Создано: 16 июня 2026 г.

Отвечено с использованием GPT-5.5 Thinking от Chat01

Вопрос

challenge.py

一道ctf的密码题 解出flag

Python

python
try: import ecdsa print("ecdsa yes", ecdsa.__version__) except Exception as e: print("ecdsa no", e)

Мысль

bash -lc cat > /mnt/data/solve.sage.py <<'PY' #!/usr/bin/env sage -python

Usage: save the challenge output up to "signature(hex)>" into out.txt,

then run: sage -python solve.sage.py < out.txt

It prints a DER-encoded ECDSA signature for b"gimme flag".

import hashlib import random import re import sys from sage.all import ZZ, GF, EllipticCurve, Matrix, inverse_mod

NIST P-521 / secp521r1 parameters

p = 2**521 - 1 N = ZZ(int("01fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa51868783bf2f966b7fcc0148f709a5d03bb5c9b8899c47aebb6fb71e91386409", 16)) a = -3 b = int("0051953eb9618e1c9a1f929a21a0b68540eea2da725b99b315f3b8b489918ef109e156193951ec7e937b1652c0bd3bb1bf073573df883d2c34f1ef451fd46b503f00", 16) Gx = int("00c6858e06b70404e9cd9e3ecb662395b4429c648139053fb521f828af606b4d3dbaa14b5e77efe75928fe1dc127a2ffa8de3348b3c1856a429bf97e7e31c2e5bd66", 16) Gy = int("011839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650", 16)

LEAK = 6 B = 1 << LEAK TARGET = b"gimme flag"

def parse_transcript(text): qx = qy = None samples = [] for line in text.splitlines(): line = line.strip() if line.startswith("public_x ="): qx = ZZ(int(line.split("=", 1)[1].strip())) elif line.startswith("public_y ="): qy = ZZ(int(line.split("=", 1)[1].strip())) else: m = re.fullmatch(r"([0-9a-fA-F]+)\s+([0-9a-fA-F]+)\s+([0-9a-fA-F]+)\s+(\d+)", line) if m: h = ZZ(int(m.group(1), 16)) r = ZZ(int(m.group(2), 16)) s = ZZ(int(m.group(3), 16)) kl = ZZ(int(m.group(4))) samples.append((h, r, s, kl)) if len(samples) < 80: raise SystemExit(f"too few samples parsed: {len(samples)}") return qx, qy, samples

def recover_key(samples, qx=None, qy=None): """Recover d from k mod 2^LEAK using a centered HNP lattice.""" m = len(samples) invB = inverse_mod(B, N) center = N // (2 * B)

text
t = [] u = [] for h, r, s, kl in samples: si = inverse_mod(s, N) # k = si*(h + r*d) mod N = kl + B*x # x = (si*r*B^-1)*d + (si*h - kl)*B^-1 mod N, and x < N/B. ti = (r * si * invB) % N ui = ((h * si - kl) * invB - center) % N # center the error around 0 t.append(ZZ(ti)) u.append(ZZ(ui)) def valid(d): if not (1 <= d < N): return False for h, r, s, kl in samples: k = (inverse_mod(s, N) * (h + r * d)) % N if k % B != kl: return False if qx is not None and qy is not None: E = EllipticCurve(GF(p), [a, b]) G = E(Gx, Gy) Q = d * G if ZZ(Q[0]) != qx or ZZ(Q[1]) != qy: return False return True # R is the coordinate scaling. B is the textbook value; larger values often # help make the embedded closest vector easier for BKZ to expose. for R in [B, 2*B, 4*B, 8*B, 16*B, 32*B, 64*B, 128*B, 256*B]: M = Matrix(ZZ, m + 2, m + 2) for i in range(m): M[i, i] = N * R for i in range(m): M[m, i] = t[i] * R M[m, m] = 1 for i in range(m): M[m + 1, i] = u[i] * R M[m + 1, m + 1] = N # LLL is sometimes enough; BKZ-20/25 is still small for this dimension. bases = [] L = M.LLL() bases.append(L) for beta in (20, 25, 30): try: L = L.BKZ(block_size=beta) bases.append(L) except TypeError: # Older Sage versions may not accept keyword args. L = L.BKZ(beta) bases.append(L) except Exception: pass for L in bases: for row in L.rows(): if abs(ZZ(row[-1])) == N: for sg in (1, -1): d = (sg * ZZ(row[-2])) % N if valid(d): return d raise SystemExit("failed to recover d; try increasing BKZ block size")

def der_len(n): if n < 128: return bytes([n]) b = n.to_bytes((n.bit_length() + 7) // 8, "big") return bytes([0x80 | len(b)]) + b

def der_int(x): b = int(x).to_bytes((int(x).bit_length() + 7) // 8 or 1, "big") if b[0] & 0x80: b = b"\x00" + b return b"\x02" + der_len(len(b)) + b

def der_sig(r, s): body = der_int(r) + der_int(s) return b"\x30" + der_len(len(body)) + body

def sign_target(d): E = EllipticCurve(GF(p), [a, b]) G = E(Gx, Gy) h = ZZ(int.from_bytes(hashlib.sha512(TARGET).digest(), "big")) while True: k = ZZ(random.randrange(1, int(N))) R = k * G r = ZZ(R[0]) % N if r == 0: continue s = (inverse_mod(k, N) * (h + r * d)) % N if s != 0: return der_sig(r, s).hex()

def main(): qx, qy, samples = parse_transcript(sys.stdin.read()) d = recover_key(samples, qx, qy) print(sign_target(d))

if name == "main": main() PY ls -l /mnt/data/solve.sage.py head -5 /mnt/data/solve.sage.py

Thought for 27m 48s

Ответ

这题是 ECDSA nonce 低位泄露sign() 每次返回 k & 63,也就是泄露 nonce 的 6 个 LSB;88 条签名样本足够用 HNP/格攻击恢复私钥 d,再对 b"gimme flag" 伪造 DER 格式 ECDSA 签名拿 flag。题目本地默认 flag 是 flag{local_test_flag},远端真实 flag 需要用远端打印出来的 88 条样本跑脚本。(challenge.py)

我给你整理好了 SageMath 解题脚本:

下载 solve.sage.py

用法:

bash
# 1. 连接题目,把它打印的内容保存到 out.txt # 内容需要包含 public_x/public_y 和 88 行 h r s k_lsb sage -python solve.sage.py < out.txt

脚本会输出一串 DER 签名 hex,把它粘到:

text
signature(hex)>

然后服务端会打印 flag。

核心推导是:

text
s*k = h + r*d mod n k = k_lsb + 2^6*x

所以:

text
x = (s^-1*r*2^-6)*d + (s^-1*h - k_lsb)*2^-6 mod n

x < n / 2^6,这就是 Hidden Number Problem。脚本用 centered lattice/BKZ 恢复 d,再签 SHA512("gimme flag")

Поделиться этим Q&A