作者:BCSEC
公眾號:DVPNET

前言

1月16日凌晨,以太坊準備進行君士坦丁堡硬分叉的前一日被披露出來了一則漏洞,該漏洞由新啟動的EIP 1283引起,漏洞危害準確的說應該是一種可能會讓一些合約存在重入漏洞的隱患,而不是一定會使合約產生重入漏洞。該漏洞在被發現之后以太坊基金會立馬宣布了停止硬分叉,并商議擇日再啟動以太坊君士坦丁堡硬分叉。

一、導致以太坊延遲硬分叉的EIP 1283到底是什么?

EIP的全稱是Ethereum Improvement Proposals(以太坊改進提案),任何人都可以上去提一些對以太坊的改進提案,不過必須得嚴謹、正式,以太坊君士坦丁堡這次漏洞就是由一個EIP引起的,這個EIP的編號是1283。EIP 1283使以太坊虛擬機使執行智能合約的引擎更高效,并降低在以太坊上運行智能合約的成本。

該提案是針對SSTORE操作碼的,該操作碼主要用于合約持久化存儲數據,EIP1283為SSTORE操作碼設計了更加合理的gas收費方式。

詳情地址如下:

https://eips.ethereum.org/EIPS/eip-1283

為什么需要EIP 1283?

EIP-1283提案由Wei Tang(@sorpass)于2018年8月1日創建,作為EIP-1087和EIP-1153的替代方案。EIP-1087由Nick Johnson創建,主要是改變EVM SSTORE運行gas費用收取方式,減少過多的gas費用成本;EIP-1153由Alexey Akhunov創建,相比EIP-1087更加便宜,gas費用計算規則更加簡單。EIP-1283提出了在SSTORE上進行gas計量的方案,為數據存儲的變化引進更加合理公平的定價方案。

其中定義了三個概念:

  • 存儲槽的原始值(original):在當前事務發生回滾(revert)后會存在的值叫原始值。
  • 存儲槽的當前值(current):在使用SSTORE操作碼之前存在的值叫當前值。
  • 存儲槽的新值(new):在使用SSTORE操作碼之后存在的值叫新值。

然后以這三個概念為基礎,設計了如下處理邏輯:

如果當前值等于新值(這是無操作),則扣除200 gas。

如果當前值不等于新值

    如果原始值等于當前值(此存儲槽未被當前執行上下文更改)

        如果原始值為 0,則扣除20000 gas。

        否則,扣除5000 gas。如果新值為 0,則在退款計數器中增加15000 gas(退款計數器中記錄的gas會退還給用戶)。

    如果原始值不等于當前值(代表此存儲槽”臟”了),則扣除200 gas。

        如果原始值不為0

            如果當前值為 0(也表示新值不為0),請從退款計數器中減少15000 gas。

            如果新值為 0(也表示當前值不為0),請向退款計數器中增加15000 gas。

        如果原始值等于新值(此存儲槽已重置)

            如果原始值為 0,則將退款計數器中增加19800 gas。

            否則,則在退款計數器中增加4800 gas。

根據如上的邏輯可以發現,當使用SSTORE操作碼的時候如果不改變任何值的時候,只消耗 200 gas。如果改變了值最終又重置為0的話也只消耗20000 + 200 - 19800 = 400 gas。

而在之前EIP 1087的邏輯中如果使用SSTORE操作碼改變了值最終又重置為0的話需要消耗20000 + 5000 - 10000 = 15000 gas。

顯然EIP 1283的處理邏輯比EIP 1087更加合理,也更加便宜,但是問題就在這里。

二、EIP 1283漏洞分析

重入漏洞是指在同一筆交易中因兩個合約互相調用而導致合約進行重復轉賬的一種現象,其產生的根源是沒有使轉賬作為事務的最后一個步驟。

比如說,如果在轉賬之后再進行狀態變更的話就很容易重入漏洞,最經典的一起事件就是The DAO事件,所以最安全的做法是一筆事務中只有一筆轉賬,且在轉賬之前做好所有狀態變更,轉賬作為最后一個操作進行,如果以這種標準來實現的話,是不會受EIP 1283影響的,所以這就是為什么說EIP 1283 只是可能使某些合約產生重入漏洞隱患。

那么,什么樣的合約容易產生這種隱患?請看以下Demo。

這是一個模擬資金共享服務的合約,資金余額由deposits變量存儲,然后由splits變量存儲分配比例。

比如有一筆資金需要a和b共同分配

  • 首先調用init函數存儲雙方的錢包地址
  • 調用deposit函數向通道充錢
  • 調用updateSplit函數來改變通道的分配率
  • 執行splitFunds函數分配資金

如果1號通道的分配率是99,那么執行splitFunds函數的時候給a分配通道中99%的資金,給b分配1%的資金。

該合約大概業務就是這樣,在EIP 1283生效之前,該合約是沒有重入漏洞的,EIP 1283生效才會存在重入漏洞。

前面提到過了,在EIP 1283中如果將一個值更改后又重置為0 ,那么只消耗400 gas。

再看看是怎么實現按比例分配的:

所以我們可以將a賬戶設置為我們的惡意合約,在合約的fallback函數中調用updateSplit函數來改變通道的分配率,使兩個地址都能分到超過通道余額總量的幣.

比如說我先給a賬戶分配100%的通道余額,再在a賬戶合約fallback函數中改變通道分配率,又給b賬戶分配100%的余額,這樣就成功套出了雙倍的錢,而且攻擊者可以一直套,直到掏空為止。

攻擊者Demo:

Ps:為了節約gas,fallback函數中使用內聯匯編來模擬調用updateSplit函數。

調用attack函數即可觸發重入漏洞。

為什么說要EIP 1283生效才會產生漏洞呢,因為該合約使用transfer進行轉賬,transfer轉賬最多消耗2300 gas,在EIP 1283生效之前對變量進行更改再重置至少需要15000 gas,而生效后只需要400 gas,2300 gas上限已經足夠做一些事情了。

三、漏洞復現

關于該漏洞的復現,ChainSecurity已經在Github上公開了。

先clone下來

git clone https://github.com/ChainSecurity/constantinople-reentrancy.git

然后README里面會告訴你怎么復現,不過在此之前先得把環境裝好,需要環境:

\1. nodejs(stable)

\2. npm

    a. truffle:npm install -g truffle

    b. ganache-cli@beta:npm i -g ganache-cli@beta

不同的系統有不同的環境搭建方式,這里不再贅述,有了以上環境就可以進行復現了,運行以下命令:

ganache-cli --hardfork=constantinople
truffle test

運行結果:

在進行攻擊之后成功增加攻擊賬戶內的余額,復現完畢。

四、修復方案

修復方案預計應該會在以太坊君士坦丁堡中刪除與EIP 1283有關的更新,目前以太坊開發者還在協商解決,不過筆者認為合約安全最終還是要合約來解決,不能依賴于公鏈本身,就像前面說的,只要合約采用的是最安全的寫法便可以避免這次君士坦丁堡分叉帶來的問題。

而且目前還沒有檢測出來有合約正好會觸發這個重入漏洞,但不排除這種可能性。

參考鏈接


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/801/