[bug 分析] Orchard 伪造漏洞: 深度代码剖析

Orchard 伪造漏洞:深度代码剖析

2026 年 5 月 29 日,安全研究员 Taylor Hornby 在 Zcash 的 Orchard 隐私池中发现了一个极其致命的正确性漏洞。这个 bug 自 2022 年 5 月 Orchard 激活以来就一直存在,理论上攻击者可以在不被察觉的情况下无限量地伪造 ZEC 代币。虽然该漏洞在 6 月 3 日通过紧急网络升级(NU6.2)完成修补,但整个事件无疑是零知识电路安全领域里一堂极其昂贵的实战课。

下面我从开发者的视角,把这个漏洞的来龙去脉彻底拆解一遍:根因是什么、哪行代码写错了、如何利用、以及最终的修复方案到底改了啥。


电路本应做什么:密码学约束的目标

要理解这个 bug,首先得搞清楚 Orchard Action 电路到底要验证哪些东西。当用户花费一个隐私 note 时,电路必须证明:

  1. 花费者确实拥有该 note(由花费验证密钥 ak 控制)
  2. 空值 nf 必须由空值密钥 nk 正确推导出来
  3. 多样化地址 (d, pk_d) 是合法的,并且与正确的传入查看密钥 ivk 绑定

其中最关键的一层绑定关系是:ivk = Commit(ak, nk, rivk)。多样化的基点 g_d = DiversifyHash(d)pk_d 共同构成地址。电路必须强制 nf 的推导过程与正确的 ivkpk_d 保持密码学绑定。

而这次漏洞,恰恰在电路层面打破了这一绑定。


根因:一个字符之差,酿成滔天大祸

bug 出在 halo2_gadgets 库中的变基标量乘法电路(variable-base scalar multiplication gadget)。

文件路径: halo2_gadgets/src/ecc/chip/mul/incomplete.rs

有漏洞的代码(概念化):

rust 复制代码
// 当时代码实际做的(错误的):
let base_point = region.assign_advice(|| "base", config.base_column, offset)?;

正确的代码(修复后):

rust 复制代码
// 应该做的:
let base_point = region.copy_advice(|| "base", config.base_column, offset)?;

两者区别极其细微,但后果天差地别:

  • assign_advice() 只是往一个单元格里写入数值,并不强制该值与其他地方的副本保持任何关系。
  • copy_advice() 则会创建一条复制约束,强制当前单元格的值必须等于另一个源单元格的值。

变基标量乘法电路的本意是使用真正的多样化基点 g_d 作为输入。但因为用了 assign_advice() 而非 copy_advice(),内部的基点变量从未被锚定到外部传入的实际基点上。

整个电路的约束系统里,压根没有这样一条等式约束:

复制代码
internal_base_point == actual_g_d

这意味着恶意证明者可以在生成证明时,随意为基点提供任意坐标。乘法校验本身依然能通过,因为电路只是在验证标量乘法是否与给定的基点匹配------既然这个基点没被约束为正确的那个,证明者就可以选择任何他们想要的基点。


后果:为什么这能打破空值与地址的绑定

Orchard Action 电路在多样化地址完整性检查中,会用到这个标量乘法。该检查要求:

复制代码
pk_d == [ivk] g_d

其中 [ivk] g_d 表示用传入查看密钥 ivk 对多样化基点 g_d 做标量乘法。

当标量乘法电路未能约束基点为 g_d 时,恶意证明者就能:

  1. 用一个任意选定的基点 g' 来代替 g_d
  2. 选一个任意标量 s,使得 pk_d == [s] g'
  3. 生成一个看似合法的证明,声称该 note 属于地址 (d, pk_d)

电路会接受这个证明,因为标量乘法校验通过了------只不过它校验的是错误的基点。

而空值的推导依赖于 nk 和其他 note 组件。一旦 pk_dg_divknk 之间的绑定被打破,攻击者就能花费他们并不真正拥有的 note。由于推导出的空值与真实 note 的空值不一致,同一个 note 可以被反复花费而不被察觉。


攻击手法:如何凭空造出无限 ZEC

Taylor Hornby 与 Anthropic 的 Claude Opus 4.8 合作,构造了一个完整的可运行利用代码,并在本地 regtest 环境中成功测试。

攻击流程大致如下:

第一步: 攻击者选定一个想要伪造的目标 note。该 note 有合法的空值 nf,正常花费时会公开发布。

第二步: 攻击者并不去证明自己拥有该 note(即正确的 nkg_d),而是往标量乘法电路里塞入一个任意基点 g'

第三步: 攻击者选择一个标量 s,使得 pk_d == [s] g' 成立,其中 pk_d 是攻击者自己控制的地址。因为基点没有被正确约束,这个检查顺利通过。

第四步: 电路生成了一个看似合法的 Orchard Action 证明,表面上是在花费一个合法的 note。但由于使用了攻击者自己选的参数,该证明计算出的空值与原始 note 的空值不同。

第五步: 网络验证该证明并通过。原始 note 并未被真正花费(其空值从未发布),而攻击者伪造的 note 却进入了流通。

整个过程可以无限重复,最终造成无限增发 ZEC。

由于 Orchard 交易是完全隐私的,链上没有任何签名或透明痕迹能检测到这种伪造。外部观察者完全无法发现异常。


修复方案:一行改动,全网升级

修复方法在概念上非常简单:把变基标量乘法电路里的 assign_advice() 替换成 copy_advice()

修复前(有漏洞):

rust 复制代码
// 在 halo2_gadgets/src/ecc/chip/mul/incomplete.rs 中
let base_point = region.assign_advice(
    || "base",
    config.base_column,
    offset
)?;
// 没有任何约束把 base_point 绑定到真正的外部基点上

修复后(已修补):

rust 复制代码
// 在 halo2_gadgets/src/ecc/chip/mul/incomplete.rs 中
let base_point = region.copy_advice(
    || "base",
    config.base_column,
    offset
)?;
// copy_advice() 强制 base_point 必须等于源单元格的值

copy_advice() 会在 halo2 电路中生成一条复制约束(copy constraint)。这迫使内部使用的基点必须与外部传入的多样化基点 g_d 完全一致。

为什么这样就有效:

  • 等式约束现在成为了电路约束系统的一部分
  • 任何试图使用不同基点的证明者都会在约束检查时失败
  • 空值推导被正确地绑定到了实际花费的 note 上
  • 多样化地址完整性检查强制要求 pk_d == [ivk] g_d,且 g_d 必须是正确的

网络升级过程:

由于修改了 Orchard 电路的验证密钥,必须通过一次网络升级来部署。时间线如下:

  1. 2026 年 5 月 29 日: Taylor Hornby 发现漏洞并秘密报告给 Zcash Open Development Lab(ZODL)
  2. 2026 年 6 月 1 日: 紧急修复完成
  3. 2026 年 6 月 2 日: Zebra 4.5.3 发布,通过软分叉暂时禁用了 Orchard 动作
  4. 2026 年 6 月 3 日: NU6.2 硬分叉在区块高度 3,364,600 激活,重新启用 Orchard,并应用了修正后的电路

为什么这个 bug 能潜伏四年之久

从 2022 年 5 月到 2026 年 5 月,这个漏洞一直没有被发现,背后有几个原因:

多轮专业审计全部漏掉

Orchard 代码经过了全球顶尖密码学家的多次审计。然而漏洞依然坚挺。这恰恰说明 ZK 电路 bug 的隐蔽性------少了一条约束,不会导致编译错误,也不会引发运行时崩溃,它只会以"正确性漏洞"的形式潜伏着,等待恶意证明者去利用。

错误藏在依赖库里

bug 位于 halo2_gadgets 这个库中,而非 Orchard 本身的代码。这导致审计时更容易被忽视,因为大家的目光都集中在 Orchard 的实现上,而对底层密码学 gadget 的检查不够深入。偏偏这个 gadget 是许多协议共同依赖的基础组件。

最终还是 AI 辅助审计揪了出来

Taylor Hornby 使用了一套自定义分析框架,结合 Anthropic 的 Claude Opus 4.8 模型。AI 模型能够系统性地执行电路级检查,自动识别出约束不足的关系。Hornby 不仅仅是收到一个警告,他还借助 AI 写出了一个完整的可利用证明。

教训很清楚:即便经过最严格审查的密码学代码,也可能藏有致命漏洞。持续、AI 增强的安全审计,已经不是"可选项",而是"必选项"。


无法回避的未知:到底有没有被利用过?

由于 Orchard 交易完全私密,我们无法从密码学上判断该漏洞在修补之前是否已经被利用过。

这就带来一个令人不安的现实:Zcash 的总供应量可能已经被悄悄增发了,而我们无从知晓。Shielded Labs 承认了这种不确定性,同时也表示谨慎乐观------他们认为实际被利用的可能性不大。

下一步计划是通过一次网络升级,让任何人都能通过密码学方式证明 Zcash 供应量的完整性。这样就能给出确凿证据,证明到底有没有发生过伪造。


给开发者的核心启示

ZK 电路与普通代码完全是两码事

在传统编程里,我们通过运行代码并检查输出来测试正确性。而在 ZK 电路中,你必须证明所有可能的输入都能满足约束。漏掉一条约束,就相当于漏掉了一条断言------只不过没有任何运行时环境会帮你捕捉到它。

assign_advice()copy_advice() 的区分至关重要

在 halo2 中,assign_advice() 只是给一个单元格赋值。而 copy_advice() 不但赋值,还强制要求该值必须与源单元格相等。如果你需要某个值在电路中与另一个值保持密码学绑定,就必须使用 copy_advice(),或者显式添加等式约束。

不仅要审计自己的代码,更要审计依赖

漏洞位于 halo2_gadgets,这是 Orchard 依赖的底层库。很多审计团队只盯着 Orchard 电路本身,却忽略了关键问题出在依赖库里。依赖审计的地位和自身代码审计一样重要。

AI 辅助审计是增效利器

这个 bug 躲过了四年的人类审计,最终被人类+AI 的组合发现。人类专家的领域知识,加上 AI 系统性地探索约束空间的能力,两者结合才取得了决定性突破。

正确性漏洞(Soundness Bug)是 ZK 体系里最危险的一类

完备性漏洞(有效证明被拒绝)最多算个麻烦。而正确性漏洞(无效证明被接受)则是灾难性的。这次漏洞就属于最恶劣的那种------允许任意创造价值且无法被检测。


结语

Zcash Orchard 伪造漏洞是一记沉重的警钟:哪怕经过最严格审计的密码学系统,也可能藏着致命缺陷。根源仅仅是一个函数调用------assign_advice() 代替了 copy_advice()------出现在标量乘法 gadget 里。这一个字符的差异,就破坏了多样化地址与空值之间的密码学绑定,理论上可能导致无限且无法检测的伪造。

修复只需要改动一行代码。但要部署这行代码,却需要一次不简单的全网升级。而该漏洞是否曾被利用,至今仍是个悬而未决的疑问。

对于所有在 ZK 电路上构建应用的开发者,这件事留下了一个朴素的教训:敬畏每一条约束。没有被约束的东西,就默认它可以被攻击。永远不要以为审计过几次就万事大吉。持续、AI 加持的安全研究,才是新的底线。

相关推荐
山东科恩光电1 小时前
当进入危险区域时,如何利用安全地毯实现事故预防?
安全
数据知道3 小时前
网络安全实战:服务指纹识别——Banner、协议探测、版本漏洞关联
网络·安全·web安全·网络安全
mennekes3 小时前
自动化产线临时配电:明装工业插座 vs 面板式工业插座 vs 模块化插座箱取舍
运维·科技·安全·制造
网安蟹佬霸5 小时前
CTF Web方向解题全攻略:从信息收集到getshell(附实战payload与脚本)
android·前端·网络·安全·web安全·网络安全·网安
保密资质顾问说6 小时前
网络安全等级保护与涉密信息系统集成资质的区别及选型分析
运维·网络·经验分享·安全·web安全
网安蟹佬霸6 小时前
Webshell免杀与检测实战:从一句话木马到加密流量
android·安全·web安全·网络安全·黑客·网安·渗透测试·
ZDGJ60997 小时前
大恒指与小恒指交易时间详解
区块链
Bruce_Liuxiaowei7 小时前
安全意识培训——大模型在安全培训中的创新应用
人工智能·安全·ai·智能体
QXWZ_IA7 小时前
化工园区安全定位如何做合规审查?
安全