算術溢出(arithmetic overflow)或簡稱為溢出(overflow)分為兩種:上溢和下溢。所謂上溢是指在運行單項數值計算時,當計算產生出來的結果非常大,大於寄存器或存儲器所能存儲或表示的能力限制就會產生上溢;
而下溢就是當計算產生出來的結果非常小,小於寄存器或存儲器所能存儲或表示的能力限制就會產生下溢。舉個例子:
在solidity 中,uint8 所能表示的範圍是0 - 255這256個數。
如果一個合約有溢出漏洞的話會導致計算的實際結果和預期的結果產生非常大的差異,這樣輕則會影響合約的正常邏輯,重則會導致合約中的資金丟失。但是溢出漏洞是存在版本限制的,在Solidity < 0.8 時溢出不會報錯,當Solidity >= 0.8 時溢出會報錯。所以當我們看到0.8 版本以下的合約時,就要注意這個合約可能出現溢出問題。
漏洞示例
有了以上的講解,相信大家對溢出漏洞都有一定的了解,下面我們來結合合約代碼來深入了解溢出漏洞:
漏洞分析
TimeLock 合約充當了時間保險庫,用戶可以將代幣通過deposit 函數存入該合約並鎖定,且至少一周內不能提現。當然用戶也可以通過increaseLockTime 函數來增加存儲時間,用戶在設定的存儲期限到期前是無法提取TimeLock 合約中鎖定的代幣的。
首先我們發現這個合約中的increaseLockTime 函數和deposit 函數具有運算功能,並且合約支持的版本是:0.7.6 向上兼容,所以這個合約在算數溢出時是不會報錯的,那麼我們就可以判斷這個合約是可能存在溢出漏洞的,這裡可利用的函數有兩個,一個是increaseLockTime 函數,一個是deposit 函數。我們先來分析這兩個函數內參數可影響的範圍再來決定如何發起攻擊:
1. deposit 函數存在兩個運算操作,第一個是影響用戶存入的餘額balances 的,這里傳入的參數是可控的所以這裡會有溢出的風險,另一個是影響用戶的鎖定時間lockTime 的,但是這裡的運算邏輯是每次調用deposit 存入代幣時會給lockTime 增加一周,由於這裡的參數不可控所以這個運算不會存在溢出風險。
2. increaseLockTime 函數是根據用戶傳入的_secondsToIncrease 參數來進行運算從而改變用戶的存入代幣的鎖定時間的,由於這裡的_secondsToIncrease 參數是可控的,所以這裡有溢出的風險。
綜上所述,我們發現可利用的參數有兩個,分別為deposit 函數中的balances 參數和increaseLockTime 函數中的_secondsToIncrease 參數。
我們先來看balances參數,如果要讓這個參數溢出我們需要有足夠的資金存入才可以(需要2^256 個代幣存入才能導致balances 溢出並歸零),如果要利用這個溢出漏洞的話,我們把大量資金存入自己的賬戶並讓自己的賬戶的balances 溢出並歸零從而清空自己的資產,我覺得在坐的各位沒有人會這麼做吧。所以這個參數可以認為在攻擊者的角度是不可用的。
我們再看_secondsToIncrease參數,這個參數是我們調用increaseLockTime 函數來增加存儲時間時傳入的,這個參數可以決定我們什麼時候可以將自己存入並鎖定的代幣從合約中取出,我們可以看到這個參數在傳入之後是直接與賬戶對應的鎖定時間lockTime 進行運算的,如果我們操縱_secondsToIncrease 參數讓他在與lockTime 進行運算後得到的結果產生溢出並歸零的話這樣我們是不是就可以在存儲日期到期前將自己賬戶中的餘額取出了呢?
攻擊合約
下面我們來看看攻擊合約:
這裡我們將使用Attack 攻擊合約先存入以太后利用合約的溢出漏洞在存儲未到期的情況下提取我們在剛剛TimeLock 合約中存入並鎖定的以太:
1. 首先部署TimeLock 合約;
2. 再部署Attack 合約並在構造函數中傳入TimeLock 合約的地址;
3. 調用Attack.attack 函數,Attack.attack 又調用TimeLock.deposit 函數向TimeLock 合約中存入一個以太(此時這枚以太將被TimeLock 鎖定一周的時間),之後Attack.attack 又調用TimeLock.increaseLockTime 函數並傳入uint 類型可表示的最大值(2^256 - 1)加1 再減去當前TimeLock 合約中記錄的鎖定時間。此時TimeLock.increaseLockTime 函數中的lockTime 的計算結果為2^256 這個值,在uint256 類型中2^256 這個數存在上溢所以計算結果為2^256 = 0 此時我們剛剛存入TimeLock 合約中的一個以太的鎖定時間就變為0 ;
4. 這時Attack.attack 再調用TimeLock. withdraw 函數將成功通過block.timestamp > lockTime[msg.sender] 這項檢查讓我們能夠在存儲時間未到期的情況下成功提前取出我們剛剛在TimeLock 合約中存入並鎖定的那個以太。
下面是攻擊流程圖:
修復建議
接下來,我們來說說如何修復這些漏洞?很明顯地,防止數據數值溢出就能修復這些漏洞了,那麼我就給大家一些防止數據數值溢出的建議吧!
1. 使用Solidity 0.8 及以上版本來開發合約,這裡還有一點:需要慎用unchecked,因為在unchecked 修飾的代碼塊裡面是不會對參數進行溢出檢查的;
2. 使用SafeMath方法庫,SafeMath只提供簡單的四則運算方法,但是在計算溢出時,它會拋出錯誤;
除此之外,作為一名合約編寫者,還需要慎用變量類型強制轉換,因為不同的類型,其數值範圍是不同的,類型強制轉換有可能導致數值溢出。
如果想了解更多的智能合約和區塊鏈知識,歡迎到區塊鏈交流社區CHAINPIP社區,一起交流學習~
社區地址:https://www.chainpip.com/