初探web3安全审计

Author Avatar
Xzhah 7月 17, 2023
  • 在其它设备中阅读本文章

初探web3安全审计(1)

TL;DR

​ 这是一篇初学solana安全审计的文章,从solana入门学习开始,自己记录了一些东西后续参考

Solana

​ 首先是solana开发,强烈安利Programming on Solana - An Introduction | paulx这一篇文章,很多概念说的非常清楚。

program与account

​ 因为在solana中program是无状态的,所以需要创建并管理一个program account来帮助存储数据。program通过对account的数据进行读写进而更新状态。

anchor入门

anchor test

​ 客户端ts测试脚本中可以使用program.methods.xxx来访问#[program]中暴露出来的api函数。

anchor constraint

​ 我理解has_one constraint的作用是对账户某个filed进行验证看是不是程序期待的那个field(比如对owner,signer等进行验证),看例子:

1
2
3
4
5
6
#[derive(Accounts)]
pub struct SetData<'info> {
#[account(mut, has_one = authority)]
pub puppet: Account<'info, Data>,
pub authority: Signer<'info>
}

这里has_one constraint会检查puppet.authority = authority.key()

Cross-Program Invocations

​ 1. 跨程序调用的时候,需要注意一个点是:在跨程序调用(Cross-Program Invocations,CPIs)中,通常情况下,你需要传递 AccountInfo 对象来表示外部程序的账户信息。这是因为 CPIs 通常涉及到与外部程序的智能合约交互,这些外部程序可能有它们自己的账户状态需要更新或操作。

​ 当你在一个程序中调用另一个程序时,你需要提供被调用程序所需的账户信息,以便它可以访问和操作这些账户。因此,通常情况下,你会为 CPIs 传递 AccountInfo 对象,以表示外部程序的账户。

​ 例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#[program]
mod puppet_master {
use super::*;
pub fn pull_strings(ctx: Context<PullStrings>, data: u64) -> Result<()> {
puppet::cpi::set_data(ctx.accounts.set_data_ctx(), data)
}
}


#[derive(Accounts)]
pub struct PullStrings<'info> {
#[account(mut)]
pub puppet: Account<'info, Data>,
pub puppet_program: Program<'info, Puppet>,
pub authority: Signer<'info>
}

impl<'info> PullStrings<'info>{
pub fn set_data_ctx(&self) -> CpiContext<'_, '_, '_, 'info, SetData<'info>> {
let cpi_program: AccountInfo<'_> = self.puppet_program.to_account_info();
let cpi_accounzaizuots = SetData {
puppet: self.puppet.to_account_info(),
authority: self.authority.to_account_info()
};
CpiContext::new(cpi_program, cpi_accounts)

这里面set_data_ctx是一个在为跨程序调用函数准备Context的函数,你可以看到,在这个函数中生成的SetData{}架构体,里面包含的账户都做了to_account_info处理。

​ 2.跨程序调用后,调用者里关于被调用程序的账户信息不会及时更新,比如上面例子中的puppet账户,如果要及时更新可以使用reload api。例子如下

1
2
3
4
5
6
puppet::cpi::set_data(ctx.accounts.set_data_ctx(), data)?;
ctx.accounts.puppet.reload()?;//更新puppet账户,该账户没有和被调用程序里的相应数据实时关联
if ctx.accounts.puppet.data != 42 {
panic!();
}
Ok(())

​ 3.获取被调用程序的返回值,可以通过set、get获取。需要注意的一点是:返回值的类型必须实现AnchorSerializeAnchorDeserialize这两个特性。比如如果返回的是MyData,那MyData的实现就是如下:

1
2
3
4
#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct MyData {
pub value: u64,
}

​ 跨程序调用获取MyData返回值的例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pub fn pull_strings(ctx: Context<PullStrings>, data: u64) -> Result<MyData> {
let cpi_program = ctx.accounts.puppet_program.to_account_info();
let cpi_accounts = SetData {
puppet: ctx.accounts.puppet.to_account_info(),
};
let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
let result = puppet::cpi::set_data(cpi_ctx, data)?;
// The below statement calls sol_get_return and deserializes the result.
// `return_data` contains the return from `set_data`,
// which in this example is of type `MyData`.
let return_data = result.get::<MyData>()?;
// ... do something with the `return_data` ...
Ok(return_data)
}

Solana程序的调试

​ 链上程序调试较困难,可以多使用测试脚本进行测试

影响判断

1.漏洞的影响判断可以参考https://immunefi.com/immunefi-vulnerability-severity-classification-system-v2-3/

心得

1.对于任何用户都能调用的函数要警惕,重点审计

2.如果是无符号数,警惕加减等操作

3.如果都是整数,做除法留意,比如是不是向下取整,合不合逻辑。或者除零等操作。

4.to_u64这种情况,可以留意小数舍去方式是否符合当前的场景

5.realloc之前留意是否检查分配大小。

6.账户检查等问题

7.access_control宏不保留调用函数的属性,报错传不上来

8.对于用户可控参数要警惕。

9.anchor的一些实现,可以用cargo expand看看具体实现,比如clock: Sysvar<’info, Clock>anchor有没有对其进行owner的验证

踩坑

Cargo.toml中子依赖版本切换

​ 在复现anchor-book中的tic-tac-toe项目使用anchor test测试时(anchor-book/programs/tic-tac-toe/programs/tic-tac-toe at master · coral-xyz/anchor-book (github.com)),对比实验发现高版本solana(1.16.10)会失败(报错:Error: failed to send transaction: Transaction simulation failed: Error processing Instruction 0: Program failed to complete)。

​ 切换到低版本solana(1.14.11),不会出现该错误。可以把clone下来的项目跑起来。但是自己anchor init新建的项目不行,问题在于anchor-lang下面的3级子依赖项constant_time_eq的版本太高了。最终把子依赖及其父依赖一起降版本即可:

1
2
3
4
anchor-lang = "=0.24.2"
solana-program = "=1.9.15"
blake3 = "=1.3.1"
constant_time_eq = "=0.1.5"

​ 注意,一定要用”=”号指定版本,否则可能会是其他可兼容的版本,达不到效果。