BEC和SMT出现的合约漏洞是怎么回事



  • 最近在币圈很多人讨论的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。


Log in to reply
 

Popular Topics

|

Looks like your connection to SCC was lost, please wait while we try to reconnect.