Anchor 框架中 Init 和 Init if needed 的安全性含义

本文深入探讨了 Anchor 框架中 initinit_if_needed 约束的工作原理,重点分析了 init_if_needed 可能导致的重初始化攻击风险。文章详细解释了攻击者如何通过使账户未初始化(例如,通过耗尽 lamports 或更改所有权)来强制重新初始化,并提供了避免此类漏洞的安全实践建议,强调在关键状态账户上应优先使用 init

🌙 Reinitialization Attack (重新初始化攻击)

程序示例 (顺便说一句,我用 Neovim 🙂): github link 为了理解重新初始化攻击如何工作,我们首先必须理解 Anchor ⚓️中 initinit_if_needed 约束的内部运作方式。

init:

简单来说,init 确保对于给定的地址(由 seeds 和 bump 决定),账户只被创建和初始化一次init 在内部执行以下两组指令:

系统级别:

  • 它使用 System Program 中的 create_account 指令。
  • 它为账户分配链上空间。
  • 它使用 Rent::minimum_balance 为免租金的 lamports 提供资金。
  • 它分配一个所有者程序 (例如,你的自定义程序)。

账户的初始化:

  • 你的程序使用 #[account] 结构定义账户数据结构。
  • 写入 8 字节的discriminator 并将其附加到你的数据(用作类型检查器)。
  • 存储初始数据,如下面的 PDA 账户结构示例所示。
##[account]
##[derive(InitSpace)]
pub struct User {
  pub user_pubkey: Pubkey,
  #[max_len(30)]
  pub user_name: Option<String>,
  pub balance: u64,
  pub user_vault_bump: u8,
  pub user_bump: u8,
}
##[account(
    init,
    payer = USER,
    space = 8 + User::INIT_SPACE,
    seeds = [
              USER,
              user.key().to_bytes().as_ref(),
              chau_config.key().to_bytes().as_ref()
            ],
    bump
)]
pub user_profile: Account<'info, User>,

init 约束的安全性 🛡️:

init 约束的安全性是严格的,不像 init_if_needed。你不能创建一个假的 PDA 并将其传递给程序,也不能初始化同一个账户两次,因为这样做会抛出一个错误。

Anchor 通过评估以下条件来检查账户是否已初始化:

  1. 如果账户有一些 lamports 并且它由 System Program 拥有,那么 Anchor 认为它是一个未初始化的账户并抛出一个错误。
  2. 如果一个账户有零 lamports,那么 Anchor init 可以创建和初始化账户(因为它是一个新账户)。

Anchor 如何检查 PDA 🔍:

  1. 当你序列化你的账户时,Anchor 会添加一个额外的 discriminator (或标签),它使用账户名称的 SHA256 哈希的前 8 个字节,以 8 个字节存储你的账户名称 (例如,上面 PDA 的 User)。
  2. 在反序列化期间,这个 discriminator 充当类型检查器。如果传递了一个恶意账户,Anchor 会将账户数据中的 discriminator 与账户类型的预期 discriminator 进行比较。如果它们不匹配,Anchor 将拒绝该账户,从而防止使用恶意账户。
  3. 在 Solana 中,每条数据都存储为原始二进制数据,因此 Anchor 无法判断一个账户是否是 User。这个 discriminator 就像一个标签,以便 Anchor 可以将其反序列化回正确的结构。

攻击场景 🔪:

  • 假设有一个指令需要一个 User PDA,如第一个示例代码所示,攻击者试图将一个恶意的 PDA 发送到 User 地址。
  • 在这里,Anchor 首先检查 Fake 账户的 discriminator,并将其与 User 的 discriminator 进行比较。如果 discriminator 不匹配,Anchor 将拒绝该账户并抛出一个错误。

你可能(并且你应该)开始欣赏 Anchor。它在底层处理了很多重要的安全检查,以及账户的序列化和反序列化(以及其他一些易出错的地方)。

注意: 这里,由 Anchor 创建的 Account 是 AccountInfo (如下所示) 的一个包装器,它可以帮助 Anchor 验证程序的所有权。

pub struct AccountInfo<'a> {
     /// Public key of the account
     pub key: &'a Pubkey,
     /// The lamports in the account.  Modifiable by programs.
     pub lamports: Rc<RefCell<&'a mut u64>>,
     /// The data held in this account.  Modifiable by programs.
     pub data: Rc<RefCell<&'a mut [u8]>>,
     /// Program that owns this account
     pub owner: &'a Pubkey,
     /// The epoch at which this account will next owe rent
     pub rent_epoch: u64,
     /// Was the transaction signed by this account's public key?
     pub is_signer: bool,
     /// Is the account writable?
     pub is_writable: bool,
     /// This account's data contains a loaded program (and is now read-only)
     pub executable: bool,
}

init_if_needed:

init 不同,init_if_needed 不那么严格,使用起来很灵活。init_if_needed 类似于 init,但它不像 init 那么严格。当你使用 init_if_needed 代替 init 时,Anchor 遵循以下检查:

  1. 账户是否存在并且已初始化?如果不是,则创建并初始化,然后继续前进。
  2. 账户是否存在但未初始化?然后初始化并重置数据,然后继续前进。
  3. 账户是否存在并且已初始化?然后它跳过初始化,让你在没有权限的情况下修改现有账户(如果没有适当的验证,这是很危险的)。

攻击场景 (重新初始化攻击) 🔪:

注意:

当出现以下情况时,Anchor 认为一个账户 "未初始化":

  • 缺少或无效的 8 字节 discriminator (与预期的哈希不匹配)。
  • 账户所有权与预期的程序不匹配。
  • 账户有零 lamports (实际上使其在链上不存在)。

攻击者流程:

代码的正常流程:

在正常情况下,init_if_needed 用于可能需要延迟初始化的账户。例如:

##[account(
  init_if_needed,
  payer = user,
  space = 8 + User::INIT_SPACE,
  seeds = [b"user", user.key().as_ref()],
  bump
)]
pub user_account: Account<'info, User>,

在这里,只有当账户不存在或未初始化时,才会安全地初始化该账户。

恶意用户如何利用这一点:

攻击者可以利用 init_if_needed 来:

  1. 通过耗尽其 lamports(将余额设置为零)或更改其所有权来取消初始化账户。
  2. 通过将同一个账户传递给使用 init_if_needed 的指令来强制重新初始化,从而欺骗程序重置账户的状态。

取消初始化账户的方法:

  1. 耗尽 Lamports:
    • 攻击者可以从账户中提取所有 lamports,使其在 Anchor 看来 "未初始化" (因为零 lamport 账户被视为不存在)。
  2. 更改所有权:
    • 如果攻击者将账户的所有者更改为另一个程序,Anchor 将认为它对于原始程序来说是未初始化的。
  3. 损坏 Discriminator:
    • 如果攻击者修改了账户数据的前 8 个字节 (discriminator),Anchor 会将其视为未初始化。

攻击发生后会发生什么?

  • 账户的数据重置为默认值(例如,balance = 0, user_name = None)。
  • 攻击者可以滥用这一点来:
    • 重置他们在贷款协议中的债务。
    • 重新获得对锁定账户的访问权限。
    • 利用依赖于账户状态的逻辑。

使用 init_if_needed 的安全实践 🛡️:

  1. 避免对关键状态账户使用 init_if_needed:
    • 如果一个账户存储重要数据(例如,用户余额、协议设置),则使用 init 来防止重新初始化。
  2. 添加显式检查:
    • 在使用 init_if_needed 之前,手动验证账户是否已经初始化。
##[account]
##[derive(InitSpace)]
pub struct User {
   pub user_pubkey: Pubkey,
   #[max_len(30)]
   pub user_name: Option<String>,
   pub balance: u64,
   pub is_initialized: bool,
   pub user_vault_bump: u8,
   pub user_bump: u8,
}
require!(user_account.discriminator.is_empty(), AlreadyInitialized);

结论:

虽然 init_if_needed 提供了灵活性,但如果使用不当,它会带来风险。对于安全至关重要的账户,始终首选 init,只有在绝对必要时才使用 init_if_needed,并采取适当的保护措施。Anchor 的 discriminator 检查有助于防止一些攻击,但开发人员必须实施额外的检查,以充分保护他们的程序免受重新初始化攻击。

参考文献:

solana developer course <br/> rare skill blog <br/> init vs init_if_needed <br/> security concern in init_if_needed<br/> stackoverflow_discussion <br/>

源代码:

https://github.com/baindlapranayraj/reinitialization-attack-demo

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

0 条评论

请先 登录 后评论
baindlapranayraj
baindlapranayraj
江湖只有他的大名,没有他的介绍。