5

零时科技|solidity智能合约基础漏洞——整数溢出漏洞

 2 years ago
source link: https://learnblockchain.cn/article/3599
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

零时科技|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 库的情况下:黑客往往会利用溢出构造一个极小值/极大值,从而绕过某些检查,使巨额恶意转账得以成功。

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

本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

  • 发表于 2天前
  • 阅读 ( 108 )
  • 学分 ( 5 )
  • 分类:智能合约

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK