Cobo安全團隊:ETH 硬分叉裡那些隱藏的風險和套利機會
TechFlow Selected深潮精選
Cobo安全團隊:ETH 硬分叉裡那些隱藏的風險和套利機會
本文將以這兩個事件作為案例,分別分析重放攻擊對分叉鏈的影響,以及協議應如何防範此類攻擊。
此篇文章由 Cobo 區塊鏈安全團隊供稿,團隊成員來自知名區塊鏈安全廠商,具備豐富的智能合約審計經驗,曾在多個 DeFi 項目中發現高危漏洞。團隊目前重點關注智能合約安全、DeFi安全等方向,研究並分享前沿區塊鏈安全技術。
我們也希望對加密數字貨幣領域有研究精神和科學方法論的終身迭代學習者可以加入我們的行列,向行業輸出思考洞察與研究觀點!
此篇是Cobo Global 的第 16 篇文章
前言
隨著 ETH 升級 PoS 共識系統,原有的 PoW 機制的 ETH 鏈在部分社區的支持下成功硬分叉(下文簡稱 ETHW)。但是,由於某些鏈上協議在設計之初沒有對可能的硬分叉做好準備,導致對應的協議在 ETHW 分叉鏈存在一定的安全隱患,其中最為嚴重的安全隱患則是重放攻擊。
在完成硬分叉後, ETHW 主網出現了至少2起利用重放機制進行的攻擊,分別是 OmniBridge 的重放攻擊和 Polygon Bridge 的重放攻擊。本文將以這兩個事件作為案例,分別分析重放攻擊對分叉鏈的影響,以及協議應如何防範此類攻擊。
重放的類型
首先,在開始分析之前,我們需要先對重放攻擊的類型做一個初步的瞭解,一般而言,我們對重放攻擊分成兩類,分別是 交易重放 和 簽名消息重放。下面,我們來分別說下這兩類重放機制的區別
交易重放
交易重放指的是將在原有鏈的交易原封不動的遷移到目標鏈的操作,屬於是交易層面上的重放,重放過後交易也是可以正常執行並完成交易驗證。最著名的案例莫過於 Wintermute 在 Optimism 上的攻擊事件,直接導致了超2000萬OP代幣的損失。但是在 EIP 155 實施以後,由於交易的簽名本身帶有 chainId (一種用於鏈本身區別與其他分叉鏈的標識符),在重放的目標鏈 chainId 不同的情況下,交易本身是無法完成重放的。
簽名消息重放
簽名消息重放區別於交易重放,是針對的用私鑰簽名的消息(e.g. Cobo is the best) 進行的重放,在簽名消息重放中,攻擊者不需要對整個交易進行重放,而只需將簽名的消息進行重放即可。在消息簽名中,以 Cobo is the best 為例,由於該消息中並不含任何和鏈相關的特殊參數,所以該消息在簽名後理論上是可以在任意的分叉鏈中均是有效的,可以驗籤通過。為了避免該消息在分叉上的重放,可以消息內容中添加 chainId,如 Cobo is the best + chainId()。在帶上特定的鏈標識符之後,在不同分叉鏈上的消息內容不同,消息簽名不同,因此無法直接進行重放複用。
OmniBridge 和 Polygon Bridge 的攻擊原理
下面我們來分析 OmniBridge 和 Polygon Bridge 的攻擊原理。首先拋出結論,這兩起攻擊事件本身都不是交易重放 攻擊,原因在於 ETHW 使用了區別於 ETH 主網的 chainId,所以直接重放交易無法被驗證通過。那麼剩下的選項就只有消息重放了,那下面我們就來逐個分析它們各自是如何在 ETHW 分叉鏈上被消息重放攻擊的。
OmniBridge
OmniBridge 是用於在 xDAI 和 ETH 主網之間進行資產轉移而使用的橋,主要依賴橋的指定的 validator 提交跨鏈消息完成跨鏈接資產的轉移。在 OmniBridge 中,validator 提交的驗證消息的邏輯是這樣的
function executeSignatures(bytes _data, bytes _signatures) public { _allowMessageExecution(_data, _signatures); bytes32 msgId; address sender; address executor; uint32 gasLimit; uint8 dataType; uint256[2] memory chainIds; bytes memory data; (msgId, sender, executor, gasLimit, dataType, chainIds, data) = ArbitraryMessage.unpackData(_data); _executeMessage(msgId, sender, executor, gasLimit, dataType, chainIds, data); }
在這個函數中,首先會根據 #L2 行的簽名檢查來確定提交的簽名是不是由指定的 validator 進行簽名,然後再在 #L11 行對 data 消息進行解碼。從解碼內容上看,不難發現,返回字段中包含了 chainId 字段,那麼是不是說明無法進行簽名消息重放呢?我們繼續分析。
function _executeMessage( bytes32 msgId, address sender, address executor, uint32 gasLimit, uint8 dataType, uint256[2] memory chainIds, bytes memory data ) internal { require(_isMessageVersionValid(msgId)); require(_isDestinationChainIdValid(chainIds[1])); require(!relayedMessages(msgId)); setRelayedMessages(msgId, true); processMessage(sender, executor, msgId, gasLimit, dataType, chainIds[0], data); }
通過追查 _executeMessage 函數,發現函數在 #L11 行對 chaindId 進行了合法性的檢查
function _isDestinationChainIdValid(uint256 _chainId) internal returns (bool res) { return _chainId == sourceChainId(); } function sourceChainId() public view returns (uint256) { return uintStorage[SOURCE_CHAIN_ID]; }
通過繼續分析後續的函數邏輯,不難發現其實針對 chainId 的檢查其實並沒有使用 evm 原生的 chainId 操作碼來獲取鏈本身的 chainId,而是直接使用存儲在 uintStorage 變量中的值,那這個值很明顯是管理員設置進去的,所以可以認為消息本身並不帶有鏈標識,那麼理論上就是可以進行簽名消息重放的。
由於在硬分叉過程中,分叉前的所有狀態在兩條鏈上都會原封不動的保留,在後續 xDAI 團隊沒有額外操作的情況下。分叉後 ETHW 和 ETH 主網上 Omni Bridge 合約的狀態是不會有變化的,也就是說合約的 validator 也是不會有變化的。根據這一個情況,我們就能推斷出 validator 在主網上的簽名也是可以在 ETHW 上完成驗證的。那麼,由於簽名消息本身不包含 chainId,攻擊者就可以利用簽名重放,在 ETHW 上提取同一個合約的資產。
Polygon Bridge
和 Omni Bridge 一樣,Polygon Bridge 是用於在 Polygon 和 ETH 主網進行資產轉移的橋。與 Omni Bridge 不同,Polygon Bridge 依賴區塊證明進行提款,邏輯如下:
function exit(bytes calldata inputData) external override { //...省略不重要邏輯 // verify receipt inclusion require( MerklePatriciaProof.verify( receipt.toBytes(), branchMaskBytes, payload.getReceiptProof(), payload.getReceiptRoot() ), "RootChainManager: INVALID_PROOF" ); // verify checkpoint inclusion _checkBlockMembershipInCheckpoint( payload.getBlockNumber(), payload.getBlockTime(), payload.getTxRoot(), payload.getReceiptRoot(), payload.getHeaderNumber(), payload.getBlockProof() ); ITokenPredicate(predicateAddress).exitTokens( _msgSender(), rootToken, log.toRlpBytes() ); }
通過函數邏輯,不難發現合約通過2個檢查確定消息的合法性,分別是通過檢查 transactionRoot 和 BlockNumber 來確保交易真實發生在子鏈 (Ploygon Chain),第一個檢查其實可以繞過,因為任何人都可以通過交易數據來構造屬於自己的 transactionRoot,但是第二個檢查是無法繞過的,因為通過查看 _checkBlockMembershipInCheckpoint 邏輯可以發現:
function _checkBlockMembershipInCheckpoint( uint256 blockNumber, uint256 blockTime, bytes32 txRoot, bytes32 receiptRoot, uint256 headerNumber, bytes memory blockProof ) private view returns (uint256) { ( bytes32 headerRoot, uint256 startBlock, , uint256 createdAt, ) = _checkpointManager.headerBlocks(headerNumber); require( keccak256( abi.encodePacked(blockNumber, blockTime, txRoot, receiptRoot) ) .checkMembership( blockNumber.sub(startBlock), headerRoot, blockProof ), "RootChainManager: INVALID_HEADER" ); return createdAt; }
對應的 headerRoot 是從 _checkpointManager 合約中提取的,順著這個邏輯我們查看 _checkpointManager 設置 headerRoot 的地方
function submitCheckpoint(bytes calldata data, uint[3][] calldata sigs) external { (address proposer, uint256 start, uint256 end, bytes32 rootHash, bytes32 accountHash, uint256 _borChainID) = abi .decode(data, (address, uint256, uint256, bytes32, bytes32, uint256)); require(CHAINID == _borChainID, "Invalid bor chain id"); require(_buildHeaderBlock(proposer, start, end, rootHash), "INCORRECT_HEADER_DATA"); // check if it is better to keep it in local storage instead IStakeManager stakeManager = IStakeManager(registry.getStakeManagerAddress()); uint256 _reward = stakeManager.checkSignatures( end.sub(start).add(1), /** prefix 01 to data 01 represents positive vote on data and 00 is negative vote malicious validator can try to send 2/3 on negative vote so 01 is appended */ keccak256(abi.encodePacked(bytes(hex"01"), data)), accountHash, proposer, sigs ); //....剩餘邏輯省略
不難發現在 #L2 行代碼中,簽名數據僅對 borChianId 進行了檢查,而沒有對鏈本身的 chainId 進行檢查,由於該消息是由合約指定的 proposer 進行簽名的,那麼理論上攻擊者也可以在分叉鏈上重放 proposer 的消息簽名,提交合法的 headerRoot,後續再通過 Polygon Bridge進行在 ETHW鏈中調用 exit 函數並提交相應的交易 merkle proof 後就可以提現成功並通過 headerRoot 的檢查。
以地址 0x7dbf18f679fa07d943613193e347ca72ef4642b9 為例,該地址就成功通過以下幾步操作完成了對 ETHW 鏈的套利
-
首先依靠鈔能力主網交易所提幣。
-
在 Ploygon 鏈上通過 Polygon Bridge 的 depositFor 函數進行充幣;
-
ETH 主網調用 Polygon Bridge 的 exit 函數提幣;
-
複製提取 ETH 主網 proposer 提交的 headerRoot;
-
在 ETHW 中重放上一步提取的 proposer 的簽名消息;
-
在 ETHW 中的 Polygon Bridge 上調用 exit 進行提幣
為什麼會發生這種情況?
從上面分析的兩個例子中,不難發現這兩個協議在 ETHW 上遭遇重放攻擊是因為協議本身沒有做好防重放的保護,導致協議對應的資產在分叉鏈上被掏空。但是由於這兩個橋本身並不支持 ETHW 分叉鏈,所以用戶並沒有遭受任何損失。但我們要考慮的事情是為什麼這兩個橋在設計之初就沒有加入重放保護的措施呢?其實原因很簡單,因為無論是 OmniBridge 還是 Polygon Bridge,他們設計的應用場景都非常單一,只是用於到自己指定的對應鏈上進行資產轉移,並沒有一個多鏈部署的計劃,所以沒有重放保護而言對協議本身並不造成安全影響。
反觀 ETHW 上的用戶,由於這些橋本身並不支持多鏈場景,如果用戶在 ETHW 分叉鏈上進行操作的話,反而會在 ETH 主網上遭受消息重放攻擊。
以 UniswapV2 為例,目前在 UnswapV2 的 pool 合約中,存在 permit 函數,該函數中存在變量 PERMIT_TYPEHASH,其中包含變量 DOMAIN_SEPARATOR。
function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external { require(deadline >= block.timestamp, 'UniswapV2: EXPIRED'); bytes32 digest = keccak256( abi.encodePacked( '\x19\x01', DOMAIN_SEPARATOR, keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline)) ) ); address recoveredAddress = ecrecover(digest, v, r, s); require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE'); _approve(owner, spender, value); }
此變量最早在 EIP712 中定義,該變量中含有 chainId,在設計之初就包含可能的多鏈場景的重放預防,但是根據 uniswapV2 pool 合約的邏輯,如下:
constructor() public { uint chainId; assembly { chainId := chainid } DOMAIN_SEPARATOR = keccak256( abi.encode( keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'), keccak256(bytes(name)), keccak256(bytes('1')), chainId, address(this) ) ); }
DOMAIN_SEPARATOR 在構造函數中已經定義好,也就是說在硬分叉後,就算鏈本身的 chainId 已經改變,pool 合約也無法獲取到新的 chianId 來更新 DOMAIN_SEPARATOR,如果未來用戶在 ETHW上進行相關授權,那麼ETHW上的 permit 簽名授權可以被重放到 ETH 主網上。除了 Uniswap 外,類似的協議還有很多,比如特定版本下的 yearn vault合約,同樣也是採用了固定 DOMAIN_SEPARATOR 的情況。用戶在 ETHW 上交互的時候也需要防範此類協議的重放風險。
協議設計之初的防範措施
對於開發者而言,在為協議本身定製消息簽名機制的時候,應該考慮後續可能的多鏈場景,如果路線圖中存在多鏈部署的可能,應該把 chainId 作為變量加入到簽名消息中,同時,在驗證簽名的時候,由於硬分叉不會改變分叉前的任何狀態,用於驗證簽名消息的 chainId不應該設置為合約變量,而應該在每次驗證前重新獲取,然後進行驗籤,保證安全性。
影響
對用戶的影響
普通在協議不支持分叉鏈的情況下,應儘量不在分叉鏈上進行任何操作,防止對應的簽名消息重放到主網上,造成用戶在主網上損失資產
對交易所和託管機構的影響
由於很多交易所本身都支持了 ETHW 代幣,所以這些由於攻擊而提取出來的代幣都有可能充值到交易所中進行拋售,但需要注意的是,此類攻擊並不是鏈共識本身的問題而導致的惡意增發,所以對交易所而言,此類攻擊無需進行額外的防範
總結
隨著多鏈場景的發展,重放攻擊從理論層面逐步變成主流的攻擊方式,開發者應當仔細考量協議設計,在進行消息簽名機制的設計時,儘可能的加入 chainId 等因子作為簽名內容,並遵循相關的最佳實踐,防止用戶資產的損失。
Cobo是亞太地區最大的加密貨幣託管機構,自成立以來已為超過500家行業頂尖機構以及高淨值人士提供卓越的服務,在保證加密資產安全存儲的前提下,同時兌現了加密資產的穩健增益,深受全球用戶信賴。Cobo專注於搭建可擴展的基礎設施,為機構管理多類型的資產提供安全託管、資產增值、鏈上交互以及跨鏈跨層等多重解決方案,為機構邁向 Web 3.0 轉型提供最強有力的技術底層支持和賦能。Cobo 旗下包含Cobo Custody、Cobo DaaS、Cobo MaaS、Cobo StaaS、Cobo Ventures、Cobo DeFi Yield Fund等業務板塊,滿足您的多種需求。
歡迎加入深潮 TechFlow 官方社群
Telegram 訂閱群:https://t.me/TechFlowDaily
Twitter 官方帳號:https://x.com/TechFlowPost
Twitter 英文帳號:https://x.com/BlockFlow_News














