算術(shù)溢出(arithmetic overflow)或簡(jiǎn)稱為溢出(overflow)分為兩種:上溢和下溢。所謂上溢是指在運(yùn)行單項(xiàng)數(shù)值計(jì)算時(shí),當(dāng)計(jì)算產(chǎn)生出來(lái)的結(jié)果非常大,大于寄存器或存儲(chǔ)器所能存儲(chǔ)或表示的能力限制就會(huì)產(chǎn)生上溢;
而下溢就是當(dāng)計(jì)算產(chǎn)生出來(lái)的結(jié)果非常小,小于寄存器或存儲(chǔ)器所能存儲(chǔ)或表示的能力限制就會(huì)產(chǎn)生下溢。舉個(gè)例子:
在solidity 中,uint8 所能表示的范圍是0 - 255這256個(gè)數(shù)。

如果一個(gè)合約有溢出漏洞的話會(huì)導(dǎo)致計(jì)算的實(shí)際結(jié)果和預(yù)期的結(jié)果產(chǎn)生非常大的差異,這樣輕則會(huì)影響合約的正常邏輯,重則會(huì)導(dǎo)致合約中的資金丟失。但是溢出漏洞是存在版本限制的,在Solidity < 0.8 時(shí)溢出不會(huì)報(bào)錯(cuò),當(dāng) Solidity >= 0.8 時(shí)溢出會(huì)報(bào)錯(cuò)。所以當(dāng)我們看到 0.8 版本以下的合約時(shí),就要注意這個(gè)合約可能出現(xiàn)溢出問(wèn)題。
漏洞示例
有了以上的講解,相信大家對(duì)溢出漏洞都有一定的了解,下面我們來(lái)結(jié)合合約代碼來(lái)深入了解溢出漏洞:

漏洞分析
TimeLock 合約充當(dāng)了時(shí)間保險(xiǎn)庫(kù),用戶可以將代幣通過(guò)deposit 函數(shù)存入該合約并鎖定,且至少一周內(nèi)不能提現(xiàn)。當(dāng)然用戶也可以通過(guò) increaseLockTime 函數(shù)來(lái)增加存儲(chǔ)時(shí)間,用戶在設(shè)定的存儲(chǔ)期限到期前是無(wú)法提取 TimeLock 合約中鎖定的代幣的。
首先我們發(fā)現(xiàn)這個(gè)合約中的increaseLockTime 函數(shù)和 deposit 函數(shù)具有運(yùn)算功能,并且合約支持的版本是:0.7.6 向上兼容,所以這個(gè)合約在算數(shù)溢出時(shí)是不會(huì)報(bào)錯(cuò)的,那么我們就可以判斷這個(gè)合約是可能存在溢出漏洞的,這里可利用的函數(shù)有兩個(gè),一個(gè)是increaseLockTime 函數(shù),一個(gè)是 deposit 函數(shù)。我們先來(lái)分析這兩個(gè)函數(shù)內(nèi)參數(shù)可影響的范圍再來(lái)決定如何發(fā)起攻擊:
1. deposit 函數(shù)存在兩個(gè)運(yùn)算操作,第一個(gè)是影響用戶存入的余額 balances 的,這里傳入的參數(shù)是可控的所以這里會(huì)有溢出的風(fēng)險(xiǎn),另一個(gè)是影響用戶的鎖定時(shí)間 lockTime 的,但是這里的運(yùn)算邏輯是每次調(diào)用 deposit 存入代幣時(shí)會(huì)給 lockTime 增加一周,由于這里的參數(shù)不可控所以這個(gè)運(yùn)算不會(huì)存在溢出風(fēng)險(xiǎn)。
2. increaseLockTime 函數(shù)是根據(jù)用戶傳入的 _secondsToIncrease 參數(shù)來(lái)進(jìn)行運(yùn)算從而改變用戶的存入代幣的鎖定時(shí)間的,由于這里的 _secondsToIncrease 參數(shù)是可控的,所以這里有溢出的風(fēng)險(xiǎn)。
綜上所述,我們發(fā)現(xiàn)可利用的參數(shù)有兩個(gè),分別為deposit 函數(shù)中的 balances 參數(shù)和?increaseLockTime 函數(shù)中的 _secondsToIncrease 參數(shù)。
我們先來(lái)看balances?參數(shù),如果要讓這個(gè)參數(shù)溢出我們需要有足夠的資金存入才可以(需要 2^256 個(gè)代幣存入才能導(dǎo)致 balances 溢出并歸零),如果要利用這個(gè)溢出漏洞的話,我們把大量資金存入自己的賬戶并讓自己的賬戶的 balances 溢出并歸零從而清空自己的資產(chǎn),我覺(jué)得在坐的各位沒(méi)有人會(huì)這么做吧。所以這個(gè)參數(shù)可以認(rèn)為在攻擊者的角度是不可用的。
我們?cè)倏?b>_secondsToIncrease?參數(shù),這個(gè)參數(shù)是我們調(diào)用 increaseLockTime 函數(shù)來(lái)增加存儲(chǔ)時(shí)間時(shí)傳入的,這個(gè)參數(shù)可以決定我們什么時(shí)候可以將自己存入并鎖定的代幣從合約中取出,我們可以看到這個(gè)參數(shù)在傳入之后是直接與賬戶對(duì)應(yīng)的鎖定時(shí)間 lockTime 進(jìn)行運(yùn)算的,如果我們操縱 _secondsToIncrease 參數(shù)讓他在與 lockTime 進(jìn)行運(yùn)算后得到的結(jié)果產(chǎn)生溢出并歸零的話這樣我們是不是就可以在存儲(chǔ)日期到期前將自己賬戶中的余額取出了呢?
攻擊合約
下面我們來(lái)看看攻擊合約:

這里我們將使用Attack 攻擊合約先存入以太后利用合約的溢出漏洞在存儲(chǔ)未到期的情況下提取我們?cè)趧倓?TimeLock 合約中存入并鎖定的以太:
1. 首先部署 TimeLock 合約;
2. 再部署 Attack 合約并在構(gòu)造函數(shù)中傳入 TimeLock 合約的地址;
3. 調(diào)用 Attack.attack 函數(shù),Attack.attack 又調(diào)用 TimeLock.deposit 函數(shù)向 TimeLock 合約中存入一個(gè)以太(此時(shí)這枚以太將被 TimeLock 鎖定一周的時(shí)間),之后 Attack.attack 又調(diào)用 TimeLock.increaseLockTime 函數(shù)并傳入 uint 類型可表示的最大值(2^256 - 1)加 1 再減去當(dāng)前 TimeLock 合約中記錄的鎖定時(shí)間。此時(shí) TimeLock.increaseLockTime 函數(shù)中的 lockTime 的計(jì)算結(jié)果為 2^256 這個(gè)值,在 uint256 類型中 2^256 這個(gè)數(shù)存在上溢所以計(jì)算結(jié)果為 2^256 = 0 此時(shí)我們剛剛存入 TimeLock 合約中的一個(gè)以太的鎖定時(shí)間就變?yōu)?0 ;
4. 這時(shí) Attack.attack 再調(diào)用 TimeLock. withdraw 函數(shù)將成功通過(guò) block.timestamp > lockTime[msg.sender] 這項(xiàng)檢查讓我們能夠在存儲(chǔ)時(shí)間未到期的情況下成功提前取出我們剛剛在 TimeLock 合約中存入并鎖定的那個(gè)以太。
下面是攻擊流程圖:

修復(fù)建議
接下來(lái),我們來(lái)說(shuō)說(shuō)如何修復(fù)這些漏洞?很明顯地,防止數(shù)據(jù)數(shù)值溢出就能修復(fù)這些漏洞了,那么我就給大家一些防止數(shù)據(jù)數(shù)值溢出的建議吧!
1. 使用Solidity 0.8 及以上版本來(lái)開(kāi)發(fā)合約,這里還有一點(diǎn):需要慎用unchecked,因?yàn)樵趗nchecked 修飾的代碼塊里面是不會(huì)對(duì)參數(shù)進(jìn)行溢出檢查的;
2. 使用SafeMath方法庫(kù),SafeMath只提供簡(jiǎn)單的四則運(yùn)算方法,但是在計(jì)算溢出時(shí),它會(huì)拋出錯(cuò)誤;
除此之外,作為一名合約編寫者,還需要慎用變量類型強(qiáng)制轉(zhuǎn)換,因?yàn)椴煌念愋?,其?shù)值范圍是不同的,類型強(qiáng)制轉(zhuǎn)換有可能導(dǎo)致數(shù)值溢出。
如果想了解更多的智能合約和區(qū)塊鏈知識(shí),歡迎到區(qū)塊鏈交流社區(qū)CHAINPIP社區(qū),一起交流學(xué)習(xí)~
社區(qū)地址:https://www.chainpip.com/