最近在币圈很多人讨论的BEC代码漏洞,损失了64亿人民币。那黑客是如何做到的?其实是通过bec代币合约的整型溢出漏洞,让自己的地址凭空产生了大量的bec代币。
什么是整型溢出
那什么是整型溢出呢?在solidity编写合约时,定义整型一般是用uint8, uint256。一个变量如果定义为uint8表示的无符号的8位整型,即表示的范围为0-255。当给这个变量赋值256时,即整型溢出变成了0,以此类推257变成了1。
下面通过合约代码实例说明:
pragma solidity ^0.4.21;
contract HelloWorld{
function add(uint8 a, uint8 b) returns (uint8){
uint8 result = a + b;
return result;
}
}
这个合约代码很简单,将传入的两个整数相加,但是我定义的返回类型是uint8,即最多表示255。
这时我们传入参数255和1,即255+1,按照我们前面说的,这时会出现整型溢出,result为0。
通过remix 执行add函数结果也为0。
BEC源码分析
回到BEC的问题上,它的问题也是类似的,只不过BEC合约是uint256的整型溢出。先看一下这笔漏洞的交易:https://etherscan.io/tx/0xad89ff16fd1ebe3a0a7cf4ed282302c06626c1af33221ebe0d3a470aba4a660f
可以看到这笔交易是通过调用bec合约的方法,分别转了57,896,044,618,658,100,000,000,000,000,000,000,000,000,000,000,000,000,000,000.792003956564819968到两个账户。
通过Input Data 可以看出是调用了batchTransfer方法实现bec代币的转账。
打开BEC代币合约源码,找到batchTransfer这个方法:
这个函数的功能是:
调用该方法的人可以从自己的账户扣除相应的bec代币,给其他账户发送等额的代币。_receivers为需要发送bec的地址数组,_value表示每个地址发送多少bec。
再来看该函数的逻辑:
uint cnt = _receivers.length;
首先取出接受地址个数,这笔交易发送给两个地址,cnt为2,这没什么问题。
uint256 amount = uint256(cnt) * _value;
然后算出总共需要消耗多少个代币,看似也没什么问题,但问题就出现在这里,我们继续看后面的逻辑。
require(cnt > 0 && cnt <= 20);
require(_value > 0 && balances[msg.sender] >= amount);
这里做了两个判断,主要看下面那个:发送者(sender)的代币余额要大于等于刚刚算出来的amount才能够继续操作进行转账,这样的逻辑判断也很合理,钱不够银行也不会给你转账。
但是问题来了:
如果在之前amount的乘法计算时,amount溢出了为0,那这个require(_value > 0 && balances[msg.sender] >= amount);判断不就失效了。
黑客正是利用了这个漏洞,因为uint256表达的范围是0到(2的256次方减1),黑客只需要向两个地址分别转入(2的255次方)数量的代币,最后合约计算出amount为2的256次方,刚好溢出为0。导致balances[msg.sender] >= amount判断失效。这样黑客就可以凭空在自己的两个账户产生大量的bec代币。
SMT合约漏洞
smt出现的合约漏洞也类似,也是通过整型溢出。
合约地址:https://etherscan.io/address/0x55f93985431fc9304077687a35a1ba103dc1e081#code
溢出攻击交易:
https://etherscan.io/tx/0x1abab4c8db9a30e703114528e31dee129a3a758f7f8abc3b6494aad3d304e43f
可以看出如果feeSmt和_value相加的结果刚好为2的256次方,出现整型溢出结果为0,第206行的判断将失效,让攻击者凭空产生代币。
如何防止整型溢出
使用SafeMath库来进行算数运算,在合约中添加代码:
library SafeMath {
function mul(uint256 a, uint256 b) internal constant returns (uint256) {
if (a == 0) {
return 0;
}
uint256 c = a * b;
assert(c / a == b);
return c;
}
function div(uint256 a, uint256 b) internal constant returns (uint256) {
// assert(b > 0); // Solidity automatically throws when dividing by 0
uint256 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
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;
}
}
将BEC合约257行的乘法运算改为:
uint256 amount = uint256(cnt).mul(_value);
这样在SafeMath执行mul时,由于c计算出为0,所以assert(c / a == b);将不通过,抛出异常。
这里讲一下require和assert的区别,assert会消耗执行该函数的gas,而require只会消耗当前执行的gas。