Reed's News
← 返回精选

AI审计发现OpenVM配对库关键漏洞:缺失子域检查导致伪造攻击

AI 77 duha 2026/7/17 2265 字 原文 ↗

缩略图

这是本系列的第二篇文章。如果你还没读过第一篇关于Cloudflare CIRCL的内容,那篇文章详细解释了我们开展这类实验的原因,以及实验流程的搭建方式。

在本文中,我们让AI审计工具zkaoOpenVM的zkVM进行检测,结果发现其客端库openvm-pairing存在一个严重的安全性漏洞——恶意证明者可借此伪造任意配对等式。需要说明的是,该漏洞并非zkVM证明系统本身的安全性问题,仅会影响使用这个存在漏洞的库的代码。

本文提及的漏洞已被分配编号CVE-2026-46669,并在OpenVM 1.6.0版本中修复。据我们所知,所有基于OpenVM开发的合作伙伴均已升级至该版本。

说明:与第一篇文章相同,AI仅生成初步的疑似漏洞,而非最终报告。之后由我们团队的人工人员验证问题、确认可利用性、评估完整影响范围及受影响项目,并负责披露流程。此次zkao生成的报告内容详尽,还附带了一个最简可行验证(PoC),因此我们仅通过快速人工筛选,就判定该漏洞值得向OpenVM团队通报。

漏洞发现过程

四个月前,我们将OpenVM纳入AI实验扫描范围,初始扫描方式与其他项目一致:先用基础提示词调用大语言模型(LLM),再用专家维护的技能库调用LLM。我们先后使用了Opus 4.6和Codex 5.3模型,在Opus 4.7和Codex 5.4发布后,又立即用新版本重新扫描。

模型返回的所有疑似漏洞均为有效观测结果,且多个被标记为“严重”或“高危”,但实际上没有一个可被利用。

我们由此提出假设:zkVM的复杂度远超普通代码库,即使给基础LLM配置30万甚至100万token的上下文窗口,也无法有效处理。zkVM各模块间的依赖密度远高于常规库——加密库通常可并行审计,只需将对应单个加密原语的文件夹分配给子代理即可。每个子代理只需处理少量代码、应用相关技能、将发现写入Markdown文件,再由主代理整合结果。这类流程借助Claude Code、Codex等主流智能编码工具即可实现,几乎无需人工干预。

但这种方法不适用于OpenVM这类复杂代码库。除了一些显而易见的简单问题,子代理的有效输出不应是漏洞列表——即使模块A和模块B各自被证明安全,二者组合后仍可能存在安全隐患。因此,“孤立式”的漏洞排查无法发现真正有影响的问题,子代理的输出应转变为模块相关知识:包括模块的假设前提、需调用方完成的任务,以及它默认依赖的不变量。

不过,如何恰当地呈现这类知识是一大难点:内容过短会遗漏漏洞所在的实现细节,过长则会超出主代理的上下文窗口,无法与其他信息整合。就目前观察来看,至少在撰写本文时,上述智能编码工具尚未能高效解决这一问题。

基于这一假设,我们决定让zkao扫描OpenVM——尽管我们最初的实验规则是,仅当基础LLM发现真实漏洞后,才会启用zkao。我们为zkao投入了大量精力进行上下文工程,将内部专家的工作方法编码为可复用的漏洞排查流程,因此它恰好适用于这种场景。经过9个半小时的扫描,zkao返回了多个发现。与之前的实验一样,我们没有时间深入核查每一个结果,但快速筛选后,一个漏洞立刻引起了我们的注意:客端库中的配对检查存在严重安全性问题。我们的假设得到了验证,数月的努力终于有了回报!

尽管本次仅发现一个漏洞,为保持与第一篇文章的一致性,我们先对该漏洞做简要概述。

漏洞严重程度与修复一览

序号 漏洞描述 AI判定严重程度 OpenVM官方严重程度 修复提交哈希 发现工具
1 openvm-pairing配对检查未对缩放因子进行正确的子域校验 严重 严重 a720e2c zkao

本次AI与项目维护方对严重程度的判定一致。

漏洞1:openvm-pairing配对检查未对缩放因子进行正确的子域校验

背景知识

配对是Groth16、基于KZG的PLONK以及BLS签名的核心技术。在这些协议中,验证者通常不会直接验证单个配对值,而是验证一组配对的乘积是否等于1:

$$ \prod_i e(P_i, Q_i) = 1. $$

通过这个是非判断,验证者可以确认SNARK证明有效、KZG开放证明正确,或签名验证通过。因此,若证明者能让虚假的配对乘积看起来等于1,所有基于该配对的系统都将失去安全性。

配对是一种双线性映射:

$$ e : G_1 \times G_2 \to G_T, $$

其中$G_1$、$G_2$、$G_T$均为阿贝尔群。在本文场景中,$G_1$和$G_2$是椭圆曲线群,$G_T$是$\mathbb{F}_{p^{12}}^{*}$的乘法子群。

配对最重要的特性是双线性:

$$ e([a]P, [b]Q) = e(P, Q)^{ab}. $$

这是配对技术实用价值的来源,但理解本次漏洞无需用到这一特性,可暂时忽略。

计算配对$e(P, Q)$主要分为两个步骤(韦伊配对除外):

第一步:米勒循环 执行米勒函数$f_{r, Q}(P)$,为简化理解可将其视为黑盒。该步骤输出$\mathbb{F}{p^{12}}^{*}$中的一个元素。若计算多组配对的乘积,电路可先执行所有米勒循环,再将输出结果相乘,记合并后的输出为$f$,即$f = \prod_i f{r, Q_i}(P_i)$。

需要注意的是,$f$并非最终的配对乘积,它只是等价类$\mathbb{F}{p^{12}}^{*} / (\mathbb{F}{p^{12}}^{*})^r$中的一个代表元素。这是Novakovic和Eagen在论文中提出的核心结论之一:米勒循环的输出仅在乘以$r$次幂的范围内唯一。换句话说,当存在非零元素$c$使得$f_1 = f_2 \cdot c^r$时,$f_1$和$f_2$代表同一配对。这种不确定性给直接的等式校验带来了困难。

第二步:最终指数运算 正是为了解决上述问题,第二步应运而生。我们将$f$取指数$h$:

$$ h = \frac{p^{12} - 1}{r} $$

这样即可消除不确定性,因为对于任意非零元素$c$,都有:

$$ (f \cdot c^r)^h = f^h \cdot c^{p^{12}-1} = f^h. $$

最后一项消失的原因是,$\mathbb{F}_{p^{12}}$中的所有非零元素都满足$x^{p^{12}-1} = 1$。经过指数运算后,结果将落入$G_T$——即$r$次单位根群。因此,真正的配对乘积校验应为:

$$ f^h = 1. $$

问题在于,在电路中直接计算指数$h$的开销极大,而让证明者在电路外计算$c$并作为提示值传入则成本很低。校验$f = c^r$的开销远小于计算$f^h$的指数运算。因此,为证明$\prod_i e(P_i, Q_i) = f^h = 1$,证明者无需直接计算$f^h$,只需提供一个非零元素$c$使得$f = c^r$——这一条件与$f^h = 1$完全等价。

这便是该优化的核心思路。OpenVM采用了Novakovic和Eagen在论文中提出的残差见证技巧:证明者提供几个额外值,电路只需校验一个低成本等式,无需执行完整的指数运算。

实际使用的优化等式与$f = c^r$略有不同:

$$ f \cdot u = c^{\lambda} \wedge u^{d^i} = 1$$

其中$\lambda = m \cdot r$是曲线特有的指数,其结构允许电路通过弗罗贝尼乌斯映射高效计算$c^\lambda$;$u$被称为缩放因子(在OpenVM代码中,BN254曲线用u表示,BLS12-381曲线用s表示)。其余符号定义为$d = \gcd(m, h)$,$i = v_d(h)$。

引入缩放因子是因为$\lambda$与原始的$r$-残差校验无法完全匹配。论文中的定理3通过要求缩放因子满足一个简单的单位根关系(即上述$u^{d^i} = 1$条件)填补了这一空白。

对于本文涉及的特定曲线,论文还提出了一种更高效的实现方式:无需直接校验$u^{d^i} = 1$,只需将缩放因子限制在子域$\mathbb{F}{p^6} \subset \mathbb{F}{p^{12}}$内即可。将<!--MATHBLOCK44-->限制到子域这种校验的成本极低。

OpenVM将$\mathbb{F}{p^{12}}$元素存储为6个$\mathbb{F}{p^2}$系数:

若元素属于$\mathbb{F}_{p^6}$子域,则其奇数索引的系数必须为0:

$$ c_1 = c_3 = c_5 = 0. $$

因此,这个对安全性至关重要的子域校验,本质上就是三个等式检查。

漏洞详情

回顾校验流程:电路获取米勒循环的合并输出$f$,仅当配对乘积经过最终指数运算后等于1时,才应判定验证通过。OpenVM为避免高开销的指数运算,转而校验以下优化关系:

$$ f \cdot u = c^\lambda. $$

但该关系的安全性建立在$u$被约束到正确子域的前提下。而OpenVM仅校验了提示值$c$非零,并未检查缩放因子是否属于$\mathbb{F}_{p^6}$。以下是修复前BLS12-381曲线的代码路径

// guest-libs/pairing/src/bls12_381/pairing.rs
let (c, s) = Self::pairing_check_hint(P, Q);
// ... 未检查s是否属于Fp6 ...
let c_conj = c.conjugate();
if c_conj == Fp12::ZERO { // 仅拒绝c为0的情况
return None;
}

BN254曲线的代码逻辑与此类似,只是判断条件为if c == Fp12::ZERO { return None; }。可见电路仅拒绝$c$为0的情况,对缩放因子则完全不做校验。

此时,优化等式已无法证明真实的配对校验结果。对于任意米勒输出$f$——即使来自虚假的配对等式——证明者只需设置:

$$ c = 1, \qquad u = f^{-1}. $$

此时$c^\lambda = 1$,待校验的关系变为:

$$ f \cdot f^{-1} = 1 = c^\lambda. $$

校验将通过。该替换方法对两种曲线均适用:BN254曲线设置$c = 1, u = f^{-1}$,BLS12-381曲线设置$c = 1, s = f^{-1}$。

通常$f^{-1}$是完整的$\mathbb{F}{p^{12}}$元素,并不属于$\mathbb{F}{p^6}$子域——而原本的子域校验正是为了阻止这类伪造行为。

还有一个容易被忽略的控制流细节:由于优化校验返回成功,本应执行完整最终指数运算的慢速 fallback 逻辑永远不会被触发。

漏洞影响

伪造任意配对校验会打破众多系统的安全基础:

  • 在BLS12-381曲线上,证明者可伪造KZG开放证明,进而破坏数据可用性、Blob验证以及PLONK或KZG验证器所依赖的多项式承诺方案。
  • 在BN254曲线上,同样的伪造行为会破坏Groth16 SNARK验证器、BLS签名校验,以及所有依赖配对等式的跨链桥或协议。
  • 任何通过OpenVM配对校验模拟以太坊地址为0x08ecPairing预编译合约的zkVM客端,都会生成伪造结果,导致EVM执行错误。

因此,所有在OpenVM客端程序中验证配对的L2 rollup、跨链桥或隐私协议,都会受到该漏洞影响。zkao将其判定为“严重”,OpenVM维护者也确认了这一评级。

修复方案

修复方法是添加缺失的子域成员校验,断言缩放因子的奇数索引系数为0:

// 只有当缩放因子属于子域Fp6时,才是可信的提示值
for i in [1, 3, 5] {
if s.c[i] != Fp2::ZERO {
return None;
}
}

像$f^{-1}$这类缩放因子通常具有非零的奇数系数,因此会被拒绝,伪造行为将无法得逞。该修复已在提交 a720e2c中实现,并随OpenVM 1.6.0版本发布。

经验总结

基础LLM在复杂代码库面前仍有局限 这是我们最初的观察,本次实验进一步验证了这一点:在两代模型的扫描结果中,所有基础LLM发现的问题最终都被归类为“信息性”而非可利用漏洞。原因正如前文所述:zkVM无法像普通库那样分解为独立原语,因此子代理向上传递的有效单元应是模块知识,而这类知识的准确表达难度极大。

缩小这一差距正是zkao上下文工程的核心目标。我们通过两种方式实现:一是启发式方法,将专家在过往人工审计中阅读代码的经验编码为规则;二是系统化方法,设计工具在不同代理间传递信息,避免超出单个上下文窗口。

zkao通过名为cryptopsy的流程发现了本次漏洞。我们将在后续文章中详细介绍该流程的工作原理,简而言之,它结合了全面分析与代码实现和学术文献的双向对照,涵盖已知攻击手段、常见陷阱、密码分析结果等内容,模拟了人工审计流程的部分环节。

自动化筛选不能仅依赖LLM生成可行PoC 我们最初尝试自动化筛选时,曾要求LLM将自身生成的报告转化为可行的PoC,想法很简单:如果PoC能运行,说明漏洞真实存在。但实际效果远不如预期——模型很擅长生成看似能运行的PoC,即使报告中的问题并不真实存在。通过添加大量无意义注释、隐藏假设、修改辅助函数、禁用校验、模拟状态或设置可疑运行参数,几乎任何虚假漏洞都能被伪装成可利用的样子。而验证这些PoC所花费的时间,甚至比直接筛选原始报告更长。

正因如此,我们后来投入大量精力改进zkao及其筛选流程,以减少误报。我们将在另一篇文章中介绍具体实现方法,以及zkao如何从漏洞筛选人员的操作中学习——经过项目初期的几次筛选后,误报率会进一步降低。

后续计划

感谢OpenVM团队快速响应,及时完成漏洞筛选与修复,并在1.6.0版本中发布了补丁。同时感谢Axiom联合创始人Yi Sun对本文提供的宝贵反馈。

这是本系列的第二篇文章。我们将继续发布其他项目中已确认并修复的漏洞,尤其是那些能帮助我们理解AI审计适用场景、局限,以及所需人工审查方式的案例。

如果你是zkVM或加密项目的维护者,且对这类研究感兴趣,我们很乐意与你合作,在漏洞被部署前及时发现并修复。这类持续AI检测正是zkao的设计目标。欢迎通过zksecurity.xyz/contact与我们联系。