Anchor 是 Solana 生態系統中最主流的開發框架。它透過聲明式帳戶驗證、自動序列化和內置安全檢查等特性,大幅降低了開發門檻。然而,框架在提供便利性的同時,也在幕後向每個程式注入了一些開發者可能並不了解的內部指令。這些「隱藏」指令在特定條件下可被攻擊者利用,造成嚴重的資金損失。
本文 Beosin 安全團隊將揭示一個關鍵漏洞模式:在低版本 Anchor 中,當開發者使用 AccountInfo 來聲明程式擁有(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 承載資金帳戶(如金庫 PDA),缺少 Anchor 類型化帳戶(如 Account)自帶的 discriminator / owner 判別,使該帳戶在 IDL 指令看來與 IDL 帳戶「無法區分」。
- 帳戶為程式所有且持有 lamports:owner == 本程式 是 IDL 指令能操作它的前提;持有 SOL 才有被清空的價值。
滿足以上條件後,攻擊者只需兩筆普通交易:先 IdlCreateBuffer 奪取 controller,再 IdlCloseAccount 轉走全部 SOL,即可完成攻擊,全程無需目標程式授予任何權限。
1.4 攻擊鏈
下面結合 Anchor 內部實現,逐步拆解攻擊者如何僅用兩條內置指令,把一個普通的資金 vault 「偽裝」成 IDL 賬戶並將其清空。
攻擊流程概覽
步驟 1 IdlCreateBuffer(treasury, signer = attacker)
└─ treasury.controller ==> attacker (成為控制器)
步驟 2 IdlCloseAccount(treasury, authority = attacker, dest = attacker)
└─ treasury.lamports ==> attacker (抽乾金庫)
第一步:IdlCreateBuffer 奪取 authority
Anchor 的內部實現大致如下:
#[derive(Accounts)] pub struct IdlCreateBuffer {
#[account(zero)] // ← Key: 一個其 discriminator 全為 0 的帳戶 pub buffer: Account,
pub authority: Signer,}
pub fn idl_create_buffer(ctx: Context) -> Result
{
let idl = &mut ctx.accounts.buffer; idl.authority = *ctx.accounts.authority.key; // 設定 attack 為權限者 Ok(())}
#[account(zero)] 的含義是:接受一個 discriminator 全為零、被程式擁有的帳戶,當作未初始化的 IDL 帳戶來初始化。而 vault 恰好同時滿足這兩個條件:
vault 的狀態
條件
由程式擁有
init_if_needed 後 owner 被設為本程式
discriminator 全零
使用 AccountInfo 類型,Anchor 不寫入 discriminator,資料全零
於是攻擊者將 vault 傳入 IdlCreateBuffer 後:
- Anchor 在 vault 的前 8 個位元組寫入 IdlAccount 的 discriminator;
- 將攻擊者公鑰寫入 authority 字段。
至此,vault 被「偽裝」成了一個以攻擊者為 authority 的 IdlAccount——而它內部的 SOL 分文未動。
第二步:IdlCloseAccount —— 清空資金
#[derive(Accounts)] pub struct IdlCloseAccount { #[account(mut, has_one = authority)] // ← 檢查 authority == signer pub account: Account, pub authority: Signer, #[account(mut)] pub destination: AccountInfo, // ← 攻擊錢包}
此時的 vault 已經完全通過校驗:
- discriminator 匹配 IdlAccount
- authority 字段 = 攻擊者公鑰
- has_one = authority 校驗通過
因此,金庫內所有 lamports(用戶存入的全部 SOL)被合法地轉入攻擊者帳戶,金庫歸零。
根本原因:
在 deposit.rs 中使用 AccountInfo 而非有類型的 Anchor Account,是本漏洞的致命根源:
(1) 具有類型的帳戶(例如 Account)在初始化時會寫入該結構體專屬的 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 / SystemAccount 等帶 discriminator 與 owner 校驗的類型承載資金帳戶,使其無法被 IDL 指令誤認。
(4) 最小化程序擁有的可清空帳戶:對存放資金的 PDA 增加明確的 owner / seeds / discriminator 約束,並在關鍵指令中校驗帳戶判別符。
結語
該漏洞本質是「框架隱藏指令 + 帳戶類型判別缺失」的組合。開發者應認識到 Anchor 預設注入的 IDL 指令是真實存在的攻擊面,升級框架版本並對資金帳戶使用強類型約束,即可有效杜絕此類未授權資金清空風險。
Beosin 是一家領先的區塊鏈安全與監管合規科技公司,專注於項目上線前的智能合約安全審計、項目運行時的安全風險監控與阻斷、盜竊追回、虛擬資產反洗錢(AML)以及調查追蹤。Beosin 已為全球 20 多個國家和地區的監管與執法機構、200 多家虛擬資產服務商以及 4500 多家 Web3 項目提供「一站式」區塊鏈合規產品 + 安全服務。歡迎點擊公眾號留言框,與我們聯繫。

