Trdelnik 简介:Solana 和 Anchor 的模糊测试框架

  • Ackee
  • 发布于 2023-08-31 16:10
  • 阅读 13

Trdelnik 是一个基于 Rust 的测试框架,旨在简化 Solana 程序的测试。它引入了模糊测试功能,通过生成大量的随机输入和调用顺序来探测程序中未知的错误和漏洞,从而提高 Solana 程序的可靠性和安全性。文章还介绍了如何使用 Trdelnik 进行模糊测试的步骤以及如何通过崩溃文件进行程序调试。

开发者需要在部署前测试其程序的可靠性和安全性。传统的单元测试通常无法揭示极端情况下的漏洞。Trdelnik 通过为 Solana 程序 引入模糊测试来解决这个问题,模糊测试会生成大量的随机输入和调用顺序,以探测意想不到的弱点。

为什么我们创建了 Trdelnik

Solana 区块链的程序大多用 Rust 编程语言编写。使用 Rust 是有充分理由的,因为它保证了内存安全,而不会像其他使用垃圾回收的编程语言那样牺牲性能。另一方面,Rust 使用了一种新的独特的所有权概念。缺点是 Rust 可能会变得非常复杂,而且学习曲线陡峭。

但是等等,你是否也听说过“只有最好的程序员才能用 Rust 编写代码,而最好的人犯的错误更少”?即使这部分是正确的,也有更好的方法来防止错误,其中一种方法就是测试。广泛而系统的测试是任何软件开发不可或缺的一部分,也是在开发早期发现错误的好方法。

大多数项目只有基本测试或根本没有测试,这怎么可能呢?信不信由你,编写好的测试并不像看起来那么容易,在某些情况下,它甚至可能比单独开发产品花费更长的时间。在快节奏的加密货币世界中,时间表通常非常短,启动新项目的压力非常大。

这就是我们开发 Trdelnik 的原因,Trdelnik 是我们基于 Rust 的测试框架,它提供了几个方便的开发者工具来测试用 Anchor 编写的 Solana 程序。Trdelnik 的主要目标是简化测试环境的设置,提供自动生成的 API 来向自定义程序发送指令,并加速测试过程。

模糊测试 Solana 程序

最近引入 Trdelnik 的一项新功能是模糊测试。它是一种自动化的软件测试技术,可以为你的程序提供生成的随机、无效或意外的输入数据。这有助于发现未知的错误和漏洞,并可能防止对你的程序进行零日漏洞利用。

有几种模糊器用于 Rust 程序,但是,在 Google 上搜索 Solana 模糊测试不会显示任何结果,这就是我们决定将此功能集成到我们的框架中的原因。在底层,Trdelnik 使用了 Google 开发的著名的模糊器 honggfuzz

在下面的文本中,我们将逐步描述如何使用 Trdelnik 进行模糊测试。

TL;DR Solana 模糊测试

## 在你的 Anchor 项目的根目录中,执行这些命令:
cargo install trdelnik-cli
cargo install honggfuzz
trdelnik init
## 现在去 ./trdelnik-tests/src/bin/fuzz_target.rs 并编辑模糊测试模板
## 要运行模糊测试,请用 fuzz_target 替换 <TARGET_NAME>
trdelnik fuzz run <TARGET_NAME>
## 要调试崩溃,传入一个包含以下路径的崩溃 *.fuzz 文件  trdelnik-tests/hfuzz_workspace/<TARGET_NAME>/<CRASH_FILE>.fuzz
trdelnik fuzz run-debug <TARGET_NAME> <CRASH_FILE_PATH>

使用 Trdelnik 进行模糊化测试的步骤

在本教程中,我们将完成包含以下步骤的完整设置:

  1. 设置一个新的 Anchor 项目
  2. 创建一个包含要检测的错误的程序
  3. 初始化 Trdelnik 测试框架
  4. 编写一个简单的模糊测试
  5. 运行模糊测试
  6. 使用模糊测试崩溃文件调试我们的程序
  7. 设置一个新的 Anchor 项目

在本教程中,我们将创建一个新的 Anchor 项目。如果你还没有 Anchor 框架,请安装 0.28.0 版本。

打开一个终端,然后转到你的项目文件夹,我们将在其中创建新项目并验证是否可以构建该项目:

anchor init my-trdelnik-fuzz-test
cd my-trdelnik-fuzz-test
anchor build

验证 Anchor 项目是否已成功初始化和构建。

创建一个包含要检测的错误的程序

接下来,我们将创建一个简单的程序,在其中故意引入一些错误,我们将尝试使用我们的模糊器来查找这些错误。打开你的程序源代码文件 programs /my-trdelnik-fuzz-test/src/lib.rs,并将 declare_id!() 宏之后的所有内容替换为以下代码:

const MAGIC_NUMBER: u8 = 254;

##[program]
pub mod my_trdelnik_fuzz_test {
   use super::*;

   pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
       let counter = &mut ctx.accounts.counter;

       counter.count = 0;
       counter.authority = ctx.accounts.user.key();

       Ok(())
   }

   pub fn update(ctx: Context<Update>, input1: u8, input2: u8) -> Result<()> {
       let counter = &mut ctx.accounts.counter;

       msg!("input1 = {}, input2 = {}", input1, input2);

       // comment this to fix the black magic panic
       if input1 == MAGIC_NUMBER {
           panic!("Black magic not supported!");
       }
       counter.count = buggy_math_function(input1, input2).into();
       Ok(())
   }
}
pub fn buggy_math_function(input1: u8, input2: u8) -> u8 {
   // comment the if statement to cause div-by-zero or subtract with overflow panic
   if input2 >= MAGIC_NUMBER {
       return 0;
   }
   let divisor = MAGIC_NUMBER - input2;
   input1 / divisor
}

##[derive(Accounts)]
pub struct Initialize<'info> {
   #[account(init, payer = user, space = 8 + 40)]
   pub counter: Account<'info, Counter>,

   #[account(mut)]
   pub user: Signer<'info>,
   pub system_program: Program<'info, System>,
}

##[derive(Accounts)]
pub struct Update<'info> {
   #[account(mut, has_one = authority)]
   pub counter: Account<'info, Counter>,
   pub authority: Signer<'info>,
}

##[account]
pub struct Counter {
   pub authority: Pubkey,
   pub count: u64,
}

这是一个简单的程序,包含两个指令:initializeupdateinitialize 指令将创建必要的数据账户,update 指令将更新链上数据。它还包含一个有意的 panic! 宏,如果程序的第一个输入等于常量 MAGIC_NUMBER,该宏将立即终止程序,从而模拟崩溃。

现在,你可以再次使用 anchor build 验证你的程序是否成功构建。

初始化 Trdelnik 测试框架

为了使用 Trdelnik 和模糊器,我们必须安装它们:

cargo install trdelnik-cli
cargo install honggfuzz

如果你已经安装了 Trdelnik,则需要升级到 0.5.0 版本。

之后,我们必须使用以下命令在我们的项目中初始化 Trdelnik 框架:

trdelnik init

此命令将自动生成包含程序 API 的 .program_client 文件夹,将必要的依赖项添加到配置文件,并生成 trdelnik 测试模板。

编写一个简单的模糊测试

模糊测试目标模板位于 trdelnik-tests/src/bin/fuzz_target.rs 文件中。可以根据你的需要修改此文件。

现在,你只需将其内容替换为以下代码即可:

use my_trdelnik_fuzz_test::entry;
use program_client::my_trdelnik_fuzz_test_instruction::*;
const PROGRAM_NAME: &str  = "my_trdelnik_fuzz_test";
use assert_matches::*;
use trdelnik_client::fuzzing::*;

##[derive(Arbitrary)]
pub struct FuzzData {
   param1: u8,
   param2: u8,
}

fn main() {
   loop {
       fuzz!(|fuzz_data: FuzzData| {
           Runtime::new().unwrap().block_on(async {
               let program_test = ProgramTest::new(PROGRAM_NAME, PROGRAM_ID, processor!(entry));

               let mut ctx = program_test.start_with_context().await;

               let counter = Keypair::new();

               let init_ix =
                   initialize_ix(counter.pubkey(), ctx.payer.pubkey(), SYSTEM_PROGRAM_ID);
               let mut transaction =
                   Transaction::new_with_payer(&[init_ix], Some(&ctx.payer.pubkey().clone()));

               transaction.sign(&[&ctx.payer, &counter], ctx.last_blockhash);
               let res = ctx.banks_client.process_transaction(transaction).await;
               assert_matches!(res, Ok(()));

               let res = fuzz_update_ix(
                   &fuzz_data,
                   &mut ctx.banks_client,
                   &ctx.payer,
                   &counter,
                   ctx.last_blockhash,
               )
               .await;
               assert_matches!(res, Ok(()));
           });
       });
   }
}

async fn fuzz_update_ix(
   fuzz_data: &FuzzData,
   banks_client: &mut BanksClient,
   payer: &Keypair,
   counter: &Keypair,
   blockhash: Hash,
) -> core::result::Result<(), BanksClientError> {
   let update_ix = update_ix(
       fuzz_data.param1,
       fuzz_data.param2,
       counter.pubkey(),
       payer.pubkey(),
   );

   let mut transaction = Transaction::new_with_payer(&[update_ix], Some(&payer.pubkey()));
   transaction.sign(&[payer], blockhash);

   banks_client.process_transaction(transaction).await

为了本教程的目的和简单起见,我们仅模糊指令参数。即 update 指令的两个参数 input1input2

如果你查看模糊目标代码,你会看到 main 函数包含一个无限循环,其中包含一个 fuzz! 宏,该宏接受一个闭包,我们在其中传递 FuzzData 结构。

让我们稍微剖析一下代码。模糊目标的入口点是 main 函数,我们在其中一遍又一遍地调用 fuzz! 宏。此宏执行闭包中的代码,并且在每个循环中,传递的 fuzz_data 都不同。

我们正在使用 arbitrary crate,它使我们能够轻松地将非结构化随机数据转换为结构化数据,就像我们在 FuzzData 结构中定义的那样。闭包代码被执行,如果程序没有崩溃,模糊测试将继续进行新的循环迭代,并且传递给闭包的 fuzz_data 被修改。如果 fuzz_data 变量包含导致我们的程序崩溃的数据,则模糊器会自动将数据存储在单独的模糊崩溃文件 ./trdelnik-tests/hfuzz_workspace/<TARGET_NAME>/<CRASH_FILE_NAME>.fuzz 中。这对于后续调试特别有用。

我们的模糊目标中闭包中执行的代码首先创建一个新的 TestProgram 并将我们的程序添加到测试环境中。然后启动测试客户端,这使我们能够将交易发送到我们的程序。有了这个,我们必须首先初始化我们的程序。我们借助 Trdelnik 创建初始化指令,Trdelnik 自动为我们生成了 initialize_ix 函数。最后,我们创建一个新的事务,我们对其进行签名并通过客户端将其发送到测试环境。太好了,我们的程序已初始化!

现在我们调用 fuzz_update_ix 函数,以便将随机数据提供给我们想要模糊的更新指令,并在其中找到错误。同样,我们使用自动生成的 update_ix 函数构造更新指令,并从 FuzzData 结构中提供随机生成的参数。最后,我们创建事务,签名并发送它。就是这样,我们的模糊目标已准备就绪!

运行模糊测试

Trdelnik 提供了一种运行模糊测试的便捷方法。在你的 Anchor 项目中的任何位置,你都可以执行以下命令:

trdelnik fuzz run <TARGET_NAME>

因此,在我们的示例中,如果我们将 <TARGET_NAME> 替换为实际名称,则会得到以下内容:

trdelnik fuzz run fuzz_target

一旦你执行此命令,整个项目都必须构建,并具有用于模糊测试的指令,因此需要一些时间。构建完成后,模糊器将自动启动,如下所示:

在顶部,你可以看到模糊测试统计信息。特别是你的程序崩溃了多少次,其中有多少次是唯一的崩溃,完成了多少次迭代等等。

要停止模糊测试,你可以简单地按 CTRL+C。由于 Trdelnik 在底层使用 Honggfuzz,因此你也可以使用环境变量将参数直接传递给模糊器。

例如:

## 超时:10 秒
## 并发模糊测试线程数:1
## 模糊测试迭代次数:10000
## 在终端中显示 Solana 日志
## 崩溃时退出
HFUZZ_RUN_ARGS="-t 10 -n 1 -N 10000 -Q --exit_upon_crash" trdelnik fuzz run fuzz_target

如果运行上述命令,模糊器将在第一次遇到崩溃后停止,并且传递 -Q 标志允许我们看到 Solana 日志。在我们的 Solana 示例程序中,我们使用 msg! 宏来显示更新指令参数 input1input2 的值,你可以从日志中看到它们的值分别为 254 和 255。

使用模糊测试崩溃文件调试我们的程序

从上面的日志中可以看到,导致我们的程序崩溃的数据存储在 ./trdelnik-tests/hfuzz_workspace/fuzz_target 文件夹中的模糊崩溃文件中。

现在可以使用此崩溃文件并在调试器中“重放”崩溃以检查错误。相应的命令是:

trdelnik fuzz run-debug <TARGET_NAME> <CRASH_FILE_PATH>

因此,在我们的示例中,如果我们将 <TARGET_NAME><CRASH_FILE_PATH> 替换为实际值。崩溃文件的名称可能不同,因此你必须进行相应的修改。

在我们的示例中,生成的命令如下:

trdelnik fuzz run-debug fuzz_target ./trdelnik-tests/hfuzz_workspace/fuzz_target/SIGABRT.PC.7ffff7c8e83c.STACK.1b3a7a7882.CODE.-6.ADDR.0.INSTR.mov____\%eax,\%ebx.fuzz

一旦执行该命令,就必须构建整个项目以进行调试。之后,模糊器会使用崩溃文件中提供的参数运行你的程序,并再次模拟崩溃以进行检查。

现在你可以在调试器中看到,线程在‘Unsupported black magic!’,programs/my-trdelnik-fuzz-test/src/lib.rs:27:13 处崩溃。

这是预期的输出,因为我们已经故意在程序中引入了一个 panic,如果 input1 变量等于我们的 MAGIC_NUMBER(即 254)。

if input1 == MAGIC_NUMBER {
    panic!("Black magic not supported!");
}

在调试器中,你可以执行 help 命令以进行进一步操作,也可以通过执行 q 命令退出。

如果你想尝试在程序中找到另一个错误,可以取消注释 if 语句

if input2 >= MAGIC_NUMBER {
       return 0;
   }

Solana 程序 programs/my-trdelnik-fuzz-test/src/lib.rs 中的 buggy_math_function 函数中,然后再次运行模糊器。一段时间后,应该会找到一个新的唯一崩溃,你可以再次像之前一样使用调试器对其进行分析。

结论

我们已经展示了如何使用 Trdelnik 框架为用 Anchor 编写的 Solana 程序 编写模糊测试。你可以在 Trdelnik 的 GitHub 存储库 中找到整个示例项目。

Trdelnik 的目标不是实现一个新的模糊器,而是提供一种方便的方法来使用现有的 honggfuzz 模糊器,而无需费力地设置测试环境。

就像在你的项目中初始化 Trdelnik 一样简单,你可以开始进行模糊测试了!

接下来的步骤将是账户和指令流模糊测试。

请继续关注未来的更多教程!

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

0 条评论

请先 登录 后评论
Ackee
Ackee
Cybersecurity experts | We audit Ethereum and Solana | Creators of @WakeFramework , Solidity (Wake) & @TridentSolana | Educational partner of Solana Foundation