溢出漏洞

173 阅读4分钟

合约溢出漏洞

1.溢出事件

1.1 BEC合约

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,该市场瞬间土崩瓦解。


1.2 SMT项目

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随即暂停了所有币种的充值提取业务。


1.3 FNT合约

2018年12月27日,以太坊智能合约Fountain(FNT)出现整数溢出漏洞,黑客利用其函数漏洞创造了: 2+115792089237316195423570985008687907853269984665640564039457584007913129639935的SMT币。


2.整数溢出简介

2.1 整数溢出原理

由于计算机底层是二进制,任何十进制数字都会被编码到二进制。溢出会丢弃最高位,导致数值不正确。如:八位无符号整数类型的最大值是255,翻译到二进制是1111 1111;当再加一时,当前所有的1都会变成0,并向上进位。但由于该整数类型所能容纳的位置已经全部是1了,再向上进位,最高位会被丢弃,于是二进制就变成了0000 0000。(注:有符号的整数类型,其二进制最高位代表正负。所以该类型的正数溢出会变成负数,而不是零。)

2.2 solidity中的整数溢出

在solidity中,对于0.8.0以下的版本,也存在整数溢出问题。解决办法就是使用SafeMath库来修补溢出漏洞,源码如下:

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

solidity 0.8以上的版本发生运行时溢出会直接revert,修复的方式就是不允许溢出。

3.攻击模拟

3.1 源码如下:

contract ZT is ERC20("ZERO TOKEN", "ZT"){
    //RULE 1
    bytes32 constant RULE_WITHDRAW_WANT = keccak256(abi.encodePacked("withdraw"));

    //RULE 2
    bytes32 constant RULE_NONE_WANT = keccak256(abi.encodePacked("depositByValue"));


    constructor()public{
        _mint(msg.sender,10000000*10**18);
    }
    function depositByWant(uint _amount)external payable{
        uint amount = _amount.mul(10**18);
        require(msg.value>=amount,"you want to trick me?");
        MkaheChange(msg.sender,amount,RULE_NONE_WANT);
    }

    function withdraw(uint _amount)external payable returns(bool){
        uint amount = _amount.mul(10**18);
        require(balanceOf(msg.sender)>=amount);
        _balances[msg.sender] = _balances[msg.sender].sub(amount);
        return MkaheChange(msg.sender,amount,RULE_WITHDRAW_WANT);
    }

    function MkaheChange(address to,uint amount,bytes32 ID)internal returns(bool){
        if(ID==RULE_NONE_WANT)
        {
            _balances[msg.sender]=_balances[msg.sender].add(amount);
            return true;
        }else if(ID==RULE_WITHDRAW_WANT){
            bool a;
            (a,)=payable(to).call.value(amount)("");
            require(a,"withdraw fail");
            return true;
        }
        else{
            return false;
        }
    }

    fallback()external payable{
        MkaheChange(
            msg.sender,
            msg.value,
            RULE_NONE_WANT
        );
    }
}
contract ZTstakepool{
    ZT token;
    uint  totalsupply;
    string symbol;
    mapping(address=>uint)internal workbalance;
    mapping(address=>bool)internal passed;

    struct userInfo{
        uint amount;
        uint duration;
        uint startTime;
    }
    mapping(address=>userInfo)internal userDetails;

    constructor()public{
        token =new ZT();
        symbol = "stTGT";
        totalsupply = token.balanceOf(address(this));
    }

    function getDetails(address account)public view returns(userInfo memory){
        return userDetails[account];
    }

    function workBalanceOf(address account)public view returns(uint){
        bool pass=passed[account];
        if(pass){
            return workbalance[account];
        }else{
            return 0;
        }
    }

    function Zt()public view returns(address){
        return address(token);
    }

    function stake(uint amount,uint blocknumber)external{
        require(blocknumber>=1,"At least 1 block");

        userInfo storage user = userDetails[msg.sender];

        user.startTime = block.number;
        user.duration = blocknumber;
        user.amount += amount;

        token.transferFrom(msg.sender,address(this),amount*10**18);
        workbalance[msg.sender] += blocknumber;

    }

    function unstake()external{
        userInfo storage user = userDetails[msg.sender];
        require(block.number>=user.startTime+user.duration,"you are in a hurry ");
        passed[msg.sender] = true;
        uint amount = user.amount;
        user.amount = 0;
        token.transfer(msg.sender,amount*10**18);
    }

    function swap(address from,address to,uint amount)external{
        require(from==address(this)&&to==address(token));
        uint balance = workBalanceOf(msg.sender);
        require(balance>=amount,"exceed");
        workbalance[msg.sender] -= amount;
        token.transfer(msg.sender,amount*10**18);
    }
}

contract setup{
    ZTstakepool public stakePool;
    ZT public erc20;
    bool solve;
    constructor()public{
        stakePool =new ZTstakepool();
        erc20 = ZT(payable(stakePool.Zt()));
    }
    function isSolved()public view returns(bool){
        return solve;
    }
    function complete()public{
        require(erc20.balanceOf(msg.sender)>=500000*10**18);
        solve = true;
    }
} 

3.2 合约漏洞查找

在源码中我们可以看到,其它两个合约由合约setup生成,所以我们先部署setup合约,通过观察可以发现要想isSolved函数返回true,需要满足erc20.balanceOf(msg.sender)>=500000*10**18,即产生溢出,因此攻击此合约只需要调用参数满足溢出条件即可,我们在上面的代码中可以看到,溢出可能发生在swap函数中。

3.3 开始攻击

先执行unstake函数使得passed[msg.sender] = true

image.png

然后我们就可以执行stake函数了(注意:此时因为我们需要溢出,所以数值应该尽可能大以满足溢出条件)

image.png

此时我们调用swap函数提取出ZT

image.png

此时已经我们查看ZT合约里的balance函数,可以看到已经产生了溢出

image.png

调用complete和isSolved完成攻击获得flag

image.png

获得flag

image.png