solidity智能合约基础漏洞——整数溢出漏洞

 

0x01 溢出攻击事件

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随即急剧贬值,价值几乎为0,该市场瞬间土崩瓦解。

2018年4月25日,SMT项目方发现其交易存在异常,黑客利用其函数漏洞创造了:

65,133,050,195,990,400,000,000,000,000,000,000,000,000,000,000,000,000,000,000+50,659,039,041,325,800,000,000,000,000,000,000,000,000,000,000,000,000,000,000 的SMT币,火币Pro随即暂停了所有币种的充值提取业务。

2018年12月27日,以太坊智能合约Fountain(FNT)出现整数溢出漏洞,黑客利用其函数漏洞创造了:

2+115792089237316195423570985008687907853269984665640564039457584007913129639935 的SMT币。

历史的血泪教训,如今不该再次出现。让我们一起缅怀这些一夜归零的代币,吸取前人经验教训。

 

0x02 整数溢出简介

  • 整数溢出原理

由于计算机底层是二进制,任何十进制数字都会被编码到二进制。溢出会丢弃最高位,导致数值不正确。

如:八位无符号整数类型的最大值是 255,翻译到二进制是 1111 1111;当再加一时,当前所有的 1 都会变成 0,并向上进位。但由于该整数类型所能容纳的位置已经全部是 1 了,再向上进位,最高位会被丢弃,于是二进制就变成了 0000 0000

注:有符号的整数类型,其二进制最高位代表正负。所以该类型的正数溢出会变成负数,而不是零。

  • 整数溢出示例(通用编程语言)

编程语言由算数导致的整数溢出漏洞司空见惯,其类型包括如下三种:

• 加法溢出

• 减法溢出

• 乘法溢出

我们先以运行在 JVM 上的 Kotlin 编程语言做加法运算来测试整数溢出为例:

fun main() {
   println(Long.MAX_VALUE + 1) // Long 是有符号的 128 位 Integer 类型
}

程序会打印出 -9223372036854775808,这其实是在编译期就没有防止整数溢出,因为编译器让溢出的代码通过编译了。

当然,也有在编译期严格检查整数溢出的编程语言。如区块链世界最火的 Rust 编程语言:

fn main() {
    dbg!(u128::MAX + 1); // u128 是无符号的 128 位 Integer 类型
}

编译这段代码,你会得到编译错误:

error: this arithmetic operation will overflow
 --> src/main.rs:2:10
  |
2 |     dbg!(u128::MAX + 1);
  |          ^^^^^^^^^^^^^ attempt to compute `u128::MAX + 1_u128`, which would overflow
  |
  = note: `#[deny(arithmetic_overflow)]` on by default

很好,这有效阻止了编译期溢出的问题。那么,如果是运行时呢?我们来读取用户输入试试:

fn main() {
    let mut s = String::new();
    std::io::stdin().read_line(&mut s).unwrap();
    dbg!(s.trim_end().parse::<u8>().unwrap() + 1); // u8 是无符号的 8 位 Integer 类型
}

运行 cargo r,输入:255,得到 panic:

thread 'main' panicked at 'attempt to add with overflow'

可以看到,在 debug 模式下,溢出会直接 panic,也就是:程序崩溃掉、停止工作。那么,release 模式下也是这样吗?

运行 cargo r —release,输入:255,打印:

[src/main.rs:4] s.trim_end().parse::<u8>().unwrap() + 1 = 0

综上,我们得到一条结论:即使在编译期严格检查溢出的程序语言,依然会有整数溢出问题。整数溢出就好像是一个魔咒,总会隔三岔五地出现,无法一劳永逸地消除。

  • 智能合约中的整数溢出(Solidity 语言)

在区块链的世界里,智能合约的 Solidity 语言中,对于 0.8.0 以下的版本,也存在整数溢出问题。

和通用型编程语言一样,我们先看看编译期是否会发生溢出:

实测,测试函数会直接发生编译错误。再来看看运行时:

实测,程序会在运行时溢出。我们建议使用 SafeMath 库来解决漏洞溢出:

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) {
    uint256 c = a / b;
    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;
  }
}

对于 Solidity 0.8.0 以上的版本,官方已经修复了这个问题。那么它到底是如何修复的?将要溢出时会发生什么来防止溢出呢?

实测,Solidity 0.8 以上的版本发生运行时溢出会直接 revert。

原来,修复的方式就是不允许溢出。int256 足够大,只要保证无法被黑客利用这一点凭空创造收益,我们就成功了。

 

0x03 漏洞合约、攻击手法

以 BEC 合约为例,合约地址为:

0xC5d105E63711398aF9bbff092d4B6769C82F793D

在 etherscan 上的地址为:

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

存在溢出漏洞的合约代码如下:

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;
  }

当时的合约版本是 ^0.4.16,小于 0.8 版本,也没有使用 SafeMath 库,因此存在整数溢出问题。

黑客传入了一个极大的值(这里为2**255),通过乘法向上溢出,使得 amount(要转的总币数)溢出后变为一个很小的数字或者0(这里变成0),从而绕过 balances[msg.sender] >= amount 的检查代码,使得巨大 _value 数额的恶意转账得以成功。

实际攻击的恶意转账记录:

https://etherscan.io/tx/0xad89ff16fd1ebe3a0a7cf4ed282302c06626c1af33221ebe0d3a470aba4a660f

 

0x04 总结

在 Solidity 0.8 版本以下,且未使用 SafeMath 库的情况下:黑客往往会利用溢出构造一个极小值/极大值,从而绕过某些检查,使巨额恶意转账得以成功。

当然,合约漏洞不仅仅只有整数溢出。除了开发者自身提高安全开发意识外,寻找专业的安全团队对合约进行全面的审计也是非常有必要的。

(完)