ZK 安全审计 — 3 份真实审计报告深度阅读
ZK 漏洞分类、underconstrained 模式、读 3 份真实 audit report
日期: 2026-12-29 方向: ZK工程 / 电路开发 阶段: Phase 4 - ZK电路开发实战 (Day 223-243) 标签: #ZK #audit #security #underconstrained #soundness
今日目标
| 类型 | 内容 |
|---|---|
| 学习 | ZK 漏洞分类、underconstrained 模式、读 3 份真实 audit report |
| 实操 | 把每个漏洞案例用 Circom 重现 + 验证 |
| 产出 | zk_audit.md(漏洞分类 + 真实案例 + 自审 checklist) |
ZK 漏洞分类(Top 10)
1. Underconstrained Circuit(最常见!)
症状:约束不足,prover 可以提交看似合理但违反业务规则的 witness。
典型例子:忘加 <==(用了 <--);没加 boolean 约束。
// BAD
template Bad() {
signal input x;
signal output y;
y <-- x * x; // 没约束 y = x*x
// prover 可以填 y = 任意值
}
2. Non-deterministic in <--
// 场景: 模拟整除
signal q;
q <-- a / b; // unsafe
// 应该:
q <-- a / b;
q * b === a; // 约束 q*b = a
但即使加了约束,如果 b = 0,prover 仍可任填 q,因为 q*0 = 0 不约束 q。
3. Missing Range Check
整数运算可能 overflow 到 field:
signal sum;
sum <== x + y; // x, y 都 < 2^30,但没保证 sum < p
// 如果 x = p-1, y = 1, sum = 0
4. Boolean Not Constrained
// BAD: s 不约束为 0/1
out <== s * a + (1-s) * b;
// prover 可填 s = 2 → out = 2a - b
5. Public Input 未参与运算
如果某个 public input 没出现在任何约束中,verifier 不会捕获它的篡改:
component main {public [hash, label]} = Foo();
// 但电路里只用了 hash,没用 label → label 可被任意篡改
6. Trusted Setup Incorrect
Phase 1 ptau 不够大、Phase 2 没做、用错 zkey。
7. Side-channel / Witness Leak
Witness 文件留在公网;prove 时间侧信道泄露。
8. Replay Attack
链上接受 proof 时没用 nonce / nullifier 防重放:
function withdraw(proof) public {
verifier.verify(proof); // 可重复调用!
transferFunds(...);
}
9. Malleability of Public Inputs
某些 proof system 中 public input 顺序错可让验证通过但对应不同业务(snarkjs pi_b endianness 经典问题)。
10. Prover-Verifier State Mismatch
例:Tornado 状态有 30 个 historical roots,但 prover 用了不在 history 的 root → 链上 verify 失败但社区可能误以为电路漏洞。
案例 1: Aztec ECNoir Audit by Trail of Bits (2023)
项目: Noir 标准库 + Barretenberg verifier 审计方: Trail of Bits 关键 finding:
Finding A: Underconstrained to_le_bits in std
std::field::to_le_bits(N) 早期版本:
// pseudo-code of broken impl
fn to_le_bits(x: Field, n: u32) -> [bool; n] {
let bits = unconstrained get_bits(x, n); // unconstrained!
// forgot: assert sum(bits[i] * 2^i) === x
bits
}
→ prover 可以填任意 bits 数组,绕过 range check。
→ 影响:所有用 std::range 的电路 affected。
→ 修复:commit 92ab1f5 加上 sum constraint。
Finding B: Recursion verifier 的 hash to curve
verifier 的 hash-to-curve 实现没正确 reject identity point。攻击者可构造让 verifier 接受非有效 group element 的 proof。
→ 修复:增加 is_on_curve() + is_in_subgroup() 检查。
教训
- 标准库的漏洞影响巨大(所有项目 affected)
- "unconstrained" 块必须配对 assert
- pairing-friendly curve 操作必须验 subgroup
案例 2: Tornado Cash Trusted Setup (2019-2020)
项目: Tornado Cash 1 ETH pool 审计: ABDK Consulting + community
关键 finding
Phase 2 ceremony 未达 100 名参与者目标
只 1100 人参与(达标),但其中部分参与者使用同一 IP / 设备,引起对 entropy 的怀疑。 → 建议:未来 ceremony 应有 attestation 证明每个 contributor 用独立硬件。
Verifier.sol 的 pi_b 顺序
snarkjs 输出 pi_b 是 (c1, c0) 顺序,但 EVM precompile 期望 (c0, c1)。如果 dApp 不正确 swap,proof 永远 verify fail(DoS)但无法盗钱。
→ 教训:永远用 snarkjs zkey export soliditycalldata 生成 calldata。
Withdraw recipient binding
如果 recipient 不绑定到 proof,relayer 可换地址。Tornado 把 recipient/relayer/fee/refund 都作为 public inputs(即使不参与电路计算,作为 dummy quadratic constraint 引入)。 → 教训:每个 public input 必须在电路中至少出现一次。
MerkleTreeWithHistory.sol 的 zero subtree hash
ZERO hash 用 keccak("tornado-cash-zero"),但实际计算时把它当 field element(mod p)。如果 mod 后的值碰巧等于某个真实 commitment,攻击者可 deposit 假 commitment 让 history 错位。 → 修复:检查 ZERO 不在用户提交的 commitment range 内。
案例 3: zkSync Era Boojum (Halborn Audit 2023)
项目: zkSync Era 的 Boojum 升级 审计: Halborn
Finding A: Goldilocks field underconstrained subtraction
Boojum 用 Goldilocks (p = 2^64 - 2^32 + 1)。某 subtraction gadget 在 underflow 时未正确 wrap:
correct: a - b if a >= b
p - (b - a) if a < b
原实现忘了 underflow case,prover 可让结果 = -1(在域内是 p-1)通过。
→ 修复:commit xxx,加 conditional add of p。
Finding B: Recursion AP-style preprocessor
zkSync 的 recursion 用 multi-layer wrapping。某 layer 的 verifier 在序列化 G2 element 时 endianness 错,让 inner proof bypass。 → 修复:明确 byte order convention + test vector。
Finding C: state root override
zkSync executor contract 在某 fork case 接受 prover 提交的 state root 但没验证 prior state root chain。 → 修复:增加 prior root check。
教训
- 不同 field(Goldilocks vs bn254)每种都有 subtle 边界
- 多层 recursion 是漏洞高发区(每层都要审)
- 链上 contract 的 state root 校验是「最后一道防线」必须严格
自审 Checklist / Self-Audit Checklist
写电路时
- 每个
<--后是否有对应===验证? - 每个 boolean signal 是否加
s * (1-s) === 0? - 每个 integer 是否做 range check?
- 是否处理了除数 = 0 的 case?
- 每个 public input 是否在电路里至少出现一次?
- 是否有 unconstrained 输出 signal?
Trusted Setup
- Phase 1 ptau 大小是否覆盖电路约束数?
- Phase 2 ceremony 是否有足够参与者?
- zkey beacon hash 是否公开记录?
- 是否所有 contributor 公开 attestation?
Solidity Verifier
- pi_b 顺序是否用
soliditycalldata生成? - verifier.sol 是否与电路 vkey 一致?
- gas 消耗是否在合理范围?
- 是否有 nullifier / nonce 防重放?
- 是否对所有 public inputs 做 sanity check?
业务逻辑
- recipient / fee / time 是否绑定到 proof?
- 是否所有 state transition 的安全前置条件都被电路验证?
- 是否区分
<==和===? - hash 用 ZK-friendly 还是 Solidity 友好?两边一致吗?
ZK 审计公司 / Top ZK Auditors
| 公司 | 专长 | 知名 audit |
|---|---|---|
| Trail of Bits | 通用 + ZK | Noir, Aztec, Worldcoin |
| Halborn | 协议 + ZK | zkSync, Polygon zkEVM |
| Veridise | 专业 ZK + 形式化 | Aleo, Polygon, Scroll |
| 0xPARC ZK Audit | 学术 + 创新 | Semaphore, Dark Forest |
| Spearbit | crowdsourced | 多个 ZK 项目 |
| Least Authority | 老牌 ZK | Zcash, Filecoin |
ZK 审计 cost:$50k - $500k 一次,依电路规模和复杂度。
形式化验证工具 / Formal Verification Tools
| 工具 | 范围 |
|---|---|
| Picus (Veridise) | 检测 Circom 的 underconstrained |
| Coda (UCSB) | Circom DSL 形式化 |
| Ecne (0xPARC) | underconstrained detection |
| Halo2 analysis tools | PSE 内部 |
Picus 已经在多个 audit 中找到 underconstrained bug,是 ZK 自动化审计前沿。
真实漏洞总数(2020-2024)
公开 ZK audit 报告中:
- 高危 finding: ~30%
- 中危: ~40%
- 低危/info: ~30%
- 最常见类别: underconstrained (40%) > range check missing (15%) > replay (10%)
关键速查
ZK audit 三大问 (mnemonic)
1. "Is every signal constrained?" → underconstrained
2. "Is every public used?" → public input integrity
3. "Is the verifier replay-safe?" → contract layer
Tools:
Picus — find underconstrained
Ecne — alternative
Halmos — Solidity verifier check
面试题
-
Q: ZK 电路最常见的漏洞类型? A: Underconstrained(约束不足)。表现:开发者用
<--给 signal 赋值但忘了加===或 boolean constraint,让 prover 可以提交不符合预期的 witness。统计上占 ZK audit findings 40% 以上。 -
Q: Trusted Setup ceremony 出问题的最大风险是什么? A: 如果 Phase 2 的 toxic waste 被任何参与者保留(且没有 destroy),该参与者可以 forge 任意 proof,盗取协议所有资金。所以多人参与(1-of-N 诚实即安全)+ 公开 attestation 极重要。Tornado 用 1100 人,Filecoin/Aztec 都有大型 ceremony。
-
Q: 如何审计一个 Circom 电路? A: (1) 读代码:每个 signal 是否被约束?(2) 用 Picus / Ecne 自动检测 underconstrained;(3) 写测试:包括 happy path + negative test (intentionally bad witness);(4) 检查 trusted setup 流程;(5) 审 Solidity verifier wrapper 防 replay;(6) 比对 ZK 内 hash 与 Solidity 端 hash 实现一致;(7) gas 估算合理。
-
Q: 你给一个新 ZK 项目做 audit 检查清单,前 3 项是? A: (1) 每个
<--后必须有对应===验证(用 grep 全局检查);(2) 每个 public input 必须在电路中至少出现一次(防止被任意篡改);(3) Solidity verifier 必须有 nullifier / nonce 防重放(最大半数审计漏掉这一层)。
明日预告
Day 243 — Week 36 复习:ZK 工程能力整合。Phase 4 实战阶段(Day 223-243)的最终总结,把 21 天技能整合成「ZK 工程师能力图谱」。