研究区块链方向的安全也有一段时间了,还没在博客记录过关于区块链安全方面的东西,所以这次分享一个还算比较有趣的区块链的漏洞吧,有什么地方理解出现了偏差还请师傅们指出。

这个漏洞有趣在哪里呢?第一个是他用了以太坊官方的防止整数溢出的库还是出了问题,第二个是转币方不会有任何损失但攻击者会得到一笔很大很大的bec。

废话不多说,贴上代码:

//255-257 line 
 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;
  }

这个函数实现了一个批量给地址转币的功能,参数类型为address类型的数组和uint256_value

溢出点在本函数的第三行

uint256 amount = uint256(cnt) * _value;

定义了amount变量 为cnt * _value_value是转出币的金额,cnt是函数传入address的数量。

_value的类型是uint256,存储范围0-256的256次方-1,如果数值超出此范围,会往上溢出,amount会反转为0。

在BEC代码中amount在进行乘运算时,并没有用SafeMath(官方防止溢出的库) 中的对应的函数防止溢出

但下面加减运算确实都用了官方库的函数来防止溢出,难道是漏掉了乘?想不明白这是什么操作。

回归正题我们走到下面判断

require(cnt > 0 && cnt <= 20);
require(_value > 0 && balances[msg.sender] >= amount);

判断一限制了传入address的数量不为空且不大于过20,

判断二是限制了转出币的数量(_value) 必须大于0 且转币方的余额大于等于amount

调用函数时传入的_value大于0即可,但是amount经过溢出后值为0,不会大于转币方的余额,所以会通过判断。

balances[msg.sender] = balances[msg.sender].sub(amount);

符合判断后进入账户余额加减步骤,转币方余额减amount

但此时amount为0,所以转币方不会有任何损失。

下面用了在循环里用了Transfer函数进行转币

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

首先在循环数组里给被转账方余额中加上传入的_value然后用了Transfer事件来记录转币,_value是直接在函数中传入的,通过上述的判断后直接在余额中加上传入的值。

我们用IDE复现一下漏洞:

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

Copy官方的代码到IDE中,使用第三个地址运行合约

batchTransfer函数中传入参数[“0xca35b7d915458ef540ade6068dfe2f44e8fa733c”,”0x14723a09acff6d2a60dcdf7aa4aff308fddc160c”],57896044618658097711785492504343953926634992332820282019728792003956564819968

receivers是两个地址,_value是数量。为什么要传这个数呢?因为代码里中是这样的:

uint cnt = _receivers.length;
uint256 amount = uint256(cnt) * _value;

cntreceivers的数量,amount也就是

2 * 57896044618658097711785492504343953926634992332820282019728792003956564819968

结果为:115792089237316195423570985008687907853269984665640564039457584007913129639936

uint256最大范围是:

115792089237316195423570985008687907853269984665640564039457584007913129639935

刚刚好触发溢出。

交易成功完成,被转账方的余额很多很多

如何修复

当然用官方库里的函数就ok

  function batchTransfer(address[] _receivers, uint256 _value) public whenNotPaused returns (bool) {
    uint cnt = _receivers.length;
    uint256 amount = uint256(cnt).mul(_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;
  }
}

我们来看下库中的mul函数

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

此函数很好理解,用assert做了预先处理,如果不成立会直接抛出异常代码停止执行,有效的防止了溢出