AI 审计代理在 Cloudflare CIRCL 中发现 7 个漏洞
人工智能与密码学(1):人工智能在Cloudflare的Circl中发现了什么
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 已能自动完成大部分验证工作。
zkSecurity用AI扫了Cloudflare的密码学库,挖出7个真实漏洞,从浮点数精度损失到访问控制完全破防。这是AI在密码学审计里第一次证明自己能找到能用的漏洞,不是纸上谈兵。虽然后面发现AI对严重性的判断还很瞎,但整体值得安全从业者一读。
![]()
我们将自己的 AI 审计流水线对准 Cloudflare 的 CIRCL 实验性密码学库,确认了七个真实漏洞,从阈值 RSA 中一个严重的 float64 精度丢失,到基于属性加密中一处彻底的访问控制失效。这七个漏洞目前均已在上游修复。这是关于我们的智能体在开源密码学中发现漏洞系列文章的第一篇。
在 zkSecurity,我们正在构建 zkao,一个 AI 审计智能体。目标说起来简单,做起来却很难:让 AI 持续不断地审视你的代码,直到其他 AI 工具能发现的漏洞都不复存在。我们在 zkao:不断累积的安全 一文中阐述了为什么这种做法至关重要。
构建 zkao 是一个反复迭代的过程,最终目标是打造一个能够找出所有可被 AI 检测到的漏洞的自动化审计器。这涉及头脑风暴新的思路和技术,系统性地将 zkSecurity 安全研究人员的专业知识编码进 zkao,确保它能检测出最新、最严重的漏洞,同时又不偏向基准测试,而且更重要的是,持续开展实验,以理解什么有效、什么无效、模型如何演进,并加深我们对用 AI 发现漏洞的理解。其中一些实验本身就产出了值得独立于产品之外分享的内容,而这正是本系列文章的主题。
还有第二个动机。这些实验是我们为 zkao 构建基准测试套件的方式,在此过程中,它们不断揭示出关于 LLM 究竟如何推理密码学的洞见:它们在哪里敏锐,在哪里盲区,以及如何放大前者、遏制后者。漏洞是可见的产出,而推理模式才是我们最关心的部分。
几个月前,我们开始在选定的代码库上运行实验。我们使用 LLM 扫描了几个开源密码学项目,采用了两种配置:
-
仅使用 LLM,配合一个简单的提示词。
-
LLM 配合技能,其中技能由我们团队中的专家维护。
然后,对于 LLM 发现真实漏洞的重要项目,我们还运行了 zkao,看看它能否自行检测出相同的问题。在大多数情况下,zkao 不仅找到了全部问题,还识别出了更复杂、更严重的问题。
结果足够好,我们决定将其整理成文。我们以 Cloudflare 的 CIRCL 作为本系列的开篇,这是一个高级密码学与后量子密码学库。在 CIRCL 上,我们的流水线产生了许多候选发现,其中有七个值得在此报告。这七个问题现已在 upstream 修复。其中大多数已在 Cloudflare 通过 HackerOne 开展的项目中得到确认并获得了赏金。
澄清:AI 产生的是候选发现,而非最终报告。我们团队中的人类仍然验证了每个问题、检查了可利用性、在需要时最小化了 POC,并处理了披露事宜。这一 human-in-the-loop 环节仍然非常重要,因为 AI 候选发现成本低廉,而可信赖的报告则不然。
尽可能减少这一步,正是 zkao 的主要设计目标之一,虽然它仍在开发中,但当前版本已经自行承担了大部分此类验证工作。
严重程度与修复一览
在展开细节之前,有一点值得指出:AI 为其自身发现所评定的严重程度是有噪声的。以下是每个 bug 由 AI 评定的等级,以及 Cloudflare 在修复后确认的等级。我们还核实了全部七个 bug 都能被当前版本的 zkao 稳定复现。
| # | Bug | AI 严重程度 | Cloudflare 严重程度 | 修复提交 | 发现者 |
|---|---|---|---|---|---|
| 1 | TSS/RSA 多项式求值中的 float64 精度损失 | 严重 | 低 | f7d2180 | Opus 4.6 + 技能 |
| 2 | 通过证明者控制的 SecParam 进行 qndleq 伪造 | 高 | 低 | 757dde4 | Opus 4.6 + 技能 |
| 3 | BLS 聚合缺失消息区分性 | 中 | 高 | 9798df7 | Opus 4.6 + 技能 |
| 4 | 通过 FillBytes 签名碰撞破坏 DLEQ 可靠性 | 高 | 低 | 19848a5 | Opus 4.6 + 技能 |
| 5 | HPKE PSK 验证绕过(通过按位或开关) | 中 | 中(重复) | a3b4fa3 | GPT-5.3 + 技能 |
| 6 | TSS/RSA 中 int64 类型的拉格朗日系数 | 高 | 中 | 751e372 | Opus 4.6 + 技能 |
| 7 | CP-ABE 访问控制被 AND 共享缺陷攻破 | 严重 | 严重 | def2fd3 | zkao |
AI 判定的严重程度与确认的严重程度之间的差距本身就是一个有趣的洞见,我们会在最后回到这一点。现在,逐一来看这七个 bug。
Bug 1:float64 中的多项式求值
这个 bug 存在于 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 法求值替换了浮点幂运算,并将所有内容保持在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则确认为高危。
VerifyAggregate 在 sign/bls 中实现了 BLS BASIC 聚合模式。该模式仅当批次中所有消息都互不相同时才安全,这正是它抵御恶意密钥攻击的防线。该函数检查了聚合配对等式,却从未检查消息是否互不相同,把这个关键要求留给了调用方。
如果没有它,标准的 rogue key 攻击就会生效。攻击者看到受害者的公钥 $\mathsf{pk}_v$ 和一条消息 $m$ 后,可以注册 $\mathsf{pk}_a = g^{\mathsf{sk}_a} - \mathsf{pk}_v$,并在完全不知道受害者私钥的情况下,伪造出针对 $(\mathsf{pk}_v, m)$ 和 $(\mathsf{pk}_a, m)$ 的聚合签名。CIRCL 没有提供任何可供回退的持有证明(proof-of-possession)基础设施,这使得缺失的这项检查更加危险。
为什么 AI 把它称为中危?我们不得而知。阅读它的推理过程,它正确地发现了缺失的差异性检查,甚至点出了 rogue key 攻击,但随后它却锚定在这样一个事实上:BASIC 模式的约定将差异性要求归于调用方。它把“调用方本应处理这一点”当作一种缓解措施,从而下调了严重性评级。
该修复使 VerifyAggregate 拒绝由重复消息构建的批次。提交 9798df7。
Bug 4:通过 FillBytes 签名碰撞破坏 DLEQ 可靠性
回到 zk/qndleq,来看这批 bug 中最微妙、坦白说也是最有趣的一个。它完全不需要触碰证明本身。
取一个诚实、有效的证明 pi,其对应的陈述为 $S_1 = (g, g_x, h, h_x)$,该证明证实 $\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{even}} = 1$)与一个看似无害的序列化选择(FillBytes 丢弃符号)之间相互作用的结果。两者单独来看都没有错。合在一起却破坏了可靠性。跨越这种边界的推理,正是模型所揭示出的东西最令我们惊讶的地方。
关于严重程度,该智能体将其评为高,因为它是一处可靠性破坏;但 Cloudflare 因其攻击复杂度高而将其确认为低。
该修复添加了一个 checkBounds 步骤,用于对计算发起挑战,要求每个输入都满足 0 < x < N。负的 -gx 带有负号,会在造成任何损害之前被拒绝。提交 19848a5。
Bug 5:HPKE PSK 校验被按位或 switch 绕过
这个几乎是一个语言层面的陷阱。在 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。它被确认为重复项。
Bug 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)
这里实际上有两个完全独立的 bug,其中任何一个都足以破坏签名。第一个是溢出:当玩家数量约为 21 时,乘积会超过 int64 的上限($\approx 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$。有 bug 的计算顺序会计算 $\delta \cdot \lfloor 15/8 \rfloor$,当 $\delta = 120$ 时结果为 $120$;而正确值 $\delta \cdot 15 / 8$ 是 $225$。
修复方案是把整个计算移到 big.Int 上,并重新调整乘除顺序,使精确整除的保证成立。对应的提交是 751e372。这两个问题彼此无关,但智能体把它们作为单个发现一起报告,出于对智能体工作的尊重,我们保留了这样的提交方式。
Bug 7:一行 AND 份额错误导致的 CP-ABE 访问控制失效
这就是 zkao 自行发现的漏洞。在确认了上述六个问题之后,我们把它指向同一个库,看看它能发现什么,结果它报告了这个漏洞。这是对 CIRCL 的密文策略属性基加密(abe/cpabe/tkn20)中访问控制保证的彻底突破,Cloudflare 已确认其有效性。
密文策略属性基加密(CP-ABE)允许你按照诸如 (location: usa AND department: finance) OR (role: admin) 这样的策略来加密消息。用户持有与其自身属性绑定的密钥,该方案保证当且仅当这些属性满足策略时用户才能解密。身处美国的财务员工和管理员可以读取该消息,其他任何人都不能,尽管所有人收到的都是同一份密文。
在内部,tkn20 将策略转换为由 AND 门和 OR 门组成的树,属性位于叶子节点,并沿着这棵树对一个秘密(即保护消息的密钥)进行秘密共享。共享必须遵循布尔逻辑:
- OR 门将完整的秘密交给两个子节点,因为满足任一分支就足够了。
- AND 门会拆分秘密,因此你需要两个子节点才能重建它。一个子节点得到随机值
r,另一个得到parent - r,只有r + (parent - r)才能恢复父节点。
要理解这个漏洞,AND 门是你唯一需要记住的部分:每个子节点必须获得部分份额,且任何一个子节点单独都不应能重建父节点。
以下是 share 实际处理 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被设为零,而最后一行覆盖了In0用parent - In1,而这只是parent。于是,一个子节点获得了整个秘密,而另一个子节点什么也没得到。AND 门不再是 AND:它的第一个叶子节点独自就能重建父节点。
请注意,这并不会破坏正确性。这两份份额仍然会加总还原为父节点(parent + 0 = parent),因此任何满足策略的密钥仍然能够解密,由旧代码生成的密文也保持兼容。它破坏的是保密性:现在单个 AND 叶子节点就能恢复出本应需要两者同时具备才能得到的秘密。
真正让它变成彻底攻破的,是这个损坏的 AND 门所处的位置。为了达到 CCA 安全性,tkn20 应用了一种 Boneh-Katz 变换,将每个策略都包裹在一个新的外层 AND 门中,而该门的左子节点是一个内部的“通配符”叶子。权威机构签发的每一个属性密钥都携带该通配符,因此每个密钥都能满足那一个叶子。现在把这两个事实放在一起:通配符叶子是某个 AND 门的第一个子节点(In0),而 In0 恰恰就是接收完整秘密的那个子节点。因此,每个密钥都持有一个能够仅凭自身就重建消息密钥的单个叶子,无论策略是什么!
虽然它看起来只是一个简单的拼写错误 bug,但真正让我们印象深刻的是 zkao 能够对 CP-ABE 这样复杂的概念进行推理,并正确评估其影响。许多 LLM 仍然会识别出这个拼写错误,但会将其视为“纵深防御”或“代码卫生”问题而不做进一步推理。这可能导致开发者低估或忽视该漏洞。
修复只需一行。正确地共享父份额,使随机份额保留在 In0 中,而 In1 成为补集:
shares[gate.In1].sub(shares[gate.Out],shares[gate.In0])// In1 = parent - random
现在 In0 保留其随机值,In1 持有 parent - In0,因此任何一个 AND 叶子节点都不会单独携带秘密。提交 def2fd3。
我们学到的几点
有三点观察让我们印象深刻。
AI 在严重性判断上表现不佳,而且这种不佳是不对称的。 再看一下顶部的表格。如果我们以 Cloudflare 确认的严重性作为“真实基准”,那么在大多数情况下,智能体高估了它所发现问题的实际影响。但在 BLS 漏洞上,情况却反了过来,它低估了一个广为人知的严重缺陷,把一个干净的恶意密钥攻击仅仅标记为中等。我们目前还没有完整的解释,也没有解决方案。我们的工作假设是,当目标是一个被众多不同应用广泛使用的库(如 CIRCL)时,这确实很难,因为影响取决于模型无法看到的下游调用方。我们认为,加入一个一致的严重性矩阵,再加上一个明确的威胁建模步骤,将有助于模型在整个系统(及其潜在集成方式)的层面而非局部代码层面来推理影响。目前,严重性判断仍然是我们交由人类负责的那部分分诊工作。
在 zkao 中,我们暂时通过让开发者借助用户配置(
zkao.md)来阐明其威胁模型和严重性偏好,以此绕过这一问题,同时我们也在持续迭代改进 zkao 的严重性设置。zkao 能够稳定复现 PoC 这一点也减少了误报,因此我们对真实问题有信心。尽管如此,我们仍在努力以更系统化的方式改进其默认严重性判定,因为这对开发者来说至关重要。同样值得注意的是,Cloudflare 是通过其漏洞赏金计划的视角来评估严重程度的,而该计划衡量的是某个漏洞是否会影响其线上服务。例如,Bug 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 会收集问题,但并不总能将它们串联起来。 Bug 6 就是明证。该智能体把两个完全独立的 bug——一个溢出和一个整数截断——打包进了同一条发现中。两者都是真实存在的,所以这确实是真正有用的工作。但它只是把它们并排呈现,而没有推理它们之间如何关联,我们在其他地方也见过同样的模式:若干真实的观察被收集在一起,却没有任何解释,也没有任何尝试将它们串联成一个更具影响力的漏洞利用。
这正是我们在 zkao 中所做的——我们构建了新颖的流程,来实现那种将彼此独立的问题串联成真正端到端漏洞利用的 bug 组合。
下一步
我们感谢 CIRCL 的维护者,他们迅速修复了所有报告的问题。这是系列文章的第一篇;随着其他项目中已确认的 bug 得到解决,我们将持续发布。
我们扫描了 200 多个密码学项目(从下载量最高的密码学 crate/包中挑选),最终得到了一千多条候选发现。因此,如今最大的瓶颈是分诊。每一条报告的 bug 都必须由我们的专家检查其技术有效性,之后才能提交给项目方,因为我们和其他人一样讨厌 AI 垃圾信息。这需要大量人力,因为我们尚未完全信任我们正在构建的自动化分诊流程(它正在变得更好)。所以我们优先选择了一些维护良好的最热门项目。
如果你在维护一个密码学项目,并且对此感兴趣,我们很乐意与你一同进行验证,以便严重的漏洞能够被及时发现并修复。如果目前还没有进行过扫描,或者你的代码库自上次扫描以来已发生重大变化,我们很乐意重新运行一次扫描。zkao 正是为这种持续性的 AI 覆盖而打造的。请通过 zksecurity.xyz/contact 联系我们。
来源:Hacker News 热门(buzzing.cc 中文翻译) · blog.zksecurity.xyz