本文深入探讨了密码学电路中缺失范围检查攻击的原理和实例,首先介绍了群论的基础知识,包括群、子群、群的阶、元素的阶、生成器和循环群等概念,以及拉格朗日定理及其推论。
在深入研究密码电路中缺失的范围检查攻击之前,有必要了解什么是群,它们的规模(阶)如何表现,以及为什么这很重要。
一个群 $G$ 是一个集合,带有一个二元运算(例如,加法或乘法),满足以下性质:
一个子集 $H \subseteq G$ 被称为子群,如果它本身在从 $G$ 继承的运算下也是一个群。
群的阶 $G$,表示为 $|G|$,是群中元素的数量。如果群有无限多个元素,则阶也可能是无限的。
元素的阶 $g \in G$ 是最小的正整数 $n$,使得 $g^n = e$,其中 $e$ 是群的单位元。如果不存在这样的 $n$,我们说 $g$ 的阶是无限的。
群 $G$ 的一个生成集合(或生成元集合)是一个子集 $S = {s_1, s_2, \dots, s_k} \subseteq G$,使得 $G$ 的每个元素都可以写成来自 $S$ 的元素及其逆元的有限乘积。
也就是说,对于每个 $g \in G$,存在一个索引序列 $i_1, i_2, \dots, i_m \in {1, \dots, k}$ 和指数 $\epsilon_1, \epsilon_2, \dots, \epsilon_m \in {\pm 1}$,使得:
$g = s_{i_1}^{\epsilon1} s{i_2}^{\epsilon2} \cdots s{i_m}^{\epsilon_m}$。
由 $S$ 生成的群表示为:
$G = \langle S \rangle = \left{ s_{i_1}^{\epsilon1} s{i_2}^{\epsilon2} \cdots s{i_m}^{\epsilon_m} \mid m \in \mathbb{N}, i_j \in {1, \dots, k}, \epsilon_j \in {\pm 1} \right}$。
如果一个群 $G$ 有一个由单个元素 $g$ 组成的生成集合,即 $G = \langle g \rangle$,我们说 $G$ 是循环群。
设 $G$ 是一个群,设 $H \subseteq G$ 是一个子群。
给定一个元素 $g \in G$,$H$ 关于 $g$ 的左陪集是集合:
$gH = { gh \mid h \in H }$。
类似地,$H$ 关于 $g$ 的右陪集是:
$Hg = { hg \mid h \in H }$。
通常,左陪集和右陪集是不同的,除非 $H$ 是一个正规子群(我们在这里不需要的概念)。
陪集的一个关键性质是,$H$ 在 $G$ 中的所有左陪集的集合形成了 $G$ 的一个划分:
也就是说,对于所有 $g_1, g_2 \in G$,要么 $g_1H = g_2H$,要么 $g_1H \cap g_2H = \emptyset$。
我们将 $H$ 在 $G$ 中的所有左陪集的集合表示为:
$G/H = { gH \mid g \in G }$。
不同左陪集的数量称为 $H$ 在 $G$ 中的指数,表示为 $[G : H]$。

一个子群 $H$ 的所有左陪集都具有与 $H$ 相同的大小。事实上,对于任何 $g \in G$,映射:
$\varphi_g : H \to gH \quad \text{定义为} \quad \varphi_g(h) = gh$
是一个双射(一个一对一且映上的函数)。
这意味着每个陪集 $gH$ 恰好包含 $|H|$ 个元素,并且没有两个陪集重叠。这种双射对应关系是 拉格朗日定理 证明的核心,我们将在接下来陈述和证明它。
设 $G$ 是一个有限群,设 $H \subseteq G$ 是一个子群。 那么:
$|H| \mid |G| \quad \text{和} \quad |G| = [G : H] \cdot |H|$,
其中 $[G : H]$ 表示 $H$ 在 $G$ 中的(左)陪集的数量。
由于 $H$ 是 $G$ 的一个子群,我们可以形成 $H$ 在 $G$ 中的左陪集的集合:
$G/H = {gH \mid g \in G}$。
我们现在观察以下事实:
不相交并:$H$ 的陪集形成了 $G$ 的一个划分。也就是说,$G$ 的每个元素都恰好属于一个陪集。所以我们可以写成: $G = \bigsqcup_{i=1}^{[G:H]} g_i H$ 对于一些代表 $g1, \dots, g{[G:H]} \in G$。
陪集的大小相等:每个左陪集 $gH$ 具有与 $H$ 相同数量的元素。这是因为由 $h \mapsto gh$ 给出的映射 $\varphi_g : H \to gH$ 是一个双射。
基数计算:由于有 $[G : H]$ 个不相交的陪集,每个的大小为 $|H|$,并且它们的并集给出了所有的 $G$,因此: $|G| = [G : H] \cdot |H|$。
设 $G$ 是一个有限群,其阶为 $n = |G|$,并设 $g \in G$。 考虑由 $g$ 生成的循环子群,表示为 $\langle g \rangle$。
设 $m = |\langle g \rangle|$,根据定义,它是元素 $g$ 的阶。
由于 $\langle g \rangle$ 是 $G$ 的一个子群,拉格朗日定理告诉我们,任何子群的阶都能整除群的阶。因此:
$m \mid n$。
换句话说,任何元素的阶都能整除群的阶。
椭圆曲线是现代密码学的核心对象。为了我们的目的,我们对在有限域上定义的椭圆曲线感兴趣,特别是它们如何形成与密码协议(以及攻击)相关的结构的阿贝尔群。
在我们定义椭圆曲线之前,我们需要理解它们在其上定义的代数结构:域。
一个域是一个集合 $F$,配备了两个运算:
它们满足以下性质:
无限域的著名例子包括有理数 $\mathbb{Q}$、实数 $\mathbb{R}$ 和复数 $\mathbb{C}$。
一个有限域(或 伽罗瓦域)是一个具有有限多个元素的域。最常见的有限域是:
在密码学中,我们通常使用 $\mathbb{F}_p$,其中 $p$ 是一个大素数。在这种域中的算术是模 $p$ 进行的:
有限域在椭圆曲线密码学和 SNARKs 中至关重要,因为它们为我们提供了一个结构化但有界定的空间来进行计算——非常适合电路和模运算。
设 $\mathbb{F}_q$ 是一个具有 $q$ 个元素的有限域(通常对于某个素数 $p$,有 $q = p$,或对于小的 $k$,有 $q = p^k$)。$\mathbb{F}_q$ 上的椭圆曲线是方程的解 $(x, y) \in \mathbb{F}_q^2$ 的集合:
$E: \quad y^2 = x^3 + ax + b$,
其中 $a, b \in \mathbb{F}_q$,且曲线是非奇异的,意味着:
$4a^3 + 27b^2 \ne 0$。
这个条件确保曲线是平滑的,没有尖点或自相交。
我们还包括一个特殊点,称为无穷远点,表示为 $\mathcal{O}$,它在群中充当单位元。

椭圆曲线 $E(\mathbb{F}_q)$ 上的点,连同 $\mathcal{O}$,在几何加法法则下形成一个阿贝尔群:
加法公式的细节在这里并不重要——只有群律是良好定义的,可高效计算的,并且赋予曲线点集合一个群结构的这个事实。
当椭圆曲线是在有限域 $\mathbb{F}_q$ 上定义时,它的点的集合 $E(\mathbb{F}_q)$ 形成一个有限循环阿贝尔群。
这意味着存在一个点 $G \in E(\mathbb{F}_q)$,使得重复将 $G$ 加到自身会生成曲线上的所有点。 也就是说:
$E(\mathbb{F}_q) = \langle G \rangle = { \mathcal{O}, G, 2G, 3G, \dots, (r-1)G }$,
对于一些素数 $r$,其中 $rG = \mathcal{O}$,$r$ 是椭圆曲线群的阶。
这种运算——将任何点写成生成元 $G$ 的倍数 $[k]G$——被称为标量乘法,标量 $k$ 存在于大小为 $r$ 的有限域中。
这使我们区分了椭圆曲线密码学中的两个域:
因此,虽然曲线的点存在于 $\mathbb{F}_q$ 中,但将点 $G$ 乘以标量的代数是由 $\mathbb{F}_r$ 中的算术控制的。
当推理密码协议中的子群结构、约束标量范围和攻击面时,这种区分变得至关重要。
椭圆曲线不仅用于传统的公钥密码学,而且在零知识证明系统中也起着至关重要的作用,特别是在基于配对的 SNARKs 中,如 Groth16 和 PLONK。
诸如 Groth16 和 PLONK 之类的证明系统利用椭圆曲线群之间的 双线性配对 来实现:
一个配对是具有以下形式的特殊函数:
$e : G_1 \times G_2 \to G_T$
其中 $G_1$、$G_2$ 和 $G_T$ 是与椭圆曲线(或一对曲线)关联的群。 函数 $e$ 满足:
在 Groth16 zk-SNARK 协议中,证明者被给予:
使用这些数据,证明者计算一个由三个群元素组成的证明:
这些群元素编码对完全赋值 $(x_1, \dots, x_m)$ 的承诺,并证明它满足电路的约束,而无需泄露私有输入。
验证者被给予:
设公共输入线性组合为:
$K_{\text{pub}} = \sum_i x_i [K_i]_1$
如果以下配对等式成立,则验证者接受该证明:
$e(A, B) = e(C, [\delta]2) \cdot e(K{\text{pub}}, [\gamma]_2) \cdot e([\alpha]_1, [\beta]_2)$
这里,$e : G_1 \times G_2 \to G_T$ 是一个双线性配对函数。该等式确保证明与电路的 QAP(二次算术程序)表示的一致性以及见证相对于公共输入的正确性。
电路中涉及的所有信号——输入、输出和中间信号——都是有限域 $\mathbb{F}_r$ 中的标量,即椭圆曲线的标量域。
当你写:
signal input a;
signal input b;
signal output c;
c <== a * b;
你是在 $\mathbb{F}_r$ 中定义了三个标量上的约束,这些标量成为证明内部结构的一部分。QAP 确保这些值满足电路的逻辑,配对检查确保证明者知道这些值而无需泄露它们。
零知识证明系统中最广泛使用的椭圆曲线之一是 BN254,在以太坊中也称为 alt_bn128。它在基于配对的 SNARKs(如 Groth16 和 PLONK)中发挥着核心作用,并通过用于快速验证的预编译在以太坊虚拟机中得到原生支持。
BN254 是一条 Barreto-Naehrig (BN) 曲线,定义在 254 位素数域上:
曲线方程以短 Weierstrass 形式书写:
$E: y^2 = x^3 + 3 \mod p$。
BN254 支持高效的双线性配对,并在 zk-SNARK 实现中得到广泛采用。然而,这条曲线并非没有注意事项,尤其是在智能合约等受限环境中。
a16z 的 ZkDrops(一个基于 SNARK 的空投系统)中发生了一个安全问题的具体例子,该问题源于对椭圆曲线域大小的处理不当。此漏洞记录在 0xPARC zk-bug-tracker 中。
ZkDrops 要求用户在申领空投时提交一个 无效值。这个无效值应该对每个申领都是唯一的确定性值,并作为 256 位整数存储在链上。SNARK 电路使用 254 位的标量域(与 BN254 曲线匹配),因此任何大于此值的值都会在证明生成期间自动模 $r$ 减少。
然而,智能合约没有验证无效值是否在标量域范围内。这允许用户提交两个不同的无效值:
$x \text{ 和 } x + r$,
这两个值在 SNARK 电路中 模 $r$ 都计算为相同的值,但对于链上验证者 来说,它们看起来是不同的,链上验证者将它们视为单独的 256 位整数。
一个恶意用户可以:
这实际上允许同一个空投申领被接受两次。
function collectAirdrop(bytes calldata proof, bytes32 nullifierHash) public {
require(!nullifierSpent[nullifierHash], "Airdrop already redeemed");
uint[] memory pubSignals = new uint[](3);
pubSignals[0] = uint256(root);
pubSignals[1] = uint256(nullifierHash);
pubSignals[2] = uint256(uint160(msg.sender));
require(verifier.verifyProof(proof, pubSignals), "Proof verification failed");
nullifierSpent[nullifierHash] = true;
airdropToken.transfer(msg.sender, amountPerRedemption);
}
修复方法是在智能合约中添加一个范围检查,以确保无效值位于标量域内:
require(uint256(nullifierHash) < SNARK_FIELD ,"Nullifier is not within the field");
这保证了链上无效值与 SNARK 电路内部使用的无效值相匹配,从而防止通过模归约进行复制。
在基于 BN254 的 zk-SNARK 电路中,所有电路内算术运算都在标量域 $\mathbb{F}_r$ 中进行。为了在这些电路中实现椭圆曲线运算,例如 Pedersen 哈希或 EdDSA 签名,我们需要一个定义在 $\mathbb{F}_r$ 上的曲线。这就是 Baby-Jubjub 的作用,这是一种专为电路内使用而设计的 twisted Edwards 曲线。
Baby-Jubjub 是在 BN254 标量域上定义的:
$r = 21888242871839275222246405745257275088548364400416034343698204186575808495617$
这允许使用 SNARK 电路内的 $\mathbb{F}_r$ 算术来表示和操作 Baby-Jubjub 点。该曲线具有以下结构:
在最近对 AvaCloud 的审核中,我们发现在处理 Baby-Jubjub 标量乘法中使用的标量值时存在一个漏洞。具体来说:
Baby-Jubjub 的基点的阶为 $q < r$(BN254 标量字段大小),但标量输入取自完整的标量字段 $\mathbb{F}_r$。 如果没有检查标量是否在正确的范围内(即小于 $q$),则当标量乘以基点时,两个不同的标量可以产生相同的曲线点。
这是因为椭圆曲线标量乘法是模群阶 $q$ 定义的。 因此:
[k]G=[k+n⋅q]Gfor any n∈Z[k]G = [k + n \cdot q]G \quad \text{对于任何 } n \in \mathbb{Z}[k]G=[k+n⋅q]Gfor any n∈Z
这意味着恶意证明者可以提交不同的标量值(例如 $k$ 和 $k+q$),这些值会产生相同的 Baby-Jubjub 点。 这些是混淆值:映射到同一点的不同标量。
这些混淆值启用了以下攻击:
通过利用缺少标量范围检查的漏洞,这实际上允许用户提取超过他们已存入的资金。
有关 Baby-Jubjub 标量混淆漏洞的更多详细信息,请参见 此处 的完整审核报告。
当一个加密系统无法正确约束其操作的值时,就会出现子群攻击——无论这些值是标量、群元素还是曲线点。正如我们所见,即使是像缺少范围检查这样的简单代数疏忽也可能产生严重的后果。
这些错误源于相同的基本原理:如果一个值存在于比预期更大的结构中,并且没有检查来强制执行正确的域,那么就会产生意想不到的行为。
在零知识密码学中,由于跨不同数学领域(群、域和曲线)证明和验证计算的复杂性,这些问题尤其棘手。 仔细验证和了解群结构——特别是阶、余因子 和 子群 ——对于确保可靠性至关重要。
- 原文链接: hexens.io/blog/subgroup-...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!