以太坊智能合約“天價bug”回顧、模擬、預防

1,DAO

在以太坊上進行DAPP開發的人應該都知道這個著名的bug,它直接導致了以太坊的分叉。
該bug是由於使用了call方法引起的循環調用;
bug威力:直接導致以太坊分叉
源代碼

function splitDAO(uint _proposalID, address _newCurator) noEther onlyTokenholders returns (bool _success) {
    // ...
    uint fundsToBeMoved = (balances[msg.sender] * p.splitData[0].splitBalance) / p.splitData[0].totalSupply;
    if (p.splitData[0].newDAO.createTokenProxy.value(fundsToBeMoved)(msg.sender) == false)
        
        throw;
    // ...
    // Burn DAO Tokens
    Transfer(msg.sender, 0, balances[msg.sender]);
    withdrawRewardFor(msg.sender); 
    // XXXXX Notice the preceding line is critically before the next few
    totalSupply -= balances[msg.sender];  
    balances[msg.sender] = 0; 
    paidOut[msg.sender] = 0;
    return true;
}
 
function withdrawRewardFor(address _account) noEther internal returns(bool _success) {
    if ((balanceOf(_account) * rewardAccount.accumulatedInput()) / totalSupply < paidOut[_account])
        throw;
    uint reward = (balanceOf(_account) * rewardAccount.accumulatedInput()) / totalSupply - paidOut[_account];
    if (!rewardAccount.payOut(_account, reward))  
        throw;
    paidOut[_account] += reward;
    return true;
}
 
function payOut(address _recipient, uint _amount) returns (bool) {
    if (msg.sender != owner || msg.value > 0 || (payOwnerOnly && _recipient != owner))
        throw;
    if (_recipient.call.value(_amount)()) { 
        PayOut(_recipient, _amount);
        return true;
    } else {
        return false;
    }
}

簡化一下,代碼就是這個意思:

function splitDAO() {
	if(balance[msg.sender] >= amount){
		msg.sender.call.value(amount); //bug1: 應該使用transfer或者send來轉賬
		balance[msg.sender] = 0;	   //bug2: 餘額清零應該放到轉賬前面
	}
}

漏洞在於,假如msg.sender是一個合約地址,call操作會觸發合約的回退函數,而餘額的清零操作是在轉賬之後,回退函數再次進入splitDAO的時候,餘額根本沒來得及清零。
黑客合約可以這樣構造:

	contract HackDAO{
		DAO dao;
		function withdraw() ownerOnly{
			.........
		}
		function() public payable{
			dao.splitDAO();
		}
	}

當然實際的情況會複雜得多,黑客首先得讓回退函數在DAO合約的餘額快不夠的時候停止攻擊,否則整個交易就會失敗。
如何防範:

function splitDAO() {
	if(balance[msg.sender] >= amount){
		balance[msg.sender] = 0;
		msg.sender.transfer(amount); 
	}
}

使用transfer或者send方法轉賬有個好處:假如轉給的是一個合約並觸發了合約的回退函數,那麼回退函數中可用的gas只有2300——根部不夠一次函數調用的。

教訓:嚴禁使用call來轉賬

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,美圖幣

該bug是由於在處理乘法操作時沒有使用safeMath,導致溢出。
bug威力:價值6億的BEC幣歸零,黑客在交易所邁出了千萬價值的BEC但後來遭到了回滾。
代碼:
在這裏插入圖片描述
紅框裏的乘法沒有做溢出檢驗,黑客構造了cnt = 2, _value = 0x8000000000000000000000000000000000000000000000000000000000000000
相乘之後溢出,amount變成了1.。。。。。。,然後程序給兩個賬戶各自轉了0x8000000000000000000000000000000000000000000000000000000000000000個BEC!

解決辦法:與餘額相關的操作一定要全部使用safeMath!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章