以太坊君士坦丁堡漏洞分析
這兩天關於以太坊延遲君士坦丁堡升級的報導鋪天蓋地,可惜到現在都沒看到一篇能把這個漏洞講透徹的,就由我來給大家解密吧。
上一篇文章給大家介紹過EIP 1283,是為了優化SSTORE指令的gas計算方式的,這次的漏洞就出在這個EIP上,可能會導致“重入攻擊”。
1.什麼是“重入攻擊”
所謂“重入攻擊”,指的是在 同一筆交易 中,合約A呼叫合約B,而合約B又反過來呼叫合約A的現象。
這種情況是必須被禁止的,因為合約A可能需要依賴自身的一些狀態來給其他賬戶轉賬,而如果在這個過程中間,合約B又反過來呼叫合約A並修改了狀態,有可能導致狀態紊亂,黑客可以通過這個漏洞“偷”走別的賬戶的錢。
2.漏洞原理分析
ChainSecurity組織最先向以太坊團隊提交了這個漏洞,他們設計了下面這個場景:
這是一個“共享支付合約”,其實就跟我們平時去食堂刷飯卡是類似的。我去管理處辦了張飯卡,往裡面充了100塊錢,現在這些錢100%都是屬於我自己的。然後我去吃了頓飯花了20,這時候我就更新一下卡里的引數:這張卡里的錢80%歸我,剩下歸食堂。然後我突然接到通知,公司要搬家了,這張卡用不上了,於是我就去管理處退卡,管理處的會計就根據這個80%的比例, 退我100*80%=80塊錢,還有100*(1-80%)=20塊錢打到食堂帳上 。請注意,這個操作必須是 原子 的,假如他先退了80塊給我,然後我在他給食堂打錢之前,把引數改成了0%,他就會給食堂帳上打100*(1-0%)=100塊錢!也就是說,雖然我只充了100塊,但是我跟食堂加起來卻得到了180塊錢,這多出來的80塊錢是哪裡來的呢?當時就是從其他充飯卡的人那裡“偷”來的啦~
具體到程式碼層面,攻擊的流程參見下圖:

黑客首先給“攻擊合約賬戶A”和一個“普通賬戶B”之間建立一條共享支付通道(辦張卡),請注意,這兩個賬號都是黑客自己控制的。
然後黑客操縱賬戶A呼叫deposit()方法往“共享支付合約”裡充了一些錢(比如100 ETH)。
接著,黑客呼叫攻擊合約的attack()方法,這個方法會接連執行下面兩個呼叫:
- 呼叫“共享支付合約”的updateSplit()方法,把分配引數更新成100%(沒毛病,這些錢都是賬戶A的)
- 呼叫"共享支付合約"的splitFunds()方法銷卡退款(理論上應該給賬戶A轉100 ETH,賬戶B轉0 ETH)
"共享支付合約"先給賬戶A轉100 ETH,呼叫賬戶A的transfer()方法。但是賬戶A是個合約,並且沒有transfer()方法,因此會呼叫到它的fallback方法。
在合約A的fallback方法裡,它再次呼叫了“共享支付合約”的updateSplit()方法,把分配引數更新成了0%(這一步是通過內聯彙編完成的,比較省gas,具體原因後面會說)。
接著,“共享支付合約”會繼續給賬戶B轉賬,但是由於分配引數變了,現在賬戶B佔100%了,所以它又給賬戶B轉了100 ETH。
可以看到, 黑客每發起一次攻擊,都可以賺100 ETH(因為兩個賬號都是他自己的),而且可以無限次數攻擊,直到把“共享支付合約”裡的錢偷光 ,太可怕了。。。
3.為什麼升級前沒有這個漏洞
實際上在此之前,EVM是考慮過重入攻擊問題的,在合約A呼叫合約B時,合約B的程式碼只能執行一些非常簡單的操作(比如傳送一個event,對應LOG指令),消耗的總gas不能超過2300,這被稱為“ 呼叫津貼(CallStipend) ”。由於CALL指令本身需要消耗700 gas,所以 實際上可用的gas只有1600 ,這對於普通指令足夠用了,比如LOG指令每個位元組只需要消耗8 gas,因此最多可以寫200個位元組來記錄這次呼叫事件。但是,SSTORE指令需要消耗5000 gas,因此如果合約B中使用了SSTORE指令,會導致Out of Gas從而中止交易的執行。因此,EVM是依靠SSTORE指令的高額油費消耗來避免重入攻擊的。

但是,這一保證被EIP 1283打破了。我們先來回顧一下EIP 1283對SSTORE的gas計算方法(不熟悉的朋友請參考前一篇文章):
- No-op狀態:收取200 gas
- Fresh狀態:
- 如果原始值是0,收取20000 gas
- 否則,收取5000 gas。如果新值是0,退還15000 gas
- Dirty狀態:收取200 gas,並檢查下面2個條件:
- 如果原始值不是0
- 如果當前值是0(說明新值不是0),收回退還的15000 gas
- 如果新值是0(說明當前值不是0),退還15000 gas
- 如果原始值等於新值(被reset回原始值了)
- 如果原始值是0,退還19800 gas
- 否則,退還4800 gas
- 如果原始值不是0
黑客發起攻擊時,先呼叫一次SSTORE把分配引數從0更改為100,進入Fresh狀態,收取20000 gas。然後在fallback函式中再次把分配引數從100更改為0,此時會進入Dirty狀態, 只會收取200 gas ,並退還19800 gas。這一數值遠遠低於1600 gas,因此黑客就可以成功地發起重入攻擊。
4.如何重現這個漏洞
ChainSecurity在github上公開了攻擊示例程式碼: https://github.com/ChainSecurity/constantinople-reentrancy
可以在Ganache上模擬測試攻擊過程:
ganache-cli --hardfork=constantinople truffle test
可以看到正常呼叫後餘額幾乎沒有什麼變化,而發起重入攻擊後賬戶增加了1 ETH:

據稱,目前為止還沒有發現合約因為該漏洞而造成損失,但是這顯然是一個極大的隱患。幸好,該漏洞在君士坦丁堡升級之前被發現,以太坊團隊決定推遲升級時間,從這一點也可以看出專案社群化運營的巨大力量~
更多文章歡迎關注“鑫鑫點燈”專欄: https://blog.csdn.net/turkeycock
或關注飛久微信公眾號: