第九届“强网”拟态防御crypto和附件链接

作者:玄网安全 发布:2026-11-01 14:38 收录:2026-11-01 16:45 2 次阅读 约 12791 字
摘要:附件链接: https://wwapq.lanzoub.com/b01bjtm29a|密码:6gpp crypto-Off the Curve 题目描述: The Roadrunner team uses Northstar Docs to distribute encrypted reports. Tomorrow…
推荐理由:本文涵盖「拟态防御」、「强网杯」、「crypto」等多个主题,重点关注 拟态防御。

附件链接:https://wwapq.lanzoub.com/b01bjtm29a|密码:6gpp

crypto-Off the Curve

题目描述:
The Roadrunner team uses Northstar Docs to distribute encrypted reports. Tomorrow's quarterly review depends on rpt-042, but the original copy cannot be opened. Northstar marks the delivery complete, and every fresh download returns the same locked file. As a member of the project review workspace, recover the contents of the original delivery.

Credentials: analyst@northstar.test / Curve-Review-2026

No attachments are provided for this challenge.

附件:无

文件名 rpt-042.envelope.json。本次信封中和解密有关的字段是:

{
  "aad": {
    "classification": "internal-test",
    "content_type": "text/plain; charset=utf-8",
    "created_at": "2026-09-16T22:40:00Z"
  },
"ciphertext": "fHZEj+LBRPb6ib/sQxSWSxCm7g1+Gdy5a9MaqdP90BQ0ViUpNnEgNoum3qVdiGgGL7bJryYdURX40k8TJ5KLLM53cCzdh5LKUYMQ3KLQA6hzSpnGm3AoMgtpyaKJ0UZcUQ3GVXy8m5DJad1Srta3eT50QiRHUmcXwMxRphSq2uwxPWazHdwSj18Iz7cSYPw4axkVAzWvh8tJeXe7on0IW3E+Fl0JUrtU28An4n2DTZntra0lPHb514bEyYK5+brc53RmCmjk2rhCDc5u5MQ+0jCo6VmFZlgIHRqJgxMOeGmoUBjZhoEA",
"info": {
    "key_version": "doc-kek-2026.09-v3",
    "project_id": "prj-northstar",
    "report_id": "rpt-042"
  },
"nonce": "62415854be17e5c755fa6ae7",
"recipient_public_key": "02315f",
"tag": "bbfc9923ab97b4e9d7c3c46dcd25335b"
}
页面上的记录 公钥 版本 WP 里怎么用
Rotated browser key / legacy-rotated-05 022308 doc-kek-2026.09-v4,Active 轮换后的新私钥,不参加 CRT
Rotated mobile key / legacy-rotated-06 024c87 doc-kek-2026.09-v4,Active 同上
Android 8 field client / legacy-android-01 025c5d doc-kek-2026.09-v3,Archived 用了,余数 2044
Browser fallback A / legacy-browser-02 02c535 doc-kek-2026.09-v3,Archived 用了,余数 149

/sdk.js 的点加和点乘都不检查点是否在基曲线上。四个 v3 客户端公钥解压后落在四条不同的弱曲线上,点的阶都是三四千的素数。诊断回包给出

SHA256("DOCSHARE-DIAG\0" || key_id || nonce || 共享点编码) 的前 16 字节

/sdk.js 公开的基曲线是
y^2 = x^3 + x + 7  (mod 65519) G = (4, 17054) n = 65444

收件人 02315f 的 x = 0x315f = 12639,解压得到 (12639, 7624),它在基曲线上。四个 v3 公钥的 Legendre 符号都是 -1,不在基曲线上。各自的 b' 和点阶是:

公钥 b'
 点阶
025c5d 46333 3257
02c535 10369 4079
022b09 53552 3863
025d04 9527 2843

sealReport 的派生是:

infoHash = SHA256(canonical(info))
binding  = SHA256("northstar-report-scalar-binding\0" || u32be(s) || infoHash)
master   = scrypt(encode(shared) || binding,
                  "northstar-report-v3\0" || nonce || infoHash,
                  N=32768, r=8, p=1, dkLen=32)
aesKey   = HMAC-SHA256(master, "northstar-aes256-gcm-key\0" || infoHash)

用空 HMAC 密钥伪造管理员

{
  "role": "admin",
  "permissions": {
    "audit": true,
    "diagnostics": true,
    "register_keys": true,
    "read": true
  }
}

在四条弱曲线上恢复服务器私钥

legacy-android-01  nonce=fb7f739f5d0c3df5  tag=1d11fc5bc92f4de314e57279ab9377f4
legacy-browser-02  nonce=878d82e617d5d542  tag=0f7762f43d46febbc3c3d94b29eeabb1
legacy-rn-03       nonce=82b96ac4930eeb9d  tag=6298235f87fe98317e1ed02028bd6127
legacy-browser-04  nonce=e8059d84281d312e  tag=644b8dfac811fcf0aad60230cfc1e375

payload:

import base64
import hashlib
import hmac
import json
import sys
import time
from urllib.error import HTTPError, URLError
from urllib.request import ProxyHandler, Request, build_opener

from Crypto.Cipher import AES

BASE = "http://oncgp8fs.nestctf.cyberpeace.cn:58080"
USERNAME = "analyst@northstar.test"
PASSWORD = "Curve-Review-2026"
P = 65519
A = 1
B = 7
V3_KEYS = (
    "legacy-android-01",
    "legacy-browser-02",
    "legacy-rn-03",
    "legacy-browser-04",
)

OPENER = build_opener(ProxyHandler({}))


def request(path, token=None, method="GET", body=None):
    headers = {"User-Agent": "Mozilla/5.0", "Accept": "application/json"}
    data = None
    if body is not None:
        data = json.dumps(body).encode()
        headers["Content-Type"] = "application/json"
    if token:
        headers["Authorization"] = "Bearer " + token
    last_error = None
    for attempt in range(4):
        req = Request(BASE + path, data=data, headers=headers, method=method)
        try:
            with OPENER.open(req, timeout=30) as response:
                return response.status, response.read()
        except HTTPError as error:
            raise SystemExit(f"{method} {path} -> HTTP {error.code}: {error.read()[:300]!r}") from error
        except (URLError, ConnectionError, TimeoutError) as error:
            last_error = error
            time.sleep(0.4 * (attempt + 1))
    raise SystemExit(f"{method} {path} failed: {last_error}")


def b64url(raw):
    return base64.urlsafe_b64encode(raw).rstrip(b"=").decode()


def mint(header, payload, key=b""):
    signing_input = b".".join((
        b64url(json.dumps(header, separators=(",", ":")).encode()).encode(),
        b64url(json.dumps(payload, separators=(",", ":")).encode()).encode(),
    ))
    signature = hmac.new(key, signing_input, hashlib.sha256).digest()
    return signing_input.decode() + "." + b64url(signature)


def add(point, other):
    if point is None:
        return other
    if other is None:
        return point
    x1, y1 = point
    x2, y2 = other
    if x1 == x2 and (y1 + y2) % P == 0:
        return None
    if x1 == x2:
        slope = ((3 * x1 * x1 + A) * pow((2 * y1) % P, P - 2, P)) % P
    else:
        slope = ((y2 - y1) * pow((x2 - x1) % P, P - 2, P)) % P
    x3 = (slope * slope - x1 - x2) % P
    y3 = (slope * (x1 - x3) - y1) % P
    return (x3, y3)


def mul(scalar, point):
    result = None
    while scalar:
        if scalar & 1:
            result = add(result, point)
        point = add(point, point)
        scalar >>= 1
    return result


def decompress(hex_key):
    raw = bytes.fromhex(hex_key)
    x = (raw[1] << 8) | raw[2]
    rhs = (x * x * x + A * x + B) % P
    y = pow(rhs, (P + 1) // 4, P)
    if (y & 1) != (raw[0] & 1):
        y = (-y) % P
    return (x, y)


def on_base_curve(point):
    x, y = point
    return (y * y - (x * x * x + A * x + B)) % P == 0


def curve_order(b_value):
    total = 1
    for x in range(P):
        rhs = (x * x * x + A * x + b_value) % P
        legendre = pow(rhs, (P - 1) // 2, P)
        if legendre == 0:
            total += 1
        elif legendre == 1:
            total += 2
    return total


def point_order(point):
    x, y = point
    b_value = (y * y - (x * x * x + A * x)) % P
    order = curve_order(b_value)
    remainder = order
    divisor = 2
    primes = []
    while divisor * divisor <= remainder:
        while remainder % divisor == 0:
            primes.append(divisor)
            remainder //= divisor
        divisor += 1 if divisor == 2 else 2
    if remainder > 1:
        primes.append(remainder)
    for prime in sorted(set(primes)):
        while order % prime == 0 and mul(order // prime, point) is None:
            order //= prime
    return b_value, order


def encode_full(point):
    if point is None:
        return bytes([0])
    x, y = point
    return bytes([4, (x >> 8) & 255, x & 255, (y >> 8) & 255, y & 255])


def recover_residue(key_id, point, order, nonce_hex, tag_hex):
    prefix = b"DOCSHARE-DIAG\x00" + key_id.encode() + bytes.fromhex(nonce_hex)
    target = bytes.fromhex(tag_hex)
    current = None
    for scalar in range(order):
        digest = hashlib.sha256(prefix + encode_full(current)).digest()[:16]
        if digest == target:
            return scalar
        current = add(current, point)
    raise SystemExit(f"no residue for {key_id}")


def crt(pairs):
    modulus = 1
    for _, factor in pairs:
        modulus *= factor
    value = 0
    for remainder, factor in pairs:
        partial = modulus // factor
        value += remainder * partial * pow(partial, -1, factor)
    return value % modulus, modulus


def canonical(value):
    return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode()


def decrypt_report(envelope, scalar, shared):
    info = envelope["info"]
    aad = envelope["aad"]
    info_hash = hashlib.sha256(canonical(info)).digest()
    binding = hashlib.sha256(
        b"northstar-report-scalar-binding\x00" + int(scalar).to_bytes(4, "big") + info_hash
    ).digest()
    master = hashlib.scrypt(
        encode_full(shared) + binding,
        salt=b"northstar-report-v3\x00" + bytes.fromhex(envelope["nonce"]) + info_hash,
        n=32768,
        r=8,
        p=1,
        dklen=32,
        maxmem=128 * 1024 * 1024,
    )
    aes_key = hmac.new(master, b"northstar-aes256-gcm-key\x00" + info_hash, hashlib.sha256).digest()
    cipher = AES.new(aes_key, AES.MODE_GCM, nonce=bytes.fromhex(envelope["nonce"]))
    cipher.update(canonical({"aad": aad, "info": info}))
    return cipher.decrypt_and_verify(
        base64.b64decode(envelope["ciphertext"]),
        bytes.fromhex(envelope["tag"]),
    )


def main():
    _, login_raw = request("/api/v1/login", method="POST", body={
        "username": USERNAME,
        "password": PASSWORD,
    })
    analyst = json.loads(login_raw)["token"]
    header_b64, payload_b64, _signature = analyst.split(".")
    payload = json.loads(base64.urlsafe_b64decode(payload_b64 + "=="))
    print("analyst role:", payload["role"])

    _, report_raw = request("/api/v1/reports/rpt-042/download", analyst)
    envelope = json.loads(report_raw)
    _, keys_raw = request("/api/v1/projects/prj-northstar/keys", analyst)
    public_keys = {item["id"]: item["public_key"] for item in json.loads(keys_raw)["keys"]}
    recipient = decompress(envelope["recipient_public_key"])
    print("recipient:", envelope["recipient_public_key"], recipient, "on_base_curve:", on_base_curve(recipient))

    payload["role"] = "admin"
    # kid 读到密钥目录外的文件时,HMAC 密钥是空字节串。
    admin = mint({"alg": "HS256", "kid": "../sdk.js", "typ": "JWT"}, payload, b"")
    _, me_raw = request("/api/v1/me", admin)
    me = json.loads(me_raw)
    print("forged role:", me["role"], "diagnostics:", me["permissions"]["diagnostics"])

    # 报告绑定 doc-kek-2026.09-v3,v4 轮换后的公钥对应另一把私钥。
    residues = []
    for key_id in V3_KEYS:
        point = decompress(public_keys[key_id])
        b_value, order = point_order(point)
        _, receipt_raw = request("/api/v1/diagnostics/compat", admin, "POST", {"key_id": key_id})
        receipt = json.loads(receipt_raw)
        residue = recover_residue(key_id, point, order, receipt["nonce"], receipt["tag"])
        residues.append((residue, order))
        print(
            f"{key_id} pub={public_keys[key_id]} b={b_value} order={order} "
            f"nonce={receipt['nonce']} residue={residue}"
        )

    scalar, modulus = crt(residues)
    if not 0 < scalar < 2**32:
        raise SystemExit(f"scalar {scalar} mod {modulus} is outside uint32")
    # 信封没有临时公钥,共享点就是服务器私钥乘以收件人公钥。
    shared = mul(scalar, recipient)
    plaintext = decrypt_report(envelope, scalar, shared)
    print("scalar:", scalar)
    print("shared:", shared)
    print(plaintext.decode())


if __name__ == "__main__":
    print("python:", sys.version.split()[0])
    main()

Immobilizer ECDSA

核心思路:40 条 secp256k1 ECDSA 签名的 nonce 最高 8 位恒为 0,构成隐藏数问题(HNP),用 LLL 格规约恢复私钥 d,再以 SHA256(d) 作为密钥流异或解密 sealed 得到 flag。

1. 题目说明

题目只给出一个 data.json,内容为汽车防盗器(immobilizer)的签名记录:

{
  "curve": "secp256k1",
"n": "115792089237316195423570985008687907852837564279074904382605163141518161494337",
"G": {
    "x": "55066263022277343669578718895168534326250603453777594175500187360389116729240",
    "y": "32670510020758816978083085130507043184471273380659243275938904335757337482424"
  },
"public_key": {
    "x": "23627227489180312529102653625669019920806999775537304645922978739257665571310",
    "y": "100753196227827319451259216774802632812381464299694707681682452911610216430556"
  },
"signatures": [
    {"msg": "immobilizer|vin=LDC613P23A0000001|ctr=0",
     "r": "91322417266516705459309644889473281836638529456978360519484264246470515462402",
     "s": "74230450154863550368806295818323115017851826111799325299082883080293361476385"},
    {"msg": "immobilizer|vin=LDC613P23A0000001|ctr=1",
     "r": "103257334126702286333674855231439453735917167735015786437067084411297866188326",
     "s": "107744251849317616725436238316208679457009219068941567640123002212877900210964"},
    "... 共 40 条,ctr = 0..39,完整 (r, s) 见第 4 节脚本中的 RS 列表 ..."
  ],
"sealed": "50bbc6a269fd17bb9f2f057da88ee2ce80504261010d80ebddcd27"
}

已知条件与约束:

  • 曲线为 secp256k1:,素域 ,基点 G 的阶为 n(256 位素数)。
  • 40 条签名的消息格式完全固定,只有计数器不同:immobilizer|vin=LDC613P23A0000001|ctr=i,i = 0..39。
  • 40 个 r 两两不同,所以不存在 nonce 直接复用。
  • sealed 为 27 字节的十六进制串,没有说明加密方式。

目标:恢复签名私钥,解开 sealed。

2. 解题思路

40 条签名、r 互不相同,但共用一个私钥
  → 每条签名都把 nonce k_i 写成私钥 d 的线性函数:k_i = A_i + B_i·d (mod n)
  → 猜测 nonce 生成存在偏置(高位为 0),即 k_i 远小于 n
  → 构成 HNP,用格 + LLL 找出短向量 (k_1, ..., k_N),同时得到 d
  → 用 d·G == Q 校验私钥
  → 用 d 派生密钥流解密 sealed

3. 解题过程

3.1 把每条签名改写成关于私钥的线性方程

关键观察: 签名量很大(40 条),又全部出自同一私钥,消息几乎相同。没有重复 r,只能从 nonce 的统计结构入手。

原理与操作: ECDSA 签名满足(在  中):

s_i = k_i^{-1} · (z_i + r_i · d)   (mod n)

移项得:

k_i = s_i^{-1}·z_i + s_i^{-1}·r_i · d = A_i + B_i·d   (mod n)
A_i = z_i · s_i^{-1} mod n,   B_i = r_i · s_i^{-1} mod n

其中 z_i 为消息摘要。题目没有说明哈希算法,这里取 ECDSA/secp256k1 最常用的 SHA-256:z_i = int.from_bytes(SHA256(msg_i), "big")。(这一选择属于假设,最终靠 d·G == Q 来确认。)

本步结果: 得到 40 个关于未知数 d 的线性同余式,A_i、B_i 均可从公开数据算出。若 k_i 是 [1, n) 上的均匀随机数,这些方程不泄露任何信息;只要 k_i 有界,就能转化为格问题。

3.2 建立 HNP 格并估计所需签名数

关键观察: 嵌入式 nonce 生成器经常把较短的随机数当作 256 位标量使用,导致高位恒为 0。假设 ,其中 。

原理与操作: 存在整数  使 。构造如下格(为保持整数,整体乘以 n),行向量:

row_i   (0 ≤ i < N) : (0, …, n·n (第 i 列), …, 0,   0,   0  )
row_N               : (B_1·n, …, B_N·n,            K,   0  )
row_{N+1}           : (A_1·n, …, A_N·n,            0,   K·n)

整数组合 d·row_N + row_{N+1} − Σ l_i·row_i 等于:

v = (k_1·n, …, k_N·n,  d·K,  K·n)

由于  且 ,v 的每个分量都不超过 K·n,因此 。格的行列式为 ,维数为 ,Gaussian heuristic 给出的典型最短长度约为

要让 v 明显短于这个值,大致需要满足 ,即所有 nonce 一共泄露的位数超过私钥的位数。若每条 nonce 高 8 位为 0(),则需要  条签名,而题目正好给了 40 条,满足条件。

LLL 规约后,找出最后一列等于 ±K·n 的行,即可读出倒数第二列 ±d·K,从而得到 d。

本步结果: 偏置位数未知,脚本从 b = 248 开始逐字节向下尝试。实际在第一个取值 b = 248 就得到了能通过公钥校验的私钥:

[+] nonce bound   : k < 2^248
[+] private key d : 65062337734878525051969737001582398336729906718165769482557093642791334506647
[+] d*G == Q      : True

3.3 校验私钥与偏置假设

关键观察: 只有找到了正确的私钥,d·G 才会等于公开的公钥 Q。这一步同时确认了 SHA-256 摘要假设和 nonce 上界假设。

原理与操作: 在 secp256k1 上计算 d·G 并与 Q 比较;再用 k_i = A_i + B_i·d mod n 反算全部 40 个 nonce,统计它们的位长。

本步结果:

[+] max k bitlen  : 248
[+] k bitlens     : [241, 243, 245, 245, 245, 245, 245, 246, 246, 246, 246, 246, 246, 246, 247, 247, 247, 247, 247, 247, 247, 247, 247, 247, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248, 248]

40 个 nonce 全部小于 ,最大位长正好是 248,位长分布与 248 位均匀随机数一致(约一半为满 248 位)。由此可以确认 nonce 的最高字节恒为 0,这也和 flag 的内容相互印证。

3.4 用私钥派生密钥流解密 sealed

关键观察:sealed 只有 27 字节,不是 16 的倍数,也没有附带 IV 或 tag,最可能是长度与明文相同的异或流加密;32 字节的哈希输出足够覆盖 27 字节。

原理与操作: 以私钥为种子生成候选密钥流,依次尝试 SHA256 / SHA512 / MD5 / SHA1 / SHA3 / BLAKE2 作用于 d 的不同编码(32 字节大端、小端、十进制串、十六进制串),把异或结果全部为可打印字符作为筛选标准。只有一个组合满足条件:

keystream = SHA256(d.to_bytes(32, "big"))[:27]
plaintext = sealed XOR keystream

本步结果:

[+] keystream     : 36d7a7c5128978cbc04d7c09cdd18da8df242a045e63ef85bea85a
[+] flag          : flag{top_byte_of_the_nonce}

27 字节解密后全部是可打印字符,符合 flag{...} 格式,内容也直接点明了漏洞(nonce 的最高字节)。

4. 完整解题脚本

脚本内嵌了全部题目数据,不依赖 data.json,需要在 SageMath 环境中运行(使用 Sage 的椭圆曲线和 LLL)。

# 运行方式: sage -python solve.py
# 流程: ECDSA nonce 高位为 0 -> HNP 格规约恢复私钥 d -> SHA256(d) 异或解密 sealed
import hashlib
from sage.all import EllipticCurve, GF, Matrix, ZZ, inverse_mod

# ---------------- 题目数据(data.json 原样内嵌) ----------------
p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F# secp256k1 素域
n = 115792089237316195423570985008687907852837564279074904382605163141518161494337
Gx = 55066263022277343669578718895168534326250603453777594175500187360389116729240
Gy = 32670510020758816978083085130507043184471273380659243275938904335757337482424
Qx = 23627227489180312529102653625669019920806999775537304645922978739257665571310
Qy = 100753196227827319451259216774802632812381464299694707681682452911610216430556
SEALED = "50bbc6a269fd17bb9f2f057da88ee2ce80504261010d80ebddcd27"
# 第 i 条签名的消息为 "immobilizer|vin=LDC613P23A0000001|ctr=i",按 i = 0..39 给出 (r, s)
RS = [
    (91322417266516705459309644889473281836638529456978360519484264246470515462402,
     74230450154863550368806295818323115017851826111799325299082883080293361476385),
    (103257334126702286333674855231439453735917167735015786437067084411297866188326,
     107744251849317616725436238316208679457009219068941567640123002212877900210964),
    (111921579415467239343630252792752981674256232668276147344997845922981734624272,
     4465739024795485622525511256112512891165302626460991727012889071473189740134),
    (19760154632518205810452223845788919876636879268134733560462131575661960107077,
     18954707186767945306957129156974936413620986888245138081593340841252273933916),
    (59900847935449140639681020624192883758556828680468938213092324661380308600140,
     739814014335417465582541295106362177359722574185228244748325990910949032683),
    (86896333256490135745535661310383599481683962310959642026052500303041385103241,
     13493672043009196320820017062023781624855449401602395800191423445145595215313),
    (26438423975169760929931944760375789902299378270550685303275126811104525919658,
     88925662851971342041983962922333967417725749611546603319229621757320212977217),
    (35319192334648160740570389894272059452089101708879677493810366846190039028673,
     62362102525904922669784176482019076640549401380929306937770096646629967284911),
    (49187482715507835518390114696436316349701087638390469522494756009225852397469,
     29855157089744994271623929303330718764529955032290152045071436736213519795010),
    (68099922010578218379510017308364056843842845022877078410722340870611304760661,
     97926516414411479977483363740921360174792736951126322543589404122761023722430),
    (88982720681186841201018384792567448088652883916296206564966468529665708526944,
     113357221877532074154325416603901430266175592429377519724522020286826432295276),
    (35983819891195969296869251304730870656621632886031032824045373506391997338953,
     74822873412975629486112867749199622258288346120848278417272591306135834921153),
    (106486034599354775517053033813191598461007949169409991368076692335915827709230,
     83663550953175141078078543258537874141920607603776621682960227347198110236848),
    (44593063361131484901035716995410318235772520097067362752746211614349323927140,
     34835439189462574316499014416823866697184221971826561255796251348638797038705),
    (25809287748339132488809626799922985467020448703720756151787538779087132391840,
     64135007087091921106274422689019350404830759124187625843765798573357597228433),
    (114660847037821190585996036846862833049156328974482870886827501878906608521059,
     58021959558008521203088580582358965384353775883605613252684094169802391887414),
    (31472887143722690798288343243725539871190046926292129521560053199068164727182,
     108840279983118147408917234515042357277312350175928288031220468731023559931071),
    (7748216005278180396229885008338700420352362191678163795150979851381885815710,
     110878367871223346875488223702631837449596695102077986040279242661991579436389),
    (102493272842989613217358374645742000017156990862823086068641581881757639495765,
     41509958933241709073203799276875800462452046014662483213737454892317499110460),
    (27705516223885099222816142353875588017978122857174560413072674163528565339047,
     43703270983506427596079706160293621073079596637355291776307934092992047951706),
    (75930752581287648338002323670665970586918998501486837906928287598988597424659,
     56607719413285013766080628077299830906733395496637902812419568858110650522076),
    (64531233869013340927564164484717355226051135791938192459232626473080330043121,
     111679362882760520299721853979360878830653866814117834171220093172003994314270),
    (97950779372995901281991153726961507266378936935495919272717802397261847557775,
     50086464542034420861434291072383030423831339273616064186971633432316262565324),
    (109176894727901159733743815434880401858818148832151269781497062967689430528188,
     76759997172428018982220500236361900863723449598512288145487629868840432678998),
    (49715392084937070526523294121725112558419665956279875324939448566616037468013,
     66525212753082011188774574787610439674267899677802024051539103362901203348128),
    (64689645862582274399626097725136594545918374646118169678586319579461170400367,
     73144643053847465284108769858476883089819545585752469122631492053442990283597),
    (63925661970767719838943702591771041140427426933235246313192571602904238506180,
     88619660904621821732945375141518815279389111451624531397776778762090735788629),
    (68636669376057650460794266041669351259298765958338238284746408411280892762350,
     86125511678846604144365927869274299654367804517501394466422249384300336373670),
    (107027162123916974424713790975590722165455131198475359961910385474631206324580,
     53339469505227315712215017277426033184379700060616290708425386809181616666284),
    (110494940453981367397571536729292986885677948445511340263299547246888296610237,
     24417732527395478000410451516222791481950022276976553914391344795094245269519),
    (66064239010490749303647553620109443793718029215509571175204674453202193003713,
     250196222186447551656550267348046979152981595246031063280269846104030847661),
    (81903308809013316738774535283101219733263437452942821548274662489719373502355,
     111910968894078087273789826302148643529077000915845731872202327644542881060261),
    (95006762650493592664452748116163084436498638646442998815684749670527080332748,
     70791141494549737059672988712638785664734339633514949525718667871191393404891),
    (39250492879172449768135561183981734940554989734861731923206475148960940217810,
     26184729599920340018168096789952539260177321766825400893757460943264642542044),
    (98741771031626979367022255942062461502697074112863130289796787129681996607238,
     66449486728421444778807881004840356041769763535127071767069316731612279165579),
    (76403910179182783071243496978692286313257003423062784990869086097689595013970,
     86822653072856388727402475555699712588678433416084285429637123104893209918011),
    (81388297455582111084803529612994646615684597584733840384596641251731154563949,
     61594487362873641385907652236268426967756165209163301630487636916729437352293),
    (23010660791018856266187136959328777217667090703210713904655343540416830494871,
     11418297390873498136060696424791034786058296918437085536466375091739104139484),
    (9301217604506122956560056718852051999582907179434991632145190707870261506428,
     70831192311916953428062191521593938196239361435853570821640664457286355000497),
    (47763002376375770930941956421456059251823248203373249255237064946670312447403,
     101737881744302500031113427248190823581071995601430648843088222854308624542656)
]
MSGS = ["immobilizer|vin=LDC613P23A0000001|ctr=%d" % i for i in range(len(RS))]

E = EllipticCurve(GF(p), [0, 7])
G = E(Gx, Gy)
Q = E(Qx, Qy)


def z_of(msg):
    # 消息摘要 z = SHA-256(msg),大端整数
    return int.from_bytes(hashlib.sha256(msg.encode()).digest(), "big") % n


# 由 s = k^-1 (z + r d) 得 k_i = A_i + B_i d (mod n)
A = [int(z_of(m) * inverse_mod(s, n) % n) for m, (r, s) in zip(MSGS, RS)]
B = [int(r * inverse_mod(s, n) % n) for (r, s) in RS]


def hnp(bits):
    # 假设 0 <= k_i < K = 2^bits。整体乘 n 保持整数格:
    #   第 i 行 (i < N) : 第 i 列为 n*n            (对应 mod n 的约简)
    #   第 N 行         : (B_1*n, ..., B_N*n, K, 0)
    #   第 N+1 行       : (A_1*n, ..., A_N*n, 0, K*n)
    # 目标短向量 = (k_1*n, ..., k_N*n, d*K, K*n),各分量量级都约为 K*n
    N, K = len(A), 2 ** bits
    M = Matrix(ZZ, N + 2, N + 2)
    for i in range(N):
        M[i, i] = n * n
        M[N, i] = B[i] * n
        M[N + 1, i] = A[i] * n
    M[N, N] = K
    M[N + 1, N + 1] = K * n
    for row in M.LLL():
        if abs(row[N + 1]) != K * n:
            continue
        sign = 1if row[N + 1] > 0else-1
        d = int(sign * row[N] // K) % n
        if d and d * G == Q:  # 用公钥校验
            return d
    returnNone


# 从 248 位开始向下逐字节尝试 nonce 上界
d = None
for bits in range(248, 127, -8):
    d = hnp(bits)
    if d:
        print("[+] nonce bound   : k < 2^%d" % bits)
        break
assert d, "lattice attack failed"
print("[+] private key d :", d)
print("[+] d*G == Q      :", d * G == Q)

# 用恢复出的 d 反算全部 nonce,检验偏置假设
ks = [(a + b * d) % n for a, b in zip(A, B)]
print("[+] max k bitlen  :", max(k.bit_length() for k in ks))
print("[+] k bitlens     :", sorted(k.bit_length() for k in ks))

# 解密 sealed:密钥流 = SHA256(d 的 32 字节大端表示)
ks_stream = hashlib.sha256(d.to_bytes(32, "big")).digest()
ct = bytes.fromhex(SEALED)
flag = bytes(c ^ k for c, k in zip(ct, ks_stream))
print("[+] keystream     :", ks_stream[:len(ct)].hex())
print("[+] flag          :", flag.decode())

samples

核心思路:利用 Kannan 嵌入将 32 维秘密、64 样本的模 7681 带误差学习(LWE)问题转化为 97 维格上的最短向量问题(SVP),通过 LLL 格基约减直接恢复二元秘密向量 s,再计算 SHA-256(bytes(s)) 作为掩码密钥流异或解密 masked_update 得到 Flag。

1. 题目说明

题目提供单文件挑战数据 samples.json,要求从给定的公开矩阵与观测向量中恢复隐藏的秘密参数,并解密 masked_update 字段获取 Flag。

原始数据包含以下 6 个字段:

  • q = 7681:有限域模数(素数)。
  • n = 32:秘密向量 s 的维度。
  • m = 64:采样方程组数量。
  • A:64 × 32 的模 q 整数矩阵。
  • b:长度为 64 的模 q 整数向量。
  • masked_update:长度为 56 个十六进制字符(28 字节)的密文数据。

完整的题目原始数据如下:

{
  "q": 7681,
"n": 32,
"m": 64,
"A": [
    [2395, 6050, 1036, 3320, 2631, 4663, 4329, 1258, 3363, 3828, 5797, 7380, 609, 2407, 6810, 4109, 330, 7238, 3281, 4261, 2091, 5694, 641, 1845, 2593, 1584, 6754, 6468, 497, 871, 2968, 3533],
    [4337, 334, 5766, 3916, 1515, 2814, 7114, 7349, 3376, 1380, 5932, 3966, 3956, 6806, 1563, 4951, 664, 3148, 711, 4742, 4178, 743, 224, 2630, 7026, 1347, 1890, 1157, 2169, 3858, 3716, 2097],
    [194, 6116, 1249, 1821, 6681, 3478, 255, 3784, 4745, 103, 1888, 6175, 5978, 6399, 941, 6547, 479, 1363, 132, 1117, 1629, 6178, 6710, 6289, 3335, 7379, 1474, 4070, 67, 7502, 1629, 5051],
    [4505, 4853, 2543, 1416, 6020, 5442, 1857, 4968, 5843, 6707, 6856, 4301, 3494, 2068, 3008, 7250, 3675, 1215, 4595, 2815, 10, 796, 1634, 6486, 129, 4737, 1709, 6542, 3578, 5592, 4456, 6315],
    [3688, 6657, 1319, 2658, 4686, 6127, 3353, 7406, 3761, 7365, 3689, 5508, 4742, 7028, 2596, 3272, 301, 2815, 7042, 1061, 5553, 808, 7411, 3544, 1394, 6951, 1903, 6957, 3834, 161, 6094, 953],
    [4113, 3282, 683, 6191, 3281, 1594, 806, 758, 2023, 4929, 38, 1392, 7615, 552, 4764, 1058, 6909, 4947, 3753, 2849, 5085, 4036, 6772, 279, 4100, 4623, 7392, 2961, 5256, 1738, 4827, 5654],
    [117, 3044, 3367, 4579, 134, 2277, 5999, 4197, 3630, 5733, 457, 2763, 4403, 1340, 6157, 2752, 4702, 1034, 6888, 6086, 7228, 4562, 6738, 4323, 1014, 1939, 5530, 6979, 170, 5292, 2988, 1280],
    [2329, 3504, 1759, 6698, 1919, 655, 4194, 4955, 6681, 4967, 6332, 2511, 5138, 2769, 2851, 1008, 6099, 6839, 2066, 2188, 920, 5993, 1404, 564, 6312, 1013, 1004, 3685, 5503, 5817, 4537, 1164],
    [2870, 4406, 5602, 3390, 4750, 4097, 5968, 4131, 2102, 6086, 4133, 2183, 6619, 608, 4943, 6695, 3892, 2678, 7668, 6665, 3680, 3299, 7197, 4002, 6475, 4743, 6350, 1092, 7479, 2519, 4708, 5251],
    [3399, 2582, 3816, 3767, 1440, 7029, 1425, 7080, 5874, 3923, 4679, 4600, 3595, 3304, 6503, 2320, 4732, 2629, 1210, 4224, 4501, 1397, 3854, 1910, 988, 7340, 6810, 793, 3026, 5309, 1956, 3230],
    [5720, 872, 5274, 508, 4891, 6078, 2704, 2698, 1227, 2974, 6919, 703, 5402, 4315, 7314, 6863, 6191, 1044, 53, 5266, 2209, 4968, 3882, 5480, 5453, 7486, 5759, 1736, 5888, 4981, 667, 2343],
    [4472, 7050, 5017, 4990, 3584, 5857, 4912, 6189, 4165, 4481, 710, 6889, 1332, 2894, 1095, 999, 4980, 6337, 4921, 3686, 7193, 6208, 6198, 5300, 5512, 6674, 3150, 734, 5509, 5619, 4987, 5753],
    [4459, 276, 5645, 1988, 3071, 4102, 5347, 1102, 1789, 73, 4603, 4275, 4941, 6, 1334, 3576, 4415, 5481, 7633, 1397, 3601, 4097, 4524, 433, 2467, 7248, 7549, 1423, 1784, 4860, 830, 412],
    [1710, 4751, 6398, 15, 153, 5457, 6836, 5363, 2185, 4161, 6870, 5279, 6698, 7449, 1299, 791, 488, 6114, 6379, 7489, 7395, 4297, 1322, 7479, 6362, 5962, 6081, 2781, 1821, 4870, 1906, 258],
    [7575, 552, 5768, 1636, 3737, 2073, 5198, 7315, 1867, 5876, 676, 629, 1610, 5843, 5335, 5374, 3258, 4399, 3918, 926, 3382, 80, 5826, 4543, 2467, 4259, 4644, 360, 2060, 4856, 7219, 2967],
    [425, 1397, 5950, 6621, 7524, 4911, 678, 5858, 2944, 886, 6381, 552, 2688, 5485, 553, 2998, 6547, 932, 3160, 510, 2925, 7389, 562, 503, 3681, 6763, 6893, 176, 7150, 5997, 7473, 7591],
    [499, 1829, 1298, 1131, 7267, 3640, 4899, 7626, 2301, 4876, 2694, 2755, 6132, 4471, 2052, 6271, 5255, 4863, 7672, 1170, 7073, 98, 6937, 5880, 4068, 4462, 1980, 6797, 1556, 4811, 2189, 1665],
    [4443, 4812, 6271, 572, 1927, 3795, 2227, 415, 4627, 5801, 6479, 1143, 267, 6951, 2307, 6256, 2128, 4352, 2179, 4398, 2407, 7654, 4604, 1108, 1947, 3392, 6818, 5111, 2241, 2830, 1794, 3699],
    [4465, 1294, 3263, 1718, 172, 7246, 5923, 4438, 1730, 939, 3153, 2768, 1880, 3879, 4972, 7623, 7182, 65, 348, 5206, 2737, 2897, 3983, 3008, 3293, 6836, 251, 6291, 4171, 1060, 5784, 5241],
    [657, 3405, 5622, 2550, 5780, 3534, 3477, 6628, 3549, 2590, 7424, 5883, 6759, 3042, 4408, 6124, 1398, 3940, 6119, 2139, 6398, 107, 5094, 1171, 1103, 319, 6771, 1073, 802, 5226, 1132, 2243],
    [4226, 4577, 1482, 6461, 6026, 6648, 7325, 2431, 1211, 3295, 28, 359, 3205, 5682, 5477, 5383, 6569, 4986, 5639, 5302, 5858, 2163, 4497, 6008, 763, 2117, 1602, 4895, 4549, 5949, 4845, 896],
    [3899, 3004, 5714, 6214, 6511, 7349, 3094, 4092, 4552, 5648, 5760, 925, 1781, 3119, 7427, 5066, 4919, 6562, 4034, 1847, 2171, 6871, 6733, 409, 5477, 3193, 3597, 5730, 6112, 3047, 7574, 3057],
    [3086, 3901, 471, 6298, 1473, 7531, 2321, 2024, 7471, 3373, 4274, 4390, 1319, 6215, 3086, 7392, 3688, 3193, 5351, 1185, 4924, 2042, 27, 4584, 2375, 2341, 612, 538, 4109, 1101, 2937, 6647],
    [3491, 5193, 3273, 5402, 1182, 2439, 2182, 6704, 7019, 105, 490, 3815, 2170, 961, 6254, 7111, 5934, 5962, 4582, 4585, 7050, 5784, 863, 3329, 5767, 4352, 6947, 2437, 6046, 104, 4311, 5251],
    [7626, 1677, 3781, 4295, 6484, 1149, 853, 3386, 6213, 1984, 2786, 2096, 6487, 3354, 1366, 606, 7334, 3779, 1206, 2235, 2439, 212, 6141, 6899, 6091, 4920, 2387, 3335, 7296, 1984, 2600, 844],
    [2480, 5903, 3271, 2241, 5863, 195, 5840, 7467, 1266, 1456, 1360, 3126, 1939, 7409, 5391, 1945, 1557, 7177, 3788, 902, 4696, 787, 259, 1528, 3007, 1383, 1656, 5299, 1538, 236, 6176, 1811],
    [6242, 4758, 3003, 4663, 2447, 3699, 1729, 1366, 189, 173, 2127, 1711, 567, 3509, 4813, 7016, 7328, 7345, 550, 2177, 5503, 6588, 2277, 3926, 6841, 7299, 2415, 5269, 2599, 390, 1258, 7076],
    [1782, 3512, 4569, 4760, 6442, 2355, 2908, 3742, 6776, 2508, 913, 6242, 2798, 3996, 400, 7433, 6127, 1969, 864, 5144, 2702, 4957, 5070, 115, 7510, 1982, 6818, 865, 522, 1807, 3302, 3414],
    [4488, 2865, 2492, 7066, 6084, 4061, 3651, 4393, 5419, 2893, 974, 360, 47, 4193, 6130, 4863, 653, 5524, 1728, 3219, 6253, 6400, 1528, 1564, 491, 3662, 4906, 4617, 4493, 461, 2590, 4712],
    [5850, 6141, 4538, 7511, 5578, 7161, 1104, 7215, 7205, 5267, 4447, 6498, 4577, 7293, 86, 6095, 1529, 5557, 3766, 3347, 665, 4222, 5842, 4143, 7325, 4226, 1777, 4221, 7480, 3604, 6354, 2732],
    [4796, 4354, 7320, 2710, 475, 5289, 2414, 4861, 6543, 2590, 4888, 2631, 3407, 525, 2813, 5251, 5075, 1091, 6215, 3790, 7460, 2383, 2573, 4160, 4103, 1343, 1629, 1270, 3811, 713, 984, 1841],
    [6646, 2070, 2342, 5521, 3643, 6009, 1011, 7368, 5566, 7254, 350, 6019, 3049, 3314, 1859, 5328, 2874, 3311, 1212, 876, 116, 4500, 740, 7041, 730, 6825, 4718, 3601, 3963, 3319, 4008, 6145],
    [3018, 4968, 4315, 1012, 362, 6804, 2302, 3786, 3584, 575, 1821, 5077, 7381, 3754, 365, 5484, 431, 2245, 1532, 3570, 3351, 3512, 740, 4665, 5768, 4873, 6396, 5581, 2182, 3108, 2614, 3222],
    [3549, 3471, 1783, 617, 7520, 4974, 2919, 6957, 2629, 1288, 3226, 872, 358, 1015, 2281, 1234, 3028, 569, 2170, 2194, 6566, 440, 3926, 2351, 1543, 6915, 4229, 1409, 5995, 9, 977, 4761],
    [6341, 7424, 5006, 1053, 7431, 4739, 1240, 4123, 2423, 1268, 3842, 7152, 3814, 1551, 1048, 2322, 2514, 1205, 5300, 3401, 42, 7241, 4962, 5384, 3760, 1181, 3747, 373, 1003, 6883, 5071, 3489],
    [2237, 2835, 6531, 5228, 2741, 5202, 1126, 1450, 3066, 4793, 4726, 2764, 1627, 5710, 3247, 1434, 2927, 3908, 1591, 6455, 1375, 4059, 4741, 5792, 6436, 7102, 5590, 4336, 2992, 5088, 2327, 4069],
    [1595, 958, 4879, 2838, 562, 6287, 3121, 1191, 1467, 1550, 170, 5160, 2939, 124, 3953, 3218, 2555, 2305, 1160, 3575, 5993, 6579, 5296, 772, 4010, 3496, 2074, 5506, 5982, 1214, 1155, 158],
    [2748, 1820, 6654, 6094, 7285, 557, 159, 2699, 2134, 6226, 1340, 5183, 3919, 443, 3418, 4866, 6665, 3000, 1842, 5190, 2582, 5478, 769, 652, 4849, 2161, 1961, 6231, 7348, 2965, 2983, 6079],
    [6400, 3366, 4311, 6523, 47, 6957, 1913, 4260, 6206, 6361, 4902, 148, 2450, 6449, 4090, 5165, 4950, 2105, 1896, 7013, 3699, 1312, 4767, 5252, 1221, 7431, 7277, 143, 2634, 3604, 843, 5575],
    [1962, 6838, 1974, 1077, 5563, 1221, 4481, 6290, 5710, 3541, 3586, 3992, 3477, 7385, 5320, 204, 1015, 2615, 2224, 1490, 4633, 6932, 1752, 4181, 1581, 5882, 82, 589, 4629, 6594, 5427, 4612],
    [5328, 1151, 4123, 6380, 1261, 5609, 4498, 5515, 6649, 4369, 5354, 6611, 4296, 336, 5952, 3036, 530, 3524, 6730, 6447, 6596, 417, 6014, 3382, 3284, 755, 616, 4398, 7505, 2405, 7414, 2745],
    [1672, 7360, 6569, 2744, 3725, 3605, 4426, 6303, 7482, 256, 4197, 6643, 6139, 1445, 4812, 6677, 6168, 4577, 5000, 2294, 1836, 7266, 464, 2690, 4152, 7154, 7173, 1034, 2730, 3874, 6632, 2160],
    [5175, 5481, 1129, 7020, 7473, 5795, 7192, 4295, 3047, 1956, 2834, 2108, 1183, 885, 566, 6564, 7659, 1861, 6146, 3198, 6260, 3546, 1, 1314, 1886, 332, 3206, 6971, 4422, 4301, 7589, 5319],
    [7623, 253, 5651, 593, 4597, 5124, 1303, 46, 5352, 3588, 6, 3408, 7512, 1368, 4471, 3551, 2681, 5326, 4669, 5646, 5864, 3901, 7655, 3628, 5046, 5968, 7126, 5376, 272, 101, 6278, 4503],
    [5562, 3250, 2938, 3614, 3129, 6689, 798, 1660, 3415, 3532, 4218, 7353, 4753, 3711, 2735, 2741, 4766, 3875, 222, 2082, 4695, 1926, 3033, 4842, 1524, 2006, 6075, 5865, 4265, 5032, 688, 1771],
    [883, 5846, 3127, 800, 5437, 6119, 2394, 2404, 2014, 902, 2272, 6269, 1057, 2733, 1160, 6893, 4576, 3611, 4246, 6019, 5520, 5131, 1979, 1898, 5840, 2, 2285, 3700, 6215, 4359, 6171, 3837],
    [4578, 2478, 224, 1644, 2949, 2592, 6564, 2000, 6368, 409, 5485, 2307, 7636, 3522, 1219, 4853, 3961, 561, 3713, 7039, 5723, 5039, 5675, 6723, 7474, 1984, 2135, 4681, 7545, 6133, 2632, 5408],
    [4392, 3094, 7507, 1619, 793, 2605, 6457, 5177, 5105, 5267, 1705, 5625, 5199, 3728, 3075, 6103, 6307, 6515, 3201, 7451, 4086, 5157, 43, 5344, 6677, 2016, 629, 4172, 5818, 3475, 7487, 4806],
    [992, 7627, 185, 4068, 3326, 6855, 6161, 7522, 3642, 7368, 74, 5711, 7310, 7653, 5329, 3742, 2869, 1390, 5107, 818, 2401, 7009, 4737, 4049, 6969, 5908, 5802, 6738, 5102, 7595, 2383, 2133],
    [1067, 6690, 1456, 6371, 7527, 3662, 4415, 2104, 3432, 6031, 5278, 2971, 2933, 2585, 4331, 5010, 996, 1017, 1103, 3450, 1797, 4112, 1528, 2230, 596, 6886, 679, 1380, 3526, 6045, 5488, 7179],
    [7009, 2613, 4346, 2289, 5072, 5657, 7320, 4487, 444, 7242, 4055, 57, 432, 2760, 7672, 1921, 4349, 3534, 4080, 3567, 4841, 1067, 6569, 1440, 1264, 4010, 5984, 3346, 2342, 39, 2109, 7506],
    [5533, 4872, 3287, 4380, 2071, 2173, 1406, 839, 3634, 3390, 2209, 6402, 952, 4296, 5170, 1957, 4307, 6704, 2800, 3391, 7151, 5739, 4234, 2707, 3794, 5603, 1882, 7516, 6504, 5862, 1126, 5968],
    [2925, 5941, 6586, 3756, 6391, 6201, 4422, 1725, 6903, 439, 2032, 2313, 2275, 2986, 6987, 3848, 3897, 2713, 1641, 4614, 1765, 2037, 2637, 1882, 1058, 1899, 3361, 6644, 4686, 4470, 3935, 5000],
    [1147, 2935, 7332, 2828, 1497, 1736, 298, 4701, 6633, 205, 930, 3924, 2632, 1298, 6514, 2618, 985, 7550, 424, 5206, 5060, 6213, 6052, 6762, 5476, 1406, 4949, 6306, 278, 4653, 5114, 6769],
    [3508, 7679, 752, 3515, 990, 3901, 5599, 5574, 935, 3022, 243, 664, 5375, 1731, 1777, 2154, 5689, 1044, 2496, 2537, 4885, 5529, 3097, 5678, 5730, 4278, 2861, 2024, 6537, 442, 5288, 7569],
    [65, 6344, 3835, 2824, 6670, 3749, 251, 6700, 2698, 791, 1349, 2626, 2811, 5838, 5782, 3124, 2654, 4886, 5295, 3801, 3052, 7309, 2306, 6663, 4781, 2322, 6610, 1594, 4054, 59, 2966, 7308],
    [5870, 1308, 3584, 5643, 6446, 7672, 993, 3211, 7173, 4540, 1013, 5777, 6581, 335, 4665, 6676, 5931, 3533, 38, 50, 1063, 3178, 5069, 2063, 5884, 4966, 7098, 4475, 7333, 4792, 4858, 5187],
    [2161, 1504, 1833, 2888, 4673, 3958, 398, 5548, 3381, 5931, 1690, 5467, 1219, 1485, 5376, 4628, 4497, 6165, 2523, 4992, 1162, 5830, 816, 7023, 5161, 2746, 2549, 1174, 4832, 2578, 6425, 5427],
    [2030, 5338, 2844, 5805, 3915, 2953, 2640, 6522, 3160, 7078, 3115, 5967, 153, 1960, 282, 5488, 5540, 6538, 5821, 981, 3709, 1928, 4684, 6513, 4440, 1895, 713, 2101, 3895, 6250, 2298, 7138],
    [2064, 3963, 1445, 2044, 7429, 614, 5571, 2579, 5168, 2715, 2637, 2679, 5349, 3172, 268, 770, 1804, 7140, 6265, 6715, 3242, 300, 3901, 4853, 6596, 6430, 5659, 6033, 1088, 1589, 6713, 2588],
    [3216, 7, 6066, 6353, 6521, 3360, 4965, 6928, 3052, 1996, 5536, 3222, 4373, 6723, 569, 2668, 532, 5104, 2945, 254, 1686, 2236, 5418, 1697, 2614, 7245, 5825, 6169, 6571, 5165, 858, 1982],
    [4198, 7431, 4888, 5990, 1734, 4970, 5194, 1272, 234, 4881, 5377, 5707, 3735, 4902, 7528, 4515, 3362, 6320, 2358, 129, 2794, 2085, 5991, 3679, 3755, 151, 1803, 2714, 1421, 1232, 5866, 2933],
    [5853, 5213, 4107, 7082, 6336, 1633, 190, 4882, 5700, 3259, 7398, 900, 5169, 6157, 2815, 3667, 5785, 1364, 5763, 839, 4242, 6171, 6811, 2475, 7103, 6144, 4162, 2676, 4505, 3692, 5047, 3371],
    [4543, 3660, 2208, 5901, 6076, 4539, 4807, 1223, 6407, 3520, 5829, 2258, 493, 2561, 1136, 2199, 6826, 7052, 5223, 5480, 6451, 1765, 5627, 5877, 3902, 4385, 2571, 3716, 3632, 7250, 3382, 6900]
  ],
"b": [415, 7439, 2926, 3777, 3990, 6956, 4725, 277, 6379, 559, 5332, 6740, 6731, 4334, 473, 903, 1782, 7451, 6060, 1788, 7590, 2804, 7446, 2244, 1973, 2562, 3406, 4449, 7173, 1807, 7263, 2188, 1669, 2814, 548, 2836, 4453, 2284, 3262, 5799, 5006, 4657, 2932, 2145, 4273, 4567, 2316, 3040, 6835, 2231, 596, 510, 3249, 1629, 4485, 5018, 3228, 2387, 6223, 1831, 6057, 4349, 270, 4080],
"masked_update": "7c53ddbd82faf811c0397982909a9498644d63fb50a6e697d7cf97f6"
}

2. 解题思路

  1. 观察参数结构(q = 7681、n = 32、m = 64、矩阵 A 和向量 b),判定其为标准 LWE 样本集 b ≡ A * s + e (mod q)。
  2. 由于秘密维度 n = 32 很小且模数 q = 7681 较大,构造 m + n + 1 = 97 维的 Kannan 嵌入格基,将求解小误差 e 与小秘密 s 的问题转化为格上的最短向量问题(SVP)。
  3. 对 97 维格基运行 LLL 约减,直接从约减基首行提取出二元秘密向量 s ∈ {0, 1}^32 和三元误差向量 e ∈ {-1, 0, 1}^64,并代回原方程校验全部 64 个样本同余关系成立。
  4. 将 32 维向量 s 按单字节序列 bytes(s) 计算 SHA-256 摘要,取前 28 字节与 masked_update 逐字节异或,解出明文 Flag。

3. 解题过程

3.1 识别 LWE 样本参数与 Kannan 嵌入建模

关键观察:输入数据给出了模数 q = 7681(NTT 友好素数,7681 = 30 * 256 + 1)、维度 n = 32、样本数 m = 64、矩阵 A(64 行 32 列)和向量 b(64 维)。在有限域 Z_q 上直接对前 32 行高斯消元得不到有意义的短向量,说明方程含有小噪声项 e,满足 LWE 关系:

b ≡ A * s + e (mod q)

原理与操作:将同余式展开为整数线性关系,存在整数向量 k ∈ Z^m 使得:

A * s - b + q * k = -e

当 s 和 e 的分量绝对值远小于 q = 7681 时,可通过 Kannan 嵌入构造 (m + n + 1) × (m + n + 1) = 97 × 97 的行格基矩阵 B(取嵌入权重 W = 1):

B = [ q * I_m    0      0 ]
    [   A^T     I_n     0 ]
    [  -b^T      0      1 ]

对于整数行系数向量 (k, s, 1),其与格基矩阵 B 相乘生成的格向量为:

v = (k, s, 1) * B = (q * k + A * s - b, s, 1) = (-e, s, 1)

该格包含的格点体积为 det(B) = q^m = 7681^64。根据高斯启发式,97 维格中最短非零向量的期望欧氏范数约为:

GH(B) ≈ sqrt(97 / (2 * pi * e)) * 7681^(64 / 97) ≈ 2.382 * 365.45 ≈ 870.5

而目标短向量 v = (-e, s, 1) 在小秘密和小误差条件下的欧氏范数上限低于 sqrt(64 + 32 + 1) ≈ 9.85,远小于高斯启发式下界 870.5,因此 LLL 格基约减可直接分离出唯一目标短向量。

本步结果:完成 97×97 维整数格基矩阵 B 的建模,目标向量形式确定为 v = (-e_0, ..., -e_63, s_0, ..., s_31, ±1)。

3.2 执行 LLL 格基约减恢复秘密向量与误差向量

关键观察:对 97×97 格基矩阵 B 执行标准 LLL 约减后,检查末位坐标绝对值为 1 的行向量。

原理与操作:使用 fpylll 构造 IntegerMatrix(97, 97) 并调用 LLL.reduction(B)。约减完成后,第 0 行(最短基向量)的末位分量 B[0, 96] 恰好为 ±1。设 sign = B[0, 96],则可按如下关系提取 e 与 s:

e[i] = -sign * B[0, i]       (0 <= i < 64)
s[j] =  sign * B[0, 64 + j]  (0 <= j < 32)

将提取出的 s 和 e 代回全部 64 个样本方程检验 (sum(A[i][j] * s[j]) + e[i]) % 7681 == b[i]。

本步结果:LLL 约减直接给出真实中间值(欧氏范数为 sqrt(39 + 14 + 1) = sqrt(54) ≈ 7.35),且 64 个方程校验全部通过:

s = [1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0]
e = [-1, 1, 0, -1, 0, 1, 1, 0, -1, -1, -1, 1, 0, 1, 1, 1, -1, 1, -1, 0, 1, 1, -1, 0, -1, 1, 1, 1, 0, 1, -1, -1, 0, -1, 1, 0, 0, -1, 0, 1, 0, 1, 1, -1, 0, 1, 0, 0, 0, 0, -1, 0, 0, -1, -1, -1, 1, 1, -1, 0, 0, 1, 1, 0]

其中秘密向量 s 为严格二元向量(元素属于 {0, 1},汉明重量为 14),误差向量 e 为严格三元小误差向量(元素属于 {-1, 0, 1})。

3.3 派生 SHA-256 掩码密钥流还原明文 Flag

关键观察:masked_update 的长度为 28 字节(7c53ddbd82faf811c0397982909a9498644d63fb50a6e697d7cf97f6),小于等于 SHA-256 的 32 字节输出长度;同时恢复出的 s 为 32 个取值在 {0, 1} 的整数组成的列表,恰可直接转换为 32 字节序列 bytes(s)。

原理与操作:将 32 维二元向量 s 转换为 32 字节流 bytes(s)(即每个分量作为单字节 \x00 或 \x01),计算其 SHA-256 二进制摘要作为掩码密钥流 keystream,再与 28 字节的 masked_update 逐字节异或:

keystream = SHA256(bytes(s))
plaintext = masked_update XOR keystream[0:28]

本步结果:计算得到的关键字节流与异或结果如下:

数据项
十六进制值 / 明文结果
bytes(s).hex()0100000000000001000000000001010000000101010001000001010100010000
SHA256(bytes(s))[:28].hex()1a3fbcdaf9979962ab660ae7f5fecbfe16220ea434d489e7b8bae38b
masked_update7c53ddbd82faf811c0397982909a9498644d63fb50a6e697d7cf97f6
异或还原明文
flag{mask_seed_from_dropout}

解密出的明文同时解释了题目背景:联邦学习安全聚合场景下,客户端使用二元 dropout mask s 作为种子派生 SHA-256 掩码隐藏模型梯度更新 masked_update。

4. 完整解题脚本

将以下脚本保存为 solve.py 即可独立运行(已内嵌全部题目数据,无需外部文件依赖):

import hashlib
from fpylll import IntegerMatrix, LLL

q = 7681
n = 32
m = 64
A = [
    [2395, 6050, 1036, 3320, 2631, 4663, 4329, 1258, 3363, 3828, 5797, 7380, 609, 2407, 6810, 4109, 330, 7238, 3281, 4261, 2091, 5694, 641, 1845, 2593, 1584, 6754, 6468, 497, 871, 2968, 3533],
    [4337, 334, 5766, 3916, 1515, 2814, 7114, 7349, 3376, 1380, 5932, 3966, 3956, 6806, 1563, 4951, 664, 3148, 711, 4742, 4178, 743, 224, 2630, 7026, 1347, 1890, 1157, 2169, 3858, 3716, 2097],
    [194, 6116, 1249, 1821, 6681, 3478, 255, 3784, 4745, 103, 1888, 6175, 5978, 6399, 941, 6547, 479, 1363, 132, 1117, 1629, 6178, 6710, 6289, 3335, 7379, 1474, 4070, 67, 7502, 1629, 5051],
    [4505, 4853, 2543, 1416, 6020, 5442, 1857, 4968, 5843, 6707, 6856, 4301, 3494, 2068, 3008, 7250, 3675, 1215, 4595, 2815, 10, 796, 1634, 6486, 129, 4737, 1709, 6542, 3578, 5592, 4456, 6315],
    [3688, 6657, 1319, 2658, 4686, 6127, 3353, 7406, 3761, 7365, 3689, 5508, 4742, 7028, 2596, 3272, 301, 2815, 7042, 1061, 5553, 808, 7411, 3544, 1394, 6951, 1903, 6957, 3834, 161, 6094, 953],
    [4113, 3282, 683, 6191, 3281, 1594, 806, 758, 2023, 4929, 38, 1392, 7615, 552, 4764, 1058, 6909, 4947, 3753, 2849, 5085, 4036, 6772, 279, 4100, 4623, 7392, 2961, 5256, 1738, 4827, 5654],
    [117, 3044, 3367, 4579, 134, 2277, 5999, 4197, 3630, 5733, 457, 2763, 4403, 1340, 6157, 2752, 4702, 1034, 6888, 6086, 7228, 4562, 6738, 4323, 1014, 1939, 5530, 6979, 170, 5292, 2988, 1280],
    [2329, 3504, 1759, 6698, 1919, 655, 4194, 4955, 6681, 4967, 6332, 2511, 5138, 2769, 2851, 1008, 6099, 6839, 2066, 2188, 920, 5993, 1404, 564, 6312, 1013, 1004, 3685, 5503, 5817, 4537, 1164],
    [2870, 4406, 5602, 3390, 4750, 4097, 5968, 4131, 2102, 6086, 4133, 2183, 6619, 608, 4943, 6695, 3892, 2678, 7668, 6665, 3680, 3299, 7197, 4002, 6475, 4743, 6350, 1092, 7479, 2519, 4708, 5251],
    [3399, 2582, 3816, 3767, 1440, 7029, 1425, 7080, 5874, 3923, 4679, 4600, 3595, 3304, 6503, 2320, 4732, 2629, 1210, 4224, 4501, 1397, 3854, 1910, 988, 7340, 6810, 793, 3026, 5309, 1956, 3230],
    [5720, 872, 5274, 508, 4891, 6078, 2704, 2698, 1227, 2974, 6919, 703, 5402, 4315, 7314, 6863, 6191, 1044, 53, 5266, 2209, 4968, 3882, 5480, 5453, 7486, 5759, 1736, 5888, 4981, 667, 2343],
    [4472, 7050, 5017, 4990, 3584, 5857, 4912, 6189, 4165, 4481, 710, 6889, 1332, 2894, 1095, 999, 4980, 6337, 4921, 3686, 7193, 6208, 6198, 5300, 5512, 6674, 3150, 734, 5509, 5619, 4987, 5753],
    [4459, 276, 5645, 1988, 3071, 4102, 5347, 1102, 1789, 73, 4603, 4275, 4941, 6, 1334, 3576, 4415, 5481, 7633, 1397, 3601, 4097, 4524, 433, 2467, 7248, 7549, 1423, 1784, 4860, 830, 412],
    [1710, 4751, 6398, 15, 153, 5457, 6836, 5363, 2185, 4161, 6870, 5279, 6698, 7449, 1299, 791, 488, 6114, 6379, 7489, 7395, 4297, 1322, 7479, 6362, 5962, 6081, 2781, 1821, 4870, 1906, 258],
    [7575, 552, 5768, 1636, 3737, 2073, 5198, 7315, 1867, 5876, 676, 629, 1610, 5843, 5335, 5374, 3258, 4399, 3918, 926, 3382, 80, 5826, 4543, 2467, 4259, 4644, 360, 2060, 4856, 7219, 2967],
    [425, 1397, 5950, 6621, 7524, 4911, 678, 5858, 2944, 886, 6381, 552, 2688, 5485, 553, 2998, 6547, 932, 3160, 510, 2925, 7389, 562, 503, 3681, 6763, 6893, 176, 7150, 5997, 7473, 7591],
    [499, 1829, 1298, 1131, 7267, 3640, 4899, 7626, 2301, 4876, 2694, 2755, 6132, 4471, 2052, 6271, 5255, 4863, 7672, 1170, 7073, 98, 6937, 5880, 4068, 4462, 1980, 6797, 1556, 4811, 2189, 1665],
    [4443, 4812, 6271, 572, 1927, 3795, 2227, 415, 4627, 5801, 6479, 1143, 267, 6951, 2307, 6256, 2128, 4352, 2179, 4398, 2407, 7654, 4604, 1108, 1947, 3392, 6818, 5111, 2241, 2830, 1794, 3699],
    [4465, 1294, 3263, 1718, 172, 7246, 5923, 4438, 1730, 939, 3153, 2768, 1880, 3879, 4972, 7623, 7182, 65, 348, 5206, 2737, 2897, 3983, 3008, 3293, 6836, 251, 6291, 4171, 1060, 5784, 5241],
    [657, 3405, 5622, 2550, 5780, 3534, 3477, 6628, 3549, 2590, 7424, 5883, 6759, 3042, 4408, 6124, 1398, 3940, 6119, 2139, 6398, 107, 5094, 1171, 1103, 319, 6771, 1073, 802, 5226, 1132, 2243],
    [4226, 4577, 1482, 6461, 6026, 6648, 7325, 2431, 1211, 3295, 28, 359, 3205, 5682, 5477, 5383, 6569, 4986, 5639, 5302, 5858, 2163, 4497, 6008, 763, 2117, 1602, 4895, 4549, 5949, 4845, 896],
    [3899, 3004, 5714, 6214, 6511, 7349, 3094, 4092, 4552, 5648, 5760, 925, 1781, 3119, 7427, 5066, 4919, 6562, 4034, 1847, 2171, 6871, 6733, 409, 5477, 3193, 3597, 5730, 6112, 3047, 7574, 3057],
    [3086, 3901, 471, 6298, 1473, 7531, 2321, 2024, 7471, 3373, 4274, 4390, 1319, 6215, 3086, 7392, 3688, 3193, 5351, 1185, 4924, 2042, 27, 4584, 2375, 2341, 612, 538, 4109, 1101, 2937, 6647],
    [3491, 5193, 3273, 5402, 1182, 2439, 2182, 6704, 7019, 105, 490, 3815, 2170, 961, 6254, 7111, 5934, 5962, 4582, 4585, 7050, 5784, 863, 3329, 5767, 4352, 6947, 2437, 6046, 104, 4311, 5251],
    [7626, 1677, 3781, 4295, 6484, 1149, 853, 3386, 6213, 1984, 2786, 2096, 6487, 3354, 1366, 606, 7334, 3779, 1206, 2235, 2439, 212, 6141, 6899, 6091, 4920, 2387, 3335, 7296, 1984, 2600, 844],
    [2480, 5903, 3271, 2241, 5863, 195, 5840, 7467, 1266, 1456, 1360, 3126, 1939, 7409, 5391, 1945, 1557, 7177, 3788, 902, 4696, 787, 259, 1528, 3007, 1383, 1656, 5299, 1538, 236, 6176, 1811],
    [6242, 4758, 3003, 4663, 2447, 3699, 1729, 1366, 189, 173, 2127, 1711, 567, 3509, 4813, 7016, 7328, 7345, 550, 2177, 5503, 6588, 2277, 3926, 6841, 7299, 2415, 5269, 2599, 390, 1258, 7076],
    [1782, 3512, 4569, 4760, 6442, 2355, 2908, 3742, 6776, 2508, 913, 6242, 2798, 3996, 400, 7433, 6127, 1969, 864, 5144, 2702, 4957, 5070, 115, 7510, 1982, 6818, 865, 522, 1807, 3302, 3414],
    [4488, 2865, 2492, 7066, 6084, 4061, 3651, 4393, 5419, 2893, 974, 360, 47, 4193, 6130, 4863, 653, 5524, 1728, 3219, 6253, 6400, 1528, 1564, 491, 3662, 4906, 4617, 4493, 461, 2590, 4712],
    [5850, 6141, 4538, 7511, 5578, 7161, 1104, 7215, 7205, 5267, 4447, 6498, 4577, 7293, 86, 6095, 1529, 5557, 3766, 3347, 665, 4222, 5842, 4143, 7325, 4226, 1777, 4221, 7480, 3604, 6354, 2732],
    [4796, 4354, 7320, 2710, 475, 5289, 2414, 4861, 6543, 2590, 4888, 2631, 3407, 525, 2813, 5251, 5075, 1091, 6215, 3790, 7460, 2383, 2573, 4160, 4103, 1343, 1629, 1270, 3811, 713, 984, 1841],
    [6646, 2070, 2342, 5521, 3643, 6009, 1011, 7368, 5566, 7254, 350, 6019, 3049, 3314, 1859, 5328, 2874, 3311, 1212, 876, 116, 4500, 740, 7041, 730, 6825, 4718, 3601, 3963, 3319, 4008, 6145],
    [3018, 4968, 4315, 1012, 362, 6804, 2302, 3786, 3584, 575, 1821, 5077, 7381, 3754, 365, 5484, 431, 2245, 1532, 3570, 3351, 3512, 740, 4665, 5768, 4873, 6396, 5581, 2182, 3108, 2614, 3222],
    [3549, 3471, 1783, 617, 7520, 4974, 2919, 6957, 2629, 1288, 3226, 872, 358, 1015, 2281, 1234, 3028, 569, 2170, 2194, 6566, 440, 3926, 2351, 1543, 6915, 4229, 1409, 5995, 9, 977, 4761],
    [6341, 7424, 5006, 1053, 7431, 4739, 1240, 4123, 2423, 1268, 3842, 7152, 3814, 1551, 1048, 2322, 2514, 1205, 5300, 3401, 42, 7241, 4962, 5384, 3760, 1181, 3747, 373, 1003, 6883, 5071, 3489],
    [2237, 2835, 6531, 5228, 2741, 5202, 1126, 1450, 3066, 4793, 4726, 2764, 1627, 5710, 3247, 1434, 2927, 3908, 1591, 6455, 1375, 4059, 4741, 5792, 6436, 7102, 5590, 4336, 2992, 5088, 2327, 4069],
    [1595, 958, 4879, 2838, 562, 6287, 3121, 1191, 1467, 1550, 170, 5160, 2939, 124, 3953, 3218, 2555, 2305, 1160, 3575, 5993, 6579, 5296, 772, 4010, 3496, 2074, 5506, 5982, 1214, 1155, 158],
    [2748, 1820, 6654, 6094, 7285, 557, 159, 2699, 2134, 6226, 1340, 5183, 3919, 443, 3418, 4866, 6665, 3000, 1842, 5190, 2582, 5478, 769, 652, 4849, 2161, 1961, 6231, 7348, 2965, 2983, 6079],
    [6400, 3366, 4311, 6523, 47, 6957, 1913, 4260, 6206, 6361, 4902, 148, 2450, 6449, 4090, 5165, 4950, 2105, 1896, 7013, 3699, 1312, 4767, 5252, 1221, 7431, 7277, 143, 2634, 3604, 843, 5575],
    [1962, 6838, 1974, 1077, 5563, 1221, 4481, 6290, 5710, 3541, 3586, 3992, 3477, 7385, 5320, 204, 1015, 2615, 2224, 1490, 4633, 6932, 1752, 4181, 1581, 5882, 82, 589, 4629, 6594, 5427, 4612],
    [5328, 1151, 4123, 6380, 1261, 5609, 4498, 5515, 6649, 4369, 5354, 6611, 4296, 336, 5952, 3036, 530, 3524, 6730, 6447, 6596, 417, 6014, 3382, 3284, 755, 616, 4398, 7505, 2405, 7414, 2745],
    [1672, 7360, 6569, 2744, 3725, 3605, 4426, 6303, 7482, 256, 4197, 6643, 6139, 1445, 4812, 6677, 6168, 4577, 5000, 2294, 1836, 7266, 464, 2690, 4152, 7154, 7173, 1034, 2730, 3874, 6632, 2160],
    [5175, 5481, 1129, 7020, 7473, 5795, 7192, 4295, 3047, 1956, 2834, 2108, 1183, 885, 566, 6564, 7659, 1861, 6146, 3198, 6260, 3546, 1, 1314, 1886, 332, 3206, 6971, 4422, 4301, 7589, 5319],
    [7623, 253, 5651, 593, 4597, 5124, 1303, 46, 5352, 3588, 6, 3408, 7512, 1368, 4471, 3551, 2681, 5326, 4669, 5646, 5864, 3901, 7655, 3628, 5046, 5968, 7126, 5376, 272, 101, 6278, 4503],
    [5562, 3250, 2938, 3614, 3129, 6689, 798, 1660, 3415, 3532, 4218, 7353, 4753, 3711, 2735, 2741, 4766, 3875, 222, 2082, 4695, 1926, 3033, 4842, 1524, 2006, 6075, 5865, 4265, 5032, 688, 1771],
    [883, 5846, 3127, 800, 5437, 6119, 2394, 2404, 2014, 902, 2272, 6269, 1057, 2733, 1160, 6893, 4576, 3611, 4246, 6019, 5520, 5131, 1979, 1898, 5840, 2, 2285, 3700, 6215, 4359, 6171, 3837],
    [4578, 2478, 224, 1644, 2949, 2592, 6564, 2000, 6368, 409, 5485, 2307, 7636, 3522, 1219, 4853, 3961, 561, 3713, 7039, 5723, 5039, 5675, 6723, 7474, 1984, 2135, 4681, 7545, 6133, 2632, 5408],
    [4392, 3094, 7507, 1619, 793, 2605, 6457, 5177, 5105, 5267, 1705, 5625, 5199, 3728, 3075, 6103, 6307, 6515, 3201, 7451, 4086, 5157, 43, 5344, 6677, 2016, 629, 4172, 5818, 3475, 7487, 4806],
    [992, 7627, 185, 4068, 3326, 6855, 6161, 7522, 3642, 7368, 74, 5711, 7310, 7653, 5329, 3742, 2869, 1390, 5107, 818, 2401, 7009, 4737, 4049, 6969, 5908, 5802, 6738, 5102, 7595, 2383, 2133],
    [1067, 6690, 1456, 6371, 7527, 3662, 4415, 2104, 3432, 6031, 5278, 2971, 2933, 2585, 4331, 5010, 996, 1017, 1103, 3450, 1797, 4112, 1528, 2230, 596, 6886, 679, 1380, 3526, 6045, 5488, 7179],
    [7009, 2613, 4346, 2289, 5072, 5657, 7320, 4487, 444, 7242, 4055, 57, 432, 2760, 7672, 1921, 4349, 3534, 4080, 3567, 4841, 1067, 6569, 1440, 1264, 4010, 5984, 3346, 2342, 39, 2109, 7506],
    [5533, 4872, 3287, 4380, 2071, 2173, 1406, 839, 3634, 3390, 2209, 6402, 952, 4296, 5170, 1957, 4307, 6704, 2800, 3391, 7151, 5739, 4234, 2707, 3794, 5603, 1882, 7516, 6504, 5862, 1126, 5968],
    [2925, 5941, 6586, 3756, 6391, 6201, 4422, 1725, 6903, 439, 2032, 2313, 2275, 2986, 6987, 3848, 3897, 2713, 1641, 4614, 1765, 2037, 2637, 1882, 1058, 1899, 3361, 6644, 4686, 4470, 3935, 5000],
    [1147, 2935, 7332, 2828, 1497, 1736, 298, 4701, 6633, 205, 930, 3924, 2632, 1298, 6514, 2618, 985, 7550, 424, 5206, 5060, 6213, 6052, 6762, 5476, 1406, 4949, 6306, 278, 4653, 5114, 6769],
    [3508, 7679, 752, 3515, 990, 3901, 5599, 5574, 935, 3022, 243, 664, 5375, 1731, 1777, 2154, 5689, 1044, 2496, 2537, 4885, 5529, 3097, 5678, 5730, 4278, 2861, 2024, 6537, 442, 5288, 7569],
    [65, 6344, 3835, 2824, 6670, 3749, 251, 6700, 2698, 791, 1349, 2626, 2811, 5838, 5782, 3124, 2654, 4886, 5295, 3801, 3052, 7309, 2306, 6663, 4781, 2322, 6610, 1594, 4054, 59, 2966, 7308],
    [5870, 1308, 3584, 5643, 6446, 7672, 993, 3211, 7173, 4540, 1013, 5777, 6581, 335, 4665, 6676, 5931, 3533, 38, 50, 1063, 3178, 5069, 2063, 5884, 4966, 7098, 4475, 7333, 4792, 4858, 5187],
    [2161, 1504, 1833, 2888, 4673, 3958, 398, 5548, 3381, 5931, 1690, 5467, 1219, 1485, 5376, 4628, 4497, 6165, 2523, 4992, 1162, 5830, 816, 7023, 5161, 2746, 2549, 1174, 4832, 2578, 6425, 5427],
    [2030, 5338, 2844, 5805, 3915, 2953, 2640, 6522, 3160, 7078, 3115, 5967, 153, 1960, 282, 5488, 5540, 6538, 5821, 981, 3709, 1928, 4684, 6513, 4440, 1895, 713, 2101, 3895, 6250, 2298, 7138],
    [2064, 3963, 1445, 2044, 7429, 614, 5571, 2579, 5168, 2715, 2637, 2679, 5349, 3172, 268, 770, 1804, 7140, 6265, 6715, 3242, 300, 3901, 4853, 6596, 6430, 5659, 6033, 1088, 1589, 6713, 2588],
    [3216, 7, 6066, 6353, 6521, 3360, 4965, 6928, 3052, 1996, 5536, 3222, 4373, 6723, 569, 2668, 532, 5104, 2945, 254, 1686, 2236, 5418, 1697, 2614, 7245, 5825, 6169, 6571, 5165, 858, 1982],
    [4198, 7431, 4888, 5990, 1734, 4970, 5194, 1272, 234, 4881, 5377, 5707, 3735, 4902, 7528, 4515, 3362, 6320, 2358, 129, 2794, 2085, 5991, 3679, 3755, 151, 1803, 2714, 1421, 1232, 5866, 2933],
    [5853, 5213, 4107, 7082, 6336, 1633, 190, 4882, 5700, 3259, 7398, 900, 5169, 6157, 2815, 3667, 5785, 1364, 5763, 839, 4242, 6171, 6811, 2475, 7103, 6144, 4162, 2676, 4505, 3692, 5047, 3371],
    [4543, 3660, 2208, 5901, 6076, 4539, 4807, 1223, 6407, 3520, 5829, 2258, 493, 2561, 1136, 2199, 6826, 7052, 5223, 5480, 6451, 1765, 5627, 5877, 3902, 4385, 2571, 3716, 3632, 7250, 3382, 6900]
]
b = [415, 7439, 2926, 3777, 3990, 6956, 4725, 277, 6379, 559, 5332, 6740, 6731, 4334, 473, 903, 1782, 7451, 6060, 1788, 7590, 2804, 7446, 2244, 1973, 2562, 3406, 4449, 7173, 1807, 7263, 2188, 1669, 2814, 548, 2836, 4453, 2284, 3262, 5799, 5006, 4657, 2932, 2145, 4273, 4567, 2316, 3040, 6835, 2231, 596, 510, 3249, 1629, 4485, 5018, 3228, 2387, 6223, 1831, 6057, 4349, 270, 4080]
masked_update = "7c53ddbd82faf811c0397982909a9498644d63fb50a6e697d7cf97f6"

def solve():
    # 构造 (m + n + 1) = 97 维 Kannan 嵌入格基矩阵 B
    dim = m + n + 1
    B = IntegerMatrix(dim, dim)
    for i in range(m):
        B[i, i] = q
    for j in range(n):
        for i in range(m):
            B[m + j, i] = A[i][j]
        B[m + j, m + j] = 1
    for i in range(m):
        B[m + n, i] = -b[i] % q
    B[m + n, m + n] = 1

    # 执行 LLL 格基约减
    LLL.reduction(B)

    # 提取满足末位嵌入分量为 ±1 的短向量
    for row_idx in range(dim):
        row = [B[row_idx, col] for col in range(dim)]
        if abs(row[-1]) == 1:
            sign = 1if row[-1] == 1else-1
            e_vec = [-sign * row[i] for i in range(m)]
            s_vec = [sign * row[m + j] for j in range(n)]
            # 验证 b == A*s + e (mod q)
            if all((sum(A[i][j] * s_vec[j] for j in range(n)) + e_vec[i]) % q == b[i] % q for i in range(m)):
                print(f"[+] Short vector found at row {row_idx}")
                print(f"[+] s = {s_vec}")
                print(f"[+] e = {e_vec}")
                keystream = hashlib.sha256(bytes(s_vec)).digest()
                ct = bytes.fromhex(masked_update)
                print(f"[+] keystream[:28] = {keystream[:len(ct)].hex()}")
                flag = bytes(x ^ y for x, y in zip(ct, keystream))
                print(f"[+] Flag: {flag.decode()}")
                return

if __name__ == "__main__":
    solve()

Restore the Plaintext

题目描述:
Please restore the plaintext in the current session and obtain the flag.

test:ncat --ssl addr port
附件:attachments.zip

核心思路:服务端是 PKCS#1 v1.5 填充预言机,并且把第一个合法乘数一并下发;用 Bleichenbacher 攻击按这个乘数收窄明文区间,直到会话明文唯一。

1. 题目说明

题目要求恢复当前会话的明文并取出 flag。附件是 server.py 和 pubkey.json。访问方式写的是 ncat --ssl addr port,本次实例为:

ncat --ssl qocp6ack.nestctf.cyberpeace.cn 58999

明文 TCP 连上去没有数据。必须先做 TLS,再按行交互。实验室证书不是公共 CA,客户端需要关闭证书校验。

pubkey.json 只给出公钥:

e = 65537
n = 26053711197494345044781792077684912715021496527090136170200684647867265472090211834710019839499107995669155152828437896116662002233291085930822051870305763730149854492858590504378536095737328118664985736152084079800882198391309368647378331467360788476262812209190311267210497136432749618866195431576788245257578944400642543842611237033327500755489384635255355477488050218362690346216157317101091056613531444602254303947713075065315141018050424970114665608081069231050100673543130979972703432232664846464287183328555447235439966638024174265363366180280032770503317829739744455034691040984563526167517235779809547052041

n 是 2048 bit,字节长度 k = (2048 + 7) // 8 = 256。私钥在服务端的 secret.json,附件里没有。

一条连接对应一次会话。服务端把 flag 编成 PKCS#1 v1.5 type 2 块,加密后先发一行,再进入预言机循环。server.py 里的发送和判定如下:

c, s = new_session(dec, message)
conn.sendall(f"session {c:x} {s}\n".encode())
while True:
    line = reader.readline(8192)
    ...
    c = int(text, 16)
    m = dec.decrypt(c)
    if is_pkcs1_v15(m, dec.k):
        conn.sendall(b"ok\n")
    else:
        conn.sendall(b"bad\n")

客户端每行提交一个十六进制密文,只可能收到 ok 或 bad。填充、密文和乘数由 pad_message、new_session、first_conforming_multiplier 生成:

def pad_message(message, k):
    ps_len = k - 3 - len(message)
    ps = bytearray(secrets.token_bytes(ps_len))
    for i, byte in enumerate(ps):
        if byte == 0:
            ps[i] = 1 + secrets.randbelow(255)
    return int.from_bytes(bytes((0x00, 0x02)) + bytes(ps) + b"\x00" + message, "big")

def new_session(dec, message):
    m = pad_message(message, dec.k)
    c = pow(m, dec.e, dec.n)
    s = first_conforming_multiplier(m, dec.n, dec.k)
    return c, s

def first_conforming_multiplier(m, n, k):
    B = 1 << (8 * (k - 2))
    s = (n + 3 * B - 1) // (3 * B)
    acc = (m * s) % n
    whilenot is_pkcs1_v15(acc, k):
        s += 1
        acc += m
        if acc >= n:
            acc -= n
    return s

因此首行 session <c_hex> <s> 的含义是:

  • c = m^e mod n
  • m 是本连接的填充块,flag 原文不变,PS 每次重抽
  • s 是从 ceil(n / (3B)) 往上找到的第一个整数,使 (m * s mod n) 也通过填充检查

填充检查是完整的 type 2,不只看前两个字节:

def is_pkcs1_v15(m, k):
    em = m.to_bytes(k, "big")
    if em[0] != 0x00 or em[1] != 0x02:
        return False
    i = 2
    while i < k and em[i] != 0:
        i += 1
    return 10 <= i < k

em[0:2] == 00 02,从下标 2 起至少 8 个非零字节,并且在块结束前出现分隔 00。断开连接后 PS 会变,必须在同一条连接里做完所有查询。

2. 解题思路

这是 Bleichenbacher 的 PKCS#1 v1.5 选择密文攻击。私钥不需要。预言机回答“解密结果是否像一条合法 type 2 块”。

记 B = 2^{8(k-2)} = 2^{2032}。以 00 02 开头的 k 字节整数正好落在 [2B, 3B)。对已知密文 c = m^e mod n,提交

c' = c * s^e mod n

服务端解密得到的是 m * s mod n。若回答 ok,则这个值落在 [2B, 3B),并且还满足 PS 条件。所有仍可能的 m 因此被切成若干整数区间。反复寻找新的合法乘数,区间宽度大约每次减少 1 bit,直到只剩一个整数。

服务端已经把第一步乘数 s1 算好并下发,本地不必从 ceil(n / (3B)) 开始盲搜。对这份模数,这个起点是 17612,随机乘数落入 [2B, 3B) 的概率约为 B/n ≈ 1/52835。跳过这一段,后面大约两千次成功收窄就能定出明文。

最后检查 m^e ≡ c (mod n),再按 00 02 || PS || 00 || flag 去掉填充。

3. 解题过程

3.1 把 ok 翻译成区间 [2B, 3B)

k = 256 字节的块如果以 00 02 开头,写成整数就是

m = 2 * 256^{254} + t = 2B + t,  0 <= t < B

所以 2B <= m <= 3B - 1。反过来,落在这个范围内的整数,其 256 字节大端表示一定以 00 02 开头。

is_pkcs1_v15 还要求前 8 个填充字节非零,并且后面某个位置有 00。因此:

  • 回答 ok 可以推出 m' ∈ [2B, 3B)
  • 回答 bad 不能推出 m' 不在这个范围内;大约四成区间内的整数会因为 PS 内部的零字节被拒绝

区间更新只在 ok 时进行,用 [2B, 3B) 做上界是可靠的。被 PS 规则拒绝的乘数直接跳过,换下一个候选。

对本次公钥:

B = 2^2032
s_start = ceil(n / (3B)) = 17612

第 4 节脚本连上后打印的 s_start=17612 就是这个值。它只和 n 有关,每条连接都一样。

3.2 用下发的 s1 做第一次收窄

设当前仍可能的明文属于若干区间。对乘数 s 和整数 r >= 0,条件

2B <= m*s - r*n <= 3B - 1

等价于

ceil((2B + r*n) / s) <= m <= floor((3B - 1 + r*n) / s)

旧区间 [a, b] 上需要考虑的 r 是

r_min = ceil((a*s - 3B + 1) / n)
r_max = floor((b*s - 2B) / n)

每个 r 和 [a, b] 求交,再把相邻区间合并。真实的 m 只要确实让 m*s mod n 落在 [2B, 3B),就一定留在新集合里。

首行给出的 s1 已经保证 (m * s1 mod n) 通过完整填充检查,所以它必然落在 [2B, 3B)。本地用 c * s1^e mod n 再问一次预言机,只是确认这条连接和公钥匹配。确认之后直接对初始区间 [2B, 3B - 1] 做上面的交。

本次复现连接的首行给出 s1 = 44949,密文 2047 bit。确认查询返回 ok 后,区间集合收成 1 段,宽度的比特数是 2017:

k=256 s_start=17612 s1=44949 c_bits=2047
after_s1 intervals=1 width_bits=2017 queries=1

宽度和乘数的大小相符。一次成功乘法把长度约 B 的区间缩成约 B/s:

log2(44949) ≈ 15.46
log2(B / s1) = 2032 - 15.46 = 2016.54

2^2016.54 的比特长度是 2017,与输出一致。

s1 更大时,同一次交可能留下多段。另一条连接上服务端给出 s1 = 131612,首次收窄后是 3 个区间、宽度 2015 bit;脚本里的 find_s_multi 负责这种情形。它不从 s+1 逐个去问(命中率只有约 1/52835),而是对每个区间生成下一个有可能映入 [2B, 3B) 的乘数,只问这些候选里最小的一个。那条连接最终也得到了同一个 flag,共用 8070 次查询。下面的复现输出是 s1 = 44949 这一次,第一步之后已经只剩单区间。

3.3 单区间时让乘数大约翻倍

只剩 [a, b] 时,按 Bleichenbacher 的 step 2c 选下一组候选,而不是继续取最小的 s。最小乘子往往只把区间削掉一点,查询次数会从线性变成按比特线性。

取

r >= ceil(2 * (b*s_prev - 2B) / n)

再在

s ∈ [ceil((2B + r*n) / b), floor((3B - 1 + r*n) / a)]

里向预言机询问。这样得到的 s 大约至少是 2 * s_prev,新区间长度大约减半,未知比特大约减 1。某个 r 的整段候选都被 PS 规则拒绝时,r 加 1 继续。

本次连接的日志和“每次成功大约减 1 bit”一致:

step=1    width_bits=2017  queries=1
step=400  width_bits=1616  queries=1520
step=800  width_bits=1214  queries=3177
step=1200 width_bits=814   queries=4541
step=1600 width_bits=416   queries=5964
step=2000 width_bits=14    queries=7482

从 step 1 到 step 400,宽度从 2017 降到 1616,399 次成功收窄去掉 401 bit。step 2000 时还剩 14 bit,随后区间变成单点。全程 7521 次预言机查询,平均每次成功收窄大约 3.7 次往返;多出来的查询就是落进 [2B, 3B) 但没通过 8 字节 PS 检查的乘数。

区间上下界相等时,该整数就是候选明文。RSA 加密是双射,再用原始密文核对:

pow(m, e, n) == c

脚本打印 reencrypt_ok,说明这个块不是“某个看起来合法的填充块”,而是本连接密文的唯一原像。

3.4 去掉 type 2 填充

256 字节大端编码后,布局是

00 02 || PS || 00 || flag

PS 的每个字节都非零,所以从下标 2 起遇到的第一个 00 就是分隔符。分隔符后面的字节就是 flag。本次解出的 ASCII 字符串是

flag{DTAW6tuCFbAKt1FiUIYOqSO9V4U71f9EOOQuNlJu}

4. 完整解题脚本

把下面的代码保存为 solve_pkcs.py。模数和指数已经写在脚本里,不读取本地 pubkey.json。主机和端口是本次实例;换实例时只改 HOST 和 PORT。

#!/usr/bin/env python3
"""Recover the PKCS#1 v1.5 session plaintext via Bleichenbacher's oracle attack."""

import socket
import ssl
import time

HOST = "qocp6ack.nestctf.cyberpeace.cn"
PORT = 58999
E = 65537
N = int(
    "26053711197494345044781792077684912715021496527090136170200684647867265472090211834710019839499107995669155152828437896116662002233291085930822051870305763730149854492858590504378536095737328118664985736152084079800882198391309368647378331467360788476262812209190311267210497136432749618866195431576788245257578944400642543842611237033327500755489384635255355477488050218362690346216157317101091056613531444602254303947713075065315141018050424970114665608081069231050100673543130979972703432232664846464287183328555447235439966638024174265363366180280032770503317829739744455034691040984563526167517235779809547052041"
)


def ceildiv(a, b):
    return (a + b - 1) // b


def merge_intervals(intervals):
    ifnot intervals:
        return []
    intervals = sorted(intervals)
    merged = [list(intervals[0])]
    for a, b in intervals[1:]:
        if a <= merged[-1][1] + 1:
            merged[-1][1] = max(merged[-1][1], b)
        else:
            merged.append([a, b])
    return [(a, b) for a, b in merged]


def narrow(intervals, s, n, B):
    """Keep every m still inside some interval with (m*s mod n) in [2B, 3B)."""
    out = []
    for a, b in intervals:
        rmin = ceildiv(a * s - 3 * B + 1, n)
        rmax = (b * s - 2 * B) // n
        if rmax < 0:
            continue
        if rmin < 0:
            rmin = 0
        for r in range(rmin, rmax + 1):
            na = max(a, ceildiv(2 * B + r * n, s))
            nb = min(b, (3 * B - 1 + r * n) // s)
            if na <= nb:
                out.append((na, nb))
    return merge_intervals(out)


class Scanner:
    """Next multipliers that can send [a, b] into [2B, 3B)."""

    def __init__(self, a, b, s_min, n, B):
        self.a, self.b, self.n, self.B = a, b, n, B
        self.s_min = s_min
        self.r = max(0, ceildiv(a * s_min - 3 * B + 1, n))
        self.cur = None
        self.high = None
        self._advance()

    def _advance(self):
        a, b, n, B = self.a, self.b, self.n, self.B
        for _ in range(2_000_000):
            low = ceildiv(2 * B + self.r * n, b)
            high = (3 * B - 1 + self.r * n) // a
            if high < self.s_min:
                new_r = ceildiv(a * self.s_min - 3 * B + 1, n)
                self.r = self.r + 1if new_r <= self.r else max(new_r, 0)
                continue
            start = max(low, self.s_min)
            if start <= high:
                self.cur, self.high = start, high
                return
            self.r += 1
        raise RuntimeError("no candidate multiplier")

    def pop(self):
        s = self.cur
        self.s_min = s + 1
        if s < self.high:
            self.cur = s + 1
        else:
            self.r += 1
            self._advance()
        return s


def find_s_multi(intervals, s_prev, n, B, oracle):
    scanners = [Scanner(a, b, s_prev + 1, n, B) for a, b in intervals]
    whileTrue:
        s_try = min(sc.cur for sc in scanners)
        if oracle(s_try):
            return s_try
        for sc in scanners:
            if sc.cur == s_try:
                sc.pop()


def find_s_single(a, b, s_prev, n, B, oracle):
    """Bleichenbacher step 2c: choose r so s roughly doubles."""
    r = max(0, ceildiv(2 * (b * s_prev - 2 * B), n))
    whileTrue:
        low = max(s_prev + 1, ceildiv(2 * B + r * n, b))
        high = (3 * B - 1 + r * n) // a
        if low <= high:
            for s in range(low, high + 1):
                if oracle(s):
                    return s
        r += 1


class CountingOracle:
    def __init__(self, fn):
        self.fn = fn
        self.queries = 0

    def __call__(self, s):
        self.queries += 1
        return self.fn(s)


def attack(n, e, c, s1, oracle, k):
    B = 1 << (8 * (k - 2))
    intervals = narrow([(2 * B, 3 * B - 1)], s1, n, B)
    ifnot intervals:
        raise RuntimeError("s1 produced an empty interval set")
    width = max(b - a for a, b in intervals).bit_length()
    print(f"after_s1 intervals={len(intervals)} width_bits={width} queries={oracle.queries}", flush=True)
    s = s1
    step = 0
    t0 = time.time()
    whileTrue:
        step += 1
        if len(intervals) == 1and intervals[0][0] == intervals[0][1]:
            m = intervals[0][0]
            if pow(m, e, n) != c:
                raise RuntimeError("recovered block does not re-encrypt to c")
            print("reencrypt_ok", flush=True)
            return m
        if step == 1or step % 400 == 0or len(intervals) != 1:
            width = max(b - a for a, b in intervals).bit_length()
            print(
                f"step={step} intervals={len(intervals)} width_bits={width} "
                f"queries={oracle.queries} elapsed={time.time() - t0:.1f}s",
                flush=True,
            )
        if len(intervals) == 1:
            a, b = intervals[0]
            s = find_s_single(a, b, s, n, B, oracle)
        else:
            s = find_s_multi(intervals, s, n, B, oracle)
        intervals = narrow(intervals, s, n, B)
        ifnot intervals:
            raise RuntimeError("interval set became empty")


class Remote:
    def __init__(self, host, port):
        ctx = ssl.create_default_context()
        ctx.check_hostname = False
        ctx.verify_mode = ssl.CERT_NONE
        raw = socket.create_connection((host, port), timeout=20)
        raw.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
        raw.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
        self.s = ctx.wrap_socket(raw, server_hostname=host)
        self.s.settimeout(60)
        self.buf = b""

    def readline(self):
        whileb"\n"notin self.buf:
            chunk = self.s.recv(8192)
            ifnot chunk:
                raise EOFError("connection closed")
            self.buf += chunk
        line, self.buf = self.buf.split(b"\n", 1)
        return line

    def query(self, c):
        self.s.sendall(b"%x\n" % c)
        line = self.readline()
        if line == b"ok":
            returnTrue
        if line == b"bad":
            returnFalse
        raise RuntimeError(f"unexpected reply {line!r}")

    def close(self):
        self.s.close()


def unpad(m, k):
    em = m.to_bytes(k, "big")
    if em[:2] != b"\x00\x02":
        raise RuntimeError("missing 00 02 prefix")
    sep = em.index(b"\x00", 2)
    if sep < 10:
        raise RuntimeError("PS shorter than 8 bytes")
    return em[sep + 1 :]


def main():
    n, e = N, E
    k = (n.bit_length() + 7) // 8
    B = 1 << (8 * (k - 2))
    s_start = ceildiv(n, 3 * B)
    conn = Remote(HOST, PORT)
    try:
        parts = conn.readline().split()
        if len(parts) != 3or parts[0] != b"session":
            raise RuntimeError(f"bad banner {parts!r}")
        c = int(parts[1], 16)
        s1 = int(parts[2])
        ifnot0 < c < n:
            raise RuntimeError("ciphertext is outside the published modulus")
        print(f"k={k} s_start={s_start} s1={s1} c_bits={c.bit_length()}", flush=True)

        def raw(s):
            return conn.query((c * pow(s, e, n)) % n)

        oracle = CountingOracle(raw)
        ifnot oracle(s1):
            raise RuntimeError("published s1 was rejected")
        m = attack(n, e, c, s1, oracle, k)
        msg = unpad(m, k)
        print(f"queries={oracle.queries}", flush=True)
        print(f"FLAG {msg.decode('ascii')}", flush=True)
    finally:
        conn.close()


if __name__ == "__main__":
    main()

转载声明:本文转载自原发布平台 (作者:玄网安全), 原文标题《第九届“强网”拟态防御crypto和附件链接》, 查看原文。 版权归原作者及原发布平台所有,本站仅作收录与展示,未对正文内容作实质性修改; 若涉及侵权请联系本站处理。