在 2022Q2,因緣際會開始拜師學習 Solidity,意外的發現自己對於 DeFi Security 蠻有熱誠,也向大佬學習了很多技術和心態。
在 2023 年,我想完成的其中一件事是把 ventral.digital 的這篇 Ethereum Smart Contract Auditor's 2022 Rewind 所提到的攻擊事件進行完整的分析,也順便當作自己的筆記。
隨便數了一下,內文大概提到了超過 80 個攻擊事件吧,如果每一個攻擊事件都進行分析、寫 Reproduce,大概會耗掉兩個月時間來練功,希望可以堅持住,哈哈。
我還沒仔細看每一個攻擊事件,打算直接一邊分析一邊當作鐵人賽寫文章了,太類似的攻擊事件或屬於無法寫 Reproduce 的漏洞(像 BNB Chain hacked 那樣?),應該會直接跳過。文章撰寫上沒有順序,對什麼攻擊事件感興趣就寫什麼,不過每個大類別至少都會分析一個吧。
XCarnival 是一個在以太坊上的去中心化 NFT 市集,用戶可以在該平台上買賣 NFT、將 NFT 進行超額抵押貸款等操作。

在 2022 年 06 月 26 日,該項目遭到駭客攻擊,駭客利用了該項目智能合約所存在的邏輯漏洞,該漏洞使駭客將超額抵押的 NFT 解除抵押後,仍能進行借款,這可對系統造成壞帳,最終 XCarnival 項目方損失了 3087 顆 ETH,當時市價相當於 3.87 Million US$。
攻擊者地址:0xb7cbb4d43f1e08327a90b32a8417688c9d0b800a
主要攻擊合約:0xf70f691d30ce23786cfb3a1522cfd76d159aca8d
被攻擊的 XNFT.sol 合約:0x39360ac1239a0b98cb8076d4135d0f72b7fd9909
那駭客是如何利用這個漏洞盜取 3087 ETH 呢?
我做了一張圖來分解駭客的攻擊流程,就讓我們搭配這張圖一步步食用吧:

攻擊者在區塊高度 15028839 部署了主要攻擊合約,合約代碼當然沒有開源,不過我們可以從 Reproduce 中大致理解攻擊合約的代碼邏輯。
TxID: 0xe4f99b2fb86a317eb16f7f288fda74ab07f0ffcbf645fb3b1a6490ca23206d09
攻擊者將它從區塊高度 15028718 買來的 BAYC#5110 轉給 Main Attack Contract,準備作為 XCarnival 超額抵押貸款的抵押品。
順帶一提,攻擊者的成本是 91.65 ETH (114K US$),有錢人才玩得起的漏洞。
賣家收到 87.0675 ETH (95%)
藝術家版稅 2.29125 ETH (2.5%)
OpenSea服務費 2.29125 ETH (2.5%)

買在進到下一步之前,稍稍讓我們整理一下目前為止的步驟:
區塊高度 15028718:買 BAYC#5110
區塊高度 15028839:部署 Main Attack Contract
區塊高度 15028847:把 BAYC#5110 轉給 Main Attack Contract
對應 txs 最底下 3 條交易紀錄:

從上圖 攻擊者的 txs 紀錄 你可以觀察到,攻擊者在轉給主攻擊合約 BAYC#5110 後,一直對主攻擊合約0xf70f691d30ce23786cfb3a1522cfd76d159aca8d 調用 0xadf6a75d 這個 Function。
那這到底是在幹嘛?我們可以透過 Blocksec Phalcon 進行 Transaction View:

將調用層級設為 Expand: 1,可以觀察到基本上主攻擊合約做的就是幾件事:
部署暫時的 Payload Contract
把 BAYC#5110 從主攻擊合約,轉移到 Payload Contract
呼叫 Payload Contract 的
0x97c1edd3函數
那 0x97c1edd3 函數在做什麼呢?將它展開至第 2 層:

STATICCALL 都是向主攻擊合約讀取狀態變數,所以基本上我們可以先略過,專注看 CALL 與 XCarnival 互動的部分。
setApprovalForAll()給 XCarnival 合約權限,讓 XCarnival 可以把 BAYC NFT 轉走pledgeAndBorrow()把 BAYC#5110 作為抵押品向 XCarnival 借錢counter()取得剛剛抵押借款的 orderId,這邊是取到orderId: 11withdrawNFT()把剛剛抵押的 BAYC#5110 解除抵押,將 NFT 退還。transferFrom()把 BAYC#5110 再轉回給主攻擊合約這個 Transaction 重複 1 ~ 5 四次,然後用相同的手法重複了 14 個 Transactions,建立了
orderId: 11到orderId: 66。
還是覺得很頭痛嗎?沒關係,這邊有 Reproduce Code 幫助你梳理攻擊者的邏輯:

0xb14b-TransparentUpgradeableProxy地址,其實是有漏洞的XNFT.sol的透明代理地址,有學過 Proxy Pattern 的你,知道 XNFT.sol 的狀態變數都是儲存在 0xb14b-TransparentUpgradeableProxy 的,所以使用者其實是在和 0xb14b-TransparentUpgradeableProxy互動。
對 DeFi Landing 稍微暸解過的你,可能會覺得很疑惑:為什麼攻擊者可以 pledgeAndBorrow() 之後,不還款就直接調用 withdrawNFT() 呢?
這其實是因為攻擊者 pledgeAndBorrow() 借出 0 元,所以只是”單純的”建立了這筆抵押紀錄 orderId。
並且合約代碼也沒有不允許債務等於 0 的訂單取消抵押 NFT:

當順利通過 withdrawNFT() 的所有檢查,也順利取回 BAYC#5110 了,就會設定狀態變數_order.isWithdraw = true。
所以攻擊者在一個 Transaction,就完成了 3 ~ 8 的步驟,分別是:
Deploy Payload Contract
Transfer BAYC#5110 from Main to Payload
Call:
XNFT.pledgeAndBorrow()將 BAYC#5110 作為抵押品,借 0 元Transfer BAYC#5110 from Payload to XNFT
Call:
XNFT.withdrawNFT()取回 BAYC#5110Transfer BAYC#5110 from XNFT to Payload
好的,目前我們已經知道攻擊者把 BAYC#5110 作為抵押品送到 XCarnival 裡,借款 0 元同時產生 order,然後將 BAYC#5110 解除抵押從 XCarnival 退回。
接下來我們來分析攻擊者究竟是怎麼把 ETH 盜領出來的。

可以看到攻擊者先調用了主攻擊合約的 start() 函數。
主攻擊合約再呼叫每一個 Payload Contract 的 0x2a3e7cec 函數。
Payload Contract 再向 XCarnival 的 XToken.sol 互動,調用 borrow() 函數將 36 ETH 借出來,漏洞利用結束。
由於在先前步驟,攻擊者已經透過調用 withdrawNFT() 解除抵押了 BAYC#5110,所以 BAYC#5110 實際上是在攻擊者手上,所以此時 borrow() 出來的 36 顆 ETH 沒有實質資產儲備,造成 XCarnival 壞帳,最終攻擊者反覆操作,盜走 3087 顆 ETH。
讓我們看一下 XToken.sol 的代碼:

如果你對 DeFi 不陌生的話,其實可以發現 XToken.sol 似乎是 Fork Compound 的代碼。
讓我們跟進 controller.borrowAllowed() 函數,看看風控模組在確定借款之前,做了什麼校驗:

從相關的 require 語句可以觀察到,controller.borrowAllowed() 主要是檢查 BAYC 系列是否為 XCarnival 支持超額抵押貸款的 NFT 系列。
上圖可以看到,又出現一個 orderAllowed(orderId, borrower),所以我們繼續往下追:

在 L49,controller 向 xNFT 合約索取有關 orderId 對應的 NFT 系列地址與抵押者,require 的要求是借款人調用 borrow(orderId) 的 orderId 是合法的、借款人就是抵押人、抵押品不能是已經被清算的狀態。

神奇的地方在於 orderAllowed() 函數並沒有檢查 orderId.isWithdraw 是否等於 false,這就讓攻擊者可以拿已經 withdraw 的 orderId 來向 XCarnival 借款!
攻擊者的思路就是利用這個漏洞,透過多個 Payload Contract 來產生已經 withdraw 過的 orderId,然後反覆向 XCarnival 借款,最後就造成損失 3087 ETH 的悲劇了。
現在,我們已經知道漏洞成因、攻擊者的利用思路,現在我們試著寫 Patch:

Compound 是專注在 ERC20 操作的協議,項目方方 fork 代碼做成 ERC-721 通用的協議還是要仔細檢查兩種協議之間商業邏輯的差異;ERC-721 確實還蠻多坑的,比方說不怎麼 safe 的 safeMint()。
在此攻擊事件發生後,我未找到 XNFT.sol 和 XToken.sol 所開源出來的 Code Repo,也沒有找到相關的審計報告,這表明項目方在 Production 上線前似乎未經過第三方代碼安全公司審計,也再次凸顯了審計的重要性。
在 2022 年 7 月 25 日,XCarnival 給 PeckShield 再次審計過,審計報告連結。

