Cobo安全團隊:ETH硬分叉裡的隱藏風險和套利機會

avatar
Cobo Labs
2年前
本文約1050字,閱讀全文需要約2分鐘
重放攻擊對分叉鏈有何影響?如何防範此類攻擊

前言

原文來源: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 邏輯可以發現:

  1. 對應的headerRoot 是從_checkpointManager 合約中提取的,順著這個邏輯我們查看_checkpointManager設置headerRoot 的地方

  2. //....剩餘邏輯省略

  3. 不難發現在#L2 行代碼中,簽名數據僅對borChianId 進行了檢查,而沒有對鏈本身的chainId 進行檢查,由於該消息是由合約指定的proposer 進行簽名的,那麼理論上攻擊者也可以在分叉鏈上重放proposer 的消息簽名,提交合法的headerRoot,後續再通過Polygon Bridge進行在ETHW鏈中調用exit 函數並提交相應的交易merkle proof 後就可以提現成功並通過headerRoot 的檢查。

  4. 以地址0x7dbf18f679fa07d943613193e347ca72ef4642b9為例,該地址就成功通過以下幾步操作完成了對ETHW 鏈的套利

  5. ETH 主網調用Polygon Bridge 的exit 函數提幣;

  6. 一級標題

複製提取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)
           )
       );
   }

一級標題

協議設計之初的防範措施

一級標題

二級標題

二級標題

二級標題

二級標題

總結

總結

對交易所和託管機構的影響

原創文章,作者:Cobo Labs。轉載/內容合作/尋求報導請聯系 report@odaily.email;違規轉載法律必究。

ODAILY提醒,請廣大讀者樹立正確的貨幣觀念和投資理念,理性看待區塊鏈,切實提高風險意識; 對發現的違法犯罪線索,可積極向有關部門舉報反映。

推薦閱讀
星球精選