漏洞隨筆:通過Jet Protocol任意提款漏洞淺談PDA與Anchor賬號驗證

據Jet Protocol 官方博客披露,他們近期修復了一個賞金漏洞,這個漏洞會導致惡意用戶可以提取任意用戶的存款資金,慢霧安全團隊對此漏洞進行了簡要分析,並將分析結果分享如下。

(來源:https://www.jetprotocol.io/posts/jet-bug-disclosure)

相關信息

Jet Protocol 是運行在Solana 上的一個借貸市場,用戶可將賬號裡的代幣(如:USDC、SOL)存入金庫,賺取年化收益,同時也可以按一定的比例借出另一種代幣。在這個過程中合約會給用戶一個note 憑證,作為用戶未來的提款憑證,用我們熟悉的字眼來說就是LP,而本次漏洞發生的原因也和這個LP 的設計有關。

我們知道和以太坊合約相比,Solana 合約沒有狀態的概念,取而代之的是賬號機制,合約數據都存儲在相關聯的賬號中,這種機制極大提升了Solana 的區塊鏈性能,但也給合約編寫帶來了一些困難,最大的困難就是需要對輸入的賬號進行全面的驗證。 Jet Protocol 在開發時使用了Anchor 框架進行開發,Anchor 是由Solana 上的知名項目Serum 團隊開發的,可以精簡很多賬號驗證及跨合約調用邏輯。

Anchor 是如何工作的呢?我們可以從Jet Protocol 的一段代碼說起:

  • programs/jet/src/instructions/init_deposit_account.rs

這裡的deposit_account 賬號就是用於存儲LP 代幣數據的賬號,用戶在首次使用時,需要調用合約生成該賬號,並支付一定的存儲費用。

而這裡的 #[account] 宏定義限定了這個賬號的生成規則:

規則1:#[account(init, payer = <target_account>, space = <num_bytes>)]
這個約束中,init 是指通過跨合約調用系統合約創建賬號並初始化,payer=depositor 意思是depositor 為新賬號支付存儲空間費用。

規則2:#[account(seeds = <seeds>, bump)]
這個約束中將檢查給定帳戶是否是當前執行程序派生的PDA,PDA (Program Derived Address) 賬號是一個沒有私鑰、由程序派生的賬號,seed 和bump 是生成種子,如果bump 未提供,則Anchor 框架默認使用canonical bump,可以理解成自動賦予一個確定性的值。

使用PDA,程序可以以編程方式對某些地址進行簽名,而無需私鑰。同時,PDA 確保沒有外部用戶也可以為同一地址生成有效簽名。這些地址是跨程序調用的基礎,它允許Solana 應用程序相互組合。這裡用的是”deposits” 字符+ reserve 賬號公鑰+ depositor 賬號公鑰作為 seeds,bump 則是在用戶調用時傳入。

規則3:#[account(token::mint = <target_account>, token::authority = <target_account>)]

這是一個SPL 約束,用於更簡便地驗證SPL 賬號。這裡指定deposit_account 賬號是一個token 賬號,它的mint 權限是deposit_note_mint 賬號,authority 權限是market_authority。

Account 的宏定義還有很多,這里略表不提,詳細可以考慮文檔:
https://docs.rs/anchor-lang/latest/anchor_lang/derive.Accounts.html

有了這些前置知識,我們就可以直接來看漏洞代碼:

  • programs/jet/src/instructions/withdraw_tokens.rs

正常情況下,用戶調用函數withdraw_tokens 提幣時,會傳入自己的LP 賬號,然後合約會銷毀他的LP 並返還相應數量的代幣。但這裡我們可以看到deposit_note_account 賬號是沒有進行任何約束的,用戶可以隨意傳入其他用戶的LP 賬號。難道使用別人的LP 賬號不需要他們的簽名授權嗎?

通過前面分析宏定義代碼,我們已經知道了market_authority 賬號擁有LP 代幣的操作權限,確實不需要用戶自己的簽名。那麼market_authority 又是一個怎麼樣的賬號呢?我們可以看這裡:

  • programs/jet/src/instructions/init_market.rs

這個market_authority 也是一個PDA 賬號。也就是說合約通過自身的調用就可以銷毀用戶的LP 代幣。那麼對於惡意用戶來說,要發起攻擊就很簡單了,只要簡單地把deposit_note_account 賬號設置為想要竊取的目標賬號,withdraw_account 賬號設置為自己的收款賬號,就可以銷毀他的LP,並把他的存款本金提現到自己的賬號上。

最後我們看一下官方的修復方法:

補丁中並未直接去約束deposit_note_account 賬號,而是去除了burn 操作的PDA 簽名,並將authority 權限改成了depositor,這樣的話用戶將無法直接調用這裡的函數進行提現,而是要通過另一個函數withdraw() 去間接調用,而在withdraw() 函數中賬號宏定義已經進行了嚴密的校驗,惡意用戶如果傳入的是他人的LP 賬號,將無法通過宏規則的驗證,將無法通過宏規則的驗證,因為depositor 需要滿足signer 簽名校驗,無法偽造成他人的賬號。

  • programs/jet/src/instructions/withdraw.rs

總結

本次漏洞的發現過程比較有戲劇性,漏洞的發現人@charlieyouai 在他的個人推特上分享了漏洞發現的心路歷程,當時他發現burn 的權限是market_authority,用戶無法進行簽名,認為這是一個bug,會導致調用失敗且用戶無法提款,於是給官方提交了一個賞金漏洞,然後就去吃飯睡覺打豆豆了。

而後官方開發者意識到了問題的嚴重性,嚴格地說,他們知道這段代碼沒有無法提現的漏洞,而是人人都可以提現啊,老鐵,一個能良好運行的bug 你知道意味著什麼嗎? !所幸的是沒有攻擊事件發生。

目前在Solana 上發生過多起黑客攻擊事件均與賬號校驗問題有關,慢霧安全團隊提醒廣大Solana 開發者,注意對賬號體系進行嚴密的審查。

Total
0
Shares
Related Posts