General
程序安全

程序安全

PDA 共享

PDA 共享攻击利用了在多个用户或域之间使用相同 Program Derived Address (PDA) 的程序,从而使攻击者能够访问不属于他们的资金、数据或权限。虽然使用全局 PDA 进行程序范围的操作看起来很优雅,但它会导致危险的交叉污染,使一个用户的操作可能影响到另一个用户的资产。

漏洞的根源在于派生 PDA 时种子(seed)特异性不足。当多个账户共享相同的 PDA 权限时,程序就无法区分合法和非法的访问尝试。攻击者可以创建引用相同共享 PDA 的账户,然后利用该 PDA 的签名权限操控其他用户的资产。

在 DeFi 协议中,这种情况尤其具有破坏性,因为 PDA 控制着代币金库、用户余额或提款权限。共享 PDA 本质上创建了一把主密钥,可以解锁多个用户的资产,将单个用户的操作变成对整个协议的潜在攻击。

Anchor

以下是一个使用基于 mint 的 PDA 进行签名的易受攻击的提款系统:

#[program]
pub mod insecure_withdraw{
    use super::*;
    //..
 
    pub fn withdraw(ctx: Context<WithdrawTokens>) -> Result<()> {
        let amount = ctx.accounts.vault.amount;
        
        let seeds = &[
            ctx.accounts.pool.withdraw_destination.as_ref(),
            &[ctx.accounts.pool.bump],
        ];
 
        transfer(
            CpiContext::new_with_signer(
                ctx.accounts.token_program.to_account_info(),
                Transfer {
                    from: ctx.accounts.vault.to_account_info(),
                    to: ctx.accounts.withdraw_destination.to_account_info(),
                    authority: ctx.accounts.pool.to_account_info(),
                },
            ),
            &amount,
            seeds,
        )?;
 
        Ok(())
    }
        
    //..
}
 
#[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>,
    /// CHECK: This is the PDA that signs for the transfer
    authority: UncheckedAccount<'info>,
    token_program: Program<'info, Token>,
}
 
#[account]
#[derive(InitSpace)]
pub struct TokenPool {
    pub vault: Pubkey,
    pub mint: Pubkey,
    pub withdraw_destination: Pubkey,
    pub bump: u8,
}

此代码存在一个关键缺陷:PDA 仅使用 mint 地址派生。这意味着同一代币类型的所有池共享相同的签名权限,从而形成了一个危险的攻击向量。

攻击者可以通过以下方式利用这一点:

  • 为相同的 mint 创建他们自己的 TokenPool
  • 将他们自己的地址设置为 withdraw_destination
  • 使用共享的 PDA 权限从任何使用相同 mint 的金库中提取代币
  • 将其他用户的资金转移到他们自己的目的地

攻击之所以成功,是因为 PDA 权限没有区分不同的资金池实例,它只关注代币类型,而不是应该访问这些资金的特定用户或资金池。

第一个改进是使 PDA 特定于个人用户或目标,并使用 Anchor 的种子和 bump 约束来验证 PDA 的推导:

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