以太坊智慧合約call注入攻擊

FLy_鵬程萬里發表於2018-08-24

這是我在先知安全大會上分享議題中的一部分內容。主要介紹了利用對call呼叫處理不當,配合一定的應用場景的一種攻擊手段。

0x00 基礎知識

以太坊中跨合約呼叫是指的合約呼叫另外一個合約方法的方式。為了好理解整個呼叫的過程,我們可以簡單將呼叫發起方合約當做傳統web世界的瀏覽器,被呼叫的合約看作webserver,而呼叫的msg則是http資料,EVM底層通過ABI規範來解碼引數,獲取方法選擇器,然後執行對應的合約程式碼。

 

當然,實際上智慧合約的執行一般在打包交易或者驗證交易的時候發生,上面的比喻只是方便理解。

在solidity語言中,我們可以通過call方法來實現對某個合約或者本地合約的某個方法進行呼叫。

呼叫的方式大致如下:

 

<address>.call(方法選擇器, arg1, arg2, …)  
<address>.call(bytes)

如上所述,可以通過傳遞引數的方式,將方法選擇器、引數進行傳遞,也可以直接傳入一個位元組陣列,當然要自己去構造msg.data的結構。

Solidity程式設計中,一般跨合約呼叫執行方都會使用msg.sender全域性變數來獲取呼叫方的以太坊地址,從而進行一些邏輯判斷等。

比如在ERC20標準中的transfer方法的實現中,就是使用msg.sender來作為扣款方:

function transfer(address _to, uint256_value) returns (bool success) {
    ….
    balances[msg.sender]-= _value;
balances[_to] += _value;
….
}

0x01 攻擊模型

Call方法注入漏洞,顧名思義就是外界可以直接控制合約中的call方法呼叫的引數,按照注入位置可以分為以下三個場景:

1. 引數列表可控
    <address>.call(bytes4 selection, arg1, arg2, ...)
2. 方法選擇器可控
   <address>.call(bytes4selection, arg1, arg2, ...)
3. Bytes可控
    <address>.call(bytesdata)
    <address>.call(msg.data)

簡單舉個例子,比如存在一個合約B,程式碼如下:

 

contract B{
    function info(bytes data){
          this.call(data) ;
    }
    function secret() public{
        require(this ==msg.sender);
        // secret operations
    }
}

其中有info和secret方法,secret方法中判斷必須是合約自身呼叫才能執行。然而這裡的info方法中有個call的呼叫,並且外界可以直接控制call呼叫的位元組陣列,因此如果外界精心構造一個data,這個data的方法選擇器指定為secret方法,那麼外部使用者就可以以合約身份呼叫到這個secret方法,這樣就會造成一定的風險。

0x02 具體場景

這裡舉兩種實際的攻擊場景:

(1) bytes注入

 

 

 

在合約程式碼中,有個approveAndCallcode方法,這個方法中允許呼叫_spender合約的某些方法或者傳遞一些資料,通過引入了_spender.call來完成這個功能。

如果外界呼叫中指定_spender為合約自身的地址,就可以以合約的身份去呼叫合約中的某些方法。比如如果我們使用合約的身份去呼叫transfer方法:

 

 

 

只需要自己去構造bytes即可,比如把transfer的_to引數指定為我們自己的賬戶地址。這樣其實就可以直接把合約賬戶中的代幣全部轉到自己的賬戶中,因為通過call注入,在transfer方法看來,msg.sender其實就是合約自己的地址。

(2) 方法選擇器注入

比如這裡有個logAndCall方法:

function logAndCall(address _to, uint _value, bytes data, string_fallback){
         …..
         assert(_to.call(bytes4(keccak256(_fallback)),msg.sender, _value, _data)) ;
         ……
}

這裡我們對_fallback引數可控,也就是說我們可以指定呼叫_to地址的任何方法,但是後面跟了三個引數,分別是msg.sender,_value_data,型別分別為address,uint256以及bytes。那麼我們是不是隻能呼叫引數型別必須為這三個的方法呢?當然不是。這裡涉及到EVM在處理calldata的一個特性。

 

 

 

比如Sample1合約中有個test方法,這個方法中有三個引數,都是uint256型別的。而Sample2通過call呼叫了Sample1的test方法,這裡傳入了5個引數,同樣是可以呼叫成功的。這是因為EVM在獲取引數的時候沒有引數個數校驗的過程,因此取到前三個引數1,2,3之後,就把4,5給截斷掉了,在編譯和執行階段都不會報錯。

利用這個特性,我們其實有很多攻擊面,比如我們可以通過logAndCall中的call注入來呼叫approve方法:

 

 

 

這裡的approve方法有兩個引數,而且型別為address和uint256,所以我們是可以呼叫成功的。這樣就可以將合約賬戶中的代幣授權給我們自己的賬戶了。

0x03 深遠的問題

ERC223標準是為了解決ERC20中對智慧合約賬戶進行轉幣場景缺失的問題,可以看作是ERC20標準的升級版。但是在很多ERC223標準的實現程式碼中就帶入了call注入的問題:

 

此外,很多合約在判斷許可權的時候會將合約自身的地址也納入到白名單中:

 

 

0x04 防護手段

針對本文提到的這個風險,作為開發者來說,需要對ERC223的實現進行排查,不要引入call注入問題,如果非要執行回撥,則可以指定方法選擇器字串,避免使用直接注入bytes的形式來進行call呼叫。對於一些敏感操作或者許可權判斷函式,則不要輕易將合約自身的賬戶地址作為可信的地址。

相關文章