13_Solana_程序安全

参考: https://github.com/coral-xyz/sealevel-attacks/tree/master


https://www.soldev.app/course/signer-auth

案例 1: 缺少 Signer Authentication

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
26
27
28
29
30
31
use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod insecure_update{
use super::*;
// ...

pub fn update_authority(ctx: Context<UpdateAuthority>) -> Result<()> {
ctx.accounts.vault.authority = ctx.accounts.new_authority.key();
Ok(())
}
}

#[derive(Accounts)]
pub struct UpdateAuthority<'info> {
#[account(
mut,
has_one = authority
)]
pub vault: Account<'info, Vault>,
pub new_authority: AccountInfo<'info>,
pub authority: AccountInfo<'info>,
}

#[account]
pub struct Vault {
token_account: Pubkey,
authority: Pubkey,
}

漏洞分析: 虽然有 has_one = authority, 但它仅检查 vault.authority.pubkey == authority.pubkey, 即检查调用程序的参数中的authority是否和程序中的vaultauthority是否一致, 并没有检查调用者是否是authority。 因此,存在被攻击的风险,即任何人只要将调用参数中的authority设置为和vault中的authority 一致, 都可以成功调用update_authority

漏洞修复:

  • 方案 1:使用 ctx.accounts.authority.is_signer 判断 authority 是否是交易的 signer
    • 缺点: 账户验证和指令逻辑验证是一起的(没有分离)
  • 方案 2:使用 Anchor 的 Signer
    • 优点: 账户验证和指令逻辑验证是分开, 在进入逻辑之前就已经做了校验
    • 缺点: 只能和 Singer 账户一起,不能和其他账户类型
  • 方案 3: 使用 #[account(signer)]
    • 作用和 Signer是一样,但是比 Signer 更灵活,支持更多账户类型
    • 比如,
      1
      2
      #[account(signer)]
      pub authority: Account<'info, SomeData>
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
26
27
28
29
30
31
32
33
34
35
use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod secure_update{
use super::*;
// ...
pub fn update_authority(ctx: Context<UpdateAuthority>) -> Result<()> {
// Signer中已经做了检查
// if !ctx.accounts.authority.is_signer {
// return Err(ProgramError::MissingRequiredSignature.into());
// }

ctx.accounts.vault.authority = ctx.accounts.new_authority.key();
Ok(())
}
}

#[derive(Accounts)]
pub struct UpdateAuthority<'info> {
#[account(
mut,
has_one = authority
)]
pub vault: Account<'info, Vault>,
pub new_authority: AccountInfo<'info>,
pub authority: Signer<'info>,
}

#[account]
pub struct Vault {
token_account: Pubkey,
authority: Pubkey,
}

案例 2: Missing owner check

https://www.soldev.app/course/owner-checks

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
26
27
28
29
30
31
32
use anchor_lang::prelude::*;

declare_id!("Cft4eTTrt4sJU4Ar35rUQHx6PSXfJju3dixmvApzhWws");

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

pub fn admin_instruction(ctx: Context<Unchecked>) -> Result<()> {
let account_data = ctx.accounts.admin_config.try_borrow_data()?;
let mut account_data_slice: &[u8] = &account_data;
let account_state = AdminConfig::try_deserialize(&mut account_data_slice)?;

if account_state.admin != ctx.accounts.admin.key() {
return Err(ProgramError::InvalidArgument.into());
}
msg!("Admin: {}", account_state.admin.to_string());
Ok(())
}
}

#[derive(Accounts)]
pub struct Unchecked<'info> {
admin_config: AccountInfo<'info>,
admin: Signer<'info>,
}

#[account]
pub struct AdminConfig {
admin: Pubkey,
}

漏洞分析:

  • admin_instruction: 检查的是输入参数指定的程序状态(state)与参数是否匹配, 并没有检查数据账户的 owner 是不是本程序帐户

    如下图, 攻击这将 B 数据账户传入给 A 程序,可以通过 A 程序的简单校验,从而修改 A 数据账户的状态

    1
    2
    3
    4
    [A程序账户]        [B程序账户]
    | |
    | |
    [A数据账户] [B数据账户]
  • 攻击案例-国库提币攻击

    • 攻击者的合约,

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      #[account]
      pub struct Vault {
      // 必须保持和被攻击者的账户结构体同名, 即必须Vault, 因为结构体名称的hash作为账户的 Discriminator,
      // 否则被攻击合约序列化的时候报错: Error Message: 8 byte discriminator did not match what was expected

      // 必须保持和被攻击账户的数据结构顺序一致,
      // 结构体内部变量名称可以不同,
      token_accountxx: Pubkey,
      authorityx: Pubkey,
      }
    • 漏洞修复: 将 vault 的 UncheckedAccount 改成 Account, anchor 为 Account 实现了 owner 安全检查

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      #[derive(Accounts)]
      pub struct SecureWithdraw<'info> {
      /// 具体检查如下:
      // has_one:
      // input_args.token_account.key == vault.token_account.key
      // input_args.authority.key == vault.authority.key
      // Acccount的Owner trait 安全检查
      // Account.info.owner == T::owner()
      // `!(Account.info.owner == SystemProgram && Account.info.lamports() == 0)`
      #[account(has_one=token_account, has_one=authority)]
      pub vault: Account<'info, Vault>,

      #[account(mut, seeds=[b"token"], bump)]
      pub token_account: Account<'info, TokenAccount>,
      #[account(mut)]
      pub withdraw_destination: Account<'info, TokenAccount>,
      pub token_program: Program<'info, Token>, // SPL Token Program固定是 TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
      pub authority: Signer<'info>,
      }

案例 3: Account Data Matching

https://www.soldev.app/course/account-data-matching

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
26
27
use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod data_validation {
use super::*;
...
pub fn update_admin(ctx: Context<UpdateAdmin>) -> Result<()> {
ctx.accounts.admin_config.admin = ctx.accounts.new_admin.key();
Ok(())
}
}

#[derive(Accounts)]
pub struct UpdateAdmin<'info> {
#[account(mut)]
pub admin_config: Account<'info, AdminConfig>,
#[account(mut)]
pub admin: Signer<'info>,
pub new_admin: SystemAccount<'info>,
}

#[account]
pub struct AdminConfig {
admin: Pubkey,
}
  • 漏洞分析:

    • update_admin缺少校验: ctx.accounts.admin_conifg.admin == ctx.accounts.admin
  • 漏洞修复:

    • 方案 1: 在update_admin增加校验 ctx.accounts.admin_conifg.admin == ctx.accounts.admin
    • 方案 2: 使用has_one约束, 为admin_config增加约束 #[account(has_one = admin)], 这样和方案 1 等效
    • 方案 3: 使用constraint约束, 为admin_config增加约束 #[account(constraint = admin_config.admin == admin.key())], 这样和方案 1 等效

示例代码

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//...
pub fn insecure_withdraw(ctx: Context<InsecureWithdraw>) -> Result<()> {
// 缺少对 authority的校验
let amount = ctx.accounts.token_account.amount;

let seeds = &[b"vault".as_ref(), &[ctx.bumps.vault]];
let signer = [&seeds[..]];

let cpi_ctx = CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
token::Transfer {
from: ctx.accounts.token_account.to_account_info(),
authority: ctx.accounts.vault.to_account_info(),
to: ctx.accounts.withdraw_destination.to_account_info(),
},
&signer,
);

token::transfer(cpi_ctx, amount)?;
Ok(())
}
// ...

#[derive(Accounts)]
pub struct InsecureWithdraw<'info> {
#[account(
seeds = [b"vault"],
bump,
// 缺少对 authority的校验
)]
pub vault: Account<'info, Vault>,

#[account(
mut,
seeds = [b"token"],
bump,
)]
pub token_account: Account<'info, TokenAccount>,
#[account(mut)]
pub withdraw_destination: Account<'info, TokenAccount>,
pub token_program: Program<'info, Token>,
pub authority: Signer<'info>,
}

#[account]
pub struct Vault {
token_account: Pubkey,
authority: Pubkey,
withdraw_destination: Pubkey,
}

修复方案: 为 vault 增加约束

1
2
3
4
5
6
7
8
9
#[account(
mut,
seeds = [b"vault"],
bump,
has_one = authority,
has_one=token_account,
has_one = withdraw_destination,
)]
pub vault: Account<'info, Vault>,

案例 4: Re-initialization Attacks (重新初始化攻击)

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
26
27
28
29
use anchor_lang::prelude::*;
use borsh::{BorshDeserialize, BorshSerialize};

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

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

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let mut user = User::try_from_slice(&ctx.accounts.user.data.borrow()).unwrap();
user.authority = ctx.accounts.authority.key();
user.serialize(&mut *ctx.accounts.user.data.borrow_mut())?;
Ok(())
}
}

#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
user: AccountInfo<'info>,
#[account(mut)]
authority: Signer<'info>,
}

#[derive(BorshSerialize, BorshDeserialize)]
pub struct User {
authority: Pubkey,
}

漏洞分析:

  • Initialize的 user 采用的 手动初始化, 没有 is_initialize标识, 可以重复初始化

修复方案:

  • 方案 1: 在User中增加is_initialize字段,并且在指令处理函数中增加 is_initialize的判断, 防止重复初始化

  • 方案 2(推荐): 使用 Anchor 的init约束, init约束通过 CPI 调用 System Program 创建一个账户,并且设置账户discrimiantor,

    • init 约束可以确保每个账户只能被初始化一次
    • init约束必须和 payerspace 一起使用
      • space: 指定账户的空间大小,这决定了支付的租金大小
        • 8字节,存放账户的discrimiantor, 即账户结构体名称的哈希
      • payer: 支付初始化账户的费用
  • 方案 3: 使用 Anchor 的 init_if_needed约束, 要谨慎:

    • 如果指定的账户不存在,它会创建并初始化该账户
    • 如果账户已经存在,它会跳过初始化步骤,直接使用现有账户。
    • init_if_needed与普通 init 的区别:
      • init 总是尝试创建新账户,如果账户已存在会失败。
      • init_if_needed 在账户存在时不会失败,而是跳过初始化。

案例 5: 相同的可修改账户

https://www.soldev.app/course/duplicate-mutable-accounts

一个”石头剪刀布”游戏程序

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
use anchor_lang::prelude::*;
use borsh::{BorshDeserialize, BorshSerialize};

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

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

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.new_player.player = ctx.accounts.payer.key();
ctx.accounts.new_player.choice = None;
Ok(())
}

pub fn rock_paper_scissors_shoot_insecure(
ctx: Context<RockPaperScissorsInsecure>,
player_one_choice: RockPaperScissors,
player_two_choice: RockPaperScissors,
) -> Result<()> {
ctx.accounts.player_one.choice = Some(player_one_choice);

ctx.accounts.player_two.choice = Some(player_two_choice);
Ok(())
}
}

#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(
init,
payer = payer,
space = 8 + 32 + 8
)]
pub new_player: Account<'info, PlayerState>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct RockPaperScissorsInsecure<'info> {
#[account(mut)]
pub player_one: Account<'info, PlayerState>,
#[account(mut)]
pub player_two: Account<'info, PlayerState>,
}

#[account]
pub struct PlayerState {
player: Pubkey,
choice: Option<RockPaperScissors>,
}

#[derive(Clone, Copy, BorshDeserialize, BorshSerialize)]
pub enum RockPaperScissors {
Rock,
Paper,
Scissors,
}

漏洞分析: RockPaperScissorsInsecureplayer_oneplayer_two 可以相同, 攻击可以传入 2 个相同的地址

漏洞修复:

  • 方案 1: 直接在指令处理函数中增加判断 ctx.accounts.player_one() != ctx.account.player_two.key()

  • 方案 2(推荐): 使用 Anchor 的 constraint,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #[derive(Accounts)]
    pub struct RockPaperScissorsSecure<'info> {
    #[account(
    mut,
    constraint = player_one.key() != player_two.key() // 检查
    )]
    pub player_one: Account<'info, PlayerState>,
    #[account(mut)]
    pub player_two: Account<'info, PlayerState>,
    }

案例 6: type-cosplay

https://www.soldev.app/course/type-cosplay

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
26
27
28
29
30
31
32
33
34
35
36
37
38
use anchor_lang::prelude::*;
use borsh::{BorshDeserialize, BorshSerialize};

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

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

pub fn admin_instruction(ctx: Context<AdminInstruction>) -> Result<()> {
let account_data =
AdminConfig::try_from_slice(&ctx.accounts.admin_config.data.borrow()).unwrap();
if ctx.accounts.admin_config.owner != ctx.program_id {
return Err(ProgramError::IllegalOwner.into());
}
if account_data.admin != ctx.accounts.admin.key() {
return Err(ProgramError::InvalidAccountData.into());
}
msg!("Admin {}", account_data.admin);
Ok(())
}
}

#[derive(Accounts)]
pub struct AdminInstruction<'info> {
admin_config: UncheckedAccount<'info>,
admin: Signer<'info>,
}

#[derive(BorshSerialize, BorshDeserialize)]
pub struct AdminConfig {
admin: Pubkey,
}

#[derive(BorshSerialize, BorshDeserialize)]
pub struct UserConfig {
user: Pubkey,
}

漏洞分析:

  • AdminConfigUserConfig 有相同的数据结构, 因此 2 个类型可以随意传参,

漏洞修复:

  • 方案 1: 使用 Anchor 的 Account类型, 为类型增加类型标识(Discriminator)

    1
    2
    3
    4
    5
    6
    7

    #[derive(Accounts)]
    pub struct AdminInstruction<'info> {
    #[account(has_one = admin)]
    admin_config: Account<'info, AdminConfig>,
    admin: Signer<'info>,
    }

案例 7: Arbitrary CPI

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
26
27
28
29
30
31
32
33
34
35
36
37
use anchor_lang::prelude::*;
use anchor_lang::solana_program;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

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

pub fn cpi(ctx: Context<Cpi>, amount: u64) -> ProgramResult {

//
solana_program::program::invoke(
&spl_token::instruction::transfer(
ctx.accounts.token_program.key,
ctx.accounts.source.key,
ctx.accounts.destination.key,
ctx.accounts.authority.key,
&[],
amount,
)?,
&[
ctx.accounts.source.clone(),
ctx.accounts.destination.clone(),
ctx.accounts.authority.clone(),
],
)
}
}

#[derive(Accounts)]
pub struct Cpi<'info> {
source: UncheckedAccount<'info>,
destination: UncheckedAccount<'info>,
authority: UncheckedAccount<'info>,
token_program: UncheckedAccount<'info>, // 没有做任何检测
}

漏洞分析:

  • 没有检查token_program , 因此,可以传入任意值
  • 直接使用原生的invoke和指令组装进行 CPI 调用,缺少安全检查

漏洞修复:

  • 方案 1: 在cpi中增加检查

    1
    2
    3
    if &spl_token::ID != ctx.accounts.token_program.key {
    return Err(ProgramError::IncorrectProgramId);
    }
  • 方案 2: 使用 Anchor 的 CPI 模块进行 CPI 调用, Anchor 在 CPI 内部做了一系列安全检查

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
26
27
28
29
30
31
32
33
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount};

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

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

pub fn cpi(ctx: Context<Cpi>, amount: u64) -> ProgramResult {
token::transfer(ctx.accounts.transfer_ctx(), amount)
}
}

#[derive(Accounts)]
pub struct Cpi<'info> {
source: Account<'info, TokenAccount>,
destination: Account<'info, TokenAccount>,
authority: Signer<'info>,
token_program: Program<'info, Token>,
}

impl<'info> Cpi<'info> {
pub fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, token::Transfer<'info>> {
let program = self.token_program.to_account_info();
let accounts = token::Transfer {
from: self.source.to_account_info(),
to: self.destination.to_account_info(),
authority: self.authority.to_account_info(),
};
CpiContext::new(program, accounts)
}
}

案例, 对战游戏: https://github.com/Unboxed-Software/solana-arbitrary-cpi/tree/starter/programs

账户结构:

1
2
3
4
5

Gameplay Program Metadata Program Metadata Fake Program

[character A] [metadata account A] [metadata account X]
[character B] [metadata account B]

漏洞分析: 因为gameplayBattleInsecure 的 metadata_program 和 player 可以任意传入,并且指令处理函数中也没有进行判断,
那么,攻击者就可以伪造 一个 Metadata Fake 程序,在 Fake 程序中为角色设置很高health, 这样,攻击者可以一直获胜

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#[derive(Accounts)]
pub struct BattleInsecure<'info> {
pub player_one: Account<'info, Character>,
pub player_two: Account<'info, Character>,


/// CHECK: manual checks 漏洞
pub player_one_metadata: UncheckedAccount<'info>,
/// CHECK: manual checks 漏洞
pub player_two_metadata: UncheckedAccount<'info>,
/// CHECK: intentionally unchecked 漏洞
pub metadata_program: UncheckedAccount<'info>,
}

漏洞修复: 使用 Anchor 自带的 Program类型, 其中做了检查account_info.key == expected_program && account_info.executable == true

1
pub metadata_program: Program<'info, CharacterMetadata>,

案例 8: Bump Seed Canonicalization

  • 对于每个 seed, 有效的 bump 值在 [0, 255]闭区间, 共256
  • 有效的 bump 值,是确保 PDA 在 ED25519 曲线之外
  • 对于单个 bump 值,有约80%概率是有效的, 因此,生成有效 bump 值是很容易的
  • Canonical bump 指的是最大有效bump值, 从255开始递减
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
26
27
28
29
30
31
32
33
34
use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

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

pub fn set_value(ctx: Context<BumpSeed>, key: u64, new_value: u64, bump: u8) -> Result<()> {
let address = Pubkey::create_program_address(
&[key.to_le_bytes().as_ref(),
&[bump]],
ctx.program_id
).unwrap();

if address != ctx.accounts.data.key() {
return Err(ProgramError::InvalidArgument.into());
}

ctx.accounts.data.value = new_value;

Ok(())
}
}

#[derive(Accounts)]
pub struct BumpSeed<'info> {
data: Account<'info, Data>,
}

#[account]
pub struct Data {
value: u64,
}
  • 漏洞分析:

    • key 和 bump 都是由外部输入,那么,就存在碰撞风险
    • PDA 没有建立与账户 1对1绑定的关系, 用户可以传入任意的有效 key 和 bump 来生成多个PDA 账户
  • 漏洞修复:

    • 方案 1: 推荐使用 find_program_address 生成有效的 canonical bump
    • 方案 2: 使用 Anchor 的 seedsbump 约束,
      • 注意: 如果不指定 bump,由 solana 自动计算,则需要消耗更多计算单元(CU)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // initialize account at PDA
    #[derive(Accounts)]
    #[instruction(key: u64)]
    pub struct BumpSeed<'info> {
    #[account(mut)]
    payer: Signer<'info>,
    #[account(
    init,
    seeds = [key.to_le_bytes().as_ref()],
    // 会自动生成 canonical bump, 来生成 PDA, 需要消耗更多计算单元
    // derives the PDA using the canonical bump
    bump,
    payer = payer,
    space = 8 + 8
    )]
    data: Account<'info, Data>,
    system_program: Program<'info, System>
    }

    #[account]
    pub struct Data {
    value: u64,
    }

  • 空投案例

案例9: Closing Account重入攻击

原理: 因为Solana的垃圾回收是整个交易结束之后才进行,而一笔交易包含多个指令, 在交易插入一笔发送“租金”的指令,这样,账户就不会回收

示例:

彩票案例

关于Anchor的close属性约束的细节:

  1. 执行时机:
    是的,close约束是在指令执行之后关闭账户。更具体地说,它是在指令的主要逻辑执行完成后,但在指令完全结束之前执行的。

  2. 执行顺序:
    在一个Anchor指令中,执行顺序通常是:

    • 首先执行所有的前置约束(比如检查账户所有者、初始化检查等)
    • 然后执行指令的主要逻辑
    • 最后执行close等后置约束
  3. 功能:
    close约束会做以下3件事:

    • 将账户的lamports(Solana的原生代币)转移到指定的接收者账户
    • 将账户数据的前8个字节设置为CLOSED_ACCOUNT_DISCRIMINATOR
    • 将账户的大小设置为0

注意:

  • close约束是在单个指令结束之前执行3个关闭操作(退钱,清零,改owner)
  • solana垃圾回收的时机是 整个交易 执行结束

案例10: PDA Sharing(PDA账户被多个账户共享)

  • PDA账户共享, 导致一个账户可以访问别人的PDA
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount};

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

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

pub fn withdraw_tokens(ctx: Context<WithdrawTokens>) -> Result<()> {
let amount = ctx.accounts.vault.amount;
let seeds = &[ctx.accounts.pool.mint.as_ref(), &[ctx.accounts.pool.bump]];
token::transfer(ctx.accounts.transfer_ctx().with_signer(&[seeds]), amount)
}
}

#[derive(Accounts)]
pub struct WithdrawTokens<'info> {
#[account(has_one = vault, has_one = withdraw_destination)]
pool: Account<'info, TokenPool>,
vault: Account<'info, TokenAccount>,
withdraw_destination: Account<'info, TokenAccount>,
authority: AccountInfo<'info>,
token_program: Program<'info, Token>,
}

impl<'info> WithdrawTokens<'info> {
pub fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, token::Transfer<'info>> {
let program = self.token_program.to_account_info();
let accounts = token::Transfer {
from: self.vault.to_account_info(),
to: self.withdraw_destination.to_account_info(),
authority: self.authority.to_account_info(),
};
CpiContext::new(program, accounts)
}
}

#[account]
pub struct TokenPool {
vault: Pubkey,
mint: Pubkey,
withdraw_destination: Pubkey,
bump: u8,
}

漏洞分析:

注意: 因为这个示例不完整,缺少详细的细节,所以只能大概表示意思即可,不必细究

  • TokenPool.vault PDA的签名seeds是 ctx.accounts.pool.mint.as_ref()&[ctx.accounts.pool.bump, 这就导致:
    • 多个Pool 会共用同一个 Vault, 这就导致所有人都可以生成一个TokenPool并将其中的 vault的余额提走
  • 指令WithdrawTokens中的 pool , 虽然做了 vault 和 withdraw_destination的比对,但是对于调用者没有进行鉴权, 任何人都可以调用

漏洞修复:

  • 使用Anchor的 seeds 和 bump约束, 将 pool.withdraw_destination作为seeds
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount};

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

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

pub fn withdraw_tokens(ctx: Context<WithdrawTokens>) -> Result<()> {
let amount = ctx.accounts.vault.amount;
let seeds = &[
ctx.accounts.pool.withdraw_destination.as_ref(),
&[ctx.accounts.pool.bump],
];
token::transfer(ctx.accounts.transfer_ctx().with_signer(&[seeds]), amount)
}
}

#[derive(Accounts)]
pub struct WithdrawTokens<'info> {
#[account(
has_one = vault,
has_one = withdraw_destination,
seeds = [withdraw_destination.key().as_ref()],
bump = pool.bump,
)]
pool: Account<'info, TokenPool>,
vault: Account<'info, TokenAccount>,
withdraw_destination: Account<'info, TokenAccount>,
token_program: Program<'info, Token>,
}

impl<'info> WithdrawTokens<'info> {
pub fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, token::Transfer<'info>> {
let program = self.token_program.to_account_info();
let accounts = token::Transfer {
from: self.vault.to_account_info(),
to: self.withdraw_destination.to_account_info(),
authority: self.pool.to_account_info(),
};
CpiContext::new(program, accounts)
}
}

#[account]
pub struct TokenPool {
vault: Pubkey,
mint: Pubkey,
withdraw_destination: Pubkey,
bump: u8,
}
  • Copyrights © 2021-2024 youngqqcn

请我喝杯咖啡吧~