AI 审计代理盯着 Cloudflare 密码学库,挑出了 7 个真漏洞
一个跑着 AI 的审计代理,盯着 Cloudflare 的密码学库看了几个月,挑出了 7 个真漏洞。
其中两个被标成 Critical,全部已经在上游修好,大部分还在 HackerOne 上拿到了赏金。找 bug 的不是安全研究员,是 Opus 4.6 和 GPT-5.3,配了一套人写的 skills。
这事是 zkSecurity 在他们博客上写的,第一篇,说后面还会有一个系列。原文链接我放这儿,技术细节比我讲得全,链接在这儿 https://blog.zksecurity.xyz/posts/circl-bugs 。被扫的库是 Cloudflare 那个做后量子和高级密码学的 CIRCL,开源在 https://github.com/cloudflare/circl 。
我盯着屏幕把这几个 bug 一个个看完,中间有那么一下没绷住。不是因为 AI 找到 bug 这件事本身,这两年 AI 挑代码毛病早就不新鲜了。让我停下来的是,它挑出来的这几个 bug,有几个是我这种天天跟代码打交道的人,扫一眼也未必看得出来的那种。
我先讲两个。你就当听个技术八卦,看完自己心里也会有个判断。
先说最直白的那个,第一个 bug,在门限 RSA 的实现里。门限签名这东西,简单讲就是把一个密钥拆成 n 份发给 n 个人,凑够一定数量的人才能签出一个有效签名,防的是单点被攻破。拆密钥要靠一个多项式,在每个人的编号上取值,取出来的就是每个人手里那份。
系数用的是 big.Int,大整数,这是对的,密码学里的数动辄几百位,普通 int 装不下。问题出在算 x 的 i 次方那一行。
xi := int64(math.Pow(float64(x), float64(i)))
好家伙。它把 x 转成了 float64,用浮点数的 Pow 算完幂,再塞回整数。
float64 的尾数只有 53 位。意思是这个数一旦超过 2 的 53 次方,大概是 9 乘 10 的 15 次方,后面的精度就被悄悄抹掉了,四舍五入,你根本收不到任何报错。而门限方案里,100 个人、门限 27 这种参数,算到 100 的 26 次方,那是 10 的 52 次方,超了 36 个数量级。就算你只有 20 个人,20 的 16 次方也早就爆了。
结果就是多项式算错,发给每个人的密钥份额是错的。运气好签名直接失败,运气不好,份额看着没毛病,凑出来的却不是原来那把钥匙。
我看到这行代码笑了一下,不是嘲笑,是那种「哦,原来大厂也会这样」的会心。你想想看,写这段代码的人肯定是懂密码学的,系数都知道要用 big.Int 了,偏偏在算个幂的时候图省事用了 math.Pow。float64 那 53 位精度的坑,是那种你知道它存在、但在具体某一行里就是会忘掉的东西。AI 不会忘。它不图省事,它就是老老实实一行行看,看到 float64 转 int64 就警觉。这是它的优势,无聊,但可靠。
Cloudflare 后来把这个定成了 Low,理由是实际触发的条件比较苛刻。这个后面还要聊,先按下。修复也简单,换成霍纳法用 big.Int 一路算到底,commit 在这儿,链接 https://github.com/cloudflare/circl/commit/f7d2180d6a77cfb283379ec6ad357ebf1d444aed 。
第一个 bug 是那种「细心就能发现」的。真正让我坐直的是第四个。
这个 bug 藏在一个叫 DLEQ 的零知识证明里。DLEQ 全称离散对数相等证明,我用大白话说,就是我要向你证明「这两组数背后是同一个秘密指数」,但我不能把那个秘密告诉你。如果一个攻击者能让验证方接受一个假的证明,这套系统就废了。
zkSecurity 的 AI 找到的攻击方式是这样的,它压根不改证明本身。
它拿一个诚实生成的、完全合法的证明,原样交给验证方,只是把配套的那个「陈述」换掉。原来的陈述里有个数叫 gx,它换成 -gx,负的。就这么一个符号的改动。
然后神奇的事情发生了,当挑战值 c 是偶数的时候,这个伪造的陈述会被接受。因为两件事刚好凑到了一起。
第一件,代数上的抵消。验证方拿 -gx 重新算的时候,负号会被 c 次方,而 (-1) 的偶数次方等于 1,负号就这么消掉了,算出来的中间值跟诚实证明一模一样。
第二件,哈希里的符号碰撞。挑战值是靠哈希那个陈述算出来的,而哈希用的是 FillBytes 这个函数。FillBytes 只写一个大整数的绝对值,把符号丢了。所以喂 -gx 进去和喂 gx 进去,哈希出来是同一个东西。
c 是偶数的概率至少是二分之一,它就是哈希输出的最低位。也就是说,随便抓一个诚实生成的证明,大概一半的概率能被这么伪造。验证方最后信了一个假命题,还觉得自己验得明明白白。
我看到这儿是真的愣了几秒。你注意到这个 bug 的构成没有,它不是某一行写错了。代数上那个 (-1) 的偶数次方等于 1,没错。序列化的时候 FillBytes 丢掉符号,单看也是个特别正常、甚至可以说合理的选择,绝对值嘛。两个单独拎出来都对的东西,凑在一起,把整个证明系统的可靠性给击穿了。

这种跨越了「代数」和「序列化」两个完全不同层面的推理,是我以前不太相信 AI 能干的活。你要同时在脑子里装着密码学的代数结构,和 Go 语言里一个字节操作函数的行为细节,还得想到把它俩联系起来。zkSecurity 自己在文里也说,恰恰是这种跨边界的推理,是他们被模型惊到最多的地方。
修复是加了个边界检查,要求每个输入都满足 0 小于 x 小于 N,负数直接在门口就被挡掉。commit,链接 https://github.com/cloudflare/circl/commit/19848a5fac78 。这个 bug Cloudflare 也定成了 Low,因为攻击复杂度高。你先记着这个「又是 Low」,马上就有意思了。
还有第七个我得讲,因为它是这批里唯一一个 zkao 自己独立找到的,前面六个是拿 Opus 和 GPT 加 skills 扫出来的,这个是他们把产品级的审计代理 zkao 单独放上去之后它自己刨出来的。
场景是属性基加密,英文缩写 CP-ABE。这东西挺酷的,你可以把一段消息按一个策略加密,比如「(在美国 而且 在财务部) 或者 (是管理员)」。每个人手里的密钥绑着自己的属性,只有属性满足这个策略的人才解得开。美国的财务员工能读,管理员能读,别人读不了,哪怕大家收到的是同一份密文。
内部实现上,它把策略变成一棵由「与门」和「或门」组成的树,叶子上挂着属性,然后把那个保护消息的秘密,顺着这棵树分发下去。逻辑必须跟布尔运算对上,
或门,把完整的秘密同时给两个孩子,因为满足任意一个分支就够了。 与门,得把秘密拆开,一个孩子拿一个随机数 r,另一个拿「父节点减 r」,只有两个凑起来才能还原父节点。
记住与门这个逻辑就行,每个孩子只能拿到一半,单独一个孩子不能还原父节点。
然后看它实际怎么处理与门的,
case Andgate:
shares[gate.In0], err = randomMatrixZp(rand, k.rows, k.cols) // In0 拿了随机数 r
...
shares[gate.In1] = newMatrixZp(k.rows, k.cols) // In1 设成了 0
shares[gate.In0].sub(shares[gate.Out], shares[gate.In1]) // In0 = 父节点 - 0
看出来了吗。随机数 r 生成完,下一秒就被扔了。In1 被设成 0,最后一行又把 In0 覆盖成「父节点减 In1」,而 In1 是 0,所以 In0 就等于整个父节点。一个孩子拿到了全部秘密,另一个啥也没有。
与门不再是与门了。它的第一个叶子,自己一个人就能还原父节点。
这还没完。这个坏掉的与门,恰好落在了一个要命的位置。为了达到更高的安全性,这套实现会用一个变换,在每个策略外面再套一层与门,而这层与门的左孩子是一个内部的「通配符」叶子。系统发出去的每一把密钥,都带着这个通配符,也就是说每一把钥匙都满足这个叶子。
现在把两件事拼起来,通配符叶子是与门的第一个孩子 In0,而 In0 恰好就是那个「会拿到全部秘密」的孩子。于是每一把密钥,都揣着一个能自己单独还原出消息密钥的叶子,不管那个策略写的是什么。
整个访问控制,归零了。任何人拿任何一把钥匙,都能解开任何密文。
而这一切的根源,就是上面那一行 In1 设成 0、随机数被白白扔掉。修复也是一行,让随机数活在 In0 里,In1 变成「父节点减 In0」,
shares[gate.In1].sub(shares[gate.Out], shares[gate.In0]) // In1 = 父节点 - 随机数
commit 在这儿,链接 https://github.com/cloudflare/circl/commit/def2fd35b8535b0b8fe84f904936ebfd84b5552b 。
zkSecurity 说,让他们印象最深的不是 AI 揪出了这个 typo,而是 AI 能理解 CP-ABE 这么绕的概念,还正确判断出了它的影响。很多模型会认出这个 typo,但会把它当成「防御纵深」或者「代码卫生」问题打发掉,不往下想了。这一想不想,差别是「一个无关紧要的小瑕疵」和「整个加密方案裸奔」。
好,三个 bug 讲完了。剩下四个我不一个个展开了,有兴趣的去看原文那张表,BLS 聚合签名缺了消息唯一性检查、HPKE 里一个位或写错了 switch 分支、还有门限 RSA 里拉格朗日系数用 int64 导致溢出加截断,都是真问题,都修了。
真正我想跟你唠的,是 zkSecurity 在文章最后总结的三点观察。因为这才是这篇东西对我这种在生产环境里给 AI 搭运行脚手架的人,最有价值的部分。我天天的活就是让模型在真实系统里跑起来、别翻车,所以他们踩的这几个点,我看着格外亲切。
第一点,AI 特别不擅长判断严重程度,而且它的偏差是不对称的。
你回头看那张表。大部分 bug,AI 自己评的严重度都比 Cloudflare 最后确认的高,它倾向于把影响往大了说。可偏偏在那个 BLS 聚合签名的 bug 上,它反过来了,把一个业内公认的严重漏洞,教科书级别的 rogue key 攻击,评成了中等。
zkSecurity 读了 AI 的推理过程,发现它其实正确识别出了那个缺失的检查,连 rogue key 攻击的名字都点出来了,但它接着钻进了一个牛角尖,BASIC 模式的规范里写着,消息唯一性这个要求应该由调用方来保证。AI 就把「调用方应该处理这个」当成了一种缓解措施,于是把严重度往下打了一档。
你看这个坑不坑。它推理没错,知识也没错,就是在最后下判断那一步,被一句「按规范这不归我管」给带偏了。这特别像我见过的一些初级工程师,技术细节门儿清,一到判断「这事到底严不严重、该不该现在就修」,就没准头了。
zkSecurity 给的解释我觉得挺诚实,他们说也没完全想明白,working hypothesis 是这样,CIRCL 是个被很多不同应用调用的库,一个 bug 到底多严重,取决于下游那些模型根本看不到的调用方。这判断本来就难。
这里还有个更微妙的东西,我得单独拎出来说。Cloudflare 是站在自己赏金项目的角度评的严重度,它衡量的是这个 bug 会不会影响到 Cloudflare 自己的线上服务。所以你看到好几个 bug 被评成 Low,包括前面那个直接击穿证明可靠性的第二个 bug。这个 Low 只代表「这段代码 Cloudflare 自己没在用,或者在 Cloudflare 的环境里影响有限」。它绝不代表这个 bug 在别人的部署里也一样无害。
一个开源密码学库,建立在它之上的可能是成百上千个项目。所以你不能看到 Cloudflare 标了 Low 就松一口气。这是我看完整篇最想提醒你的一句,严重度这东西,离开了具体的使用场景就没有意义。AI 判断不好,是因为它看不见场景;人判断得好,也是因为人心里装着场景。眼下这一步,zkSecurity 还是老老实实交给人来做。
第二点,更有意思,模型的配对不是对称的,而且角色会翻转。
这批 bug 里,六个中的五个是 Claude Opus 4.6 配他们的 skills 找到的。同样的 skills、同样的系统提示词,GPT-5.3 大多数时候是在做验证,而不是发现,只有那个 HPKE 的 bug 是它自己刨出来的。
按说这种分工你会觉得挺稳定吧,一个负责找,一个负责验。结果几周之后他们换了当时最新的组合,Opus 4.7 和 GPT-5.4 重跑了一遍,角色基本对调了。这回是 GPT-5.4 找到的 bug 更多,而 Opus 4.7 只能验证它们。
我看到这段的时候,心里咯噔一下。厉害了。这意味不了别的,就是说你今天总结出的任何「哪个模型更擅长干哪个活」的结论,下个月可能就全反过来。前沿一直在动,而且会一直动下去。
这个观察对我的实际影响是很直接的。我们做的那层让模型干活的工程,如果里面硬编码了「审计这一步用 Claude,生成那一步用 GPT」这种假设,那这套东西的保鲜期可能就几周。真正该做的,是把系统做成模型无关的,让它自己在当下挑表现最好的那个,而不是让我去猜下个月谁最强。zkSecurity 说他们做 zkao 就是奔着 model-agnostic 去的,这条我举双手赞成。
第三点,AI 会把问题一个个捡起来,但不太会把它们串成一条链。
那个门限 RSA 拉格朗日系数的 bug,AI 其实一次找到了两个完全独立的问题,一个是 int64 溢出,一个是整数截断,任意一个都足以毁掉签名。这当然是有用的活,两个都是真的。但它是把两个问题并排摆在那儿,当成一个发现提交的,没有去推理它们之间的关系,也没试着把它们串成一个更狠的、端到端的利用链。

这个观察我特别有共鸣。AI 现在很像一个特别勤奋、特别细的排查员,它能把地上所有的钉子都给你捡起来,整整齐齐摆好。但让它把这些钉子拼成一把能扎穿轮胎的道钉板,它还差点意思。安全领域最值钱的恰恰是后者,把几个看似无害的小问题组合成一个真正的攻击。这一步,目前还是人的活,或者说,是下一代工具要去啃的硬骨头。
聊到这儿,我自己心里那个判断慢慢清楚了。
我们过去两年被灌了太多「AI 要取代 XX」的叙事,安全审计也是被点名的高危工种之一。但你把这篇东西认认真真读一遍,看到的其实是一幅更真实、也更有意思的图景,AI 在「无聊但需要极度细心」的地方,比人强,它不会累,不会图省事,不会因为写代码写到凌晨就把 float64 那 53 位精度给忘了。它甚至能在代数和序列化的夹缝里,推理出一个人类专家都得盯着看半天的攻击。
但它在「需要判断」的地方,在「需要把碎片拼成全局」的地方,在「离开代码本身、去想真实世界里这到底有多严重」的地方,还差得远。zkSecurity 那句话我记下来了,AI 生成候选发现是廉价的,而可信的报告是昂贵的。中间那道从「候选」到「可信」的坎,现在还牢牢地踩在人的脚下。
这让我想起 unix 那套老哲学,一个程序只做好一件事。现在的 AI 审计,越来越像这么一个各司其职的流水线,模型负责海量、细致、不知疲倦的初筛,人负责判断、串联、和最后拍板。zkSecurity 说他们扫了 200 多个密码学项目,攒下一千多个候选发现,现在最大的瓶颈不是找不到 bug,而是没有足够的人力去甄别这一千多个哪些是真的。你品品这句话,瓶颈已经从「发现」挪到了「判断」。
这大概就是我看完这篇最深的一个感受。工具越来越强,但强的方式,不是替你思考,而是把那些消耗你注意力的、机械的、需要熬夜死磕的部分接过去,好让你能把有限的判断力,留给真正重要的那几个决定。
至于哪天 AI 连判断和串联都做得比人好,那是另一篇文章的事了。反正就目前,那把从候选到可信的钥匙,还在你手里攥着。攥好了。