使用 Trident 进行模糊测试 I

这篇文章详细介绍了使用 Trident 进行模糊测试的过程,包括设置 Trident 工作空间、编写模糊测试、以及运行测试与获取结果等步骤。文章涉及了 Rust 代码示例来演示如何初始化程序、处理账户和执行模糊测试,同时指出了潜在的函数缺陷和运行中的错误处理。

使用 Trident I 进行模糊测试

[!IMPORTANT] 有关 Trident 的更多详细信息,请查看 Trident 文档 Trident 文档

目录


程序问题概述

程序示例

程序结构

本节中指定的程序具有以下结构

资产账户

程序在其初始化指令中创建一个资产账户,该账户具有以下字段

  1. authority:负责资产的创建和对资产的管理权限。
  2. mint:与资产账户相关联,也在初始化指令中初始化。
  3. counter:在初始化过程中计算得出。
##[account]
pub struct Asset {
    pub authority: Pubkey,
    pub mint: Pubkey,
    pub counter: u64,
}

impl Asset {
    pub const LEN: usize = 32 + 32 + 8;
}
初始化上下文

初始化指令期望以下账户

  1. signer:新的资产管理权限者。
  2. asset:将要初始化的新资产账户。
  3. mint:与资产相关联的mint账户,也将被初始化。
  4. metadata_account:存储与mint账户相关的附加数据,将通过CPI初始化到Metaplex Metadata程序。
  5. 其余是进行账户初始化所需的程序和CPI。

[!TIP] 有关 Metaplex Metadata 程序的更多详细信息,请查看 Token Metadata 文档 Token Metadata

##[derive(Accounts)]
pub struct InitializeContext<'info> {
    // 1.
    #[account(mut)]
    pub signer: Signer<'info>,

    // 2.
    #[account(
        init,
        payer = signer,
        space = 8 + Asset::LEN,
        seeds = [b"asset",signer.key().as_ref(),mint.key().as_ref()],
        bump
    )]
    pub asset: Account<'info, Asset>,

    // 3.
    #[account(
        init,
        payer = signer,
        mint::decimals = 9,
        mint::authority = signer,
    )]
    pub mint: Account<'info, Mint>,

    // 4.
    /// CHECK: 将被初始化
    #[account(mut)]
    pub metadata_account: UncheckedAccount<'info>,

    // 5.
    pub mpl_token_metadata: Program<'info, Metadata>,
    pub system_program: Program<'info, System>,
    pub token_program: Interface<'info, TokenInterface>,
}
初始化指令
  1. 使用指定的名称、符号和URI创建元数据账户
  2. 调用 buggy_math_function,它的结果将被分配到 counter 字段。
pub fn _initialize_ix(
    ctx: Context<InitializeContext>,
    input1: u8,
    input2: u8,
    name: String,
    symbol: String,
    uri: String,
) -> Result<()> {
    // 1.
    ctx.accounts.create_metadata(name, symbol, uri)?;

    let asset = &mut ctx.accounts.asset;

    asset.authority = ctx.accounts.signer.key();
    asset.mint = ctx.accounts.mint.key();
    // 2.
    asset.counter = buggy_math_function(input1, input2).into();
    Ok(())
}

漏洞

[!CAUTION] buggy_math_function是不正确的,包含以下问题。

pub fn buggy_math_function(input1: u8, input2: u8) -> u8 {
      let divisor = 254 - input2;
      input1 / divisor
}
  1. 如果 input2 == 254 -> 会发生除以零的恐慌。
  2. 如果 input2 > 254 -> 会发生减法溢出的恐慌。

编写模糊测试

步骤 1 - 初始化 Trident

在 Anchor 工作区中,调用

trident init

这将初始化新的 Trident 工作区并准备所有所需的文件和模糊模板。

[!tip] 如果你想添加新的模糊测试模板,请调用

trident fuzz add

步骤 2 - 添加所需依赖项

trident-tests/fuzz_tests/Cargo.toml 中,添加编写模糊测试所需的依赖项。

[!tip] 在大多数情况下,这些与你在程序 Cargo.toml 中指定的依赖项相同。

在我们提供的示例中,我们需要添加以下依赖项

[[bin]]
name = "fuzz_0"
path = "fuzz_0/test_fuzz.rs"

[package]
name = "fuzz_tests"
version = "0.1.0"
description = "Created with Trident"
edition = "2021"

[dependencies]
honggfuzz = "0.5.56"
arbitrary = "1.3.0"
assert_matches = "1.4.0"

## --- 已添加 ---
anchor-spl = { version = "0.30.1", features = ["metadata"] }

[dependencies.trident-client]
version = "0.7.0"

[dependencies.trident-lesson-part-i]
path = "../../programs/trident-lesson-part-i"

步骤 3 - 添加所需的使用声明

trident-tests/fuzz_tests/fuzz_0/accounts_snapshots.rs 中添加所需的使用声明。

[!tip] 在我们的例子中,我们处理的是 MetadataMintTokenInterface,因此我们需要添加

use anchor_spl::{metadata::Metadata, token::Mint, token_interface::TokenInterface};

步骤 4 - 指定创世程序

trident-tests/fuzz_tests/fuzz_0/test_fuzz.rs 中,指定所有应包含在测试环境中的程序。

[!tip] 在我们的例子中,我们使用CPI到Metaplex Token Metadata,因此我们还需要在创世中包含此程序

  1. 为你的程序创建新的 FuzzingProgram 实例。
  2. 为 Metaplex Token Metadata 程序创建新的 FuzzingProgram 实例。
  3. 使用指定的两个 FuzzingPrograms 初始化 ProgramTest。

[!note] 注意 Metaplex Token Metadata 的入口函数指定为 None。这是因为我们正在包含 SBF 二进制文件,它会自动从 trident-genesis 文件夹中读取。

[!important] 不要忘记指定你希望模糊测试器生成的指令类型,在这种情况下我们希望生成 FuzzInstruction_trident_lesson_part_i。如果你在 Anchor 环境中有多个程序,你可以决定要让模糊测试器生成哪些程序的指令。


// ...
pub type FuzzInstruction = FuzzInstruction_trident_lesson_part_i;
// ...

fn main() {
    loop {
        fuzz_trident!(fuzz_ix: FuzzInstruction, |fuzz_data: MyFuzzData| {

            // 1.
            let fuzzing_program1 = FuzzingProgram::new(
                PROGRAM_NAME_TRIDENT_LESSON_PART_I,
                &PROGRAM_ID_TRIDENT_LESSON_PART_I,
                processor!(convert_entry!(entry_trident_lesson_part_i))
            );

            // 2.
            let fuzzing_program2 = FuzzingProgram::new(
                "metaplex-token-metadata",
                &anchor_spl::metadata::ID,
                None
            );

            // 3.
            let mut client =
                ProgramTestClientBlocking::new(&[fuzzing_program1,fuzzing_program2])
                    .unwrap();

            let _ = fuzz_data.run_with_runtime(PROGRAM_ID_TRIDENT_LESSON_PART_I, &mut client);
        });
    }
}

步骤 5 - 编写模糊测试

[!important] 编写模糊测试分为 3 个基本步骤

  1. 指定 FuzzAccounts 作为账户存储。
  2. 指定 get_data() 函数。
  3. 指定 get_accounts() 函数。
  4. 可选地,你可以指定不变性检查。
    • 不变性检查非常适合在指令之前和之后比较账户。如果指令通过,你可以比较账户是否按预期更新。
指定 FuzzAccounts 存储

[!tip] 并不需要为所有账户指定类型。例如,对于众所周知的程序,如系统程序、token程序或Metaplex程序,我们不需要存储。这意味着在 get_accounts() 函数中我们不需要从存储中获取程序。

[!note] 对于metadata_account,可以存储在类型为 PdaStore 的存储中。然而在我们的示例中,我们只有一个指令,因此我们不需要存储和重复使用metadata_account,此外,代替手动指定种子,我们可以使用Metaplex的 find_pda 函数。

    #[doc = r" 使用 AccountsStorage<T>,其中 T 可以是以下之一:"]
    #[doc = r" Keypair, PdaStore, TokenStore, MintStore, ProgramStore"]
    #[derive(Default)]
    pub struct FuzzAccounts {
        asset: AccountsStorage<PdaStore>,
        // metadata_account: AccountsStorage<PdaStore>,
        mint: AccountsStorage<Keypair>,
        // mpl_token_metadata: AccountsStorage<todo!()>,
        signer: AccountsStorage<Keypair>,
        // system_program: AccountsStorage<todo!()>,
        // token_program: AccountsStorage<todo!()>,
    }
指定 get_data() 函数

[!note] 对于指令输入数据,我们无需做任何事情,除了在 InitializeIx 结构中分配正确的数据字段。这是因为我们可以自动生成随机数据并将其发送到指令。

impl<'info> IxOps<'info> for InitializeIx {
    // ...
    fn get_data(
        &self,
        _client: &mut impl FuzzClient,
        _fuzz_accounts: &mut FuzzAccounts,
    ) -> Result<Self::IxData, FuzzingError> {
        let data = trident_lesson_part_i::instruction::InitializeIx {
            input1: self.data.input1,
            input2: self.data.input2,
            name: self.data.name.clone(),
            symbol: self.data.symbol.clone(),
            uri: self.data.uri.clone(),
        };
        Ok(data)
    }
    // ...
}
指定 get_accounts() 函数

[!note] 在 get_accounts() 中,我们指定从账户存储中创建或重复使用哪些账户。当你的程序包含多个指令时,这特别有帮助。在这种情况下,你可以混合发送到指令的账户,看看是否存在未经授权的访问。

impl<'info> IxOps<'info> for InitializeIx {

    // ...
    fn get_accounts(
        &self,
        client: &mut impl FuzzClient,
        fuzz_accounts: &mut FuzzAccounts,
    ) -> Result<(Vec<Keypair>, Vec<AccountMeta>), FuzzingError> {
        let signer = fuzz_accounts.signer.get_or_create_account(
            self.accounts.signer,
            client,
            10 * LAMPORTS_PER_SOL,
        );

        let mint = fuzz_accounts.mint.get_or_create_account(
            self.accounts.mint,
            client,
            10 * LAMPORTS_PER_SOL,
        );

        let metadata_account =
            anchor_spl::metadata::mpl_token_metadata::accounts::Metadata::find_pda(
                &mint.pubkey(),
            );

        let asset = fuzz_accounts
            .asset
            .get_or_create_account(
                self.accounts.asset,
                &[b"asset", signer.pubkey().as_ref(), mint.pubkey().as_ref()],
                &trident_lesson_part_i::ID,
            )
            .unwrap();

        let signers = vec![signer.clone(), mint.clone()];
        let acc_meta = trident_lesson_part_i::accounts::InitializeContext {
            signer: signer.pubkey(),
            asset: asset.pubkey,
            mint: mint.pubkey(),
            metadata_account: metadata_account.0,
            mpl_token_metadata: anchor_spl::metadata::ID,
            system_program: solana_sdk::system_program::ID,
            token_program: anchor_spl::token::ID,
        }.to_account_metas(None);
        Ok((signers, acc_meta))
        // ...
    }
}

步骤 6 - 运行与结果

要运行特定的模糊测试:

## trident fuzz run fuzz_0
trident fuzz run <FUZZ_TARGET>
模糊测试输出

[!important] Honggfuzz提供的输出如下

  1. 模糊测试迭代次数。
  2. 反馈驱动模式 = Honggfuzz根据反馈生成数据(即基于覆盖率进展的反馈)。
  3. 每秒平均迭代次数
  4. 找到的崩溃次数(恐慌不变性检查失败
------------------------[  0 days 00 hrs 00 mins 01 secs ]----------------------
  Iterations : 688 (out of: 1000 [68%]) # -- 1. --
  Mode [3/3] : Feedback Driven Mode # -- 2. --
      Target : trident-tests/fuzz_tests/fuzzing.....wn-linux-gnu/release/fuzz_0
     Threads : 16, CPUs: 32, CPU%: 1262% [39%/CPU]
       Speed : 680/sec [avg: 688] # -- 3. --
     Crashes : 1 [unique: 1, blocklist: 0, verified: 0] # -- 4. --
    Timeouts : 0 [10 sec]
 Corpus Size : 98, max: 1048576 bytes, init: 0 files
  Cov Update : 0 days 00 hrs 00 mins 00 secs ago
    Coverage : edge: 10345/882951 [1%] pc: 163 cmp: 622547
---------------------------------- [ LOGS ] ------------------/ honggfuzz 2.6 /-
CrashFile 结果

要查看发现的 Crashfile 的结果。

[!tip] CrashFiles 默认存储在 trident-tests/fuzz_tests/fuzzing/hfuzz_workspace/<FUZZ_TARGET> 中。

trident fuzz run-debug <FUZZ_TARGET> <PATH_TO_CRASHFILE>

[!important] 如果在调试期间出现以下错误消息:

:personality set failed: Operation not permitted

运行以下命令:

echo 'settings set target.disable-aslr false' >~/.lldbinit

[!important] 如果出现以下错误消息:

ImportError: cannot import name 'SBData' from 'lldb' (unknown location)

在此查看解决方案 llvm-project-issue

[!important] 当前开发阶段的调试输出非常冗长,并包含 lldb 部分。我们正在努力改善这一体验。在下图中,你可以看到所提供调试输出的示例。

  1. 交易日志系列
  2. 在指令中发送的数据结构
  3. 恐慌崩溃,基于模糊测试是否在 Solana 程序中出现恐慌或不变性检查失败。

CrashFile execution

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

0 条评论

请先 登录 后评论
Ackee-Blockchain
Ackee-Blockchain
江湖只有他的大名,没有他的介绍。