揭露并修复Aleo中的一个通胀漏洞 - ZKSECURITY

本文揭示了2024年11月在Aleo主网上发现的一个严重通胀漏洞的细节,该漏洞源于不安全的输入/输出提交方式,可能导致攻击者绕过最终确认逻辑,非法铸造代币。漏洞发现后,立即报告给Aleo团队并迅速修复,未发现实际利用。修复方案通过增加显式检查确保交易输入/输出数据类型的正确性。

2024 年 11 月,我们发现了 Aleo 主网中的一个通胀漏洞。我们立即向 Aleo 团队报告了这个漏洞。漏洞被识别并迅速修复。幸运的是,经过彻底扫描后,未检测到任何利用行为。

感谢 Aleo 团队的迅速和专业的行动。

在本文中,我将解释该漏洞的背景、它可能如何被利用以及如何修复它。

Aleo 如何工作?

Aleo 是一个利用 ZKP(零知识证明)来实现隐私和可编程性的区块链网络。Aleo 使用从 Marlin 改编的 Varuna 证明系统。在 Aleo 上,用户生成交易执行的证明,验证者(网络)通过验证证明来验证交易的有效性。这种方法使网络高效,因为验证者不需要重新执行交易。此外,用户可以通过隐藏执行细节来保持他们的隐私。

Transition

Aleo 的交易由 transition 组成。一个 Transition 代表 Aleo 合约的一个函数的执行。Aleo 的合约语言是 Leo,一种直接的类 Rust 的 DSL(领域特定语言)。有关 Leo 的更多细节,请阅读 探索 Leo:Aleo 程序安全入门。下面是 Aleo 上的一个最小的可执行函数(一个 transition):

transition add_private_number(public a: u32, private b: u32) -> u32 {
    let c: u32 = a + b;
    return c;
}

在代码中,add_private_number 接受一个公共输入 a 和一个私有输入 b,将两个数字相加并返回私有结果 c(在 Aleo 上,输入和输出默认是私有的)。由于 bc 是私有的,交易中的值被加密并且不会泄露给其他人。只有交易发送者可以解密加密的值。当用户发送一个调用该函数的交易时,网络确保 cab 的和,而不知道 bc 的值。

Record 类型

目前,一个 transition 可以处理像 boolu64 这样的简单数据类型,这些类型是可克隆的,不适合表示资产(比如 token)。为此,Aleo 引入了 Record 模型。Record 是一种特殊的数据类型,它基本上是一个屏蔽池的叶子。它泛化了 Zcash 的设计,允许各种私有对象。

一个 transition 可以接受一个 Record 类型作为输入,并输出一个 Record 类型。不同之处在于,一个 Record 只能通过从一个 transition 返回来创建。每个 Record 都有一个所有者,并且只能由所有者花费。当一个 Record 作为 transition 的输入传递时,它会被消耗掉。当它被消耗时,会发出一个类似于 Zcash 的 nullifier,这确保了 Record 不能再次使用。换句话说,一个 Record 代表不可克隆的资产,这类似于 Move 语言 中的 Object 概念,但对其他人来说是私有的和不可见的。

一个程序可以定义自己的带有自定义字段的 Record 类型。这些字段默认也是私有的。只有创建者和所有者能够解密这些私有字段。下面是一个示例程序,用于 探索 Leo:Aleo 程序安全入门

program example_program0.aleo {
    record Token {
        owner: address,
        amount: u128
    }

    transition private_transfer_token(private receiver: address, private input_token: Token) -> Token {
        let new_token: Token = Token {
            owner: receiver,
            amount: input_token.amount
        };
        return new_token;
    }
}

上面的代码实现了私有 token 转移。它定义了一个名为 Token 的 record,带有 owneramount 字段,这些字段默认是私有的。private_transfer_token transition 接受 Token record 作为输入,消耗它,并输出一个具有相同数量和新所有者的新 token。input_token 将被消耗,因为它被作为函数的输入传递。new_token 将被创建,因为它是由函数返回的。这确保了 token 的总供应量保持不变。由于 private_transfer_token 是链下执行的,并且输入/输出是私有的,网络不会知道发送者、接收者和数量。

链上 Finalize 逻辑

Transition 对于链下计算非常有用。但是,它无法访问链上状态。例如,对于一个去中心化交易所(DEX)执行两个 token 之间的交换,它将需要获取和更新当前的链上价格。为此,在 Aleo 中,一个函数可以定义一个 finalize 逻辑,该逻辑在链上公开执行。

Finalize 逻辑可以读取和写入链上状态(例如,下面示例中的 data_map)。当用户创建一个交易时,他们会在本地执行 transition 并生成证明,并且 transition 可以发出一个 Future 类型,该类型指定要在链上执行的逻辑。一旦交易被网络验证和接受,所有验证者将执行 Future。这种方法还可以防止多个用户生成用于更新相同值的证明,这可能会导致 多用户应用程序的冲突更新问题。这是一个利用 finalize 功能的 Aleo 程序:

program example_program1.aleo {

    mapping data_map: address => u64;

    // transition is executed off-chain
    async transition square_counter(public a: u64) -> Future {
        let square: u64 = a * a;
        return finalize_square_counter(self.caller, square);
    }

    // finalize is executed on-chain
    async function finalize_square_counter(caller: address, square: u64) {
        let v: u64 = data_map.get_or_use(caller, 0u64);
        data_map.set(caller, square + v);
    }
}

square_counter transition 接受一个输入 a 并计算它的平方。然后它输出 Future 类型,其中包含函数(finalize_square_counter)和参数(self.callersquare)。这里的 Future 代表在证明时“延迟”的链上执行逻辑,类似于 Rust 或 JavaScript 中的 async task。在这个例子中,链上执行逻辑是由合约定义的 finalize_square_counter 函数。由于该函数将以类似于以太坊的方式被所有验证者公开执行,因此它可以读取和写入链上 data_map 映射。给定 callersquare,该函数检索 caller 的 slot 中的先前值,添加 square 并写回映射。

Leo playground 中使用 leo run square_counter 1u64 运行上面的 square_counter transition,你可以得到结果:

Future Output

你可以看到 transition 的输出正好是一个 Future 类型。该 future 包含程序 id、函数名和该函数的参数。如果 transition 有效,那么验证者将使用给定的参数执行 finalize 逻辑。

最后,我们可以对 Aleo 中使用所有这些功能的交易的完整生命周期有一个高层次的了解。在这里,用户想要生成一个智能合约方法的执行,该方法已经部署在 Aleo 上:

  1. 用户在本地执行 transition,生成 transition 的输入和输出。

  2. 用户为该 transition 生成一个执行证明(使用 ZKP),并将其包含在一个交易中,以及:

    • 每个使用的输入(以及产生的输出)的加密承诺
    • 每个公共输入/输出的相关明文数据,以及每个私有输入/输出的加密数据。

最后,用户将所有这些内容在一个交易中发送到网络。

  1. 验证者验证交易的证明。也就是说,给定函数和输入的承诺,输出的承诺是正确的。验证者还验证给定的输入/输出数据是否正确地对应于该承诺。

  2. 如果交易证明有效,则网络接受该交易。如果 transition 输出一个 Future,那么执行相应的链上 finalize 逻辑。

现在你已经了解了足够多的知识来理解我们发现的漏洞。让我们开始吧!

漏洞

作为我们与 Aleo 持续安全流程的一部分,我们设法发现了一个重要的漏洞。该漏洞是由一种不安全的承诺输入/输出的方式引起的,允许攻击者绕过 finalization 逻辑。

在 Aleo 中,一个 transition 由验证者分两步检查:

  1. 验证者首先检查用户交易中包含的执行证明,以确保相关的声明:“给定执行的函数和对其输入的承诺,生成的输出承诺是正确的”。
  2. 然后,他们验证给定的输入/输出数据是否正确地对应于该承诺。

下面是检查输出数据的第二步的代码片段。给定输出,verify 函数检查 Output 是否正确地对应于输出承诺/哈希。这是每个验证者为每个交易执行的链下代码。你能找到漏洞是在哪里引入的吗?

/// The transition output.
#[derive(Clone, PartialEq, Eq)]
pub enum Output<N: Network> {
    [...]
    /// The ciphertext hash and (optional) ciphertext.
    Private(Field<N>, Option<Ciphertext<N>>),
    /// The output commitment of the external record. Note: This is **not** the record commitment.
    ExternalRecord(Field<N>),
    /// The future hash and (optional) future.
    Future(Field<N>, Option<Future<N>>),
}

impl<N: Network> Output<N> {
    pub fn verify(&self, function_id: Field<N>, tcm: &Field<N>, index: usize) -> bool {
        // Ensure the hash of the value (if the value exists) is correct.
        let result = || match self {
            [...]
            Output::Private(hash, Some(value)) => {
                match value.to_fields() {
                    // Ensure the hash matches.
                    Ok(fields) => match N::hash_psd8(&fields) {
                        Ok(candidate_hash) => Ok(hash == &candidate_hash),
                        Err(error) => Err(error),
                    },
                    Err(error) => Err(error),
                }
            }
            Output::Future(hash, Some(output)) => {
                match output.to_fields() {
                    Ok(fields) => {
                        // Construct the (future) output index as a field element.
                        let index = Field::from_u16(index as u16);
                        // Construct the preimage as `(function ID || output || tcm || index)`.
                        let mut preimage = Vec::new();
                        preimage.push(function_id);
                        preimage.extend(fields);
                        preimage.push(*tcm);
                        preimage.push(index);
                        // Ensure the hash matches.
                        match N::hash_psd8(&preimage) {
                            Ok(candidate_hash) => Ok(hash == &candidate_hash),
                            Err(error) => Err(error),
                        }
                    }
                    Err(error) => Err(error),
                }
            }
            [...]
            Output::ExternalRecord(_) => Ok(true),
        };
        [...]
    }
}

该漏洞源于不同输出类型的哈希/承诺模式。Output 的承诺模式仅包括数据,而不吸收它的子类型。这意味着我们可以创建两个具有相同承诺的不同子类型的 Output。例如,Output::Future(hash, Some(output)) 通过将数据连接到 preimage 并获取它的哈希来检查。Output::Private(hash, Some(value)) 通过直接哈希 value 来检查。然后我们可以通过简单地将 Output::Private 中的 value 设置为 Output::Futurepreimage 来创建具有完全相同哈希的 Output::Private

这意味着承诺模式不具有约束力。不幸的是,Aleo 仅依赖于承诺检查来进行输入/输出验证,而没有其他地方的显式类型检查。因此,攻击者可以用交易中不同类型的数据替换输出,并且仍然可以通过检查。这种设计缺陷引入了一个关键漏洞。

更进一步,请记住 Future 数据表示要在链上执行的 finalized 逻辑。如果 Future 数据被另一种类型的数据(例如,Output::Private)替换,那么 finalize 逻辑将不会在链上执行。通过这种方式,攻击者可以绕过其交易中的任何 finalize 逻辑。其中一个影响是攻击者可以发行任意数量的 Aleo token。

考虑以下存在于 Aleo 的 token 合约中的函数:

program example_program2.aleo {
    record Token {
        owner: address,
        amount: u64
    }

    mapping public_balance: address => u64;

    async transition transfer_public_to_private(recipient: address, amount: u64) -> (Token, Future) {
        let new_token: Token = Token {
            owner: recipient,
            amount: amount
        };
        let f: Future = finalize_transfer(self.caller, amount);
        return (new_token, f);
    }

    async function finalize_transfer(caller: address, amount: u64) {
        let balance: u64 = public_balance.get_or_use(caller, 0u64);
        let new_balance: u64 = balance - amount; // transaction will revert if underflow happens
        public_balance.set(caller, new_balance);
    }
}

transfer_public_to_private 将公共 token(存储在链上的 public_balance 映射中)转移到私有 token(由 record 存储)。它在一个交易中发行新的 token record 并减去相同数量的余额。如果公共余额不足,交易将回滚。但是,如果跳过 finalize_transfer 逻辑,则整个交易将部分执行。新的 token record 仍然会由 transition 创建,但链上余额不会被减去。通过这种方式,攻击者可以发行任意数量的 token。

以下是可以执行攻击的方式:

  1. 攻击者使用 recipient 作为自己的地址和大的 amount 执行 transfer_public_to_private transition。然后攻击者像往常一样生成证明和交易。
  2. 该交易包含 transition 的两个输出:一个 Output::Token 类型和一个 Output::Future 类型。攻击者将 Output::Future 替换为具有相同承诺值的 Output::Private 类型的数据。攻击者将交易发送到网络。
  3. 验证者验证交易并确定它是有效的:交易的证明是有效的(因为它没有被触及),并且附加的输入/输出数据是有效的(因为它们具有正确的承诺)。
  4. 验证者接受该交易。它创建发出的 Token record。它发现输出中没有 Future 类型(已被 Output::Private 类型替换),然后不会执行链上 finalize 逻辑。
  5. 在执行交易后,新的 Token 被创建,但链上公共余额未被减去。通过这种方式,攻击者免费创建了一个新的 Token

请注意,如果发生此类攻击,我们可以通过检查输入/输出数据不匹配来找到它。

修复

在发现该问题后,我们立即将其报告给 Aleo 团队。Aleo 团队确认了该问题,并扫描了所有现有交易以查找利用的迹象。幸运的是,没有发现任何利用。然后提出了 修复 并合并。由于承诺模式涉及协议的多个部分(包括电路),因此修改起来具有挑战性。因此,通过添加显式检查以确保 transition 的输入/输出数据类型是正确的来修复它。

pub fn verify_execution(&self, execution: &Execution<N>) -> Result<()> {
    [...]
    // Ensure the input and output types are equivalent to the ones defined in the function.
    // We only need to check that the variant type matches because we already check the hashes in
    // the `Input::verify` and `Output::verify` functions.
    let transition_input_variants = transition.inputs().iter().map(Input::variant).collect::<Vec<_>>();
    let transition_output_variants = transition.outputs().iter().map(Output::variant).collect::<Vec<_>>();
    ensure!(function.input_variants() == transition_input_variants, "The input variants do not match");
    ensure!(function.output_variants() == transition_output_variants, "The output variants do not match");
    [...]
}

后来,该修复程序被部署到所有验证者。添加了更多测试,以确保检测到格式错误的输入和输出。

时间线

  • 2024 年 11 月 24 日 发现该问题并报告给 Aleo。Aleo 确认了该问题。
  • 2024 年 11 月 25 日 Aleo 团队扫描了整个区块链历史记录,未发现任何利用证据。
  • 2024 年 12 月 3 日 该修复程序 被提出并合并。
  • 2024 年 12 月 4 日 该修复程序以及其他正常升级被推广到所有验证者。

总结

我们发现了 Aleo 中的一个关键漏洞,该漏洞可能允许任意 token 铸造。我们立即将该问题报告给 Aleo 团队,并共同努力修复了该问题。

从这次经历中获得的主要经验是,在承诺和哈希数据时,遵循 “TLV” 模式(类型、长度、数据)的重要性。这有助于确保承诺是安全的并防止利用。

安全是一个持续和发展的过程。对于其安全实践不透明的零知识项目更容易出现漏洞。通过不断提高标准并严格审查系统,我们可以及早发现和缓解关键问题。随着 zk 生态系统的发展,我们仍然致力于保护 zk 项目并为其长期可靠性和安全性做出贡献。

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

0 条评论

请先 登录 后评论
zksecurity
zksecurity
Security audits, development, and research for ZKP, FHE, and MPC applications, and more generally advanced cryptography.