智能合约安全事故回顾(2)-BEC溢出攻击
严格的说“千年虫”属于程序的一个BUG。因为在上个世纪,计算机的存储空间很小,使用人员为了最大化利用计算机的存储空间,规定了在计算机中存储年份的时候使用两位数字来表示,如“1998”年,那么计算机中的存储值则为“98”。当千禧年来临的时候,这个值就会变成“00”,这个时候计算机就不知道这个“00”代表的是1900年还是2000年,所以才会出现刚才文章开始的乌龙事件,这也是我们本文要讲述的”溢出“的概念。
事件介绍
2018年4月23日,一款名为BEC的代币被黑客攻击。黑客利用合约内的漏洞,短时间向外部的账户转入了天价的合约代币, 导致该代币价格迅速缩水几乎归零。攻击手法被披露的24小时内,就有三十多个合约被类似手法攻击。
漏洞原因
在solidity语言中,对int类型的数据变量规定了长度,如uint8代表的是无符号的8位整数,即0到255。假如有下面一个简单的合约:
pragma solidity ^0.4.25;
contract test{
function add(uint8 _a) public pure returns(uint8){
return _a+1;
}
}
可以发现传入的参数是一个uint8类型变量,它的范围在0-255,如果输入的值是255,那么返回的结果会是什么呢?有兴趣的读者可以到remix中试一下,返回值会是0,如果输入256,返回结果会是1。造成这样的原因主要跟数据在计算机中的存储有关,计算机只给uint8的类型变量分配了长度为8的空间,最大值为255,如果超过这个值会产生进位之后被截断,导致存储的8位全部都是0,这就造成了整数溢出。
下面看一下BEC合约中的一个函数:
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;
}
这个函数的目的是实现一个批量转账的功能,receivers是接受者的数组,value是转账金额。重点关注
uint256 amount = uint256(cnt) * _value;
这里定义了一个uint256类型的变量amount来接收转账的总金额,后续会通过这个金额的值和用户所发送的金额比较来判断用户是否能够发送这么多的代币。那么重点来了,如果uint256(cnt) * _value的值超过uint256,不就产生了溢出了吗?攻击者通过传递两个账户,__value为2的255次方(实际上是转换成了16进制),2*2^255=2^256完成了溢出,amount的值为0。这个逻辑下的amount能够通过后面的所有校验,最后发送给两个账户的值确是2的255次方的代币。
BEC车祸现场连接:https://etherscan.io/tx/0xad89ff16fd1ebe3a0a7cf4ed282302c06626c1af33221ebe0d3a470aba4a660f。
防范
跟攻击手段一样,针对BEC的溢出攻击的防范也非常简单。我们可以利用safeMath库来避免这种情况。把 uint256(cnt) * _value改成 uint256(cnt) .mul(value)即可。相信稍微有点solidity编程经验的读者都能知道怎么做。BEC遭受的整数溢出的攻击原理非常简单,但是在攻击手段被披露的24小时内就有30多个合约被攻击,这也不得不引起我们的重视和思考:合约本身并不具备安全的属性,却动辄承载上千万价值的代币,我们过往对于合约的安全评估是否过于乐观?