Cobo Security Team: Hidden Risks and Arbitrage Opportunities in ETH Hard Forks

avatar
Cobo Labs
2 years ago
This article is approximately 1004 words,and reading the entire article takes about 2 minutes
How do replay attacks affect forked chains? How to protect against such attacks

foreword

Original source: Cobo Global

foreword

As ETH upgrades the PoS consensus system, the ETH chain of the original PoW mechanism has successfully hard forked with the support of some communities (hereinafter referred to as ETHW). However, because some on-chain protocols were not prepared for possible hard forks at the beginning of the design, the corresponding protocols have certain security risks in the ETHW fork chain, and the most serious security risks are replay attacks.

After completing the hard fork, there were at least two attacks using the replay mechanism on the ETHW main network, namelyOmniBridgereplay attack andPolygon Bridgefirst level title

type of replay

First of all, before starting the analysis, we need to have a preliminary understanding of the types of replay attacks. Generally speaking, we divide replay attacks into two categories, namelyandandSigned message replaysecondary title

transaction replay

secondary title

Signed message replay

first level title

Attack principle of OmniBridge and Polygon Bridge

secondary title

OmniBridge

OmniBridge is a bridge used for asset transfer between xDAI and the ETH mainnet, which mainly relies on the designated validator of the bridge to submit cross-chain messages to complete the transfer of cross-link assets. In OmniBridge, the logic of the verification message submitted by the validator is as follows

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);
    }

In this function, firstly, according to the signature check of line #L2, it is determined whether the submitted signature is signed by the specified validator, and then the data message is decoded in line #L11. From the decoded content, it is not difficult to find that the returned field contains the chainId field, so does it mean that the signed message cannot be replayed? Lets continue the analysis.

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);
    }

By tracing the _executeMessage function, it is found that the function has checked the validity of chaindId in line #L11 function _isDestinationChainIdValid(uint256 _chainId) internal returns (bool res) {

        return _chainId == sourceChainId();
    }

    function sourceChainId() public view returns (uint256) {
        return uintStorage[SOURCE_CHAIN_ID];
    }

By continuing to analyze the subsequent function logic, it is not difficult to find that the check for chainId actually does not use the original chainId opcode of evm to obtain the chainId of the chain itself, but directly uses the value stored in the uintStorage variable, then this value is obvious It is set by the administrator, so it can be considered that the message itself does not have a chain identifier, so in theory, it is possible to replay the signed message.

secondary title

Polygon Bridge

Like Omni Bridge, Polygon Bridge is a bridge for asset transfer between Polygon and ETH mainnet. Unlike Omni Bridge, Polygon Bridge relies on block proofs for withdrawals, the logic is as follows:

function exit(bytes calldata inputData) external override {
//...omit unimportant logic
        // 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()
        );
    }

Through the function logic, it is not difficult to find that the contract determines the legitimacy of the message through two checks, respectively, by checking transactionRoot and BlockNumber to ensure that the transaction actually occurs in the sub-chain (Ploygon Chain). The first check can actually be bypassed, because anyone You can construct your own transactionRoot through transaction data, but the second check cannot be bypassed, because you can find out by looking at the logic of _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;
    }

The corresponding headerRoot is extracted from the _checkpointManager contract. Following this logic, we look at the place where _checkpointManager sets the 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
        );
//....remaining logic omitted

It is not difficult to find that in the #L2 line of code, the signature data only checks the borChianId, but not the chainId of the chain itself. Since the message is signed by the proposer specified by the contract, the attacker can also theoretically Replay the proposers message signature on the forked chain, submit the legal headerRoot, and then use the Polygon Bridge to call the exit function in the ETHW chain and submit the corresponding transaction merkle proof, then the withdrawal can be successful and pass the headerRoot check.

Taking the address 0x7dbf18f679fa07d943613193e347ca72ef4642b9 as an example, this address has successfully completed the arbitrage on the ETHW chain through the following steps

  1. First of all, rely on the banknote ability to withdraw coins from the mainnet exchange.

  2. Deposit coins on the Polygon chain through the depositFor function of Polygon Bridge;

  3. The ETH main network calls the exit function of Polygon Bridge to withdraw coins;

  4. Copy and extract the headerRoot submitted by the ETH main network proposer;

  5. Replay the signature message of the proposer extracted in the previous step in ETHW;

  6. first level title

Why is this happening?

From the two examples analyzed above, it is not difficult to find that these two protocols encountered replay attacks on ETHW because the protocols themselves did not have good anti-replay protection, resulting in the assets corresponding to the protocols being hollowed out on the forked chain. However, since the two bridges themselves do not support the ETHW fork chain, users have not suffered any losses. But the thing we have to consider is why didnt these two bridges add replay protection measures at the beginning of their design? In fact, the reason is very simple, because whether it is OmniBridge or Polygon Bridge, the application scenarios they designed are very single, and they are only used to transfer assets to the corresponding chain designated by themselves. There is no plan for multi-chain deployment, so there is no replay protection There is no security impact on the protocol itself.

In contrast, users on ETHW, since these bridges do not support multi-chain scenarios, if users operate on the ETHW forked chain, they will suffer message replay attacks on the ETH main network instead.

Taking UniswapV2 as an example, currently in the pool contract of UnswapV2, there is a permit function, and there is a variable PERMIT_TYPEHASH in this function, which contains the variable 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);
    }

This variable was first defined in EIP712, which contains chainId, which included possible replay prevention for multi-chain scenarios at the beginning of the design, but according to the logic of the uniswapV2 pool contract, it is as follows:

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)
           )
       );
   }

first level title

Precautions at the beginning of protocol design

first level title

secondary title

Impact on users

secondary title

Implications for exchanges and custodians

Summarize

Summarize

With the development of multi-chain scenarios, replay attacks have gradually become a mainstream attack method from the theoretical level. Developers should carefully consider the protocol design. When designing the message signature mechanism, add factors such as chainId as the signature content as much as possible. And follow the relevant best practices to prevent the loss of user assets.

Original article, author:Cobo Labs。Reprint/Content Collaboration/For Reporting, Please Contact report@odaily.email;Illegal reprinting must be punished by law.

ODAILY reminds readers to establish correct monetary and investment concepts, rationally view blockchain, and effectively improve risk awareness; We can actively report and report any illegal or criminal clues discovered to relevant departments.

Recommended Reading
Editor’s Picks