以太坊EIP-1283 sstore 重入漏洞拆解分析

昨天下班的時侯剛好在 etherscan 上查代碼,瞄了一眼君士坦丁堡硬分叉的倒計時,發現離激活剛好還差 10000 個區塊,抓緊了這 10 秒的時間,截圖留念。


10k to go

今天上午,安全審計團隊 ChainSecurity 猝不及防的發布了一份漏洞分析簡報。為此,以太坊核心開發團隊觸發了緊急響應,正式宣布君士坦丁堡升級延期,重啟時間未定。
……回想昨天自己發朋友圈時的激動興奮,感覺被狠狠打了臉。

下午讀完 ChainSecurity 的簡報,寫的很干練,可惜對不熟悉 ETH 的朋友們不夠友好,需要梳理半天的邏輯。所以抖個膽,自己寫個更詳細的拆解,我也是新入行,有錯誤的話,見諒。

sstore 重入漏洞拆解分析

理解原合約

ChainSecurity 給的被攻擊案例很有代表性,完整的被攻擊合約點此查看

這是個 “雙人共享收款錢包” 合約(以下簡稱:錢包合約),我們來舉個正常情況下的使用例子:

  1. 為地址 「小A」、「小B」 登記一個編號為 1 的共享錢袋
調用錢包合約功能:init(錢袋1 , 小A, 小B)
  1. 設定收到 ETH 后的分賬比例為 小A:小B = 7:3
調用錢包合約功能:updateSplit(錢袋1, 70)
// 即「小A」獲得 70%,「小B」獲得其余部分
  1. 「小C」向錢包合約地址的錢袋 1 支付 10ETH
「小C」調用錢包合約功能:deposit(錢袋1) + 10ETH
// 此次交互 tx 攜帶 10ETH 的金額
// 此時錢包合約地址余額:10ETH
// 其中,登記為「錢袋1」的余額:10ETH
  1. 清算 錢袋1 的余額,合約自動依照設定比例支付給「小A」、「小B」
調用錢包合約功能:splitFunds(錢袋1)
// 最終,以下兩個轉賬函數 (transfer) 將依次執行
A.transfer(「錢袋1余額」 * 「分賬比」 / 100);
B.transfer(「錢袋1余額」 * (100 - 「分賬比」) / 100);
//「小A」獲得 (10ETH * 70 / 100) = 7ETH
//「小B」獲得 (10ETH * 30 / 100) = 3ETH

So far, so good.
感嘆下以太坊的靈活性,公平公正的分帳,童叟無欺。

攻擊搭建

那么,哪里會有問題呢???
ChainSecurity 給出了攻擊的具體方式,完整的攻擊者合約點此查看

「黑客X」將此合約部署到鏈上后,可獲得一個攻擊者合約地址(以下簡稱:攻擊合約)。

我們繼續有請善良的「小A」「小B」「小C」,重新操作以上的 [步驟1~步驟3]

  1. 「黑客X」登場,為「攻擊合約」與「黑客X」登記了錢袋2,并對此錢袋支付 10ETH。
    目前的錢包合約狀態為:
此時錢包合約地址總余額:20ETH
        
其中,登記為「錢袋1」的余額:10ETH
登記為「錢袋2」的余額:10ETH
// 錢袋1:屬于「小A」與「小B」
// 錢袋2:屬于「攻擊合約」與「黑客X」
  1. 「黑客X」調用攻擊合約的攻擊函數。攻擊代碼段如下,注意,此函數位于「攻擊合約」內:
function attack(address victim) {
    // victim 為被攻擊的錢包合約地址
    PaymentSharer x = PaymentSharer(victim);
    // 繼承錢包合約的各個函數
    x.updateSplit(2, 100);
    // 子步驟 1:錢袋2的「分賬比」設置為「攻擊合約」獲取 100%
    x.splitFunds(2);
    // 子步驟 2:清算 錢袋2 的余額
    // 攻擊達成:「攻擊合約」與「黑客X」各獲得 10ETH
  }

目前的錢包合約狀態為:

此時錢包合約地址總余額:0ETH【他人余額被異常提取】
        
其中,登記為「錢袋1」的余額:10ETH
【此為賬簿數組中的記賬參數,非真實余額,目前已被黑客侵吞,無法實際兌付】
登記為「錢袋2」的余額:0ETH
// 錢袋1:屬于「小A」與「小B」
// 錢袋2:屬于「攻擊合約」與「黑客X」

充值 10ETH,提現 20ETH,Happy~

。。。。。。
。。。。。
。。。。
。。。
。。

等等。。。啥玩意兒?這就攻擊達成了???你啥都沒解釋啊!!!
別急,我們這就來拆解攻擊邏輯:)

攻擊邏輯拆解圖

由于涉及合約間交互,做個一圖流:

攻擊拆解

按照標號逐步看,也可以結合代碼。因該寫的比較通俗了。

關鍵點:上圖[流程 3]與[流程 6]的兩次轉賬 transfer() 間,直接默認 “不會發生分賬系數的變化” ,沒有額外的狀態檢查。結果被人利用合約漏洞,插入[流程 3-4]后重入,實現了記賬總額 200% 的異常提款。

可能還有同學有疑問,為什么 sstore 的 Gas 消耗 5000 時(以太坊現行邏輯)就沒出過問題?代碼不是一樣么,也跑的通啊。

這里還有個額外的知識點,為了讓轉賬函數 transfer() 更具擴展性,交易內額外觸發的 transfer() 操作,默認會帶上 2300 Gas 的“零錢”處理 fallback 中可能存在的后續操作(拆解圖中的 3*)。

以太坊現行邏輯中,單次 sstore 消耗 5000 Gas,“零錢”根本不夠扣。合約邏輯雖然不嚴密,但系統的資源限制事實上給大家兜了底。
君士坦丁堡更新,會令特殊條件下的 sstore 消耗大幅降低到 1700 Gas 的“可能性閾值”以下(用 call 觸發還需要消耗 600 Gas),兜底已經失效,變成了真正的攻擊入口。

總結

這是以太坊核心層的漏洞么?
我并不這么認為,作為合約的編寫者,需要應對的是各種復雜而晦澀的邏輯陷阱。這一漏洞應當在合約編寫時就有所防范,做好異常處理,而不是寄希望于“一個預置參數肯定不會變化,所以某些事情不用考慮,絕不可能發生”、“別人都這么做,所以我就 Ctrl-CV 一套”。

這是以太坊核心開發者的過錯么?
sstore 的 Gas 計算調整早在 2018 年 8 月 1 日便列入了協議改進提案(EIP-1283),于 11 月被接受為正式改進,如此大幅降低消耗至 1700 Gas 可觸發閾值以下的調整,理應更慎重的考慮舊合約的兼容性、安全性。

“無法篡改的技術負債”、“永遠的前向兼容”。So good. So bad.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容