Cobo 보안 팀: ETH 하드포크의 숨겨진 위험 및 재정 거래 기회

avatar
Cobo Labs
2년 전
이 글은 약 2027자,전문을 읽는 데 약 3분이 걸린다
리플레이 공격은 분기된 체인에 어떤 영향을 미칩니까? 그러한 공격으로부터 보호하는 방법

머리말

원본 소스: Cobo Global

머리말

ETH가 PoS 합의 시스템을 업그레이드함에 따라 원래 PoW 메커니즘의 ETH 체인은 일부 커뮤니티(이하 ETHW라고 함)의 지원을 받아 성공적으로 하드 포크되었습니다. 그러나 일부 온체인 프로토콜은 설계 초기에 가능한 하드 포크에 대비하지 않았기 때문에 해당 프로토콜은 ETHW 포크 체인에서 특정 보안 위험을 가지고 있으며 가장 심각한 보안 위험은 재생 공격입니다.

하드 포크를 완료한 후 ETHW 메인 네트워크에서 재생 메커니즘을 사용하는 공격이 최소 두 번 있었습니다.OmniBridge리플레이 공격과Polygon Bridge첫 번째 레벨 제목

재생 유형

먼저 분석을 시작하기 전에 재생 공격 유형에 대한 사전 이해가 필요하며 일반적으로 재생 공격을 두 가지 범주로 나눕니다.그리고그리고서명된 메시지 재생보조 제목

트랜잭션 재생

보조 제목

서명된 메시지 재생

첫 번째 레벨 제목

OmniBridge와 Polygon Bridge의 공격 원리

보조 제목

OmniBridge

OmniBridge는 xDAI와 ETH 메인넷 간의 자산 전송에 사용되는 브리지로, 주로 브리지의 지정된 검증자에 의존하여 교차 링크 자산 전송을 완료하기 위해 교차 체인 메시지를 제출합니다. 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);
    }

이 기능에서는 먼저 L2 라인의 서명 확인에 따라 제출된 서명이 지정된 검증자에 의해 서명되었는지 여부를 확인한 다음 데이터 메시지를 #L11 라인에서 디코딩합니다. 디코딩된 내용에서 반환된 필드에 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) 내부 반환(bool res) {

        return _chainId == sourceChainId();
    }

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

이어지는 함수 로직을 계속 분석해보면 chainId에 대한 검사가 실제로 체인 자체의 chainId를 얻기 위해 evm의 원래 chainId opcode를 사용하지 않고 uintStorage 변수에 저장된 값을 직접 사용한다는 사실을 어렵지 않게 발견할 수 있습니다. 그렇다면 이 값은 명백하다. 관리자가 설정한 것이기 때문에 메시지 자체에 체인 식별자가 없다고 볼 수 있기 때문에 이론적으로는 서명된 메시지를 재생하는 것이 가능하다.

보조 제목

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

함수 로직을 통해 트랜잭션이 실제로 서브체인(플로이곤 체인)에서 발생하는지 확인하기 위해 transactionRoot와 BlockNumber를 확인하여 컨트랙트가 각각 두 가지 확인을 통해 메시지의 적법성을 판단하는 것을 어렵지 않게 확인할 수 있다. 트랜잭션 데이터를 통해 자신만의 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는 확인하지 않는 것을 어렵지 않게 찾을 수 있습니다. 분기된 체인에서 제안자의 메시지 서명을 재생하고 합법적인 headerRoot를 제출한 다음 Polygon Bridge를 사용하여 ETHW 체인에서 종료 기능을 호출하고 해당 트랜잭션 머클 증명을 제출하면 출금이 성공하고 headerRoot 검사를 통과할 수 있습니다.

주소 0x7dbf18f679fa07d943613193e347ca72ef4642b9를 예로 들면 이 주소는 다음 단계를 통해 ETHW 체인에서 차익 거래를 성공적으로 완료했습니다.

  1. 우선, 메인넷 거래소에서 코인을 인출할 수 있는 지폐 기능에 의존하십시오.

  2. Polygon Bridge의 depositFor 기능을 통해 Polygon 체인에 코인을 입금하십시오.

  3. ETH 메인 네트워크는 Polygon Bridge의 종료 기능을 호출하여 코인을 인출합니다.

  4. ETH 메인넷 제안자가 제출한 headerRoot를 복사하고 추출합니다.

  5. ETHW에서 이전 단계에서 추출한 제안자의 서명 메시지를 재생합니다.

  6. 첫 번째 레벨 제목

왜 이런 일이 발생합니까?

위에서 분석한 두 가지 예에서 이 두 프로토콜이 ETHW에 대한 재생 공격에 직면했음을 발견하는 것은 어렵지 않습니다. 프로토콜 자체에 좋은 재생 방지 보호 기능이 없기 때문에 프로토콜에 해당하는 자산이 분기된 체인에서 비어 있기 때문입니다. . 그러나 두 브리지 자체가 ETHW 포크 체인을 지원하지 않기 때문에 사용자는 손실을 입지 않았습니다. 그러나 우리가 고려해야 할 것은 이 두 브리지가 설계 초기에 재생 방지 조치를 추가하지 않은 이유는 무엇입니까? 사실 그 이유는 매우 간단합니다. OmniBridge든 Polygon Bridge든 그들이 설계한 애플리케이션 시나리오는 매우 단일하고 자산을 자신이 지정한 해당 체인으로 전송하는 데만 사용되기 때문입니다. 체인 배포이므로 재생 보호가 없습니다. 프로토콜 자체에 대한 보안 영향은 없습니다.

반대로 ETHW의 사용자는 이러한 브리지가 다중 체인 시나리오를 지원하지 않기 때문에 사용자가 ETHW 분기 체인에서 작업하는 경우 대신 ETH 메인 네트워크에서 메시지 재생 공격을 받게 됩니다.

예를 들어 UniswapV2를 예로 들면 현재 UnswapV2의 풀 계약에는 허가 기능이 있으며 이 기능에는 변수 DOMAIN_SEPARATOR를 포함하는 변수 PERMIT_TYPEHASH가 있습니다.

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

이 변수는 설계 초기에 다중 체인 시나리오에 대한 가능한 재생 방지를 포함하는 chainId를 포함하는 EIP712에서 처음 정의되었지만 uniswapV2 풀 계약의 논리에 따르면 다음과 같습니다.

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

첫 번째 레벨 제목

프로토콜 설계 초기 주의사항

첫 번째 레벨 제목

보조 제목

사용자에게 미치는 영향

보조 제목

거래소 및 관리인에 대한 시사점

요약하다

요약하다

다중 체인 시나리오의 발전과 함께 재생 공격은 이론적 수준에서 점차 주류 공격 방법이 되었습니다.개발자는 프로토콜 설계를 신중하게 고려해야 합니다.메시지 서명 메커니즘을 설계할 때 서명 내용으로 chainId와 같은 요소를 추가하십시오. 그리고 사용자 자산의 손실을 방지하기 위해 관련 모범 사례를 따르십시오.

창작 글, 작자:Cobo Labs。전재 / 콘텐츠 제휴 / 기사 요청 연락처 report@odaily.email;违규정 전재 법률은 반드시 추궁해야 한다.

ODAILY는 많은 독자들이 정확한 화폐 관념과 투자 이념을 수립하고 블록체인을 이성적으로 바라보며 위험 의식을 확실하게 제고해 달라고 당부했다.발견된 위법 범죄 단서에 대해서는 관련 부서에 적극적으로 고발하여 반영할 수 있다.

추천 독서
편집자의 선택