共计 2445 个字符,预计需要花费 7 分钟才能阅读完成。
算术溢出(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 函数。咱们先来剖析这两个函数内参数可影响的范畴再来决定如何发动攻打:
- deposit 函数存在两个运算操作,第一个是影响用户存入的余额 balances 的,这里传入的参数是可控的所以这里会有溢出的危险,另一个是影响用户的锁定工夫 lockTime 的,然而这里的运算逻辑是每次调用 deposit 存入代币时会给 lockTime 减少一周,因为这里的参数不可控所以这个运算不会存在溢出危险。
- increaseLockTime 函数是依据用户传入的 _secondsToIncrease 参数来进行运算从而扭转用户的存入代币的锁定工夫的,因为这里的 _secondsToIncrease 参数是可控的,所以这里有溢出的危险。
综上所述,咱们发现可利用的参数有两个,别离为 deposit 函数中的 balances 参数和 increaseLockTime 函数中的 _secondsToIncrease 参数。
咱们先来看 balances 参数,如果要让这个参数溢出咱们须要有足够的资金存入才能够(须要 2^256 个代币存入能力导致 balances 溢出并归零),如果要利用这个溢出破绽的话,咱们把大量资金存入本人的账户并让本人的账户的 balances 溢出并归零从而清空本人的资产,我感觉在坐的各位没有人会这么做吧。所以这个参数能够认为在攻击者的角度是不可用的。
咱们再看 _secondsToIncrease 参数,这个参数是咱们调用 increaseLockTime 函数来减少存储工夫时传入的,这个参数能够决定咱们什么时候能够将本人存入并锁定的代币从合约中取出,咱们能够看到这个参数在传入之后是间接与账户对应的锁定工夫 lockTime 进行运算的,如果咱们操纵 _secondsToIncrease 参数让他在与 lockTime 进行运算后失去的后果产生溢出并归零的话这样咱们是不是就能够在存储日期到期前将本人账户中的余额取出了呢?
攻打合约
上面咱们来看看攻打合约:
这里咱们将应用 Attack 攻打合约先存入以太后利用合约的溢出破绽在存储未到期的状况下提取咱们在刚刚 TimeLock 合约中存入并锁定的以太:
- 首先部署 TimeLock 合约;
- 再部署 Attack 合约并在构造函数中传入 TimeLock 合约的地址;
- 调用 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;
- 这时 Attack.attack 再调用 TimeLock. withdraw 函数将胜利通过 block.timestamp > lockTime[msg.sender] 这项查看让咱们可能在存储工夫未到期的状况下胜利提前取出咱们刚刚在 TimeLock 合约中存入并锁定的那个以太。
上面是攻打流程图:
修复倡议接下来,咱们来说说如何修复这些破绽?很显著地,避免数据数值溢出就能修复这些破绽了,那么我就给大家一些避免数据数值溢出的倡议吧!
1. 应用 Solidity 0.8 及以上版本来开发合约,这里还有一点:须要慎用 unchecked,因为在 unchecked 润饰的代码块外面是不会对参数进行溢出检查的;
2. 应用 SafeMath 办法库,SafeMath 只提供简略的四则运算办法,然而在计算溢出时,它会抛出谬误;
除此之外,作为一名合约编写者,还须要慎用变量类型强制转换,因为不同的类型,其数值范畴是不同的,类型强制转换有可能导致数值溢出。
如果想理解更多的智能合约和区块链常识,欢送到区块链交换社区 CHAINPIP 社区,一起交流学习~ 社区地址:https://www.chainpip.com/