Aztec Connect 黑客攻击深度剖析:结算边界绕过漏洞

Aztec Connect 黑客攻击深度剖析:结算边界绕过漏洞

2026 年 6 月 14 日,一名攻击者在一个原子交易中,从已废弃的 Aztec Connect RollupProcessor 合约(地址 0xff1f2b4adb9df6fc8eafecdcbf96a2b351680455)中盗走了约 219 万美元。该合约自 2024 年 3 月起就已废弃,且管理密钥已被销毁,变得不可变,无法打补丁。

这起事件堪称一堂教科书式的安全课,告诉我们:L1 验证逻辑与零知识证明承诺之间哪怕只有一丝一毫的不一致,都可能酿成灾难性的资金损失。下面我们就来拆解到底发生了什么、为什么会发生,以及如何防范。


根本原因:结算边界不匹配

漏洞的根源在于两个关键组件之间的结构性空隙:

  • L1 结算循环的遍历范围
  • ZK 公开输入的哈希承诺范围

在正常的安全模型下,我们假设每个公开输入槽要么在 L1 合约层得到验证,要么被 ZK 电路约束为 publicValue == 0。而这次攻击恰恰利用了 numRealTxsdecoded_slots 之间的缺口。

关键点在于:numRealTxs 直接从 calldata 偏移量 4516 处读取,链上没有任何约束 。而 decoded_slots 为了满足 SHA256 预编译的数据布局,被向上取整到 numTxsPerRollup 的倍数。这个取整操作制造出了一个"空隙区域",攻击者可以随意填充。

SHA256 预编译覆盖了全部 32 个槽(8192 字节 = 32 × 256B),并且这些空隙槽也被 ZK 证明所承诺。然而,L1 结算循环只遍历第一个槽。空隙槽 2...32 在 L1 层根本没有得到任何验证。而 ZK 电路中本应对这些空隙槽的 publicValue 约束为 0,但要么被绕过了,要么压根儿就没施加。

这种双路径的分叉就是漏洞的核心:同样的 calldata 被两条路径消费,但它们的上界不同。ZK 证明认了 32 个槽,而 L1 结算逻辑只认 1 个槽。


攻击步骤详解

第一阶段:铸造 --- 凭空产生 L2 余额(Rollup #13277~13283,共 7 次)

攻击者 EOA(0x0f18d8b44a740272f0be4d08338d2b165b7edd17)调用了主入口合约(0x06f585f74e0da633ae813a0f23fb9900b61d0fcd),使用选择器 0x6f3ce701。主合约依次将调用分发给三个 Relayer 合约,每个 Relayer 都硬编码了恶意的 Rollup calldata。

每次 calldata 的关键参数:

  • numRealTxs = 1rollupSize = 1024numInnerRollups = 32
  • 槽 1 (L1 可见):proofId = 0(空操作),publicValue = 0
  • 槽 2~32 (31 个空隙槽,L1 不可见):proofId = 1(存款),publicValue = NpublicOwner = 攻击者的 L2 地址

每次调用都附带了相应的 ZK 证明,而该电路并未约束空隙槽的 publicValue 为 0。

Relayer 合约依次调用了 RollupProcessor.processRollup()

  • Relayer A 处理了 Rollup #13277~13281(5 次)
  • Relayer B 处理了 Rollup #13282~13283(2 次)

每次调用内部发生了什么:

  • Verifier 成功验证了 ZK 证明 ------ SHA256 承诺覆盖了全部 32 个槽
  • L1 结算循环的 end = 1 × TX_PUBLIC_INPUT_LENGTH = 1 个槽,只处理了空操作
  • 空隙槽 2...32 中的伪造存款虽然被 ZK 提交进了新的 Merkle 根,但 L1 完全没检查

于是,攻击者的 L2 余额累积了 7 × 31N 的未经审核的充值,而 L1 资金池纹丝未动。

第二阶段:提款 --- 变现离场(Rollup #13284~13290,共 7 次)

攻击者利用 7 个提款 Rollup 将全部虚增的 L2 余额兑换为 L1 资产:

  • Rollup #13284:270,513.054 DAI
  • Rollup #13285:167.890 wstETH
  • Rollup #13286:4,873.857 yvDAI
  • Rollup #13287:16.570 yvWETH
  • Rollup #13288:9,273.734 LUSD
  • Rollup #13289:359.047 yvLUSD
  • Rollup #13290:908.987 ETH

这个单笔原子交易成功执行,gasUsed = 4,513,539。合约层面的操作无法部分回滚。


代码分析:问题出在哪

Decoder.sol 中的隐患

numRealTxs 直接从 calldata 偏移量读取,链上没有任何校验。这等于把信任完全交给了 calldata ------ 合约假定 calldata 能准确反映真实交易数量。

而取整得到 decoded_slots 的过程本身就制造了隐式缺口。合约默认 ZK 电路会处理超出 numRealTxs 的槽,但这个假设在电路不强制约束时是不安全的。

solidity 复制代码
// 简化示意:Decoder 中读取 numRealTxs
uint256 numRealTxs = uint256(calldata[4516:4524]); // 无任何检查
uint256 decoded_slots = ((numRealTxs + numTxsPerRollup - 1) / numTxsPerRollup) * numTxsPerRollup;

RollupProcessorV3.sol 中的结算循环

结算循环只遍历 numRealTxs 个槽,而不是 decoded_slots。这意味着超出 numRealTxs 的槽:

  1. 被纳入了 SHA256 承诺(因为 SHA256 处理了所有 decoded_slots
  2. 被 ZK 证明验证(理论上)
  3. 但未被 L1 合约的结算逻辑验证
solidity 复制代码
// 简化示意:L1 结算循环
for (uint256 i = 0; i < numRealTxs; i++) {
    // 只处理真实交易,忽略空隙槽
    processPublicInput(i);
}
// SHA256 却已经覆盖了 decoded_slots 个槽

根本问题在于:L1 合约把空隙槽的验证完全外包给了 ZK 电路,却没有设置备用检查机制。

SHA256 预编译的行为

SHA256 预编译会处理全部 32 个槽,不管 numRealTxs 是多少。这本身没问题 ------ 预编译不知道交易边界。但跟 L1 循环的有限范围一结合,就形成了验证盲区。


正确的修复方案

修复方案 1:让结算循环与 SHA256 承诺范围对齐

问题 :L1 结算循环只覆盖 numRealTxs 个槽,而 SHA256 覆盖所有 decoded_slots

修复 :结算循环必须遍历 所有 被 ZK 证明承诺的槽,而不只是 numRealTxs。循环上界应为 decoded_slots 或与之相等的派生值,确保与 SHA256 输入大小一致。

效果:这样一来,每个影响状态根的槽都会被 L1 合约独立验证,缺口被彻底堵死。

注意:这会增加 Gas 成本,但安全需要代价。另一种思路是在计算 SHA256 承诺之前,先验证空隙槽已归零。

修复方案 2:在链上校验 numRealTxs

问题numRealTxs 直接来自 calldata,没有任何约束。

修复 :从实际交易数据结构中推导 numRealTxs,而不是盲目信任 calldata。或者强制要求 numRealTxs 必须等于非零公开输入槽的数量。

效果:攻击者无法再操纵空隙大小。

修复方案 3:在 ZK 电路中强制约束空隙槽

问题 :电路没有约束空隙槽的 publicValue 为 0。

修复 :在电路中添加明确约束,确保任何超出 numRealTxs 的槽,其 publicValue == 0

效果 :堵上电路层的验证缺口。但 仅凭此还不够 ------ L1 合约仍然必须验证所有被承诺的槽。

修复方案 4:L1 对 ZK 公开输入进行二次校验

问题:L1 合约完全依赖 ZK 电路来验证空隙槽。

修复:L1 合约应当独立检查每一个被 ZK 证明承诺的公开输入槽是否满足合约不变量。具体包括:

  • 遍历所有被承诺的槽
  • 验证存款槽对应着真实的待处理存款
  • 验证提款槽背后有真实的 L2 余额支撑

效果:形成纵深防御。即使 ZK 电路有漏洞,L1 合约也能作为兜底验证者。

修复方案 5:废弃合约的资产迁移

问题:合约已废弃且不可变,但仍存有遗留用户资产。

修复:在放弃管理权限之前,项目团队应当:

  • 将所有资产迁移到新合约
  • 设置充足的提款窗口,并提前公告
  • 考虑引入"断路器",能在紧急情况下冻结操作
  • 如果可能,保留更新验证密钥或暂停提款的机制

效果:废弃合约不应存有大量价值。若发现攻击向量,必须留有应对手段。


Aztec 2.0 的类似事件

值得一提的是,这不是孤立事件。2026 年 6 月 17 日,攻击者利用另一个但概念相似的漏洞,从原始 Aztec 2.0 Rollup 合约中盗走了约 220 万美元。

在那次事件中,逃生舱电路接受了一笔没有真实存款支撑的提款。根本原因同样是两个本应强制匹配的值被解耦了:一个是证明资金所有权的成员根,另一个是针对实时链验证的结算根。两者被错误地分离开来。

攻击者针对伪造的账本证明了所有权,却在 L1 上提交了真实的账本。两项检查各自独立通过,合约却无从察觉其中的不一致。


给开发者的意见

  • 永远不要盲目信任 calldata。numRealTxs 这样的参数必须在链上验证或推导,而非直接采信。

  • L1 合约是最终仲裁者。 不要将关键验证完全外包给 ZK 电路,必须部署备用验证逻辑。

  • 对齐至关重要。 L1 结算循环的上界必须严格与 ZK 公开输入哈希承诺的范围保持一致。

  • 废弃阶段同样是安全高风险期。 在放弃管理密钥之前,务必确保合约已清空或采取了妥善的防护措施,否则会成为攻击者的绝佳目标。

  • 纵深防御。 多个验证层(L1 合约、ZK 电路、SHA256 承诺)应当互为冗余,而非互补。如果某一层失效,其他层必须能捕获问题。

  • 测试边界情况。numRealTxs 不等于 decoded_slots 时会怎样?当空隙槽包含非零值时会怎样?这些恰恰是攻击者会反复试探的场景。


结论

Aztec Connect 事件揭示了一个区块链安全的基本准则:层间一致性不容妥协。 当 L1 合约与 ZK 证明系统对"哪些槽算数"存在分歧时,攻击者就会乘虚而入。

这 219 万美元并非通过什么复杂的密码学破解被盗,而是被一个本该在设计评审阶段就揪出的逻辑缺口所吞噬。教训再清楚不过:凡事验证,不信任何输入,并确保你的验证循环覆盖范围与承诺范围精确吻合。

对于持有遗留资产的废弃合约,教训同样冷酷:如果无法更新,那就清空它。

相关推荐
MartinYeung51 小时前
[论文学习]Latent Fusion Jailbreak: 融合有害与无害表征以诱导大语言模型产生不安全输出
学习·安全·语言模型
飞飞传输1 小时前
机器人行业数据流转深度研究:内外网数据摆渡平台应用现状与趋势
大数据·运维·安全
tech讯息2 小时前
企业 AI Agent 对接外部服务如何安全集成?—— 多租户业务场景优先选用 WebSocket 方案
人工智能·websocket·安全
数据知道2 小时前
网络安全实战:渗透测试全流程 SOP——从授权到报告交付
服务器·网络·安全·web安全·网络安全
ZDGJ60992 小时前
恒指期货交易策略适合新手吗
区块链
猫咪宝妖2 小时前
【信息安全工程师】第一天考前准备
网络·安全·web安全
箐箐子衿3 小时前
SSTI服务端模板注入漏洞
安全
IT小白杨4 小时前
海外业务账号安全保障:主流浏览器产品底层机制对比
数据库·python·安全·自动化·安全架构·指纹浏览器
数据知道4 小时前
网络安全实战:信息收集实战——子域名、端口、指纹、社工信息聚合
网络·安全·web安全·网络安全·servlet