前言
原文來源:Cobo Global
前言
前言
原文來源:Cobo GlobalOmniBridge隨著ETH 升級PoS 共識系統,原有的PoW 機制的ETH 鏈在部分社區的支持下成功硬分叉(下文簡稱ETHW)。但是,由於某些鏈上協議在設計之初沒有對可能的硬分叉做好準備,導致對應的協議在ETHW 分叉鏈存在一定的安全隱患,其中最為嚴重的安全隱患則是重放攻擊。Polygon Bridge一級標題
在完成硬分叉後, ETHW 主網出現了至少2起利用重放機制進行的攻擊,分別是
一級標題和和二級標題二級標題
交易重放
二級標題
二級標題
一級標題
一級標題
二級標題
OmniBridge
一級標題
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);
}
OmniBridge 和Polygon Bridge 的攻擊原理
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);
}
二級標題
return _chainId == sourceChainId();
}
function sourceChainId() public view returns (uint256) {
return uintStorage[SOURCE_CHAIN_ID];
}
在這個函數中,首先會根據#L2 行的簽名檢查來確定提交的簽名是不是由指定的validator 進行簽名,然後再在#L11 行對data 消息進行解碼。從解碼內容上看,不難發現,返回字段中包含了chainId 字段,那麼是不是說明無法進行簽名消息重放呢?我們繼續分析。
二級標題
Polygon Bridge
通過追查_executeMessage 函數,發現函數在#L11 行對chaindId 進行了合法性的檢查function _isDestinationChainIdValid(uint256 _chainId) internal returns (bool res) {
function exit(bytes calldata inputData) external override {
通過繼續分析後續的函數邏輯,不難發現其實針對chainId 的檢查其實並沒有使用evm 原生的chainId 操作碼來獲取鏈本身的chainId,而是直接使用存儲在uintStorage 變量中的值,那這個值很明顯是管理員設置進去的,所以可以認為消息本身並不帶有鏈標識,那麼理論上就是可以進行簽名消息重放的。
// 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()
);
}
二級標題
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;
}
二級標題
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
);
和Omni Bridge 一樣,Polygon Bridge 是用於在Polygon 和ETH 主網進行資產轉移的橋。與Omni Bridge 不同,Polygon Bridge 依賴區塊證明進行提款,邏輯如下:
//...省略不重要邏輯
通過函數邏輯,不難發現合約通過2個檢查確定消息的合法性,分別是通過檢查transactionRoot 和BlockNumber 來確保交易真實發生在子鏈(Ploygon Chain),第一個檢查其實可以繞過,因為任何人都可以通過交易數據來構造屬於自己的transactionRoot,但是第二個檢查是無法繞過的,因為通過查看_checkBlockMembershipInCheckpoint 邏輯可以發現:
對應的headerRoot 是從_checkpointManager 合約中提取的,順著這個邏輯我們查看_checkpointManager設置headerRoot 的地方
//....剩餘邏輯省略
不難發現在#L2 行代碼中,簽名數據僅對borChianId 進行了檢查,而沒有對鏈本身的chainId 進行檢查,由於該消息是由合約指定的proposer 進行簽名的,那麼理論上攻擊者也可以在分叉鏈上重放proposer 的消息簽名,提交合法的headerRoot,後續再通過Polygon Bridge進行在ETHW鏈中調用exit 函數並提交相應的交易merkle proof 後就可以提現成功並通過headerRoot 的檢查。
以地址0x7dbf18f679fa07d943613193e347ca72ef4642b9為例,該地址就成功通過以下幾步操作完成了對ETHW 鏈的套利
ETH 主網調用Polygon Bridge 的exit 函數提幣;
一級標題
複製提取ETH 主網proposer 提交的headerRoot;
在ETHW 中重放上一步提取的proposer 的簽名消息;
一級標題
一級標題
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);
}
反觀ETHW 上的用戶,由於這些橋本身並不支持多鏈場景,如果用戶在ETHW 分叉鏈上進行操作的話,反而會在ETH 主網上遭受消息重放攻擊。
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)
)
);
}
一級標題
協議設計之初的防範措施
一級標題
二級標題
二級標題
二級標題
二級標題
總結
總結
對交易所和託管機構的影響