算術(shù)溢出(arithmetic overflow)或簡稱為溢出(overflow)分為兩種:上溢和下溢。所謂上溢是指在運行單項數(shù)值計算時,當(dāng)計算產(chǎn)生出來的結(jié)果非常大月培,大于寄存器或存儲器所能存儲或表示的能力限制就會產(chǎn)生上溢;
而下溢就是當(dāng)計算產(chǎn)生出來的結(jié)果非常小,小于寄存器或存儲器所能存儲或表示的能力限制就會產(chǎn)生下溢瞭稼。舉個例子:
在solidity 中,uint8 所能表示的范圍是0 - 255這256個數(shù)腻惠。
如果一個合約有溢出漏洞的話會導(dǎo)致計算的實際結(jié)果和預(yù)期的結(jié)果產(chǎn)生非常大的差異环肘,這樣輕則會影響合約的正常邏輯,重則會導(dǎo)致合約中的資金丟失妖枚。但是溢出漏洞是存在版本限制的廷臼,在Solidity < 0.8 時溢出不會報錯,當(dāng) Solidity >= 0.8 時溢出會報錯绝页。所以當(dāng)我們看到 0.8 版本以下的合約時荠商,就要注意這個合約可能出現(xiàn)溢出問題。
漏洞示例
有了以上的講解续誉,相信大家對溢出漏洞都有一定的了解莱没,下面我們來結(jié)合合約代碼來深入了解溢出漏洞:
漏洞分析
TimeLock 合約充當(dāng)了時間保險庫,用戶可以將代幣通過deposit 函數(shù)存入該合約并鎖定酷鸦,且至少一周內(nèi)不能提現(xiàn)饰躲。當(dāng)然用戶也可以通過 increaseLockTime 函數(shù)來增加存儲時間,用戶在設(shè)定的存儲期限到期前是無法提取 TimeLock 合約中鎖定的代幣的臼隔。
首先我們發(fā)現(xiàn)這個合約中的increaseLockTime 函數(shù)和 deposit 函數(shù)具有運算功能嘹裂,并且合約支持的版本是:0.7.6 向上兼容,所以這個合約在算數(shù)溢出時是不會報錯的摔握,那么我們就可以判斷這個合約是可能存在溢出漏洞的寄狼,這里可利用的函數(shù)有兩個,一個是increaseLockTime 函數(shù)氨淌,一個是 deposit 函數(shù)泊愧。我們先來分析這兩個函數(shù)內(nèi)參數(shù)可影響的范圍再來決定如何發(fā)起攻擊:
1. deposit 函數(shù)存在兩個運算操作,第一個是影響用戶存入的余額 balances 的盛正,這里傳入的參數(shù)是可控的所以這里會有溢出的風(fēng)險删咱,另一個是影響用戶的鎖定時間 lockTime 的,但是這里的運算邏輯是每次調(diào)用 deposit 存入代幣時會給 lockTime 增加一周豪筝,由于這里的參數(shù)不可控所以這個運算不會存在溢出風(fēng)險痰滋。
2. increaseLockTime 函數(shù)是根據(jù)用戶傳入的 _secondsToIncrease 參數(shù)來進行運算從而改變用戶的存入代幣的鎖定時間的摘能,由于這里的 _secondsToIncrease 參數(shù)是可控的,所以這里有溢出的風(fēng)險即寡。
綜上所述徊哑,我們發(fā)現(xiàn)可利用的參數(shù)有兩個,分別為deposit 函數(shù)中的 balances 參數(shù)和?increaseLockTime 函數(shù)中的 _secondsToIncrease 參數(shù)聪富。
我們先來看balances?參數(shù)莺丑,如果要讓這個參數(shù)溢出我們需要有足夠的資金存入才可以(需要 2^256 個代幣存入才能導(dǎo)致 balances 溢出并歸零),如果要利用這個溢出漏洞的話墩蔓,我們把大量資金存入自己的賬戶并讓自己的賬戶的 balances 溢出并歸零從而清空自己的資產(chǎn)梢莽,我覺得在坐的各位沒有人會這么做吧。所以這個參數(shù)可以認(rèn)為在攻擊者的角度是不可用的奸披。
我們再看_secondsToIncrease?參數(shù)昏名,這個參數(shù)是我們調(diào)用 increaseLockTime 函數(shù)來增加存儲時間時傳入的,這個參數(shù)可以決定我們什么時候可以將自己存入并鎖定的代幣從合約中取出阵面,我們可以看到這個參數(shù)在傳入之后是直接與賬戶對應(yīng)的鎖定時間 lockTime 進行運算的轻局,如果我們操縱 _secondsToIncrease 參數(shù)讓他在與 lockTime 進行運算后得到的結(jié)果產(chǎn)生溢出并歸零的話這樣我們是不是就可以在存儲日期到期前將自己賬戶中的余額取出了呢?
攻擊合約
下面我們來看看攻擊合約:
這里我們將使用Attack 攻擊合約先存入以太后利用合約的溢出漏洞在存儲未到期的情況下提取我們在剛剛 TimeLock 合約中存入并鎖定的以太:
1. 首先部署 TimeLock 合約样刷;
2. 再部署 Attack 合約并在構(gòu)造函數(shù)中傳入 TimeLock 合約的地址仑扑;
3. 調(diào)用 Attack.attack 函數(shù),Attack.attack 又調(diào)用 TimeLock.deposit 函數(shù)向 TimeLock 合約中存入一個以太(此時這枚以太將被 TimeLock 鎖定一周的時間)置鼻,之后 Attack.attack 又調(diào)用 TimeLock.increaseLockTime 函數(shù)并傳入 uint 類型可表示的最大值(2^256 - 1)加 1 再減去當(dāng)前 TimeLock 合約中記錄的鎖定時間镇饮。此時 TimeLock.increaseLockTime 函數(shù)中的 lockTime 的計算結(jié)果為 2^256 這個值,在 uint256 類型中 2^256 這個數(shù)存在上溢所以計算結(jié)果為 2^256 = 0 此時我們剛剛存入 TimeLock 合約中的一個以太的鎖定時間就變?yōu)?0 箕母;
4. 這時 Attack.attack 再調(diào)用 TimeLock. withdraw 函數(shù)將成功通過 block.timestamp > lockTime[msg.sender] 這項檢查讓我們能夠在存儲時間未到期的情況下成功提前取出我們剛剛在 TimeLock 合約中存入并鎖定的那個以太储藐。
下面是攻擊流程圖:
修復(fù)建議
接下來,我們來說說如何修復(fù)這些漏洞嘶是?很明顯地钙勃,防止數(shù)據(jù)數(shù)值溢出就能修復(fù)這些漏洞了,那么我就給大家一些防止數(shù)據(jù)數(shù)值溢出的建議吧聂喇!
1. 使用Solidity 0.8 及以上版本來開發(fā)合約辖源,這里還有一點:需要慎用unchecked,因為在unchecked 修飾的代碼塊里面是不會對參數(shù)進行溢出檢查的授帕;
2. 使用SafeMath方法庫同木,SafeMath只提供簡單的四則運算方法浮梢,但是在計算溢出時跛十,它會拋出錯誤;
除此之外秕硝,作為一名合約編寫者芥映,還需要慎用變量類型強制轉(zhuǎn)換,因為不同的類型,其數(shù)值范圍是不同的奈偏,類型強制轉(zhuǎn)換有可能導(dǎo)致數(shù)值溢出坞嘀。
如果想了解更多的智能合約和區(qū)塊鏈知識,歡迎到區(qū)塊鏈交流社區(qū)CHAINPIP社區(qū)惊来,一起交流學(xué)習(xí)~
社區(qū)地址:https://www.chainpip.com/