AI 审计代理在 Cloudflare CIRCL 中发现 7 个漏洞
阅读原文· blog.zksecurity.xyzzkSecurity用AI扫了Cloudflare的密码学库,挖出7个真实漏洞,从浮点数精度损失到访问控制完全破防。这是AI在密码学审计里第一次证明自己能找到能用的漏洞,不是纸上谈兵。虽然后面发现AI对严重性的判断还很瞎,但整体值得安全从业者一读。
zkSecurity 的 AI 审计代理 zkao 持续扫描 Cloudflare 的 CIRCL 密码学库,使用 Opus 4.6 + skills 和 GPT-5.3 + skills 等模型发现并确认了 7 个真实漏洞。其中包括阈值 RSA 中 float64 精度丢失(AI 自评 Critical)和属性基加密(CP-ABE)访问控制完全失效(Critical,由 zkao 自行发现)。所有漏洞已在上游修复,多数在 HackerOne 上获得确认和奖励。AI 生成的候选发现仍需人工验证,但 zkao 已能自动完成大部分验证工作。
![]()
我们将AI审计管线对准了Cloudflare的CIRCL实验性密码学库,确认了七个真实漏洞,从阈值RSA中的关键性float64精度丢失,到基于属性的加密中完整的访问控制失效。这七个漏洞均已被上游修复。这是本系列的第一篇文章,后续将介绍我们的智能体在开源密码学项目中发现的更多漏洞。
zkSecurity正在构建zkao,一个AI审计智能体。目标说起来简单做起来难:让AI持续审视你的代码,直到其他AI工具能发现的所有漏洞都被消除。我们在《zkao:安全复利》一文中阐述了这种方法为何重要。
构建zkao是一个迭代过程,最终目标是创建一个能自动审计、发现AI所能检测到的所有漏洞的审计器。这涉及构思新想法和新方法,将zkSecurity安全研究人员的专业知识系统性地编码进zkao,确保它能检测最新、最严重的漏洞而不偏向于基准测试,并且——关键的是——持续进行实验,以理解什么有效、什么无效、模型如何演进,并加深我们对AI漏洞发现的理解。其中一些实验本身产出了值得分享的成果,与产品无关——这正是本系列文章要讲述的内容。
还有第二个动机。这些实验是我们为zkao构建基准测试套件的方式,在此过程中,它们不断揭示LLM实际上如何推理密码学:它们在哪些方面敏锐,在哪些方面盲目,以及如何放大前者、控制后者。虽然漏洞是可见的输出,但我们最关心的是推理模式。
几个月前,我们开始在选定的代码库上运行实验。我们使用LLM扫描了几个开源密码学项目,采用两种配置:
-
仅LLM,配合简单的提示词。
-
LLM + 技能,技能由我们团队的专家维护。
接着,针对大语言模型发现真实漏洞的重要项目,我们还运行了 zkao 来检验它能否自主检测出同样的问题。在大多数情况下,zkao 不仅找出了所有漏洞,还识别出更复杂、更严重的问题。
结果足够理想,我们决定将其整理成文。我们以 Cloudflare 的 CIRCL(一个先进后量子密码学库)作为本系列的开端。在 CIRCL 上,我们的流水线产出了大量候选发现,其中有七个值得在此报告。这七个漏洞现已全部在上游修复。其中大部分已通过 Cloudflare 在 HackerOne 上的项目得到确认并获得奖金。
说明:AI 产出的是候选发现,而非最终报告。我们团队的人工依然负责验证每个问题、检查可利用性、按需精简概念验证(POC)以及处理披露流程。这一人工介入环节仍然非常重要,因为 AI 候选发现成本低廉,而可信报告则不然。
精简这一环节正是 zkao 的主要设计目标之一。虽然它仍在完善中,但当前版本已经承担了相当一部分验证工作。
严重程度与修复概览
在详细介绍之前,有一点值得说明:AI 对自己发现的漏洞所分配的严重程度存在噪声。以下是每个漏洞的 AI 评分,以及 Cloudflare 在修复后确认的评分。我们还确认了当前版本的 zkao 可以稳定复现所有七个漏洞。
| # | 漏洞 | AI 严重程度 | Cloudflare 严重程度 | 修复提交 | 发现者 |
|---|---|---|---|---|---|
| 1 | TSS/RSA 多项式求值中的 float64 精度损失 | 严重 | 低 | f7d2180 | Opus 4.6 + skills |
| 2 | 通过证明者控制的 SecParam 实现的 qndleq 伪造 | 高 | 低 | 757dde4 | Opus 4.6 + skills |
| 3 | BLS 聚合缺少消息唯一性 | 中 | 高 | 9798df7 | Opus 4.6 + skills |
| 4 | 通过 FillBytes 符号碰撞导致的 DLEQ 正确性破坏 | 高 | 低 | 19848a5 | Opus 4.6 + skills |
| 5 | 通过按位或切换绕过 HPKE PSK 验证 | 中 | 中(重复) | a3b4fa3 | GPT-5.3 + skills |
| 6 | TSS/RSA 拉格朗日系数在 int64 中 | 高 | 中 | 751e372 | Opus 4.6 + skills |
| 7 | 通过与门共享漏洞破坏 CP-ABE 访问控制 | 严重 | 严重 | def2fd3 | zkao |
AI 评估的严重性与最终确认的严重性之间的差距本身就是一个有趣的见解,我们将在最后回顾这一点。现在,七个漏洞逐一来看。
漏洞 1:float64 中的多项式求值
这个漏洞存在于 CIRCL 的门限 RSA 实现中(tss/rsa)。门限签名使用 Shamir 风格秘密共享将秘密拆分给 n 个参与者。Deal() 在每个参与者的索引处计算一个秘密多项式。系数本应是 big.Int,也确实如此,但计算 x^i 项的方式如下:
// tss/rsa/rsa_threshold.go
xi:=int64(math.Pow(float64(x),float64(i)))
float64 的尾数有 53 位。一旦 $x^i$ 超过 $2^{53}$(大约 $9 \times 10^{15}$),在转换回整数之前结果就会被静默舍入。例如,有 100 个参与者且门限为 27 时,在 $x = 100$ 且 $i = 26$ 下计算,需要 $100^{26} = 10^{52}$,这超出了 $2^{53}$ 36 个数量级。甚至 $x = 20$、$i = 16$ 时就已经出错了。
其后果是多项式求值出错,因此分配给参与者的密钥分片是错误的。根据参数的不同,签名组合要么直接失败,要么产生看似正确但无法重构出目标密钥的分片。我们的智能体将此标记为严重,因为它会导致生成错误的密钥分片,从而破坏协议的正确性。Cloudflare 最终将问题评估为低严重性,理由是受影响的条件在实际中出现的可能性很低。
修复方案是将浮点数幂运算替换为代码自身的 TODO 注释一直建议的霍纳方法(Horner's method)求值,所有计算保持在 big.Int 中。提交记录 f7d2180。
漏洞 2:通过证明者控制的安全参数伪造 DLEQ 证明
这个漏洞位于 zk/qndleq 中,这是 CIRCL 针对 $(\mathbb{Z}/n\mathbb{Z})^*$ 中平方子群的 DLEQ(离散对数相等性)证明。DLEQ 证明用于证明两个对共享相同的离散对数;如果攻击者能让验证者接受一个针对虚假陈述的证明,那么这个证明系统就被攻破了。
该证明中的挑战以 Fiat-Shamir 风格推导得出,其位长度由 SecParam 控制。问题在于 SecParam 位于 Proof 结构体内部:
typeProofstruct{
Z,C*big.Int
SecParamuint
}
在验证过程中,代码会使用证明本身提供的 `SecParam` 重新计算挑战。该字段是攻击者可控制的。如果将 `SecParam` 设为 1,挑战就会坍缩为单个比特,值为 $0$ 或 $1$:每次伪造尝试相当于一次抛硬币。如果将 `SecParam` 设为 8,暴力破解大约需要 $2^8 = 256$ 次尝试。无论哪种情况,健全性都不复存在。
这是一个反复出现的典型漏洞实例:本应由验证者固定的安全参数,却被从证明者提供的数据中读取。修复方案是将 `SecParam` 从证明中移除,并让 `Verify` 将其作为显式参数传入,由验证者设定。提交编号 757dde4。
漏洞 3:BLS 聚合验证缺少消息区分要求
这是该批次中 AI 低估的漏洞。智能体将其标记为中等。实际上,这是一个教科书级的恶意公钥攻击,属于公认的关键级缺陷;我们将其报告为关键级,而 Cloudflare 确认其为高危。
`sign/bls` 中的 `VerifyAggregate` 实现了 BLS BASIC 聚合模式。该模式仅在批次中所有消息互不相同时才是安全的,这是其抵御恶意公钥攻击的防御机制。该函数检查了聚合配对等式,但从未检查消息是否互不相同,将这个关键要求留给了调用方。
缺少该检查,就会受到标准恶意公钥攻击的影响。攻击者看到受害者的公钥 $\mathsf{pk}_v$ 和一条消息 $m$ 后,可以注册 $\mathsf{pk}_a = g^{\mathsf{sk}_a} - \mathsf{pk}_v$,并伪造一个覆盖 $(\mathsf{pk}_v, m)$ 和 $(\mathsf{pk}_a, m)$ 的聚合签名,而无需知道受害者的私钥。CIRCL 没有附带任何可依赖的持有证明基础设施,这使得缺失的检查更加危险。
AI 为什么将其判定为中等风险?我们不得而知。阅读它的推理过程发现,它确实正确识别出了缺少消息区分检查,甚至提到了恶意公钥攻击,但随后它锚定在一个事实上:BASIC 模式的合约将消息区分要求放在了调用方身上。它把“调用方理应处理这个问题”作为一种缓解措施,从而降低了严重等级。
修复方案是让 `VerifyAggregate` 拒绝包含重复消息的批次。提交编号 9798df7。
漏洞 4:通过 `FillBytes` 签名碰撞导致的 DLEQ 健全性破坏
回到 zk/qndleq,讨论批次中最微妙、坦率地说也是最有趣的错误。它完全不需要触碰证明本身。
取一个针对语句 $S_1 = (g, g_x, h, h_x)$ 的诚实有效证明 $\pi$,该语句断言 $\log_g(g_x) = \log_h(h_x) = x$。一个不知道 $x$ 的攻击者将同一个 $\pi$ 出示给验证者,但将其配对一个不同的语句 $S_2 = (g, -g_x, h, h_x)$,其中 $-g_x$ 是负数 big.Int `new(big.Int).Neg(gx)`。
只要挑战 $c$ 是偶数,这个伪造的语句就会被接受,因为两件事恰好同时成立。
代数消项。验证者从 $-g_x$ 重新计算其值,符号因子直接提出:
$$(-g_x)^c \bmod N = (N - g_x)^c \bmod N = (-1)^c \cdot g_x^c \bmod N.$$当 $c$ 为偶数时,$(-1)^c = 1$,因此验证者重构出与诚实证明者完全相同的中间值。
哈希中的符号碰撞。挑战值是通过哈希语句得出的,哈希过程使用 FillBytes,该函数写入 big.Int 的绝对值并去掉符号。因此 `doChallenge(..., -gx, ...)` 和 `doChallenge(..., gx, ...)` 会得到相同的哈希值。
这里,$c$ 为偶数的概率至少为 $1/2$(它仅仅是哈希输出的低比特位),因此攻击在大约一半的诚实生成的证明上都能成功。验证者会相信 $\log_g(-g_x) = \log_h(h_x)$,而这是错误的。以下便是概念验证的核心:
// honest proof for (g, gx, h, hx), selected to have an even challenge c
gxNeg:=new(big.Int).Neg(gx)// -gx, attacker needs no knowledge of x
forgedAccepted:=proof.Verify(g,gxNeg,h,hx,N)// accepted!
这个错误的突出之处在于,它不是一行马虎的代码。它是一个代数恒等式($(-1)^{\text{偶数}} = 1$)与一个看似无害的序列化选择(FillBytes 丢弃符号)之间的相互作用。各自单独看都没有问题,但它们结合在一起就破坏了可靠性。跨越这种边界进行推理,正是模型让我们最感意外的地方。
在严重性评级上,智能体将其评为高,因为它破坏了可靠性,但 Cloudflare 确认其为低,因为攻击复杂度较高。
修复方法是在挑战计算中添加一个 checkBounds 步骤,要求每个输入都满足 $0 < x < N$。负数 $-g_x$ 带有负号,会在造成任何损害之前被拒绝。提交 ID: 19848a5。
错误 5:通过按位或开关绕过了 HPKE PSK 验证。
这几乎是一个语言层面的陷阱。在 HPKE 的 verifyPSKInputs 函数中,switch 标签使用了按位或编写:
// hpke/util.go
casemodeBase|modeAuth:// 0x00 | 0x02 == 0x02, i.e. only modeAuth
casemodePSK|modeAuthPSK:// 0x01 | 0x03 == 0x03, i.e. only modeAuthPSK
在 Go 语言中,case a | b: 是一个单独的 case,其值为两个常量的按位或结果,而不是两个独立的 case。因此 case modePSK | modeAuthPSK 实际上等同于 case 0x03,而 modePSK(0x01)根本不会匹配到任何 case。原本用于在 PSK 模式下拒绝缺失 PSK 的分支被直接跳过了。
效果是:SetupPSK(..., nil, nil) 会使用一个空的 PSK 继续执行,而不是被拒绝。PSK 模式本应要求提供 PSK 材料;这一缺陷静默地丢弃了身份验证前提条件,使得部署以比配置更弱的模式运行。修复方法是将按位或改为逗号分隔的多个 case(即 case modePSK, modeAuthPSK:)。提交号为 a3b4fa3。该问题被确认为重复报告。
漏洞 6:int64 中的拉格朗日系数
回到 tss/rsa 中,一旦份额分配完毕,组合签名就需要进行拉格朗日插值。computeLambda 函数使用 int64 类型构建了每个拉格朗日系数的分子和分母:
// tss/rsa/rsa_threshold.go
num:=int64(1)
den:=int64(1)
for_,s:=rangeS{
jprime:=int64(s.Index)
ifjprime==j{continue}
num*=i-jprime// overflows int64 for moderate player counts
den*=j-jprime
}
lambda.Div(big.NewInt(num),big.NewInt(den))// truncating integer division
lambda.Mul(delta,&lambda)
这里实际上存在两个完全独立的漏洞,任何一个都足以破坏签名。第一个是溢出:当大约有 21 个参与者时,乘积会超过 int64 的上限(约 $9.2 \times 10^{18}$)并静默地回绕,因为 Go 语言在整数溢出时不会触发 panic。随后 computeLambda 会返回一个错误且通常为负的系数。
第二个是截断问题,即使没有发生溢出也会出现。代码先计算 num / den,然后才乘以 delta。在 Shoup 的方案中,$\delta \cdot \text{num}$ 保证能被 den 整除,但单独的 num 并不总能被整除,只要份额索引不是连续的——而这对于 $t$-of-$n$ 子集来说是正常情况。以 3-of-5 方案为例,组合份额 $\{1, 3, 5\}$:对于其中一个系数,$\text{num} = (0-3)(0-5) = 15$,$\text{den} = (1-3)(1-5) = 8$。有缺陷的顺序计算出 $\delta \cdot \lfloor 15/8 \rfloor$,当 $\delta = 120$ 时结果为 $120$;而正确的值 $\delta \cdot 15 / 8$ 应是 $225$。
这个修复将整个计算迁移到 big.Int,并重新排列了乘法与除法的顺序,从而保证了整除性成立。该修复对应提交 751e372。这两个问题并不相关,但智能体将它们作为单一发现一起报告,我们出于对其工作的尊重,保留了这个提交方式。
漏洞 7:由于一行 AND 共享错误导致的 CP-ABE 访问控制破坏
这是 zkao 自行发现的一个漏洞。在确认了上述六个问题之后,我们让它对同一个库进行探查以查看还能发现什么,它报告了此问题。它完全破坏了 CIRCL 的密文策略属性基加密(abe/cpabe/tkn20)中的访问控制保证,Cloudflare 已确认其有效性。
密文策略属性基加密(CP-ABE)允许你根据策略(例如(位置:美国 且 部门:财务)或(角色:管理员))加密消息。用户持有与其自身属性绑定的密钥,该方案保证:当且仅当这些属性满足策略时,用户才能解密。美国财务部门的员工和管理员可以读取消息,而其他任何人都不能,即使所有人都收到相同的密文。
在内部,tkn20 将策略转化为一个由 AND 和 OR 门组成的树,叶子节点为属性,并将一个秘密(保护消息的密钥)沿该树进行秘密共享。共享必须遵循布尔逻辑:
- OR 门将完整秘密赋予两个子节点,因为满足任意一个分支就足够了。
- AND 门则拆分秘密,因此你需要两个子节点才能重建它。一个子节点获得随机值 r,另一个子节点获得 parent - r,只有 r + (parent - r) 才能恢复父节点。
要理解这个漏洞,你只需记住 AND 门这一关键点:每个子节点必须获得一个部分份额,且任一子节点单独都不能重建父节点。
以下是共享实际处理 AND 情况的方式:
// abe/cpabe/tkn20/internal/tkn/formula.go
caseAndgate:
shares[gate.In0],err=randomMatrixZp(rand,k.rows,k.cols)// In0 = random r
...
shares[gate.In1]=newMatrixZp(k.rows,k.cols)// In1 = 0
shares[gate.In0].sub(shares[gate.Out],shares[gate.In1])// In0 = parent - 0
随机份额被生成后立即丢弃。In1 被设为零,最后一行用 parent - In1 覆盖了 In0,结果就是 parent。所以一个子节点接收了完整秘密,而另一个子节点什么也没得到。AND 门不再是一个 AND:它的第一个叶子节点独自重建了父节点。
请注意,这并不破坏正确性。两个份额相加仍然等于父节点(父节点 + 0 = 父节点),因此任何满足策略的密钥依然能够解密,旧代码生成的密文也保持兼容。但被破坏的是保密性:单个AND叶子节点现在就能恢复出本应同时需要两个叶子节点才能得到的秘密。
而让这个问题升级为彻底安全漏洞的关键,在于这个被破坏的AND门出现在哪里。为了实现CCA安全,tkn20运用了Boneh-Katz变换,将每个策略包裹在一个新的外部AND门中,该门的左子节点是一个内部"通配符"叶子节点。权威机构颁发的每一个属性密钥都携带这个通配符,因此每个密钥都能满足该叶子节点。现在把两个事实结合起来:通配符叶子节点是一个AND门的第一个子节点(In0),而In0恰好是接收完整秘密的子节点。因此,每个密钥都持有一个单独的叶子节点,该节点能独立重构出消息密钥,完全无视具体策略!
尽管这看起来像是一个简单的拼写错误漏洞,但令我们印象深刻的是,zkao能够推理CP-ABE这样复杂的概念,并准确评估其影响。许多大语言模型虽然也能识别出这个拼写错误,但往往只会将其视为"纵深防御"或"代码规范"问题,而不再进一步推理。这可能导致开发者低估或忽略该漏洞。
修复只需一行代码。正确地对父节点进行份额分配,让随机份额保留在In0中,In1则变为互补份额:
shares[gate.In1].sub(shares[gate.Out],shares[gate.In0])// In1 = parent - random
现在In0保留其随机值,In1持有父节点减去In0的值,因此两个AND叶子节点都无法单独携带秘密。提交编号:def2fd3。
我们学到的一些东西
有三点观察让我们记忆犹新。
AI 在严重性判断上表现不佳,而且这种不佳是单向不对称的。再看一眼顶部的表格。如果我们把 Cloudflare 确认的严重性作为“事实依据”,那么大多数情况下,智能体高估了它发现的问题的影响。但在 BLS 漏洞上,它却走向了另一个极端,低估了一个广为人知的严重缺陷,将一个清晰的恶意密钥攻击标记为仅中等严重性。我们目前还没有完整的解释,也没有解决方案。我们的工作假设是:当目标是一个像 CIRCL 这样被众多不同应用程序使用的库时,判断严重性确实很困难,因为影响取决于下游调用方,而模型无法看到它们。我们认为,增加一个一致的严重性矩阵,再加上一个明确的威胁建模步骤,能帮助模型从整个系统(及其潜在的集成方式)的层面来推理影响,而不是只看局部代码。目前来看,严重性判断仍然是分类处理中我们信任人类来负责的部分。
在 zkao 中,我们暂时通过让开发者通过用户配置文件(zkao.md)来明确他们的威胁模型和严重性偏好,以此绕开这个问题,并且我们持续迭代改进 zkao 的严重性设置。由于 zkao 能够稳定地生成概念验证,这也减少了误报,因此我们对真实问题有信心。不过,我们仍在努力以更系统的方式改进其默认严重性分配,因为这对开发者至关重要。
同样值得注意的是,Cloudflare 是透过其漏洞赏金计划的视角来评估严重性的,该计划衡量一个漏洞是否影响其线上服务。例如,漏洞 2 被评为低严重性,尽管它完全破坏了证明的可靠性。这个评级仅表明受影响的代码要么未被 Cloudflare 服务使用,要么在 Cloudflare 环境中影响有限。这并不意味着在其他部署中影响也如此之小——对于一个可能被众多项目作为基础构建的库来说,这一点尤其重要。
模型配对并非对称的,角色也可能互换。六个漏洞中有五个是由 Claude Opus 4.6 搭配我们的技能包发现的。在相同的技能包和相同的系统提示词下,GPT-5.3 主要还是进行验证而非发现;表中那个 HPKE 漏洞是它自己找出来的。我们原本没指望这种分工能维持稳定,事实也确实如此。几周后,我们用当时最新的配对——Opus 4.7 和 GPT-5.4——重新扫描了一遍,结果角色几乎完全颠倒:GPT-5.4 发现了更多漏洞,而 Opus 4.7 却只能做验证了。这很好地提醒我们,不要根据某个特定模型名称就过度拟合结论。前沿已再次前进,而且还将继续前进。我们会在另一篇文章中探讨这一点。
这个模式正是我们构建 zkao 要“与模型无关”的原因——让它的性能保持最佳状态,而你无需去猜测下个月哪个模型最好。
这个 AI 会收集问题,但并不总能将它们串联起来。漏洞 6 就很能说明问题。这个智能体把两个完全独立的 bug——一个溢出和一个整数截断——打包成了同一个发现。两个 bug 都是真实的,所以这确实是有用的工作。但它只是把它们并列呈现,而没有推理它们之间如何关联。我们在其他地方也看到过同样的模式:多个真实的观察结果被收集在一起,却没有给出任何解释,也没有尝试将它们串联成更具影响力的利用链。
这正是我们在 zkao 中构建全新流程的地方——实现那种将独立问题串联成真正的端到端利用链的漏洞组合能力。
下一步计划
感谢 CIRCL 维护团队,他们迅速修复了所有已报告的问题。这是本系列的第一篇文章;随着其他项目的确认漏洞得到修复,我们将继续发布相关内容。
我们扫描了超过 200 个加密项目(从下载量最高的密码学 crates/包中挑选),最终得到了超过一千个候选发现。结果是,目前最大的瓶颈在于分类筛选。每个报告的 bug 在到达项目方之前,都必须由我们的专家进行技术有效性核查,因为我们和所有人一样讨厌 AI 垃圾信息。这需要大量的人力投入,因为我们还未能完全信任当前正在构建的自动化分类流程(不过它在不断改善)。因此,我们优先处理了一些维护较好、最受欢迎的项目。
如果你维护一个加密项目并且对此感兴趣,我们很乐意与你一起验证,以便及时发现并修复严重 bug。如果尚未扫描过,或者自从上次扫描后你的代码库发生了重大变化,我们很乐意重新进行一次扫描。这种持续的 AI 覆盖正是 zkao 的定位所在。请通过 zksecurity.xyz/contact 联系我们。
zkSecurity 为加密系统提供审计、研究和开发服务,涵盖零知识证明、MPC、FHE、共识协议等领域。
Learn More →