Solana 60 天课程

2025年02月27日更新 89 人订阅
原价: ¥ 66 限时优惠
专栏简介 开始 Solana - 安装与故障排除 Solana 和 Rust 中的算术与基本类型 Solana Anchor 程序 IDL Solana中的Require、Revert和自定义错误 Solana程序是可升级的,并且没有构造函数 Solidity开发者的Rust基础 Rust不寻常的语法 Rust 函数式过程宏 Rust 结构体与属性式和自定义派生宏 Rust 和 Solana 中的可见性与“继承” Solana时钟及其他“区块”变量 Solana 系统变量详解 Solana 日志、“事件”与交易历史 Tx.origin、msg.sender 和 onlyOwner 在 Solana 中:识别调用者 Solana 计算单元与交易费用介绍 在 Solana 和 Anchor 中初始化账户 Solana 计数器教程:在账户中读写数据 使用 Solana web3 js 和 Anchor 读取账户数据 在Solana中创建“映射”和“嵌套映射” Solana中的存储成本、最大存储容量和账户调整 在 Solana 中读取账户余额的 Anchor 方法:address(account).balance 功能修饰符(view、pure、payable)和回退函数在 Solana 中不存在的原因 在 Solana 上实现 SOL 转账及构建支付分配器 使用不同签名者修改账户 PDA(程序派生地址)与 Solana 中的密钥对账户 理解 Solana 中的账户所有权:从PDA中转移SOL Anchor 中的 Init if needed 与重初始化攻击 Solana 中的多重调用:批量交易与交易大小限制 Solana 中的所有者与权限 在Solana中删除和关闭账户与程序 在 Anchor 中:不同类型的账户 在链上读取另一个锚点程序账户数据 在 Anchor 中的跨程序调用(CPI) SPL Token 的运作方式 使用 Anchor 和 Web3.js 转移 SPL Token Solana 教程 - 如何实现 Token 出售 基础银行教程 Metaplex Token 元数据工作原理 使用Metaplex实施代币元数据 使用 LiteSVM 进行时间旅行测试 Solana Token-2022 标准规范 生息代币第一部分 计息代币第二部分 Solana 指令自省 Solana 中的 Ed25519 签名验证 Solana - Switchboard 预言机使用 原生Solana:程序入口与执行 原生 Solana :读取账户数据 原生 Solana :Borsh 序列化 原生 Solana:使用 invoke 和 invoke signed 进行跨程序调用 原生 Solana :创建存储账户 (一) 原生 Solana:创建存储账户 二 原生 Solana: 函数分发 原生 Solana:关键安全检查 Rust 程序到 SBF 编译 sBPF 虚拟机和指令集介绍 跟踪 sBPF 指令执行和计算成本 Solana 程序执行与输入序列化 指令处理器和运行时设置 sBPF 内存布局和寄存器约定 使用 sBPF 汇编读取 Solana 指令输入 Solana 系统调用:sBPF 汇编中的日志记录

原生 Solana:关键安全检查

本文详细介绍了Solana原生程序开发中的多项关键安全检查,包括验证账户所有权、系统变量和程序ID、要求签名者、强制可写账户、跨程序调用后状态重载以及防范代币账户粉尘攻击和PDA账户抢占。文章通过具体代码示例和真实案例(如Wormhole漏洞)深入剖析了潜在的安全风险及其修复方法,旨在帮助开发者构建更安全的Solana程序。

原生 Solana:基本安全检查

在我们之前的原生 Solana 教程中,我们为了保持示例简洁并专注于核心主题而省略了安全检查。

在本教程中,我们将介绍原生 Solana 程序的基本安全检查:验证账户所有权、验证系统变量 (sysvar) 和程序 ID、要求签名者、强制账户可写、CPI 后重新加载状态以及处理代币账户“灰尘攻击”。

验证账户所有权

在使用账户数据之前,请检查其所有者是否与预期的程序 ID 匹配。否则,攻击者可能会传递一个由他们控制的带有恶意数据的账户。

在 Anchor 中,使用 Account<'info, T> 定义账户会自动检查该账户是否由你的程序拥有。对于外部账户,你可以添加 #[account(owner = <ID>)] 属性来强制特定程序 ID 拥有该账户。

例如,假设我们有一个 Config 账户,用于控制提款。如果我们不验证 Config 是否由我们的程序拥有,攻击者可以传递一个带有虚假数据的假账户,并在不应该提款时进行提款。

Copyuse borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{account_info::AccountInfo, program_error::ProgramError};

#[derive(BorshDeserialize, BorshSerialize)]
struct Config { withdraw_cap: u64 }

pub fn withdraw(config: &AccountInfo, amount: u64) -> Result<(), ProgramError> {
    // Missing check: config.owner == program_id
    let cfg = Config::try_from_slice(&config.try_borrow_data()?)?;

    if amount <= cfg.withdraw_cap {
        // proceed to transfer funds...
    }

    Ok(())
}

为了解决这个问题,我们在反序列化或使用 config 账户的数据之前,检查它是否由我们的程序拥有。

Copyuse solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey};

pub fn withdraw(config: &AccountInfo, amount: u64, program_id: &Pubkey) -> Result<(), ProgramError> {
        // Check that the config account is owned by our program
    if config.owner != program_id { return Err(ProgramError::IncorrectProgramId); }

    // Rest of the function...

    Ok(())
}

验证系统变量 (Sysvar) 和程序 ID

当你需要像 Clock 系统变量或系统程序这样的系统变量时,务必验证它是真实的。攻击者可以传递带有被操纵数据的虚假账户。

在 Anchor 程序中,这通过使用 Sysvar<'info, Clock>Program<'info, System> 类型来强制执行,这些类型会为你处理检查。但我们必须在原生程序中手动检查 ID。

在这个例子中,传递给 withdraw_timelockclock 账户预期是 Clock 系统变量,但如果没有验证,攻击者可以传递一个带有被操纵时间戳的虚假 clock 账户,从而提前提款。

Copy// Vulnerable: fake sysvar allows time manipulation
use solana_program::{account_info::AccountInfo, program_error::ProgramError, clock::Clock};

#[derive(BorshDeserialize, BorshSerialize)]
struct TimeLock {
    unlock_time: i64,
    amount: u64,
}

pub fn withdraw_timelock(timelock: &AccountInfo, clock: &AccountInfo) -> Result<(), ProgramError> {
    // Missing: verify clock.key == sysvar::clock::ID
    let timelock = TimeLock::try_from_slice(&timelock.try_borrow_data()?)?;

    // Attacker can pass fake clock account with manipulated timestamp
    let clock = Clock::from_account_info(clock)?;

    if clock.unix_timestamp >= timelock.unlock_time {
        // Process early withdrawal with fake timestamp
    }
    Ok(())
}

作为修复,请务必确保系统变量和程序账户地址匹配。

Copyuse solana_program::{sysvar, program_error::ProgramError, account_info::AccountInfo};
// Rest of the code...

pub fn withdraw_timelock(timelock: &AccountInfo, clock: &AccountInfo) -> Result<(), ProgramError> {
    // Fix: verify clock.key == sysvar::clock::ID
    if clock.key != &sysvar::clock::ID {
        return Err(ProgramError::InvalidAccountData);
    }

    // Rest of the function...

    Ok(())
}

真实案例

2022 年 2 月,Solana 上的 3.2 亿美元 Wormhole 桥漏洞利用事件的发生,是因为程序没有确保提供的系统程序地址与实际的系统程序地址匹配。攻击者传入了一个虚假的系统账户,绕过了签名检查,使他们未经授权地铸造了 120,000 wETH(封装的 ETH)。

这就是为什么 Solana 程序必须始终验证系统账户和系统变量是否与其官方 ID 匹配。 你可以阅读 CertiK 对此的完整分析。

要求签名者

当你的程序将某个操作限制为特定权限(例如,管理员)时,仅仅检查账户的公钥是否与预期匹配是不够的。攻击者可以将真实的管理员账户作为非签名者包含在交易中,从而通过该检查。你还必须验证该账户是否实际签署了交易(admin.is_signer),这证明私钥所有者已批准该操作。在 Anchor 中,#[account(signer)] 属性为你处理了这一点。

下面是一个易受攻击的原生 Rust 示例,其中一个函数更新 config 账户中的提款上限:

Copy// Vulnerable: checks admin key but not that admin actually signed

#[derive(BorshDeserialize, BorshSerialize)]
struct Config { admin: Pubkey, withdraw_cap: u64 }

pub fn update_withdraw_cap(config: &AccountInfo, admin: &AccountInfo, new_cap: u64) -> Result<(), ProgramError> {
    let mut cfg = Config::try_from_slice(&config.try_borrow_data()?)?;

    // Missing check: require admin.is_signer

    if *admin.key != cfg.admin { return Err(ProgramError::InvalidAccountData); }
    cfg.withdraw_cap = new_cap;
    cfg.serialize(&mut &mut config.try_borrow_mut_data()?[..])?;
    Ok(())
}

为了解决这个问题,要求管理员是签名者,并且与存储的管理员密钥匹配:

Copypub fn update_withdraw_cap(config: &AccountInfo, admin: &AccountInfo, new_cap: u64) -> Result<(), ProgramError> {
    let mut cfg = Config::try_from_slice(&config.try_borrow_data()?)?;

    // Ensure admin is a signer
    if !admin.is_signer { return Err(ProgramError::MissingRequiredSignature); }

    // Ensure admin is the expected admin
    if !admin.key != cfg.admin { return Err(ProgramError::InvalidAccountData); }

    cfg.withdraw_cap = new_cap;
    cfg.serialize(&mut &mut config.try_borrow_mut_data()?[..])?;
    Ok(())
}

要求数据或 Lamport 修改的可写账户

如果你的程序需要修改账户的 lamport 或数据,客户端必须在交易中将该账户标记为可写,并且你的程序应验证其是否可写。如果账户未标记为可写,尝试修改它将导致交易失败并出现错误(例如,“Readonly account changed”)。

要在 TypeScript 客户端中将账户标记为可写,我们在交易构建期间将其 isWritable 标志设置为 true

Copy// web3.js: set isWritable = true for accounts you will modify
import { TransactionInstruction, PublicKey } from "@solana/web3.js";

const ix = new TransactionInstruction({
  programId,
  keys: [\
    { pubkey: userAccount, isSigner: false, isWritable: true }, // needs mutation\
    { pubkey: payer, isSigner: true, isWritable: false },\
  ],
  data: Buffer.from([]),
});

我们的程序可以使用 AccountInfo 结构体的 is_writable 字段检查账户是否可写。

Copypub fn update_user_balance(user_account: &AccountInfo) -> Result<(), ProgramError> {
        // Check if supplied user account is writable before proceeding
    if !user_account.is_writable {
        return Err(ProgramError::InvalidAccountData);
    }

    // Rest of the function...
    Ok(())
}

在 Anchor 中,你通过 #[account(mut)] 来强制执行这一点,它在你的函数运行之前检查 is_writable = true

每次 CPI 后重新加载账户状态

CPI 可以修改账户数据。在 CPI 调用之后,你的程序必须重新读取账户,然后才能根据其做出决策。

下面是未重新加载时可能发生的情况(使用原生 Rust 程序代码):


Copy// Vulnerable: using stale account data after CPI
use solana_program::{
    account_info::AccountInfo,
    program::{invoke},
    instruction::Instruction,
    program_error::ProgramError
};
use borsh::BorshDeserialize;

#[derive(BorshDeserialize)]
struct VaultState {
    balance: u64,
    is_locked: bool,
}

pub fn withdraw_after_cpi_vulnerable(
    vault: &AccountInfo,
    external_program: &AccountInfo,
    cpi_instruction: Instruction,
    amount: u64
) -> Result<(), ProgramError> {
    // Read vault state before CPI
    let vault_state = VaultState::try_from_slice(&vault.try_borrow_data()?)?;

    // Perform CPI that might modify the vault
    invoke(&cpi_instruction, &[vault.clone(), external_program.clone()])?;

    // Vulnerable: using stale vault_state after CPI
    // The CPI might have changed vault.is_locked to true
    if !vault_state.is_locked && vault_state.balance >= amount {
        // Process withdrawal with stale data
    }...

剩余50%的内容订阅专栏后可查看

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论