Solidity的三種合約間的調用方式 call、delegatecall 和 callcode

0x00 前言git

Solidity(http://solidity.readthedocs.io/en/v0.4.24/) 是一種用與編寫以太坊智能合約的高級語言,語法相似於 JavaScript。github

Solidity 編寫的智能合約可被編譯成爲字節碼在以太坊虛擬機上運行。Solidity 中的合約與面向對象編程語言中的類(Class)很是相似,在一個合約中一樣能夠聲明:狀態變量、函數、事件等。同時,一個合約能夠調用/繼承另一個合約。編程

在 Solidity 中提供了 call、delegatecall、callcode 三個函數來實現合約之間相互調用及交互。正是由於這些靈活各類調用,也致使了這些函數被合約開發者「濫用」,甚至「肆無忌憚」提供任意調用「功能」,致使了各類安全漏洞及風險:安全

2017.7.20,Parity Multisig電子錢包版本 1.5+ 的漏洞被發現,使得攻擊者從三個高安全的多重簽名合約中竊取到超過 15 萬 ETH(https://blog.zeppelin.solutions/on-the-parity-wallet-multisig-hack-405a8c12e8f7) ,其事件緣由是因爲未作限制的 delegatecall 函數調用了合約初始化函數致使合約擁有者被修改。app

2018.6.16,「隱形人真忙」在先知大會上演講了「智能合約消息調用攻防」(https://paper.seebug.org/625/)的議題,其中提到了一種新的攻擊場景—— call 注⼊,主要介紹了利用對 call 調用處理不當,配合必定的應用場景的一種攻擊手段。接着於 2018.6.20,ATN 代幣團隊發佈「ATN抵禦黑客攻擊的報告」(https://paper.seebug.org/621/),報告指出黑客利用 call 注入攻擊漏洞修改合約擁有者,而後給本身發行代幣,從而形成 ATN 代幣增發。編程語言

由此本文主要是針對 Solidity 合約調用函數call、delegatecall、callcode 三種調用方式的異同、濫用致使的漏洞模型並結合實際案例進行分析介紹。函數

0x01 Solidity 的三種調用函數

在 Solidity 中,call 函數簇能夠實現跨合約的函數調用功能,其中包括 call、delegatecall 和 callcode 三種方式。區塊鏈

如下是 Solidity 中 call 函數簇的調用模型:測試

<address>.call(...) returns (bool) <address>.callcode(...) returns (bool) <address>.delegatecall(...) returns (bool)ui

這些函數提供了靈活的方式與合約進行交互,而且能夠接受任何長度、任何類型的參數,其傳入的參數會被填充至 32 字節最後拼接爲一個字符串序列,由 EVM 解析執行。

在函數調用的過程當中, Solidity 中的內置變量 msg 會隨着調用的發起而改變,msg 保存了調用方的信息包括:調用發起的地址,交易金額,被調用函數字符序列等。

三種調用方式的異同點

  • call: 最經常使用的調用方式,調用後內置變量 msg 的值會修改成調用者,執行環境爲被調用者的運行環境(合約的 storage)。
  • delegatecall: 調用後內置變量 msg 的值不會修改成調用者,但執行環境爲調用者的運行環境。
  • callcode: 調用後內置變量 msg 的值會修改成調用者,但執行環境爲調用者的運行環境。

經過下面的例子對比三種調用方式,在 remix 部署調試,部署地址爲 0xca35b7d915458ef540ade6068dfe2f44e8fa733c:

pragma solidity ^0.4.0; contract A {    address public temp1;    uint256 public temp2;    function three_call(address addr) public {        addr.call(bytes4(keccak256("test()")));                 // 1        //addr.delegatecall(bytes4(keccak256("test()")));       // 2        //addr.callcode(bytes4(keccak256("test()")));           // 3    } } contract B {    address public temp1;    uint256 public temp2;    function test() public  {        temp1 = msg.sender;        temp2 = 100;    } }

在部署後能夠看到合約 A 的變量值: temp1 = 0x0, temp2 = 0x0,一樣合約 B 的變量值也是: temp1 = 0x0, temp2 = 0x0。

如今調用語句1 call 方式,觀察變量的值發現合約 A 中變量值爲 0x0,而被調用者合約 B 中的 temp1 = address(A), temp2 = 100:

如今調用語句2 delegatecall 方式,觀察變量的值發現合約 B 中變量值爲 0x0,而調用者合約 A 中的 temp1 = 0xca35b7d915458ef540ade6068dfe2f44e8fa733c, temp2 = 100:

如今調用語句3 callcode 方式,觀察變量的值發現合約 B 中變量值爲 0x0,而調用者合約 A 中的 temp1 = address(A), temp2 = 100:

0x02 delegatecall 「濫用」問題

delegatecall: 調用後內置變量 msg 的值不會修改成調用者,但執行環境爲調用者的運行環境。

原理

在智能合約的開發過程當中,合約的相互調用是常常發生的。開發者爲了實現某些功能會調用另外一個合約的函數。好比下面的例子,調用一個合約 A 的 test() 函數,這是一個正常安全的調用。

function test(uint256 a) public {    // codes } function callFunc() public {    <A.address>.delegatecall(bytes4(keccak256("test(uint256)")), 10); }

可是在實際開發過程當中,開發者爲了兼顧代碼的靈活性,每每會有下面這種寫法:

function callFunc(address addr, bytes data) public {    addr.delegatecall(data); }

這將引發任意 public 函數調用的問題:合約中的 delegatecall 的調用地址和調用的字符序列都由用戶傳入,那麼徹底能夠調用任意地址的函數。

除此以外,因爲 delegatecall 的執行環境爲調用者環境,當調用者和被調用者有相同變量時,若是被調用的函數對變量值進行修改,那麼修改的是調用者中的變量。

利用模型

下面的例子中 B 合約是業務邏輯合約,其中存在一個任意地址的 delegatecall 調用。

contract B {    address owner;    function callFunc(address addr, bytes data) public {        addr.delegatecall(data);        //address(Attack).delegatecall(bytes4(keccak256("foo()")));  //利用代碼示意    } }

攻擊者對應這種合約能夠編寫一個 Attack 合約,而後精心構造字節序列(將註釋部分的攻擊代碼轉換爲字節序列),經過調用合約 B 的 delegatecall,最終調用 Attack 合約中的函數,下面是 Attack 合約的例子:

contract Attack {    address owner;    function foo() public {        // any codes    } }

對於 delegatecall 「濫用」的問題,實際的漏洞效果取決於 Attack 合約中的攻擊代碼,可能形成的安全問題包括:

  1. 攻擊者編寫一個轉帳的函數,竊取合約 B 的貨幣
  2. 攻擊者編寫設置合約擁有者的函數,修改合約 B 的擁有者

delegatecall 安全問題案例

Parity MultiSig錢包事件

2017.7.20,Parity Multisig電子錢包版本 1.5+ 的漏洞被發現,使得攻擊者從三個高安全的多重簽名合約中竊取到超過 15 萬 ETH ,按照當時的 ETH 價格來算,大約爲 3000 萬美圓。

其事件緣由是因爲未作限制的 delegatecall 能夠調用 WalletLibrary 合約的任意函數,而且其錢包初始化函數未作校驗,致使初始化函數能夠重複調用。攻擊者利用這兩個條件,經過 delegatecall 調用 initWallet() 函數,最終修改了合約擁有者,並將合約中的以太幣轉到本身的帳戶下。

下面是存在安全問題的代碼片斷:

(Github/parity: https://github.com/paritytech/parity/blob/4d08e7b0aec46443bf26547b17d10cb302672835/js/src/contracts/snippets/enhanced-wallet.sol)

a. delegatecall 調用代碼: (contract Wallet is WalletEvents)

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

b. initWallet() 與 initMultiowned() 代碼片斷: (contract WalletLibrary is WalletEvents)

function initWallet(address[] _owners, uint _required, uint _daylimit) {    initDaylimit(_daylimit);    initMultiowned(_owners, _required); } ... function initMultiowned(address[] _owners, uint _required) {    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; }

其中錢包初始化函數 initMultiowned() 未作校驗,能夠被屢次調用,存在安全隱患,但因爲其位於 WalletLibrary 合約下,是不能直接調用的。黑客利用 Wallet 合約中的 delegatecall 調用 WalletLibrary 合約的 initWallet() 函數,初始化整個錢包,將合約擁有者修改成僅黑客一人,隨後進行轉帳操做。

黑客攻擊鏈:

除了上述 delegatecall 濫用的案例,在分析研究的過程當中,發現有部分蜜罐合約利用 delegatecall的特性(拷貝目標到本身的運行空間中執行),在代碼中暗藏後門,暗中修改轉帳地址,致使用戶丟失貨幣。有關 delegatecall 蜜罐的詳情請參考「以太坊蜜罐智能合約分析」,其中的 「4.2 偷樑換柱的地址(訪問控制):firstTest」小節。

0x03 call 安全問題

call: 最經常使用的調用方式,調用後內置變量 msg 的值會修改成調用者,執行環境爲被調用者的運行環境。

call 注入是一種新的攻擊場景,由「隱形人真忙」在先知大會上演講「智能合約消息調用攻防」議題上提出,緣由是對 call 調用處理不當,配合必定的應用場景的一種攻擊手段。

call 注入原理

call 調用修改 msg.sender 值

一般狀況下合約經過 call 來執行來相互調用執行,因爲 call 在相互調用過程當中內置變量 msg 會隨着調用方的改變而改變,這就成爲了一個安全隱患,在特定的應用場景下將引起安全問題。

外部用戶經過 call 函數再調用合約函數:

高度自由的 call 調用

在某些應用場景下,調用函數能夠由用戶指定;下面是 call 函數的調用方式:

<address>.call(function_selector, arg1, arg2, ...) <address>.call(bytes)

從上面能夠看出,call 函數擁有極大的自由度:

  1. 對於一個指定合約地址的 call 調用,能夠調用該合約下的任意函數
  2. 若是 call 調用的合約地址由用戶指定,那麼能夠調用任意合約的任意函數

爲了便於理解,能夠將智能合約中的 call 函數類比爲其餘語言中的 eval 函數,call 函數至關於給用戶提供了隨意調用合約函數的入口,若是合約中有函數以 msg.sender 做爲關鍵變量,那麼就會引起安全問題。

call 函數簇調用自動忽略多餘參數

call 函數簇在調用函數的過程當中,會自動忽略多餘的參數,這又額外增長了 call 函數簇調用的自由度。下面的例子演示 call 自動忽略多餘參數:

pragma solidity ^0.4.0; contract A {    uint256 public aa = 0;    function test(uint256 a) public {        aa = a;    }    function callFunc() public {        this.call(bytes4(keccak256("test(uint256)")), 10, 11, 12);    } }

例子中 test() 函數僅接收一個 uint256 的參數,但在 callFunc() 中傳入了三個參數,因爲 call 自動忽略多餘參數,因此成功調用了 test() 函數。

call 注入模型

call 注入引發的最根本的緣由就是 call 在調用過程當中,會將 msg.sender 的值轉換爲發起調用方的地址,下面的例子描述了 call 注入的攻擊模型。

contract B {    function info(bytes data){        this.call(data);        //this.call(bytes4(keccak256("secret()"))); //利用代碼示意    }    function secret() public{        require(this == msg.sender);        // secret operations    } }

在合約 B 中存在 info() 和 secret() 函數,其中 secret() 函數只能由合約本身調用,在 info() 中有用戶能夠控制的 call 調用,用戶精心構造傳入的數據(將註釋轉爲字節序列),便可繞過 require()的限制,成功執行下面的代碼。

對於 call 注入的問題,實際形成的漏洞影響取決於被調用的函數,那麼可能的安全問題包括:

1.權限繞過

如同上面的例子,合約將合約自己的地址做爲權限認證的條件之一,但因爲 call 的調用會致使 msg.sender 變量值更新爲調用方的值,因此就會引發權限繞過的問題。

function callFunc(bytes data) public {    this.call(data);    //this.call(bytes4(keccak256("withdraw(address)")), target); //利用代碼示意 } function withdraw(address addr) public {    require(isAuth(msg.sender));    addr.transfer(this.balance); } function isAuth(address src) internal view returns (bool) {    if (src == address(this)) {        return true;    }    else if (src == owner) {        return true;    }    else {        return false;    } }

上述例子表示了權限繞過致使的任意用戶提取貨幣。,withdraw() 函數設計的初衷爲只能有合約擁有者和合約自己能夠發起取款的操做;但因爲 call 的問題,只要用戶精心拼接字符序列調用 call,從而調用 withdraw() 函數,就能夠繞過 isAuth() 並取款。

2.竊取代幣

在代幣合約中,每每會加入一個 call 回調函數,用於通知接收方以完成後續的操做。但因爲 call調用的特性,用戶能夠向 call 傳入 transfer() 函數調用,便可竊取合約地址下代幣。

下面的例子表示了用戶傳入 transfer() 函數致使竊取代幣。

function transfer(address _to, uint256 _value) public {    require(_value <= balances[msg.sender]);    balances[msg.sender] -= _value;    balances[_to] += _value; } function callFunc(bytes data) public {    this.call(data);    //this.call(bytes4(keccak256("transfer(address,uint256)")), target, value); //利用代碼示意 }

該例子是代幣合約的代碼片斷,用戶傳入精心構造的字符序列以經過 call 來調用 transfer() 函數,並傳入 transfer() 的參數 _to 爲本身的地址;經過 call 調用後, transfer() 函數執行時的 msg.sender 的值已是合約地址了,_to 地址是用戶本身的地址,那麼用戶就成功竊取了合約地址下的代幣。

call 注入案例

1.ATN代幣增發

2018.5.11,ATN 技術人員收到異常監控報告,顯示 ATN Token 供應量出現異常,經過分析發現 Token 合約因爲存在漏洞受到攻擊。該事件對應了上文中的第一種利用模型,因爲 ATN 代幣的合約中的疏漏,該事件中 call 注入不但繞過了權限認證,同時還能夠更新合約擁有者。

在 ATN 項目中使用到了 ERC223 和 ds-auth 庫,兩個庫在單獨使用的狀況下沒有問題,同時使用時就會出現安全問題,如下是存在安全問題的代碼片斷。 (Github/ATN: https://github.com/ATNIO/atn-contracts)

a. ERC223 標準中的自定義回調函數: (Github/ERC223: https://github.com/Dexaran/ERC223-token-standard)

function transferFrom(address _from, address _to, uint256 _amount, bytes _data, string _custom_fallback) public returns (bool success) {    ...    if (isContract(_to)) {        ERC223ReceivingContract receiver = ERC223ReceivingContract(_to);        receiver.call.value(0)(bytes4(keccak256(_custom_fallback)), _from, _amount, _data);    }    ... }

b. ds-auth 權限認證和更新合約擁有者函數: (Github/ds-auth: https://github.com/dapphub/ds-auth)

... function setOwner(address owner_) public auth {    owner = owner_;    emit LogSetOwner(owner); } ... modifier auth {    require(isAuthorized(msg.sender, msg.sig));    _; } function isAuthorized(address src, bytes4 sig) internal view returns (bool) {    if (src == address(this)) {        return true;    } else if (src == owner) {        return true;    } else if (authority == DSAuthority(0)) {        return false;    } else {        return authority.canCall(src, this, sig);    } }

黑客經過調用 transferFrom() 函數,並傳入黑客本身的地址做爲 _from 參數, ATN 合約的地址做爲 _to 參數,並傳入 setOwner() 做爲回調函數;在執行過程當中,因爲 call 調用自動忽略多餘的參數,黑客的地址將做爲 setOwner() 的參數成功執行到函數內部,與此同時,call 調用已經將 msg.sender轉換爲了合約自己的地址,也就繞過了 isAuthorized() 的權限認證,黑客成功將合約的擁有者改成了本身;隨後調用 Mint() 函數爲本身發行代幣,最後黑客再次調用 setOwner() 將權限還原,企圖銷燬做案現場。

黑客攻擊鏈:

得力於 ATN 代幣團隊及時發現問題,並高效的解決問題,這次事件並未對 ATN 代幣形成較大的波動;ATN 代幣團隊封鎖了黑客帳戶,也銷燬了由黑客發行的 1100W 個代幣,最後在交易所的配合下追蹤黑客。

2.大量代幣使用不安全代碼

對於第二種利用模型,在目前公開的智能合約中,仍有很多合約使用這種不安全的代碼,爲了實現通知接收方以完成後續的操做,加入了一個高度自由的回調函數方法。如下是存在安全隱患的代碼片斷:

(etherscan: https://etherscan.io/address/0xbe803e33c0bbd4b672b97158ce21f80c0b6f3aa6#code)

... function transfer(address _to, uint256 _value) public returns (bool success) {    require(_to != address(0));    require(_value <= balances[msg.sender]);    require(balances[_to] + _value > balances[_to]);    balances[msg.sender] -= _value;    balances[_to] += _value;    Transfer(msg.sender, _to, _value);    return true; } ... function approveAndCallcode(address _spender, uint256 _value, bytes _extraData) public returns (bool success) {    allowed[msg.sender][_spender] = _value;    Approval(msg.sender, _spender, _value);    if(!_spender.call(_extraData)) { revert(); }    return true; } ...

黑客經過調用 approveAndCallcode() 函數,將合約地址做爲 _spender 參數,並將 transfer() 的調用轉換爲字節序列做爲 _extraData 參數,最終調用 transfer() 函數。在 transfer() 函數中,_to 參數爲黑客的地址,而此時 msg.sender 的值已是合約自己的地址了,黑客經過這種方式,成功竊取了合約地址中的代幣。

黑客攻擊鏈:

對於上述所描述的安全問題目前還不能形成直接的經濟損失。在對這類智能合約的審計過程當中,發現目前大量的代幣合約不會使用到合約自己的地址做爲存儲單元,也就是說 合約地址所對應的代幣量爲 0 (balances[address(this)] == 0)。但這種不安全的代碼很難猜想到在後續的發展中,會引發什麼樣的問題,應該保持關注並避免這種不安全的代碼。

0x04 callcode 安全問題

callcode: 調用後內置變量 msg 的值會修改成調用者,但執行環境爲調用者的運行環境。

因爲 callcode 同時包含了 call 和 delegatecall 的特性,經過上文對 call 和 delegatecall 的安全問題進行了分析和舉例,能夠得出的結論是 call 和 delegatecall 存在的安全問題將同時存在於 callcode 中,這裏再也不進行詳細的分析。

0x05 總結

目前,區塊鏈技術極高的熱度促使該技術不斷的投入到了生產環境中,但尚未完整的技術流水線,也沒有統一的行業規範,同時 Solidity 語言如今版本爲 0.4.25,尚未發佈第一個正式版本,致使基於區塊鏈技術的產品出現各類安全漏洞,部分漏洞能夠直接形成經濟損失。

針對文中所提到的安全隱患,這裏給開發者幾個建議:

  1. call、callcode、delegatecall調用的自由度極大,而且 call 會發生 msg 值的改變,須要謹慎的使用這些底層的函數;同時在使用時,須要對調用的合約地址、可調用的函數作嚴格的限制。
  2. call 與 callcode 調用會改變 msg 的值,會修改 msg.sender 爲調用者合約的地址,因此在合約中不能輕易將合約自己的地址做爲可信地址。
  3. delegatecall 與 callcode 會拷貝目標代碼到本身的環境中執行,因此調用的函數應該作嚴格的限制,避開調用任意函數的隱患。
  4. 智能合約在部署前必須經過嚴格的審計和測試。
相關文章
相關標籤/搜索