百萬美金的火線排雷:深度解讀DeFi資產授權漏洞
本文來自Amber Group,作者吳家志。原文標題:《Exploiting Primitive FinanceApproval Flaws》。
事件摘要:2月24日,一篇關於 Primitive Finance (以太坊鏈上衍生品協議)的漏洞分析報告在圈內引發關注,報告描述了三個白帽攻擊(以安全漏洞排查為目的的黑客攻擊)以及漏洞原理。一個多月後的 4月14日,以Amber Group 區塊鏈安全專家吳家志博士為代表的團隊,發現了一個錢包地址有超過 100 萬美金的資產( 500 WETH)存在風險。在本地復現攻擊後,其團隊通過 Immunefi ( DeFi漏洞懸賞平台)聯繫了 Primitive項目方,並成功協助潛在的受害者重置了 WETH 授權,解除危機。這篇文章將介紹該團隊如何在模擬環境中利用此漏洞,以及如何通過區塊鏈數據分析找到潛在的受害者錢包地址。
原理:智能合約中的裂隙
在目前 EVM (以太坊虛擬機)及 ERC-20 (以太坊智能合約的一種協議標準)的架構中,當用戶與智能合約交互時,智能合約本身缺少一個能從代碼層面捕捉到 ERC-20 轉賬事件的回調機制。例如當 Alice 給 Bob 發 100 個 XYZ 代幣時,Bob 的 XYZ 餘額會被更新在XYZ 合約裡。但是 Bob 如何知道他的 XYZ 變多了呢?他可以查 Etherscan(以太坊瀏覽器) 或者其錢包 App 自動從以太坊節點取得的最新餘額。如果 Alice 將 100 XYZ 發給一個智能合約 Charlie,Charlie 如何得知他的 XYZ 餘額增加了呢?
事實上 Charlie 沒辦法在收到 100 XYZ 的當下主動取得他最新的餘額,原因是這個轉賬是在 XYZ 合約上發生的,不在 Charlie 合約。智能合約部署完成後就像操作系統一樣,是一堆代碼放在某個地方,需要被調用了才會發生作用。為了解決這個難題,在 ERC-20 標準有一個被廣泛使用的機制 ---approve()/transferFrom() 。
舉例來說,當 Alice 需要往 Charlie 存入 100 個 XYZ 代幣時,Alice 可以事先授權 Charlie 使用她的 100 XYZ 額度,此時 Charlie 的 deposit() 函數就可以在一個交易裡通過 transferFrom() 主動將 Alice 錢包裡的 100 XYZ 取出,並且更新 Charlie 合約的狀態(例如增加 Alice 的 XYZ 存款餘額cXYZ)。為了減少摩擦,很多 DApp 甚至會讓用戶授權無限多的 XYZ 額度給項目方地址,這樣可以讓後續的 transferFrom() 調用直接成功,免除掉多次授權的點擊以及手續費,這等同於將 Charlie 加白。這個方案留下了一個隱患,萬一 Charlie 作惡或是被攻擊了,Alice 的資產就會有危險。
這個發生於 2020 年 6 月 18 日的意外證實了一個被控制或存在問題的智能合約可以如何被利用並且造成資產損失,如下代碼所示,safeTransferFrom() 雖然名為 safe 的 transferFrom 卻意外被宣告成公開函數,導致任何人都可以使用 Bancor 合約的身份轉移任意用戶(from)任意數量(value)的任意資產(token)到任意的地址(to)。
簡單舉例來說,如果 Alice 正好使用過 Bancor 並且授權 Bancor 無限額度使用她的 DAI,則一旦她的錢包裡 DAI 餘額大於零時,黑客就可以立即把她的 DAI 轉走。
診斷:黑客是怎樣繞開"安檢"的?
根據上文的漏洞分析報告所述,這個外部函數有一個類似的漏洞,但無法像 Bancor 的漏洞一樣被直接利用。事實上,攻擊者需要偽造兩個 ERC20 代幣合約,一個 Uniswap 資金池,並且發起一筆 Uniswap 閃電貸繞過下圖標註的 msg.sender == address(this)檢查。聽起來複雜,但對於有經驗的黑客來說,這並不是太困難。
Primitive 為何需要實現 flashMintShortOptionsThenSwap() 這樣一個接口呢?其實是有特定使用場景的,在 openFlashLong() 函數可以看到,flashMintShortOptionsThenSwap() 會被封裝在一個 Uniswap 的 flash-swap 調用參數裡,在第 1371 行觸發 flash-swap 之後,由回調函數 UniswapV2Call() 調起。此時由於 UniswapV2Call() 在 Primitive 合約裡,便可以通過上述 msg.sender == address(this)檢查。
值得注意的是,在 openFlashLong() 函數裡,第 1360 行寫的是 msg.sender,表示在正常的情況下,Primitive 只能使用調用者本身的資金,然而攻擊者可以通過偽造的 pair 以及 params 用類似於 1371 行的方式直接調用 Primitive 合約的 UniswapV2Call() 並繞過 flashMintShortOptionsThenSwap() 的檢查。由於 params 在這情況下可以完全被控制,1360 行的 msg.sender 便可以被替換成任意曾經授權 Primitive 的錢包地址,然後通過 flashMintShortOptionsThenSwap() 裡的 transferFrom() 調用盜取資產。
追蹤:找出可能的受害者
如果一個黑客碰巧知道某位"大戶"曾授權有問題的合約,他可以輕易利用這個漏洞盜取受害人大量的資金。然而,這件事情如果僅使用區塊瀏覽器是很難做到的,尤其在合約已經部署了較長時間,並有大量用戶量的情況下。其中需要分析的數據並非是靠人工搜索 Etherscan 能夠實現的。
Google Cloud Public Datasets (由 Google 托管在BigQuery的數據集)在此時可發揮作用。由於每一個成功的 approve() 調用都會在以太坊上發出一個 Approval() 事件,我們可以通過 BigQuery (Google的雲數據倉庫解決方案,用於處理"大數據"報告)服務找出所有事件並且通過一些方法過濾出我們感興趣的部分,例如 _spender是 Primitive 合約的所有事件。
下面是我們在 GCP 上實際用來找出潛在受害者使用的 SQL 語句,其中第五行可以看到我們限定搜索的以太坊數據庫及記錄事件的表,第七行過濾出 Approval() 事件,第八行過濾了特定的 _spender。此外,第六行將區塊高度範圍設定在 Pirmitive 合約部署之後,這可以大幅降低 BigQuery 掃過的數據量,這類的 SQL 優化會直接反應在你的 GCP 賬單裡。
接下來,我們可以進一步優化 SQL 查詢將已經通過 approve(_spender, 0) 重置授權的賬號從清單中刨除,得到最終的賬號列表。有了最終的列表,我們利用一個腳本監控著這些賬號,並且在這些危險賬號收到大量資產時發出預警,因為這很可能會造成嚴重的損失。
在一個星期三的清晨,機器人發出了預警,有一個可能的受害人在北京時間4 月 13 日清晨 5 點 24 分收到了將近 500 WETH 的資產,價值超過一百萬美金。相較於已公開的三次白帽攻擊,這個受害人如果被成功攻擊,所損失的金額將高於稍早的三個案例的總和。
我們在北京時間 9:32 緊急聯繫了 Primitive 項目的漏洞賞金計劃運營方 Immunefi 並且向他們展示我們如何(重新)利用這個漏洞在模擬環境中盜取受害人的500 WETH,並且提供包括下面的截屏等證據。
在 Primitive 團隊的幫助下,潛在的受害人於 10:03 將 WETH 授權重置,解除危機。
兩天後,Primitive 團隊也針對此發現給予漏洞獎勵並發布公開致謝。該筆賞金發稿前已捐助給CryptoRelief(一家致力於援助印度新冠疫情的救濟基金)。
復現:分布拆解漏洞的利用
漏洞利用的第一步,我們需要準備兩個 ERC20 合約:Redeem 及Option。
其中 Redeem 合約是一個標準的 ERC20,我們只需要基於 OpenZeppelin 的實現將 mint() 接口暴露出來,方便我們控制代幣數量,如下所示:
Option 合約會相對複雜一點,從下面的代碼片段可以看到,我們需要刻意構造一些全局變量(例如 redeemToken),以及公開函數(例如 getBaseValue()),這些都是在 Primitive 的業務邏輯會用到的。此外,我們還需要傳入三個參數來初始化Option 合約:
· redeemToken: 稍早構造的 Redeem 合約地址
· underlyingToken: 攻擊目標賬號所持有的資產合約地址
· beneficiary: 受益人地址,也就是攻擊成功後將受害人資產轉移的目標地址
這裡需要特別說明的是 mintOptions() 這個函數,從上面的代碼可以看到,它會直接把所有的underlyingToken 發給 beneficiary 地址。這是因為下面的 內部函數 mintOptionWithUnderlyingBalance() 函數在被 flashMintShortOptionsThenSwap() 時會將 underlyingToken 發給 Option 代幣合約,並且通過 mintOptions() 調用鑄造 Option 代幣。因此,我們在偽造的 Option 合約裡,可以直接把 mintOptions() 當作一個提幣調用,將 underlyingToken 發給 beneficiary(也就是發起攻擊的地址),用於之後歸還閃電貸的資金。
接下來,我們可以用剛剛創建的 Redeem 及 Option 代幣創建一個 Uniswap 的流動性池子,這個池子的地址將用來接收從受害人錢包轉出的資金。事實上,每個 Uniswap 池子裡有等價的兩種資產,例如 WETH 及 Redeem(也就是 Option.redeemToken()),為了完成漏洞利用,我們必須為池子注入流通性。Redeem 是我們自己創建的,可以鑄造無限量的代幣,但 WETH 呢?
在閃電貸的幫助下,我們基本上可以利用無限數量的資金來做如何事情,只要確保能在一個交易中歸還資金即可。在這個案例中,我們使用 Aave V2 的閃電貸借了相當於受害人總資產 99.7% 的資金存入上述的流動性池。
根據 Aave 的設計,需要實現一個回調函數 executeOperation() 執行獲得貸款資金後的操作(例如調用 Lib.trigger()),並且在最後通過 approve() 調用授權 Aave 合約取走閃電貸的資產以及手續費。
總結
在基於 EVM 的智能合約世界裡,approve()/transferFrom() 是長久以來存在的固有問題。對於 DeFi 用戶而言,需要多留意你的錢包地址是否正允許著其他人使用你的資產,並且定期重置資產使用權。對於項目方而言,需要在上線之前花更多心思和時間從各種可能的角度測試,甚至攻擊你的代碼,因為你正在編程的,是每位用戶的真金白銀。
關於作者
吳家志受聘於全球領先的加密金融智能服務提供商Amber Group ,作為區塊鏈安全專家。他畢業於美國北卡州立大學計算機專業,獲得博士學位,師從安卓安全領域領軍者蔣旭憲教授,在美國讀書期間一直從事系統安全研究,主要方向為虛擬化安全、安卓系統安全。吳家志博士在全球安卓安全領域有很大的影響,發表過多篇科技論文,在安卓系統漏洞安全方面經驗豐富。他於2017年開始轉戰至區塊鏈安全領域,曾擔任全球第一家去中心化匿名眾測平台DVP(Decentralized Vulnerability Platform)負責人,號召全網白帽黑客一起尋找開源底層代碼中的漏洞。