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 漏洞简介

在编程语言里面,因为算术运算导致的整数溢出漏洞屡见不鲜

在solidity语言中,变量支持的类型从uint8-uint256,int8-int256。每一个整型变量只能存储固定大小数值范围内的数。uint表示无符号数。比如uint8只能储存0->2^8-1范围内的数字。当一个uint8类型的变量值为255时,在进行加一,就会发生进位,导致整体翻转为零。

简单来说,就是Solidity整形变量被赋值高于或者低于可以表示的范围时 值会发生改变 一般会溢出为2的uint类型次方 -1 或者 0

  • 上溢:会溢出为0
  • 下溢:会溢出为2^n-1

根据运算形式又可以分为

  • 加法溢出
  • 乘法溢出
  • 减法溢出

0x03 简单演示

0x01加法溢出

在本地JavaScript VM 部署之后可以查看max与_overflow的值

如果uint8 类型的变量达到了它的最大值(2^8 - 1),如果在加上一个大于0的值便会变成0

可以看到max+1=256.超出了uint8能表示的范围,导致发生了加法上溢。

0x02乘法溢出

在本地JavaScript VM 部署之后可以查看max与_overflow的值

如果uint8 类型的变量超过了它的最大值(2^8 - 1),最后它的值就会回绕变成0

可以看到max*2=256.超出了uint8能表示的范围,导致发生了乘法上溢。

0x03减法溢出

在本地JavaScript VM 部署之后可以查看min与_overflow的值

如果uint8 类型的变量达到了它的最小值(0),如果在减去一个大于0的值便会变成2^8-1(uin8类型的最大值)

可以看到min-1=-1.超出了uint8能表示的范围,导致发生了减法下溢。

0x04 案例分析

下面将分别从三个案例分别分析加法,乘法,减法的整数溢出

0x01 SMT

在etherscan上的地址为:https://etherscan.io/address/0x55f93985431fc9304077687a35a1ba103dc1e081#code

攻击记录:https://etherscan.io/tx/0x1abab4c8db9a30e703114528e31dee129a3a758f7f8abc3b6494aad3d304e43f

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

function transferProxy(address _from, address _to, uint256 _value, uint256 _feeSmt,
        uint8 _v,bytes32 _r, bytes32 _s) public transferAllowed(_from) returns (bool){

        if(balances[_from] < _feeSmt + _value) revert(); //溢出点,这里存在整数加法溢出

        uint256 nonce = nonces[_from];
        bytes32 h = keccak256(_from,_to,_value,_feeSmt,nonce);
        if(_from != ecrecover(h,_v,_r,_s)) revert();

        if(balances[_to] + _value < balances[_to]
            || balances[msg.sender] + _feeSmt < balances[msg.sender]) revert();
        balances[_to] += _value;
        Transfer(_from, _to, _value);

        balances[msg.sender] += _feeSmt;
        Transfer(_from, msg.sender, _feeSmt);

        balances[_from] -= _value + _feeSmt;
        nonces[_from] = nonce + 1;
        return true;
    }

函数分析:

  • 实现可以签名的转账功能

  • 判断发送者的balances是否小于fee与value的和,如果小于就revert,就是如果发送者的balances大fee与value的和就继续往下执行。这里因为_value,_feeSmt两个参数可控,可以构造两个大叔造成加法溢出

  • ecrecover是对函数的签名进行验证
  • 判断_value与_feeSmt是否为零
  • 执行转账,同时改变转账账户和接收账户的余额,交易次数加一

分析交易详情

Function: transferProxy(address _from, address _to, uint256 _value, uint256 _feeSmt, uint8 _v, bytes32 _r, bytes32 _s)

MethodID: 0xeb502d45
[0]:  000000000000000000000000df31a499a5a8358b74564f1e2214b31bb34eb46f
[1]:  000000000000000000000000df31a499a5a8358b74564f1e2214b31bb34eb46f
[2]:  8fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
[3]:  7000000000000000000000000000000000000000000000000000000000000001
[4]:  000000000000000000000000000000000000000000000000000000000000001b
[5]:  87790587c256045860b8fe624e5807a658424fad18c2348460e40ecf10fc8799
[6]:  6c879b1e8a0a62f23b47aa57a3369d416dd783966bd1dda0394c04163a98d8d8

可以明显的看到_value, _feeSmt两个都很大,相加起来等于

0x10000000000000000000000000000000000000000000000000000000000000000

超出了uint256能储存的范围,产生了上溢,导致_feeSmt + _value变成了0,从而绕过了余额的检查,导致了恶意转账的发生

0x8fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff=
65133050195990359925758679067386948167464366374422817272194891004451135422463
0x7000000000000000000000000000000000000000000000000000000000000001=
50659039041325835497812305941300959685805618291217746767262693003461994217473

发生的转账

当时的市价约为0.7元.....然后那哥们对着UGT又玩了一次。

0x02 EBC

在etherscan上的地址为:https://etherscan.io/address/0xc5d105e63711398af9bbff092d4b6769c82f793d#code

攻击记录:https://etherscan.io/tx/0xad89ff16fd1ebe3a0a7cf4ed282302c06626c1af33221ebe0d3a470aba4a660f

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

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

函数分析:

  • 实现的是批量转账功能,接收的参数是地址数组和转账金额
  • _value由用户控制,可以实现cnt * _value > 2^256 - 1使得 amount置零
  • 使用require语句对地址数组和账户余额进行判断
  • 通过上述判断之后,对地址数组里面的地址转账

分析交易详情

Function: batchTransfer(address[] _receivers, uint256 _value)

MethodID: 0x83f12fec
[0]:  0000000000000000000000000000000000000000000000000000000000000040
[1]:  8000000000000000000000000000000000000000000000000000000000000000
[2]:  0000000000000000000000000000000000000000000000000000000000000002
[3]:  000000000000000000000000b4d30cac5124b46c2df0cf3e3e1be05f42119033
[4]:  0000000000000000000000000e823ffe018727585eaf5bc769fa80472f76c3d7

这里涉及到区块链的参数编码,可以参考PIKACHU师傅的文章

可以看到此时的

_receivers.length=2
_value=8000000000000000000000000000000000000000000000000000000000000000

两者相乘得到2^256,超出uint256能储存的范围,产生了上溢,导致amount变成了零,从而绕过了第二个语句对账户余额的判断。

发生的转账

0x03 BTCR

在etherscan上的地址为:https://etherscan.io/address/0x6aac8cb9861e42bf8259f5abdc6ae3ae89909e11#code

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

function distributeBTR(address[] addresses) onlyOwner {
    for (uint i = 0; i < addresses.length; i++) {
        balances[owner] -= 2000 * 10**8;
        balances[addresses[i]] += 2000 * 10**8;
        Transfer(owner, addresses[i], 2000 * 10**8);
    }
}

函数分析:

  • 实现代币的批量分配,但是只能转账固定的数额
  • 每分配一次,就减去相应的数值

攻击分析:

由于合约部署的时候将onlyOwner设置为合约人的账户地址,所以该漏洞只有Owner可以利用

  • 因为没有判断Owner的账户是否有足够的余额,所以导致了减法的整型下溢出
  • 在部署时,balances[owner] = 21000000 * 10^8,也就是说最多执行10500次Transfer()就会产生下溢出
  • 这样会导致改代币严重的供给关系失衡,导致代币市值严重下跌

0x05 防御方式

0x01 算术运算前后验证

  • 加法运算的和一定大于加数和被加数
  • 乘法运算的积一定大于乘数和被乘数
  • 减法运算的差一定小于两者的和,或者至少小于其中一个

0x02 SafeMath

SafeMath时OpenZeppelin 维护的一套智能合约函数库中用来处理算术逻辑的函数库

pragma solidity ^0.4.25;

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

再次演示,为了方便展示结果,使用uint8

contract OverFlow {
    using SafeMath for uint8;
    //加法溢出
    function add_overflow() returns (uint8 _overflow) {
        uint8 max =2**8 - 1;
        return max.add(1);
    }

     //乘法溢出
    function mul_overflow() returns (uint8 _underflow) {
        uint8 mul = 2**7;
        return mul.mul(2);
    }

    //减法溢出

    function sub_underflow() returns (uint8 _underflow) {
        uint8 min = 0;
        return min.sub(1);
    }
}

部署成功后,此时不论调用哪个函数都会报错。

0x06 总结

开发智能合约时,如果不严格检查用户的输入的话,会将用户的输入带入执行计算,这就有可能带来安全风险。同时在前面案例分析中,SMT和EBC合约源码中有使用SafeMth,但是在运算忘记添加,导致了漏洞的发生。由于区块链的不可篡改性质,一旦部署上链的合约无法进行漏洞修复。虽然可以通过拉黑ETH地址的方式阻止攻击者提现,但是是在攻击发现后,依旧无法阻止在未发现攻击行为之前攻击者短时间的大额提现。

点击收藏 | 1 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖