這個系列已經寫到第十天,先整理一下前面討論過的東西。
Day 1 到 Day 3 是準備:工具沒有報錯,合約是不是就安全?漏洞有哪些分類方式?Slither、Foundry、Certora 各自交出的證據又差在哪裡?
Day 4 到 Day 6 開始跑 Slither。先在 FlippazOne 上找到沒有權限檢查的提款函式,再拆 Slither 怎麼把原始碼變成 AST、CFG、SlithIR,最後看它的 Impact 和 Confidence 代表什麼。
Day 7 到 Day 9 看 Slither 抓不到、或抓得到的例子。Level Finance 的重複領取逃過了 Slither,Grim Finance 的 Reentrancy 則抓得到。最後自己寫了一個 detector,標出批次輸入的風險。
接下來讓我們討論要怎麼將問題記錄下來
Day 6 提過,Slither 的 Impact 和 Confidence 描述的是這一類 Finding 的影響和分析信心。所以每一筆 arbitrary-send-eth 都會標 High,就算它在你的合約裡一毛錢都送不出去。實際損失多少、函式能不能被呼叫、合約裡有沒有資產,還是要另外確認。
Finding 出來之後,還要自己再判斷一次。
那要怎麼判斷?我參考 Sherlock、Code4rena、Cantina 三個審計平台的判定標準,把每筆 Finding 拿三個問題來問:
| 軸 | 判斷依據 | 高 | 低 |
|---|---|---|---|
| 可利用性 | 誰能觸發?前置條件做得出來嗎? | 任何人都能呼叫,不需要特殊狀態 | 只有 owner 能呼叫,或前置條件做不出來 |
| 資產影響 | 觸發之後,誰的什麼東西受損? | 資金被轉走、鎖住,或合約停擺 | 只是少了一個 event 或寫法不好看 |
| 證據強度 | 目前手上的證據能證明到哪裡? | 有 PoC 或事故交易重現 | 只有 detector 的一行描述 |
Sherlock 要求寫出完整的攻擊路徑和實際損失,Code4rena 依資金損失、功能不能用、griefing 分級,Cantina 則是用影響乘上可能性的矩陣。
三軸看完之後,就能訂出優先程度。可利用性和資產影響都高,例如任何人都能直接轉走資金,是 High,要最先處理。只在特定條件下才會出事,或是會讓功能不能用但錢不會被拿走,是 Medium。可利用性或資產影響其中一個是低,例如只有 owner 能呼叫,或根本沒有資產受影響,就是 Low 或 Informational,排在最後。證據強度不影響等級,但會決定這個判斷站不站得住,證據不夠的要先補 PoC。
FlippazOne 有 40 筆 Finding,下面挑兩筆 Slither 都標 High 的來看,兩筆最後的優先程度差很多。
ownerWithdrawAllTo:HighSlither 的判斷是 arbitrary-send-eth,Impact High、Confidence Medium。
function ownerWithdrawAllTo(address toAddress) public {
(bool success, ) = toAddress.call{value: address(this).balance}("");
require(success, "Failed to withdraw funds.");
}
先看可利用性。函式是 public,沒有 onlyOwner,也沒有任何 require 檢查呼叫者是誰,連拍賣有沒有結束都不檢查。所以任何人、任何時間都能呼叫,也不需要先做什麼準備。可利用性是高。
再看資產影響。轉出的金額是 address(this).balance,也就是合約裡全部的 ETH,裡面包含還沒退回去的出價。收款地址 toAddress 是呼叫者自己傳進來的,所以錢會直接進攻擊者的地址。資產影響也是高。
最後是證據強度。這筆不用自己寫 PoC,Day 4 看過的事故交易就是證據:2022 年 7 月 6 日,有人呼叫這個函式,傳入自己的地址,把合約裡的 ETH 全部轉走了。證據強度是高。
三軸都是高,訂為 High。
projectProxy:InformationalSlither 的判斷是 uninitialized-state,Impact High、Confidence High,Confidence 比上一筆還高。
Slither 說 projectProxy 從來沒有被初始化過。它是一個 mapping:
mapping(address => bool) public projectProxy;
我在整份合約裡搜尋 projectProxy,只出現兩次,一次是上面的宣告,另一次在 isApprovedForAll:
function isApprovedForAll(address _owner, address operator) public view override(ERC721) returns (bool) {
OpenSeaProxyRegistry proxyRegistry = OpenSeaProxyRegistry(proxyRegistryAddress);
if (address(proxyRegistry.proxies(_owner)) == operator || projectProxy[operator]) return true;
return super.isApprovedForAll(_owner, operator);
}
沒有任何函式會寫入 projectProxy,所以每個地址查出來都是預設值 false。projectProxy[operator] 永遠不成立,這個條件等於不存在。
先看可利用性。攻擊者沒辦法讓 projectProxy 變成 true,因為根本沒有寫入的地方。可利用性是低。
再看資產影響。這個條件如果會成立,某個地址就能操作別人的 NFT,那才危險。但它永遠是 false,結果只是少了一條授權的路,不會多出一條。沒有人會因此多拿到權限,也沒有資產受影響。資產影響是低。
證據強度是高,因為只要讀程式碼就能確認,這兩行就是全部用到它的地方。
可利用性和資產影響都是低,訂為 Informational,不算漏洞。它比較像開發者原本想做「專案方代理授權」,最後忘了寫設定函式。
兩筆的 Impact 都是 High,projectProxy 的 Confidence 還更高,但最後一筆是 High,一筆是 Informational。
訂好優先程度之後,要寫成報告交出去。各平台的格式不太一樣,下面參考 Sherlock 報告的欄位,把 ownerWithdrawAllTo 寫一次。Sherlock 會把等級放在標題前面,High 的第一筆寫成 H-1。
H-1:任何人都能轉走合約裡全部的 ETH
摘要
FlippazOne.ownerWithdrawAllTo 沒有權限檢查,任何人都能把合約裡的 ETH 全部轉到自己的地址。
原因
FlippazOne.sol:1359-1362 的 ownerWithdrawAllTo 是 public,沒有 onlyOwner,函式裡也沒有檢查 msg.sender。收款地址 toAddress 由呼叫者傳入,轉出金額是 address(this).balance。
前置條件
合約裡有 ETH,例如拍賣期間累積的出價。
攻擊路徑
ownerWithdrawAllTo(攻擊者地址)。address(this).balance 全部轉給攻擊者。影響
合約裡全部的 ETH 被轉走,包括得標者付的錢,以及還沒退回給其他出價者的錢。
證據
2022 年 7 月 6 日的事故交易就是這條路徑,見 Day 4。
修正建議
加上 onlyOwner。同一份合約的 ownerWithdrawTo、ownerWithdraw、ownerWithdrawAll 也都要一起加。
跟前面三軸對照,攻擊路徑和前置條件在回答可利用性,影響在回答資產影響,證據在回答證據強度。前面訂優先程度時問過的問題,寫報告時要一個一個寫出來。
不過 ownerWithdrawAllTo 的證據是現成的事故交易,大部分的漏洞沒有現成的交易可以看。下一篇回到 Level Finance,把它的重複領取縮成一份可以測試的合約,開始自己準備證據。