General
程序安全

程序安全

复活攻击

复活攻击利用了 Solana 的账户关闭机制,通过在同一笔交易中将“死亡”账户重新激活。

当您通过转移账户中的 lamports 来关闭账户时,Solana 并不会立即对其进行垃圾回收;账户只有在交易完成后才会被清理。这种延迟会产生一个危险的窗口,攻击者可以通过将 lamports 发送回这些账户来“复活”已关闭的账户,从而留下可能仍被您的程序信任的僵尸账户和过时数据。

这种攻击之所以成功,是因为对账户生命周期的基本误解。开发者通常认为关闭账户会使其立即不可用,但实际上,账户在交易结束之前仍然可以访问。攻击者可以在您的关闭指令前后插入一条转账指令,将账户的租金豁免金额退回,从而阻止垃圾回收并使账户保持在可被利用的状态。

在某些协议中,这种攻击尤其具有破坏性,例如账户关闭代表最终操作的场景:完成托管、解决争议或销毁资产。被复活的账户可能会欺骗您的程序,使其认为这些操作从未完成,从而可能导致双重支付、未经授权的访问或协议操控。

Anchor

请考虑以下易受攻击的指令,该指令用于关闭一个程序账户:

#[program]
pub mod insecure_close{
    use super::*;
 
    //..
 
    pub fn close(ctx: Context<Close>) -> Result<()> {
        let dest_starting_lamports = ctx.accounts.destination.lamports();
 
        **ctx.accounts.destination.lamports.borrow_mut() = dest_starting_lamports
            .checked_add(ctx.accounts.account_to_close.to_account_info().lamports())
            .unwrap();
        **ctx.accounts.account_to_close.to_account_info().lamports.borrow_mut() = 0;
 
        Ok(())
    }
        
    //..
}
 
#[derive(Accounts)]
pub struct Close<'info> {
    /// CHECK: This account will not be checked by Anchor
    pub owner: UncheckedAccount<'info>,
   #[account(
        mut,
        has_one = owner
    )]
    pub program_account: Account<'info, ProgramAccount>,
 
}

这段代码看起来是正确的:它将账户中的所有 lamports 转移到目标地址,这应该会触发垃圾回收。然而,账户的数据仍然保持不变,并且在同一笔交易中仍然可以访问该账户。

攻击者可以通过创建包含多个指令的交易来利用这一点:

  • 指令 1:调用您的关闭函数以清空账户中的 lamports
  • 指令 2:将 lamports 转回“已关闭”的账户(复活)
  • 指令 3:在后续操作中使用复活的账户

结果是一个僵尸账户,从程序逻辑上看似已关闭,但实际上仍然可以正常运行,并保留其所有原始数据。这可能导致:

  • 双重支付:多次使用“已关闭”的托管账户
  • 授权绕过:重新激活本应停用的管理员账户
  • 状态损坏:操作本应不再存在的账户

最安全的解决方案是使用 Anchor 的 close 约束,它可以自动处理安全关闭:

#[derive(Accounts)]
pub struct Close<'info> {
    #[account(mut)]
    pub owner: Signer<'info>,
   #[account(
        mut,
        close = owner,
        has_one = owner
    )]
    pub program_account: Account<'info, ProgramAccount>,
}

或者,您可以像这样添加 signer 账户约束:

#[derive(Accounts)]
pub struct Close<'info> {
    #[account(signer)]
    /// CHECK: This account will not be checked by Anchor
    pub owner: UncheckedAccount<'info>,
    /// CHECK: This account will not be checked by Anchor
    pub new_owner: UncheckedAccount<'info>,
   #[account(
        mut,
        has_one = owner
    )]
    pub program_account: Account<'info, ProgramAccount>,
}

对于自定义关闭逻辑,请实现完整的安全关闭模式:

pub fn update_ownership(ctx: Context<UpdateOwnership>) -> Result<()> {
    let account = ctx.accounts.account.to_account_info();
 
    let dest_starting_lamports = ctx.accounts.destination.lamports();
 
    **ctx.accounts.destination.lamports.borrow_mut() = dest_starting_lamports
        .checked_add(account.lamports())
        .unwrap();
    **account.lamports.borrow_mut() = 0;
 
    let mut data = account.try_borrow_mut_data()?;
    for byte in data.deref_mut().iter_mut() {
        *byte = 0;
    }
 
    let dst: &mut [u8] = &mut data;
    let mut cursor = std::io::Cursor::new(dst);
    cursor
        .write_all(&anchor_lang::__private::CLOSED_ACCOUNT_DISCRIMINATOR)
        .unwrap();
 
    Ok(())
}

Pinocchio

在 Pinocchio 中,手动实现关闭模式:

self.program_account.realloc(0, true)?;
self.program_account.close()?;
 
let mut data_ref = self.program_account.try_borrow_mut_data()?;
data_ref[0] = 0xff;
Blueshift © 2025Commit: fd080b2