安全審計系列:一文看懂什么是預編譯合約漏洞?

發表於 2023-03-30 09:50 作者: Beosin

2022年5月,白帽組織pwning.eth向Moonbeam提交了一個關於預編譯合約的嚴重漏洞,該漏洞能使得攻擊者任意轉移他人資產,當時該漏洞所影響資金高達1億美元。

據了解,該漏洞涉及對非標准以太坊預編譯的調用。這些地址允許EVM通過智能合約訪問Moonbeam的一些核心功能(如XC-20、質押和民主pallet),這些功能並不存在於基礎的EVM中。通過DELEGATECALL,一個惡意的智能合約可以回調訪問另一方的預編譯存儲。

普通用戶不會遇到這個問題,這需要他們主動向該惡意智能合約發送交易。然而,對於其他允許任意調用外部智能合約的智能合約來說(比如部分允許回調的智能合約),這是一個問題。在這些情況下,不法使用者能對DEX執行對惡意智能合約的調用,該智能合約將能夠訪問僞裝DEX的預編譯,並可能將合約中的余額轉移到任何其他地址。

接下來跟着Beosin安全研究團隊來看一下該漏洞的利用原理與實現過程。

什么是預編譯合約?

在EVM中,一份合約代碼會被解釋成一個個的指令並執行,在每條指令執行過程中,EVM都會對執行條件進行檢查,也就是gas費是否充足,若gas不足,則會拋出錯誤。

EVM虛擬機在執行交易的過程中數據存儲並不是基於寄存器,而是基於棧的操作,每次數據讀寫操作都必須從棧頂开始,所以導致其運行效率非常低,加上每一條指令都需要進行運行檢查,那么在對一個相對復雜的運算進行執行時,可能需要大量的時間成本,而在區塊鏈中,正需要很多這種復雜的運算,例如加密函數、哈希函數等,導致很多函數在EVM環境中執行是不現實的。

預編譯合約便是EVM爲了一些不適合在EVM中執行的較爲復雜的庫函數(多用於加密、哈希等復雜運算)而設計的一種折中方案,主要用於一些計算復雜但邏輯簡單且調用頻繁的一些函數或邏輯固定的合約。

部署預編譯合約需要發起EIP提案,審核通過後將同步到各個客戶端。例如以太坊實現的某些預編譯合約:ercecover()(橢圓曲线公鑰恢復,地址0x1)、sha256hash()(Sha256Hash計算,地址0x2)、ripemd160hash()(Ripemd160Hash計算,地址0x3)等,這些函數都被設置成了一個固定的gas花費,而不用在調用過程中按照字節碼進行gas計算,大大降低了時間成本與gas成本。並且由於預編譯合約通常是在客戶端用客戶端代碼實現,不需要使用EVM,所以運行速度快。

關於Moonbeam項目漏洞

在Moonbeam項目中,Balance ERC-20 precompile 提供了一個 ERC-20 接口來處理 balance的原生代幣,合約可以使用address.call的方式對預編譯合約進行調用,此處address爲預編譯地址,下列是moonbeam修復之前的代碼預編譯合約調用的代碼。

fn execute(&self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult> {  match handle.code_address() {    

// Ethereum precompiles :    

a if a == hash(1) => Some(ECRecover::execute(handle)),    

a if a == hash(2) => Some(Sha256::execute(handle)),    

a if a == hash(3) => Some(Ripemd160::execute(handle)),    

a if a == hash(5) => Some(Modexp::execute(handle)),    

a if a == hash(4) => Some(Identity::execute(handle)),    

a if a == hash(6) => Some(Bn128Add::execute(handle)),    

a if a == hash(7) => Some(Bn128Mul::execute(handle)),    

a if a == hash(8) => Some(Bn128Pairing::execute(handle)),    

a if a == hash(9) => Some(Blake2F::execute(handle)),    

a if a == hash(1024) => Some(Sha3FIPS256::execute(handle)),    

a if a == hash(1025) => Some(Dispatch::<R>::execute(handle)),    

a if a == hash(1026) => Some(ECRecoverPublicKey::execute(handle)),    

a if a == hash(2048) => Some(ParachainStakingWrapper::<R>::execute(handle)),    

a if a == hash(2049) => Some(CrowdloanRewardsWrapper::<R>::execute(handle)),    

a if a == hash(2050) => Some(      

Erc20BalancesPrecompile::<R, NativeErc20Metadata>::execute(handle),    

),

a if a == hash(2051) => Some(DemocracyWrapper::<R>::execute(handle)),    

a if a == hash(2052) => Some(XtokensWrapper::<R>::execute(handle)),    

a if a == hash(2053) => Some(RelayEncoderWrapper::<R, WestendEncoder>::execute(handle)),    

a if a == hash(2054) => Some(XcmTransactorWrapper::<R>::execute(handle)),    

a if a == hash(2055) => Some(AuthorMappingWrapper::<R>::execute(handle)),    

a if a == hash(2056) => Some(BatchPrecompile::<R>::execute(handle)),    

// If the address matches asset prefix, the we route through the asset precompile set    

a if &a.to_fixed_bytes()[0..4] == FOREIGN_ASSET_PRECOMPILE_ADDRESS_PREFIX => {      

Erc20AssetsPrecompileSet::<R, IsForeign, ForeignAssetInstance>::new()        

.execute(handle)    

}    

// If the address matches asset prefix, the we route through the asset precompile set    

a if &a.to_fixed_bytes()[0..4] == LOCAL_ASSET_PRECOMPILE_ADDRESS_PREFIX => {      

Erc20AssetsPrecompileSet::<R, IsLocal, LocalAssetInstance>::new().execute(handle)    

}    

_ => None,  

}

}

上述代碼是由Rust語言實現的moonbase預編譯合約集的執行方法(fn execute()),該方法會匹配調用的預編譯合約地址,然後交由不同的預編譯合約去處理輸入的data。執行方法傳入的handle(預編譯交互句柄)包括了call(call_data)中的相關內容,以及交易上下文信息等。

因此當要調用ERC20預編譯代幣合約時,需通過0x000...00802.call(“fanction(type)”,parameter)的方式(0x802=2050),便能調用ERC20預編譯代幣合約的相關函數。

但上述moonbase預編譯合約集的執行方法存在一個問題,即未檢查其他合約的調用方式。如果使用delegatecall(call_data)而不是call(call_data)的方式調用預編譯合約及,便會出現問題。

接下來我們先看一下使用delegatecall(call_data)和call(call_data)的區別:

1.使用EOA账戶在合約A中利用address.call(call_data)調用另一個合約B的函數時,執行環境是在合約B中,使用的調用者信息(msg)是合約A,如下圖。

2.利用delegatecall調用時,執行環境是在合約A中,使用的調用者信息(msg)是EOA,而無法修改合約B中的存儲數據。如下圖。

無論通過什么方式調用,EOA信息和合約B無法通過合約A綁定到一起,這使得合約之間的調用是安全的。

因此由於moonbase預編譯合約集的執行方法(fn execute())未檢查調用的方式。那么當使用delegatecall去調用預編譯合約,也會在預編譯合約中去執行相關方法並寫入預編譯合約的存儲中。即如下圖所示,當EOA账戶去調用了一個攻擊者編寫的惡意合約A,A中使用delegatecall的方式去調用了預編譯合約B。這將會在A和B中同時寫入調用後的數據,實現釣魚攻擊。

漏洞利用過程

攻擊者可以部署以下釣魚合約,並通過釣魚等方式誘使受害用戶調用釣魚函數-uniswapV2Call,而函數會再次調用實現了delegatecall(token_approve)的stealLater函數。

根據上述介紹規則,攻擊合約調用代幣合約的approve函數授權(asset=0x000...00802),當用戶調用uniswapV2Call之後,會在釣魚合約和預編譯合約的storage中同時寫入授權,攻擊者只用調用預編譯合約的transferfrom函數便可將用戶代幣全部轉移出去。

漏洞修復

隨後开發者在moonbase預編譯合約集的執行方法(fn execute())中判斷了EVM執行環境的地址是否和預編譯地址一致,以確保只能使用call()方式對0x000...00009地址以後的預編譯合約合約進行調用,項目方修復之後的代碼如下:

安全建議

關於這個問題,Beosin安全團隊建議,項目方在項目开發過程中需要考慮到delegatecall和call的不同之處,被調用合約能通過delegatecall進行調用的,需要全方位思考其應用場景以及底層原理,做好嚴格的代碼測試。建議在項目上线前,尋找專業的區塊鏈審計公司進行全面的安全審計。

標題:安全審計系列:一文看懂什么是預編譯合約漏洞?

地址:https://www.coinsdeep.com/article/11862.html

鄭重聲明:本文版權歸原作者所有,轉載文章僅為傳播信息之目的,不構成任何投資建議,如有侵權行為,請第一時間聯絡我們修改或刪除,多謝。

你可能還喜歡
熱門資訊