在使用Anchor时将会遇到的脆弱性

  • zellic
  • 发布于 2022-08-17 12:10
  • 阅读 12

该文章深入探讨了在Solana开发中,即便使用Anchor框架,仍然可能遇到的安全隐患和常见问题。文章通过多个实例,分析了种子碰撞、CPI风险、账户重载等潜在的安全漏洞,并提醒开发者在使用Anchor时需谨慎,以确保Solana程序的安全性。

Solana 开发容易出错。人们常常提到 Anchor 作为解决这个问题的方法:“用 Anchor 编写,你的 Solana 程序将会安全。”但不要着急!即使使用 Anchor,仍然有很多陷阱。让我们来探讨一些。

背景

编写安全的 Solana 程序并不是一件容易的事情。在创建 Solana 程序时,开发者可能会遇到许多陷阱。为了在这个生态系统中安全操作,开发者需要对运行时保持的不变性、在什么条件下这些不变性不再有保证,以及一系列的账户混淆、混淆的副代理、整数溢出和不足等经典安全漏洞类别十分了解。

Anchor 的创建是因为裸用 Solana 极易出错。Anchor 是一个 Solana 框架,旨在使编写程序的过程比没有使用它时更简单更安全。Anchor 解决了编写 Solana 智能合约时你将面临的许多最常见和最重要的问题。以下是一些示例:

  • 账户混淆
  • 账户类型
  • 账户地址
  • 账户活跃性
  • 所有权
  • 缺失的账户约束

作为一家智能合约审计公司,我们审查了很多 Solana 程序,尤其是那些用 Anchor 编写的程序。尽管 Anchor 解决了 Solana 程序中许多主要问题,但仍然存在许多问题,无论如何都会出现。

种子碰撞

程序派生地址 (PDA) 是一种以编程方式派生非曲线账户地址的方法。它们通常用于跨程序调用 (CPI),以及创建保存状态信息的账户。例如,流动性池程序可能创建一个 PDA 来跟踪分配给特定目的的资金,或使用 PDA 来保存协议的全局配置。

在 Anchor 中使用 PDA 非常简单——我们定义组成种子用于派生账户地址的元素,然后就可以开始了。以下是一个 示例↗

#[derive(Accounts)]
pub struct ChangeUserName<'info> {
    pub user: Signer<'info>,
    #[account(
        mut,
        seeds = [b"user-stats", user.key().as_ref()],
        bump = user_stats.bump
    )]
    pub user_stats: Account<'info, UserStats>, // <-- PDA 账户
}

在这里,我们使用一个静态字符串 user-stats 和来自用户账户的固定长度(32 字节)公钥地址来验证所提供的 UserStats 账户的地址。这是定义和使用 PDA 上种子的一种典型示例。

因为没有两个不同的用户共享一个公钥,所以他们的 UserStats 账户也将拥有唯一的地址。然而,对于 PDA,这不是总是如此!碰撞可能导致在最佳情况下出现拒绝服务漏洞,而在另一种情况下完全被攻陷。

以下是一个 CreateNewProduct 指令账户保护的示例,它根据产品名称和一个前导静态字符串创建一个产品 PDA。

#[derive(Accounts)]
#[instruction(product_name: String)]
pub struct CreateNewProduct<'info> {
   #[account(mut)]
   pub user: Signer<'info>,
   #[account(
       init,
       payer = user,
       space = 8 + Product::SIZE,
       seeds = [b"product", product_name.as_ref()]
   )]
   pub product: Account<'info, Product>, // <-- PDA 账户
   pub system_program: Program<'info, System>,
}

它看起来没有任何问题,但是我们在考虑程序中的其他 PDA 时开始看到问题。

#[instruction(product_name: String)]
pub struct CreateNewBid<'info> {
    #[account(mut)]
    pub user: Signer<'info>,
    #[account(
        mut,
        seeds = [b"product", product_name.as_ref()],
        bump = product.bump
    )]
    pub product: Account<'info, Product>, // <-- PDA 账户
    #[account(
        init,
        payer = user,
        space = 8 + Bid::SIZE,
        seeds = [product_name.as_ref(), user.key().as_ref()]
    )]
    pub bid: Account<'info, Bid>, // <-- PDA 账户
    pub system_program: Program<'info, System>,
}

在这里,我们添加了一个 Bid PDA 账户,用于保存用户对某一产品的竞标。该竞标的范围是针对一个产品和一个用户。虽然起初看起来是安全的,因为这个 Bid 和这个 Product 不能相互碰撞,但攻击者可以利用这一点来阻止用户提交竞标或产品被创建。

在某些情况下,攻击者可以创建一个产品,该产品生成与 Bid 账户相同的程序地址。这会阻止特定用户为该产品创建竞标。

这是因为种子数组中的元素实际上被视为一个大的字节集合,并以 32 字节块进行处理。因此,这个种子(被攻击者的 Product 账户使用)

[b”product”, b”youshouldbuythisproductfast”]

和这个种子(被受害者的 Bid 账户使用)

[b”pr”, 0x6f64756374796f7573686f756c646275797468697370726f6475637466617374]

生成相同的 PDA 公钥。在 solana-program crate 文档中关于 Pubkey 有一个令人失望的警告,提到了这种特定行为。

错误使用 Anchor 的 ctx.remaining_accounts

正如我们在上面的种子碰撞示例中的代码样本中看到的,Anchor 的指令账户保护定义了固定数量的账户及其之间的约束。这些约束对于维护协议的不变性是重要的,但使用固定数量的账户这一限制可能会有所限制。

许多情况下,需要向指令传递可变数量的账户进行处理。例如,在尝试兑付池中的参与者或动态调用另一个程序时。为此,Anchor 暴露了 ctx.remaining_accounts 以访问未包含在原始指令账户保护中的任何额外账户。

值得注意的是,ctx.remaining_accounts 中的账户完全没有 Anchor 通常提供的任何保护。这意味着,如果你打算将这些账户用于任何敏感操作,你需要执行以下一些检查:

  • 账户所有权
  • 账户类型(anchor 使用 8 字节的区分符进行此检查)
  • 账户活跃性(anchor 的区分符可以解决这个问题——基本上是‘数据字段是否设置为 0x00...00’)
  • 账户地址
  • 对于 PDA 确保 bump 是否正确!

使用 ctx.remaining_accounts 可能出错的地方很多。所有与 ctx.remaining_accounts 中的 AccountInfo 类型的交互都需要谨慎对待。

带有 CPI 的混淆代理

Solana 是一个程序的组合生态系统。从调用系统程序、创建新账户,到将流动性转发给 Solend 生成收益,跨程序调用是编写 Solana 程序的正常组成部分。

尽管 CPI 带来了良好的效果,但它们也隐藏了危险。当进行 CPI 时,转发的账户以及签署了交易的账户会携带它们的 is_signer 状态。这就意味着,如果程序 A 调用程序 B,并且某些账户已经签署了交易,程序 B 可以使用该账户作为签名者。这一点非常重要。这意味着程序 B 可以创建新账户、发送 lamports 和任何相关代币(只要相关代币账户也被传递),以及更多操作。

#[derive(Accounts)]
pub struct RiskyInstruction<'info> {
    pub lending_program: AccountInfo<'info>, // <-- 无账户验证

    #[account(
        mut,
        seeds = [ b"pool" ],
        bump = pool.bump
    )]
    pub pool: Account<'info, Pool>
    #[account]
    pub caller: Signer<'info>,
    pub system_program: Program<'info, System>,
}

pub fn risky_instruction(ctx: Context<RiskyInstruction>, amount: u64) -> Result<()> {
    // ...

    let account_infos = [
        ctx.accounts.caller.to_account_info().clone(),
        ctx.accounts.lending_program.to_account_info().clone(),
        ctx.accounts.pool.to_account_info().clone(),
        ctx.accounts.system_program.to_account_info().clone(),
    ];

    let instruction = create_lending_instruction(
        ctx.accounts.lending_program.key(), // <-- 我们所调用的程序
        ctx.accounts.caller.key(),
        ctx.accounts.pool.key(),
        amount
    );

    // v----- 如果我们不完全信任 lending_program 账户,则是危险的
    invoke_signed(
        &instruction,
        &account_infos,
        &[&[b"pool"]]
    )?;

    // ...
}

这个问题与 invoke_signed 的问题相叠加,invoke_signed 通常在与 PDA 的交互中使用。PDA 账户经常被用作协议中的授权账户。如果对 invoke_signed 的调用传递了这个授权账户以及所需的签名种子,那么就引入了实质性的第三方风险。目标程序可能会被调用者动态传递,从而允许攻击者利用授权 PDA 调用他们的恶意程序。即便是静态定义的程序,如果能被升级,也会引入风险。

账户重新加载

CPI 能够引入的问题并不止于此。虽然 Anchor 为你自动做了很多事情,但有一件事是它不做的,那就是在 CPI 后更新反序列化的账户。

例如,假设你有一个 Mint 账户,并且即将为调用者铸造一些代币,以便跟踪他们对流动性池的贡献。你对代币程序进行 CPI 铸造这些代币,然后从 Mint 账户读取当前供应量以进行后续运算。虽然直观上,你可能期望供应量是准确的,但 Anchor 中的账户在 CPI 之后不会更新它们的数据!

let authority_seeds = /* seeds */;

let mint_to = MintTo {
    mint: self.liquidity_mint.to_account_info(),
    to: self.user.to_account_info(),
    authority: self.liquidity_mint_authority.to_account_info()
};

msg!("铸造前的供应量: {}", self.liquidity_mint.supply);

anchor_spl::token::mint_to(
    CpiContext::new_with_signer(
        self.token_program.to_account_info(),
        mint_to,
        authority_seeds
    ),
    amount
)?;

msg!("铸造后的供应量: {}", self.liquidity_mint.supply); // 保持不变!

为了获得预期行为,确保在账户上调用 Anchor 的 reload 方法。这将刷新结构的字段,以获取当前底层数据。

结论

Anchor 是提高你的 Solana 程序安全性的绝佳方法,但这并不是灵丹妙药。在基于 Anchor 的 Solana 程序中依然有几种方式可以出现漏洞。这些漏洞的影响范围可以从拒绝服务到攻击者窃取你的资金。

致谢

感谢 Hana↗ 帮助校对本文。

关于我们

Zellic 专注于保护新兴技术。我们的安全研究人员在从财富 500 强到 DeFi 巨头的最有价值目标上发现了漏洞。

开发者、创始人和投资者信任我们的安全评估,以便快速、自信地发布,而没有重大漏洞。凭借我们在现实世界攻防安全研究方面的背景,我们能够发现他人忽视的问题。

联系我们↗ 进行更好的审计。真实的审计,而不是模糊的盖章。

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

0 条评论

请先 登录 后评论
zellic
zellic
Security reviews and research that keep winners winning. https://www.zellic.io/