2024 年3 月28 日,Prisma Finance 遭遇攻擊,目前累計虧損約1,100 萬美元。攻擊發生後,Prisma Finance 緊急暫停了項目,並告知用戶趕緊取消委託授權(https://twitter.com/PrismaFi/status/1773371030129524957)。
攻擊簡述
Prisma Finance 是一個非託管、去中心化的,以抵押以太坊LST(流動性質押代幣)鑄造穩定幣的項目。例如用戶可以透過抵押wstETH來鑄造mkUSD,這個過程可以創建一個trove,trove 可以理解為是一個記錄指定borrower 的抵押借貸情況的寶庫,這個寶庫有一個trove manager,用來管理寶庫的抵押物以及借貸幣(鑄造的穩定幣)。
本次的漏洞合約是MigrateTroveZap 合約,該合約的主要功能是將使用者的抵押品從一個trove manager 遷移到另一個trove manager。因此創建trove 的borrower 就可能會授權MigrateTroveZap 合約對其trove 進行操作。然而MigrateTroveZap 合約中的「onFlashloan」函數缺乏輸入驗證,從而允許攻擊者透過MigrateTroveZap 合約操作其他borrower 的trove,將其他borrower 的trove 中的抵押品數量變少,但是債務基本上不變,從而盜取其他borrower的抵押品。
攻擊中涉及的關鍵地址
本次攻擊涉及多筆交易,我們僅以下面這比交易為例來對攻擊進行分析。
攻擊交易:https://etherscan.io/tx/0xe15fa959627871845f2f5bbfbd7529e6d2aff20ab14ece743f11641700bd7188
攻擊EOA:
0x7e39e3b3ff7adef2613d5cc49558eab74b9a4202
攻擊者(合約):
0xd996073019c74b2fb94ead236e32032405bc027c
受害者(Prisma Finance TroveManager):
0x1cc79f3f47bfc060b6f761fcd1afc6d399a968b6
漏洞合約(Prisma Finance MigrateTroveZap):
0xcc7218100da61441905e0c327749972e3cbee9ee
一個被利用的borrower,簡稱BorrowerA:
0xcbfdffd7a2819a47fcd07dfa8bcb8a5deacc9ea8
穩定幣mkUSD:
0x4591dbff62656e7859afe5e45f6f47d3669fbb28
質押物wstETH:
0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0
BorrowerOperations 合約:
0x72c590349535ad52e6953744cb2a36b409542719
攻擊流程分析
01、攻擊準備
攻擊者觀測borrower 借貸情況,尋找抵押率較高的borrower,在這比攻擊交易中,找到的borrower 為BorrowerA。
攻擊者取得BorrowerA 在TroveManager 中的collateralToken(wstETH)和debtToken(mkUSD)的數量,分別為「824,599,953,913,164,625,273」、「598,174,188,906,625,273」、「598,174,188,906,4096,096,096,096,009」 4155,大概估計一下抵押率為570%,遠遠超過了最小抵押率MCR 110%。這給後續攻擊創造了條件。
02、攻擊實施
攻擊實施階段主要是攻擊者透過mkUSD 的閃電貸服務,呼叫到漏洞合約MigrateTroveZap 的「onFlashLoan」函數,「onFlashLoan」函數可以對攻擊者指定的borrower 的trove 進行置換,所謂置換就是先關閉這個trove,再給borrower 開啟一個新的trove。這個功能本來是用來將抵押物遷移到不同的trove manager。然而這個功能存在漏洞,一是它沒有校驗新開的trove 是否和之前的trove 具有同樣數量的抵押物,二是透過mkUSD 的閃電貸服務,攻擊者可以操控別人的trove。具體的攻擊步驟如下:
1、利用mkUSD 的閃電貸服務調用到漏洞合約MigrateTroveZap 的“onFlashLoan”函數,並在此時傳入上述觀測好的受害者BorrowerA 的地址以及準備新開的trove 的抵押品數量。攻擊者呼叫mkUSD 的「flashLoan」函數進行閃電貸,借出mkUSD 給MigrateTroveZap(MigrateTroveZap 合約用來自動將同樣的抵押物遷移到不同的trove manager),並在「data」參數中指定了後續操作需要用到的BorrowerA 的地址、TroveManager 的地址、創建trove 時抵押的wstETH 的數量,具體傳入的參數如下所示:
mkUSD.flashLoan:
在「flashLoan」函數中,首先會為MigrateTroveZap 鑄造「598,174,188,906,400,741,697,930」這麼多mkUSD,然後呼叫MigrateTroveZap 的回呼函數「onFlashLoan」,問題就出在了這個回調函數中。
2.在MigrateTroveZap 的「onFlashLoan」函數中對BorrowerA 的trove 進行更換,之所以MigrateTroveZap 能操作BorrowerA 的trove,是因為BorrowerA 對MigrateTroveZap 進行了委託授權。原本BorrowerA 的trove 抵押率比較高,但是抵押率比較高的trove 被關閉掉,取而代之的是一個債務一樣,但是抵押率更低的trove,這個操作能夠成功是因為更換trove 時並沒有校驗新舊trove的抵押物數量是否一致,最終多餘的抵押品數量會被留在MigrateTroveZap 中。
詳細過程如下:
在MigrateTroveZap 的o「nFlashLoan」函數中,會將指定account 的抵押物從troveManagerFrom 遷移到troveManagerTo,在本次呼叫中,從攻擊者傳入的「data」參數中解析出來,這兩個參數均指定為了TroveManager 的地址。
MigrateTroveZap.onFlashLoan:
攻擊者指定的account 是BorrowerA,因此首先會調用BorrowerOperations 的“closeTrove”函數將BorrowerA 的trove 關閉掉,關閉之前會通過modifier “callerOrDelegated”檢查BorrowerA 是否授權調用發起者(在這裡是MigrateTroveZap)代理它進行關閉trove 的操作。由於BorrowerA 確實授權了MigrateTroveZap ,因此判斷通過,並且此時並不處於recovery mode,因此BorrowerOperations 最終調用TroveManager 的“closeTrove”函數關閉trove,在這個過程中,將BorrowerA 的抵押品wstETH(數量為“824,599,953,599,953,5967,257,5996” ) 轉給MigrateTroveZap。隨後burn 掉MigrateTroveZap 的mkUSD,數量為「597,974,188,906,400,741,697,930」。
BorrowerOperations.closeTrove:
緊接著BorrowerOperations 的「openTrove」函數被調用,此時傳入的參數「_collateralAmount」為攻擊者在「data」參數中指定的「192,125,967,324,963,177,654」,而「_debtAmount」為新的攻擊者數量」。上閃電貸的手續費,數值為「598,712,545,676,416,502,365,458」。經過計算,現在並不處於recovery mode,因此最終用來計算抵押率的debt 的數量“compositeDebt”為“_debtAmount”+ debt borrowing 費+ debt gas 補償,具體數值為“598,912,545,682,977,9016,59699682,977,012,0005,0005” _debtAmount」和價格計算出來的ICR (Individual Collateral Rate) 約為133%,大於MCR (最小抵押率) 110%,符合創建trove 的條件。隨後MigrateTroveZap 給TroveManager 轉移「_collateralAmount」數量的wstETH,MigrateTroveZap 拿到給其鑄造的「_debtAmount」數量的mkUSD。這番操作相當於攻擊者利用MigrateTroveZap 合約,將BorrowerA 本來有很多抵押品的trove,換成了抵押品較少的trove,而攻擊者想要盜取的,正是這一部分抵押品差額。
BorrowerOperations.openTrove:
3.回呼結束以後,回到mkUSD 的flashLoan 函數。此時MigrateTroveZap 持有的mkUSD 的數量正好比需要償還的mkUSD 的數量多一些,透過burn 掉MigrateTroveZap 的mkUSD 成功償還了閃電貸的債務。至此, MigrateTroveZap 相當於額外獲得了約632 個wstETH 。接下來攻擊者就開始想辦法把這632 個wstETH 給套到自己手上了。
03、收割贓物款
收割贓物的步驟主要是攻擊者將#攻擊實施#步驟中盜取的留在MigrateTroveZap 合約中的wstETH 提取出來。攻擊者依然是利用MigrateTroveZap 合約的漏洞。這次攻擊者先自己開了一個抵押率較低的trove,然後透過mkUSD 的閃電貸服務進入到漏洞合約MigrateTroveZap 的「onFlashLoan」函數,對自己的trove 進行更換。
相較於#攻擊實施# 階段對BorrowerA 的trove 的更換,不同之處在於,攻擊者對自己trove 的更換,是要從低抵押率的trove 更換為高抵押率的trove,而用到的抵押物正是MigrateTroveZap 中多出來的wstETH。之所以能用到MigrateTroveZap 的wstETH,是因為攻擊者也授權MigrateTroveZap 對其trove 進行管理,那麼在更換trove 時,抵押物都是在TroveManager 和MigrateTroveZap 之間流轉。所以在開一個新的trove 時,如果用到的抵押物數量增多,就會直接扣除MigrateTroveZap 中的抵押品數量。
最終攻擊者的trove 在更換之後擁有了更多的抵押物,攻擊者就可以自己發起關閉trove 的調用,將抵押物提取出來,從而將盜取的抵押物收入囊中。
詳細過程如下:
- 攻擊者從Balancer 中閃電貸出1 個wstETH,從而調用到攻擊者的「receiveFlashLoan」函數。
- 在「receiveFlashLoan」函數中,攻擊者首先授權BorrowerOperations 使用其wstETH,然後再授予MigrateTroveZap 代理權。
- 接著攻擊者呼叫BorrowerOperations 的「openTrove」函數為自己建立一個trove,使用閃電貸出的1 個wstETH 進行抵押,借2000 個mkUSD,此時的抵押率大約是188%。這是攻擊者為了後續操作所做的一個準備。傳入的參數詳細資訊如下:
- 攻擊者再次呼叫mkUSD 合約的flashLoan 函數進行閃電貸,這次是藉2000 個mkUSD 給MigrateTroveZap,跟#攻擊實施# 階段類似,給MigrateTroveZap 鑄造了2000 個mkUSD,並觸發了MiglashTroveZapon 的回調」函數。這次攻擊者傳入的「data」參數如下,攻擊者指定的想要遷移的trove 是攻擊者上一個步驟所建立的trove。
- 在函數「onFlashLoan」裡,同樣先執行「closeTrove」函數,TroveManager 將1 個wstETH 轉給MigrateTroveZap,然後burn 掉MigrateTroveZap 約2000 個mkUSD。接著執行「openTrove」函數,這次攻擊者指定的「_collateralAmount」值為「633,473,986,588,201,447,619」,幾乎是MigrateTroveZap 中所有的wstETH,當然其中包括了在#12 個實施階段套取的632 個實施階段。此時「_debtAmount」具體值為「2,001,800,000,000,000,000,000」。抵押率大概1195%。 trove 創建成功,MigrateTroveZap 將約633 個wstETH 轉給TroveManager,並給MigrateTroveZap 鑄造了約2001 個mkUSD。
- 回呼結束以後,回到mkUSD 的flashLoan 函數。 MigrateTroveZap 償還閃電貸債務,持有的mkUSD 被burn 掉。
- 到這一步,攻擊者自己創建的trove 仍然存在,不同的是,經過前面幾步在mkUSD 中的閃電貸,攻擊者將自己的trove 中抵押品的數量提升到了約633 個wstETH。
- 攻擊者呼叫BorrowerOperations 的「closeTrove」函數,關閉trove,從TroveManager 處拿到約633 個wstETH,並burn 掉對應的mkUSD 債務。
- 攻擊者償還在Balancer 中藉出的1 個wstETH。
至此,收割贓物結束,攻擊者獲利約632 個wstETH。
資金流追蹤
本次攻擊共涉及三個EOA,如下:
- Exploiter 1: 0x7E39E3B3ff7ADef2613d5Cc49558EAB74B9a4202
- Exploiter 2: 0x7Fe83f45e0f53651b3ED9650d2a2C67D8855e385
- Exploiter 3: 0x7C9FC6E2B908e858F30c5c71a20273315Efd5cf8
Exploiter 2 和Exploiter 3 共獲利約200 個ETH,而另外的約3200 個ETH 則是被聲稱是白帽的Exploiter 1 獲取(https://etherscan.io/tx/0xc2825fd6dd05e8ec9f271d63efdebdeb 09)。目前Prisma Finance 專案方仍在與聲稱是白帽的Exploiter 1 溝通經費退還事宜。
Exploiter 1 向專案方開出了以下條件:
- Prisma 團隊需要進行一次線上新聞發布會;
- 團隊所有成員都必須現場露面和出示身份證明;
- 向Prisma 的所有使用者、投資者和Exploiter 1 表示道歉和感謝;
- 具體介紹本次事故裡面的問題所在:智慧合約審計方是誰,以及Prisma 將來提高安全性的計畫;
- Prisma 需要承認Exploiter 1 在這起事件中沒有任何責任,Exploiter 1 純粹是在幫助Prisma 修復問題;
- Prisma 需要在12 小時內修改事後總結中所有具有指控性的措詞。
透過ZAN 的KYT 服務,我們可以看到,Exploiter 1 將資金轉到了三個不同的地址上,最終這些資金一部分流入了Tornado Cash 中。詳細資金流轉請查看連結:https://zan.top/kyt/controller/transaction?entity=0x7e39e3b3ff7adef2613d5cc49558eab74b9a4202&ecosystem=ethereum
轉入Tornado Cash 資金流動狀況
Exploiter 2 的資金流動:https://zan.top/kyt/controller/transaction?entity=0x7fe83f45e0f53651b3ed9650d2a2c67d8855e385&ecosystem=ethereum
Exploiter 3 的資金流動:https://zan.top/kyt/controller/transaction/?entity=0x7C9FC6E2B908e858F30c5c71a20273315Efd5cf8&ecosystem=ethereum
安全建議
透過分析本次攻擊事件,我們有以下建議:
- 委託授權要慎重。一是專案方需要仔細衡量專案中是否需要委託授權的邏輯,如果需要,那麼一定要對相應操作進行嚴格的權限校驗以及輸入校驗,防止攻擊者利用該授權,傳入不合法的參數,篡改項目中關鍵變數。二是委託授權應該有時間限制,長期的授權,容易被使用者忽略,卻被攻擊者利用。
- 項目設定暫停機制。建議專案方在設計專案邏輯時,建立完善的暫停機制,以因應突發的意外事件,及時停損。
本文由ZAN Team 的Cara 撰寫,作者個人X 帳號@Cara6289。