Solana Anchor开发框架的 IDL 指令攻击分析

當前語言暫無此文章翻譯,已顯示原文。
本文Beosin安全团队将揭示一个关键漏洞模式:在低版本 Anchor 中,当开发者使用 AccountInfo<'info> 来声明程序拥有(program-owned)的 PDA 账户时,攻击者可以通过 Anchor 自动注入的 IDL 指令,仅需两步操作就能夺取账户控制权并清空其中的所有 SOL,整个过程无需获取任何特权。

Solana Anchor开发框架的 IDL 指令攻击分析

Anchor是 Solana 生态中最主流的开发框架。它通过声明式账户验证、自动序列化和内置安全检查等特性,极大地降低了开发门槛。然而,框架在提供便利性的同时,也在幕后向每个程序注入了一些开发者可能并不了解的内部指令。这些“隐藏”指令在特定条件下可以被攻击者利用,造成严重的资金损失。

本文Beosin安全团队将揭示一个关键漏洞模式:在低版本Anchor 中,当开发者使用AccountInfo<'info>来声明程序拥有(program-owned)的PDA账户时,攻击者可以通过 Anchor 自动注入的 IDL 指令,仅需两步操作就能夺取账户控制权并清空其中的所有SOL,整个过程无需获取任何特权

一、IDL 指令及相关机制分析

1.1 IDL 指令

Anchor 默认会在每一个程序中注入一组 IDL(Interface Definition Language,接口定义语言)管理指令,除非在构建时显式启用 no-idl feature。这些指令包括:

IdlCreateAccount:创建一个链上 IDL 账户

IdlWrite:向 IDL 账户 / 缓冲账户写入数据

IdlSetAuthority:变更 IDL 账户的 authority(控制者)

IdlCloseAccount:关闭 IDL 账户,并将其全部 lamports 转给指定接收方

IdlResizeAccount:调整 IDL 账户大小

IdlCreateBuffer:创建 IDL 缓冲账户(IDL Buffer)

IdlSetBuffer:用缓冲账户中的数据覆盖正式 IDL 账户

这些指令的设计初衷是用于链上 IDL 管理,但它们对程序拥有的账户具有特殊的操作能力(读写数据、变更控制者、关闭并转走 lamports)——这正是漏洞的核心所在。攻击者不需要调用开发者编写的任何业务指令,直接调用这些内置指令即可。

1.2 IDL 缓冲账户

IDL 缓冲账户(IDL Buffer)是 Anchor 为了分段上传大型 IDL 数据而引入的临时账户。由于完整的 IDL(JSON 压缩后)可能超过单笔交易的大小限制,Anchor 允许先通过 IdlCreateBuffer 建立一个缓冲区,再用多笔IdlWrite 分批写入,最后通过IdlSetBuffer 一次性提交。

关键点在于 IDL 账户 / 缓冲账户的数据结构:它以一个固定布局的头部开头,头部中包含一个 authority: Pubkey 字段(本文测试输出中称之为controller)。IDL 指令通过这个字段判断“谁有权操作该账户”。

而问题在于:低版本 Anchor 的 IdlCreateBuffer 处理逻辑会把传入的、由本程序拥有的任意账户当作缓冲账户来初始化,并将 authority 直接设置为交易签名者。也就是说,只要一个账户的 owner 是本程序(例如程序的 PDA 金库),攻击者就能把自己写成它的 controller,随后再用 IdlCloseAccount 名正言顺地把账户里的 SOL 全部转走。

1.3 漏洞触发条件

该漏洞的触发需要同时满足以下条件:

  • 使用低版本 Anchor:危险的 IDL 指令未做充分的账户判别,允许把业务账户误当作 IDL 账户操作。
  • 未启用 no-idl:程序在构建时保留了默认注入的 IDL 指令入口,攻击面对外暴露。
  • 以 AccountInfo 声明程序拥有的 PDA:开发者用裸的AccountInfo<'info> 承载资金账户(如金库 PDA),缺少 Anchor 类型化账户(如 Account<'info, T>)自带的 discriminator / owner 判别,使该账户在 IDL 指令看来与 IDL 账户“无法区分”。
  • 账户为程序所有且持有 lamports:owner == 本程序 是 IDL 指令能操作它的前提;持有 SOL 才有被清空的价值。

满足以上条件后,攻击者只需两笔普通交易:先 IdlCreateBuffer 夺取 controller,再 IdlCloseAccount 转走全部 SOL,即可完成攻击,全程无需目标程序授予任何权限。

1.4 攻击链

下面结合 Anchor 内部实现,逐步拆解攻击者如何仅用两条内置指令,把一个普通的资金 vault “伪装”成 IDL 账户并将其清空。

攻击流程概览

Step 1 IdlCreateBuffer(treasury, signer = attacker)  
└─ treasury.controller ==> attacker  (become controller)
Step 2 IdlCloseAccount(treasury, authority = attacker, dest = attacker) 
 └─ treasury.lamports  ==> attacker  (drain the treasury)

第一步:IdlCreateBuffer 夺取 authority

Anchor 内部实现大致如下:

#[derive(Accounts)]pubstructIdlCreateBuffer<'info> { 
#[account(zero)]     // ← Key: an account with its discriminator all 0 pub buffer:Account<'info,IdlAccount>, 
pub authority:Signer<'info>,} 
pub fn idl_create_buffer(ctx:Context<..>) ->Result<()> 
{
letidl=&mut ctx.accounts.buffer; idl.authority=*ctx.accounts.authority.key;// set attack as authorityOk(())}

#[account(zero)]的含义是:接受一个discriminator 全为零、被程序拥有的账户,当作未初始化的IDL 账户来初始化。而vault 恰好同时满足这两个条件:

vault 的状态

条件

owned by program

init_if_needed 后 owner 被设为本程序

discriminator 全零

使用AccountInfo 类型,Anchor 不写入 discriminator,数据全零

于是攻击者将 vault 传入 IdlCreateBuffer 后:

  • Anchor 往 vault 前 8 字节写入 IdlAccount 的 discriminator;
  • 将攻击者公钥写入 authority 字段。

至此,vault 被“伪装”成了一个以攻击者为 authority 的 IdlAccount——而它内部的 SOL 分文未动。

第二步:IdlCloseAccount —— 清空资金

#[derive(Accounts)]pubstructIdlCloseAccount<'info> { #[account(mut, has_one=authority)] // ← check authority == signer pub account:Account<'info,IdlAccount>, pub authority:Signer<'info>, #[account(mut)] pub destination:AccountInfo<'info>, // ← attack wallet}

此时的 vault 已经完全通过校验:

  • discriminator 匹配 IdlAccount
  • authority 字段 = 攻击者公钥
  • has_one = authority 校验通过

因此vault 内所有 lamports(用户存入的全部 SOL)被合法地转入攻击者账户,金库归零。

根本原因:

在deposit.rs 中使用AccountInfo<'info> 而非有类型的 Anchor Account,是本漏洞的致命根源:

(1) 有类型的 Account(如 Account<'info, Vault>)在初始化时会写入该结构体专属的 8 字节 discriminator;

(2) 一旦写入了自身的 discriminator,Anchor 便无法再把它当作 IdlAccount(discriminator 不匹配),第一步的 IdlCreateBuffer 就会失败,攻击链随之断裂。

二、案例分析

以下测试基于Anchor 0.31.0构建的跨链桥合约PoC。测试模拟一个真实的桥金库(Bridge Treasury),其 owner 为桥程序本身,内含用户存入的 1.001281 SOL。攻击者钱包最初持有 2 SOL,且不具备任何特权。

项目

数值 / 说明

桥程序 (Bridge Program)

CJutNlr8d3oxdb11ReOLaZd5jPsqUxgwt2mv5e7equtE

金库账户 (Treasury)

DxGkzaMhP5Wy824GhoehErdD6MudfuEPGPwaV2s77FsM

金库 Owner

=桥程序(program-owned PDA)

金库初始余额

1.001281 SOL(用户存入资金)

初始 Controller

0x0000...0000(全零,未设置)

攻击者钱包初始余额

2.000000 SOL

所需特权

NONE(无需任何授权)

交易笔数

2 笔

金库最终余额

0.000000 SOL(被清空)

攻击者获利

+1.001281 SOL

PoC 运行截图(tests/poc-idl-hijack.ts):

可以看到,Step 1 的IdlCreateBuffer把攻击者设置为金库的controller(余额不变);Step 2 的IdlCloseAccount将金库1.001281 SOL 全部转入攻击者钱包,金库余额归零。

修复/防护建议:

(1) 升级Anchor 版本:最新版本已修复该问题(IDL 指令会严格区分 IDL 账户与业务账户),避免使用低版本Anchor是最直接、最根本的防御手段。

(2) 构建时启用no-idl:对生产环境程序显式关闭IDL 指令注入,从源头消除该攻击面。

(3)使用类型化账户而非裸AccountInfo:以Account<'info, T>/SystemAccount等带discriminator 与 owner 校验的类型承载资金账户,使其无法被 IDL 指令误认。

(4) 最小化程序拥有的可清空账户:对存放资金的PDA 增加明确的 owner / seeds / discriminator 约束,并在关键指令中校验账户判别符。

结语

该漏洞本质是“框架隐藏指令 + 账户类型判别缺失”的组合。开发者应认识到 Anchor 默认注入的 IDL 指令是真实存在的攻击面,升级框架版本并对资金账户使用强类型约束,即可有效杜绝此类未授权资金清空风险。

分享至:

作者:Beosin

本文為PANews入駐專欄作者的觀點,不代表PANews立場,不承擔法律責任。

文章及觀點也不構成投資意見

圖片來源:Beosin如有侵權,請聯絡作者刪除。

關注PANews官方賬號,一起穿越牛熊
PANews APP
BTC跌破64000美元,日內下跌 0.23%
PANews 快訊