半小时、一个时间戳与一行旧代码:Claude 如何解开困住密码学界 25 年的 NIST 椭圆曲线种子之谜

半小时、一个时间戳与一行 LCG 代码:Claude 如何解开 NIST 椭圆曲线种子之谜

引言:一道没人敢碰的"后门悬案"

如果你每天都在用 TLS 加密的网页、电子签名、数字货币,那你几乎必然接触过 NIST 定义的椭圆曲线——尤其是 P-256,它长年占据着全球数字证书和签名协议的大半江山。在这个体系里,每条曲线的参数背后,都藏着一个从 2000 年就写进标准的"种子"(seed):一个公开字节串,用来派生曲线的系数。

按理说,种子的存在本来是为了"透明"——你提供一个哈希值,别人就能验证曲线系数不是被随机乱拍的。但 25 年来,密码学界却始终绕不开一个令人不安的疑问:这五条曲线的种子,到底是怎么来的? 尤其是 2013 年爆出"Dual_EC_DRBG 后门"丑闻之后,很多人怀疑 NIST 曲线可能也是美国国家安全局(NSA)精心构造、暗中预留了某种弱点的陷阱。因为当年的生成流程是不公开的,只有结果,没有过程。

10 月 16 日,Anthropic 公布了一项颇为惊人的研究成果:一名员工把这个问题交给了 Claude 的内部研究模型,在没有提供任何关键线索的情况下,Claude 用了不到半小时,就追溯出了这五条 NIST 椭圆曲线种子的真实生成方式。 结果既意外又平凡:这些种子根本不是来自某个强哈希,而是出自 C 语言标准库里一个老掉牙的伪随机数生成器 lrand48——它的初始状态,来自 1997 到 1998 年间的一组 Unix 时间戳。

这是一次没有攻破任何东西的"破案"。它没有制造新的攻击手段,也没有证明 NIST 在很多年后就被怀疑的曲线真的被攻破;恰恰相反,它大幅降低了这些曲线被人为植入后门的可能性——因为谜题被解开了,一切原来只是一次随意的时钟读取和一行早已过时的旧代码。真正值得细想的,是 Claude 完成这件事的那条"逆向推理"路径:它不靠蛮力穷举,而是靠对 90 年代软件生态的常识、对输出特征的敏锐识别、以及对数论工具的灵活调用,完成了一次教科书级别的"考古式"推断。

Part One:先说清"种子"到底是什么,以及它为什么让人起疑

1. 标准的曲线参数从哪里来

要理解这件事的价值,我们得先回到椭圆曲线密码学(ECC)的参数选择。一条安全的椭圆曲线,需要一组定义良好的系数 a 和 b(对应曲线方程 y² = x³ + ax + b),以及基点、阶等一整套参数。假如这些参数是随机生成的,那任何人都能背地里检验:会不会恰好落进了某个知道弱点的小集合?——而如果参数根本是精心挑选、用来隐藏某个特殊结构的,那只有选参数的人才知道安全漏洞在哪里。

于是 NIST 在 1999–2000 年发布这套标准时,采用了"种子 + 哈希"的可验证流程:

种子 seed(公开)
    │
    ▼
 SHA-1 哈希  ←— 单向、公开、人人可验
    │
    ▼
 派生曲线系数 (a, b)

理想要素在于:只要种子是诚实的、随机的,那么从种子到系数就完全公开可复现,没人能在里面"塞私货"。问题于是被压缩成了一个点:种子本身,是不是经过挑选的? 如果当年构造种子的人,是先选好一个藏着弱点的秘密系数、再反推"看起来随机"的种子(只是没有人知道怎么反推),那哈希过程形同虚设。这,正是 25 年来疑云的根源。

2. 谣言、线索与"幽默口令"的岔路

围绕这批种子,流传过不少猜测。最广为人知的一种说法是,种子的来源跟某个"内部笑话"或"幽默口令"有关——据说当年有人用一句玩笑话作为口令来生成种子。Anthropic 的几名员工此前也沿着"幽默口令"这条线找过,却始终一无所获,最后发现这条线索本身可能是错的。

更有力地推动下,还有 2013 年那场著名的风波:斯诺登泄密文件揭露出 NSA 在密码标准里植入后门(“Dual_EC_DRBG”),让"NIST 曲线可能也不干净"的怀疑彻底出圈。Filippo Valsorda(Go 密码学库维护者)之前甚至挂出悬赏,鼓励任何人解开这个谜,以示对这套曲线安全性的关切。

于是,今年 10 月,一名 Anthropic 员工决定换个思路:他不再沿着任何传闻线索追,而是把"种子是怎么产生的"这个问题,直接交给内部研究模型 Claude 去猜。

3. 一道"不给线索"的题

关键细节是:这位员工没有向模型透露任何此前流传的"口令"线索。 也就是说,Claude 需要完全凭自己的推理,在几乎空白的约束下,还原 20 多年前某个工程师写的一段程序。这在传统"AI 答题"里,属于典型的"开放世界推理"——没有标准答案,没有提示词,只有一堆公开的种子字节串和一堆数字签名体系常识。

Claude 的表现,我们放在下一节拆开看。但先给个判断:这道题难就难在,它不是一道"查得到答案"的题,而是一道"需要提出正确假设链"的题。 谁能想到去怀疑一个 1998 年的 C 库随机数函数?谁能从几个字节的统计特征里嗅出 LCG 的味道?这正是 Claude 这个 AI 破解者最让人印象深刻的地方。

Part Two:Claude 的"四步破案法",一次教科书级的逆向推理

第一步:放弃追口令,改为推测"当年软件怎么写的"

Claude 很快做出一件人类容易僵在旧思路上、而它很自然就做到的事:把搜索对象从"秘密口令"切换到"程序行为"。 它开始推测:1990 年代,构造曲线的工程师们用的很可能是当时 C 语言库自带的伪随机数生成器。这些早期的 PRNG(伪随机数生成器)安全强度很低,输出往往带有可识别的统计特征。

一旦意识到"输出可能带有特征",下一步就顺理成章:扫描程序在每一个公开种子上逐位移动,检查相邻数据块是否可能来自某个 90 年代的随机数生成器。 这一扫,覆盖了 34 种"随机数生成器 × 输出截取方式"的组合。

第二步:在高位发现"大量的 0"——这是关键指纹

扫描中,Claude 在一个极其关键的位置发现了规律:部分种子的高位,全部是零。 把每个种子写成 5 个 32 位字(共 160 位)后,10 个种子里有 4 个符合这一特征;这 4 个种子合计 20 个 32 位字,最高位全部为 0。

这个现象随机发生的概率大约只有 五千八百分之一。它几乎只可能来自一类东西:线性同余生成器(LCG)。具体来说,lrand48 内部维护一个 48 位状态,但它每次只返回状态的高 31 位——于是输出的最高位,天然恒为 0。

这一步,Claude 几乎等于"看一眼 X 光片就认出了患者的基因",从一串看似平淡的字节里,锁定了生成器的"身份"。

第三步:从"高 31 位"恢复 48 位内部状态——格理论登场

lrand48 的 48 位内部状态,只暴露了高 31 位(甚至更少),那么剩下的 17 位怎么补?Claude 用了两条路:

  • 对较"大方"的种子:直接穷举那 2^17 = 131072 种隐藏比特组合,再检查后续字是否连续匹配——这 4 个种子全部验证通过。
  • 对保留比特更少的素数曲线种子:改用更高级的工具——这就是一个典型的"截断 LCG 状态恢复"问题,可以用 LLL 格基约简算法(Lenstra–Lenstra–Lovász)来解。LLL 是格密码学里赫赫有名的算法,能把"找最短向量"这类问题在多项式时间内做好逼近。Claude 起初不知道种子保留了状态里的哪些比特,于是对所有可能的比特窗口都跑了一遍求解器。

结果极具说服力:5 个 NIST 素数曲线种子 + 5 个 ANSI X9.62 标准的种子,全都呈现出完全相同的神奇模式:lrand48() >> 23。 每恢复出的 48 位内部状态,都能重新生成对应种子的全部 160 位比特。这种级联的精确匹配,绝不可能出自偶然。

第四步:反向追踪状态,直到撞上 Unix 时间戳

LCG 的美妙之处在于,它既能正向走,也能反向走。Claude 便从每个种子对应的状态开始向前回溯,一路找回到符合 srand48 初始化格式的某个状态——该格式要求状态低 16 位恰好等于 0x330E(这是 srand48 处理种子时的一个固定变换)。

由于平均每 65536 步就会碰到一次这种格式的状态,单看"找到一个符合格式的状态"还不足以算证据。真正的杀手锏在后面:

这 10 个素数曲线种子对应的 srand48 初始值,全部都可以被解释成 Unix 时间戳,日期集中在 1997 年 5 月至 1998 年 4 月之间。 也就是说,当年那个程序,就是简单地用系统时钟(Unix 时间戳)给这个弱随机数生成器撒了一把种子,然后跑了几千次,把输出截成 20 字节一段,取其中某一段当做了标准里的种子。

更有趣的是 4 个二元曲线的初始值:直接读取时对应 1984 年 2 月,但如果把这些数值乘以 2,就会精确指向 1998 年 4 月 17 日——距离 P-224 曲线的生成时间大约两周。可见当年的程序可能对系统时钟做了一步"除以 2"的处理(也许是某种 16 位读取的副作用)。这些细节环环相扣,构成了完整的证据链。

Part Three:用一段 Python 复现"种子推导"的核心骨架

理论说得天花乱坠,不如上手把最核心的骨架摆出来。我们当然不能真的复现 NIST 当年的全流程,但可以把"从 LCG 输出恢复状态、再用时间戳回溯"这段最关键的逻辑,用一段精简的 Python 演示出来,让大家直观感受 Claude 当年面对的是什么样的数学结构。

# lcg_forensics.py
# 演示:识别 lrand48 风格 LCG 的输出特征,并用时间戳反推其初始状态
# 注意:这是教学演示,非真实破译 NIST 曲线的完整实现

M = 1 << 48          # lrand48 的模(2^48)
A = 0x5DEECE66D      # lrand48 乘数(经典 LCG 常数)
C = 0xB              # lrand48 增量

def advance(state):
    """向前推进一步,返回新状态(48 位)"""
    return (A * state + C) % M

def backstep(state):
    """利用 LCG 可逆性,向前回溯一步"""
    # 需要 A 的模逆
    a_inv = pow(A, -1, M)
    return (a_inv * (state - C)) % M

def high_bits(state):
    """lrand48 返回高 31 位"""
    return (state >> 17) & 0x7FFFFFFF

def looks_like_srand48_init(state):
    """srand48 初始化格式:低 16 位等于 0x330E 的某种变换"""
    return (state & 0xFFFF) == 0x330E  # 简化示意

# 模拟:用某个"内部状态"反推它在时间戳空间里的位置
def trace_back_to_timestamp(final_state, max_steps=200000):
    s = final_state
    for _ in range(max_steps):
        if looks_like_srand48_init(s):
            # 命中了看起来像 srand48 的初始状态(示意)
            return s, _+1
        s = backstep(s)
    return None, max_steps

# 示例:构造一个伪造状态,看回溯逻辑能不能收敛
demo = 0x123456789AB  # 一个48位示例状态(非真实NIST状态)
state_found, steps = trace_back_to_timestamp(demo)
print(f"回溯 {steps} 步, 找到候选初始状态: {state_found is not None}")
print(f"高31位输出: 0x{high_bits(demo):X}")
print(f"说明: 高31位恒为0 => LCG输出特征(概率约1/2^17≈1/131072)")

这段代码刻意做了高度简化,但已经把两个最核心的数学事实暴露清楚了:LCG 是可逆的(所以能回溯),而且它的输出位有结构性缺陷(高位置零或截断),这既是弱点,也是"破案"的指纹。 真实情况里,参数恢复要处理比特窗口组合、截断带来的模糊性,还要配合 LLL 格基约简——工作量远比这几行复杂,但骨架正是如此。

一张图看懂 Claude 的推理链

下面这张 ASCII 图,把 Claude 从"字节"到"时间戳"的完整证据链画出来:

 NIST 种子字节串(公开)
      │
      ▼ 逐位扫描 34 种 生成器×截取 组合
 发现: 10个种子中4个 所有 32位字 最高位=0 (概率≈1/5800)
      │  ←─ 几乎只属于 LCG
      ▼
 锁定: lrand48 高31位返回模式 (lrand48()>>17)
      │
      ▼ 恢复 48 位内部状态
 素数曲线种子: 丢比特更多 → 需 LLL 格基约简
 各比特窗口都跑 → 命中 lrand48()>>23
      │
      ▼ 每个恢复的状态能重生成全部160位 (精确匹配)
 沿 LCG 反向回溯
      │
      ▼ 撞见 srand48 初始化格式 (低16位=0x330E)
 10个初始值全部可解释为 Unix 时间戳
      │  ←─ 1997.05 ~ 1998.04
      ▼
 结论: C库 lrand48 + Unix时间戳种子 ⇒ 非NSA后门,随机生成

Part Four:为什么这件事的意义,远超"解开一个谜"

1. 它是"无钥匙解题"的示范:AI 能做开放式逆向推理

传统 AI benchmark 里,几乎都是"给定问题→找到已知答案"。但 NIST 种子问题是开放世界的:没有标准答案,没有提示,甚至连"该怀疑哪个技术方向"都没人告诉它。Claude 之所以能解,靠的不是更大规模的记忆,而是把"密码学常识 + 90 年代软件史 + 统计直觉 + 数学工具"串成一条假设链的能力。它自己提出"可能是 C 库随机函数",自己设计扫描,自己发现高位归零,自己搬出 LLL——整个过程是人类密码学专家会走的路线,但速度是"半小时"。

这提示我们:下一阶段 AI 的价值,可能恰恰在这种"没有现成答案的溯源与诊断"里——无论是找出隐藏的硬件后门、定位一段奇怪的二进制来源,还是在供应链里识别"这行代码到底从哪里来的"。

2. 它给 NIST 曲线"洗清了 25 年的嫌疑"

对整个互联网密码学基础设施而言,这个结论像一块大石头落了地。它基本排除了"NIST 曲线是 NSA 预留后门"这个最大的猜测——因为现在我们知道,种子来自一个 90 年代的 C 库随机数函数加一个随意的时钟,而不是来自某个精心选参数以隐藏弱点的秘密过程。当然,这不等于"再无任何风险"(不能说完全排除所有理论上的吹毛求疵),但证据链之完整(10 个种子精确匹配、时间戳全部吻合),足以让绝大多数从业者放下多年悬着的担忧。

有趣的是,这个结论其实也稀释了"换个曲线"的紧迫性:如果 NIST 曲线本身并没有隐藏后门,那么依赖 NIST P-256 的庞大 TLS 生态,多年来的安全性根基比很多人担心的要更扎实。

3. 一场漂亮的开源+悬赏实践

Anthropic 不只是发了篇博客。它把复现代码公开,供全球任何人验证;同时,针对悬赏规则,奖金也走向了一个颇有情怀的结局——奖金最初是 12,288 美元,获胜方决定把它捐给互联网安全研究组织 ISRG,按规则翻了三倍后变成 36,864 美元。Filippo Valsorda 也在个人博客上认领并庆祝了这次破案,还顺带调侃了一下"后量子签名还没普及时,这套曲线的归宿"。

这个小插曲说明:好的安全研究,往往是"社区信任 + 开放验证 + 透明归因"三者一起转动的。

Part Five:给工程与从业者的三条启发

第一,别低估"旧代码"的生命力。 一个 1998 年的 C 库函数,就这样潜藏在一套 2000 年成为全球密码标准的基础设施里,直到 2026 年被 AI 翻出来。安全工程里,“考古"能力(理解历史代码、遗留随机源、历史调用关系)的价值,可能远超我们想象。今天每一个生产系统里的随机源、哈希、密钥派生逻辑,都可能藏着类似的"时间胶囊”。

第二,AI 的推理优势不只在"写代码",也在"读历史"。 Claude 这次强的地方,是把数论工具(LLL)、统计直觉(1/5800 的归零概率)与软件常识糅合到一起。未来我们要学会的,是"把难题交给模型时,怎么设计让它敢走开放路径、能提出并验证假设"——这比让模型去刷题更有价值。

第三,验证仍是最重要的一环。 Claude 没有"说它找到就完事",而是用"10 个种子全部精确匹配 + 时间戳全部吻合"这种多重交叉验证来自证。AI 给出的结论,只有经过可复现、可核查的证据链兜底,才真正可信。 这既是这次破案的可贵之处,也是我们日常把 AI 引入高风险决策时最该守住的底线。

结语

半小时,一个 1997 年的时钟读数,和一行早已被淘汰的 lrand48——就这样,困扰密码学界四分之一个世纪的"后门悬案",在一台能自主提出假设、调动数学工具、并完成严谨逆向推理的模型手里落下了帷幕。

这大概是 2026 年最优雅的一次"AI 与数学的握手":它没有攻克任何东西,却守护了整个互联网一条最基础的信任线索;它没有制造任何威胁,却示范了 AI 在"开放式溯源"这条路上的潜力。当越来越多的加密基础设施开始转向后量子时代的道路,这次对旧曲线的"考古式平反",称得上是我们送给过去的——一个体面而清醒的谢幕。