Nhóm bảo mật Cobo: Rủi ro tiềm ẩn và cơ hội chênh lệch giá trong các đợt hard fork của ETH

avatar
Cobo Labs
2năm trước
Bài viết có khoảng 2550từ,đọc toàn bộ bài viết mất khoảng 4 phút
Làm thế nào để các cuộc tấn công phát lại ảnh hưởng đến chuỗi rẽ nhánh? Làm thế nào để bảo vệ chống lại các cuộc tấn công như vậy

lời tựa

Nguồn gốc: Cobo Global

lời tựa

Khi ETH nâng cấp hệ thống đồng thuận PoS, chuỗi ETH của cơ chế PoW ban đầu đã hard fork thành công với sự hỗ trợ của một số cộng đồng (sau đây gọi là ETHW). Tuy nhiên, do một số giao thức trên chuỗi không được chuẩn bị cho các hard fork có thể xảy ra khi bắt đầu thiết kế, nên các giao thức tương ứng có một số rủi ro bảo mật nhất định trong chuỗi fork ETHW và rủi ro bảo mật nghiêm trọng nhất là các cuộc tấn công lặp lại.

Sau khi hoàn thành hard fork, đã có ít nhất hai cuộc tấn công sử dụng cơ chế phát lại trên mạng chính ETHW, cụ thể làOmniBridgetấn công lại vàPolygon Bridgetiêu đề cấp đầu tiên

loại phát lại

Trước hết, trước khi bắt đầu phân tích, chúng ta cần hiểu sơ bộ về các loại tấn công lặp lại.Nói một cách tổng quát, chúng ta chia tấn công lặp lại thành hai loại, đó làPhát lại tin nhắn đã kýtiêu đề phụ

phát lại giao dịch

tiêu đề phụ

Phát lại tin nhắn đã ký

tiêu đề cấp đầu tiên

Nguyên tắc tấn công của OmniBridge và Polygon Bridge

tiêu đề phụ

OmniBridge

OmniBridge là cầu nối được sử dụng để chuyển giao tài sản giữa xDAI và mạng chính ETH, chủ yếu dựa vào trình xác thực được chỉ định của cầu nối để gửi thông báo chuỗi chéo nhằm hoàn tất việc chuyển giao tài sản liên kết chéo. Trong OmniBridge, logic của thông báo xác minh do trình xác thực gửi như sau

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

Trong chức năng này, trước tiên, theo kiểm tra chữ ký của dòng #L2, xác định xem chữ ký đã gửi có được ký bởi trình xác thực được chỉ định hay không, sau đó thông báo dữ liệu được giải mã trong dòng #L11. Từ nội dung được giải mã, không khó để nhận thấy rằng trường được trả về có chứa trường chainId, vậy điều đó có nghĩa là thông báo đã ký không thể được phát lại? Hãy tiếp tục phân tích.

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

Bằng cách truy tìm hàm _executeMessage, người ta thấy rằng hàm này đã kiểm tra tính hợp lệ của chaindId trong dòng #L11 function _isDestinationChainIdValid(uint256 _chainId) internal return (bool res) {

        return _chainId == sourceChainId();
    }

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

Bằng cách tiếp tục phân tích logic chức năng tiếp theo, không khó để nhận thấy rằng việc kiểm tra chainId thực sự không sử dụng opcode chainId ban đầu của evm để lấy chainId của chính chuỗi, mà sử dụng trực tiếp giá trị được lưu trữ trong biến uintStorage, thì giá trị này hiển nhiên do quản trị viên đặt nên có thể coi bản thân tin nhắn không có mã định danh chuỗi nên về lý thuyết có thể phát lại tin nhắn đã ký.

tiêu đề phụ

Polygon Bridge

Giống như Omni Bridge, Polygon Bridge là cầu nối để chuyển tài sản giữa mạng chính Polygon và ETH. Không giống như Omni Bridge, Polygon Bridge dựa vào bằng chứng khối để rút tiền, logic như sau:

function exit(bytes calldata inputData) external override {
//... bỏ qua logic không quan trọng
        // 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()
        );
    }

Thông qua logic chức năng, không khó để thấy rằng hợp đồng xác định tính hợp pháp của thông báo thông qua hai lần kiểm tra, tương ứng bằng cách kiểm tra transactionRoot và BlockNumber để đảm bảo rằng giao dịch thực sự xảy ra trong chuỗi con (Ploygon Chain). kiểm tra thực sự có thể được bỏ qua, bởi vì bất kỳ ai Bạn có thể tạo giao dịchRoot của riêng mình thông qua dữ liệu giao dịch, nhưng không thể bỏ qua kiểm tra thứ hai, bởi vì bạn có thể tìm ra bằng cách xem logic của _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 tương ứng được trích xuất từ ​​hợp đồng _checkpointManager. Theo logic này, chúng tôi xem xét nơi _checkpointManager đặt 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
        );
//.... logic còn lại bị bỏ qua

Không khó để nhận thấy rằng trong dòng mã #L2, dữ liệu chữ ký chỉ kiểm tra borChianId chứ không kiểm tra chainId của chính chuỗi. Phát lại chữ ký thông báo của người đề xuất trên chuỗi rẽ nhánh, gửi headerRoot hợp pháp, sau đó sử dụng Polygon Bridge để gọi hàm thoát trong chuỗi ETHW và gửi bằng chứng merkle giao dịch tương ứng, sau đó việc rút tiền có thể thành công và vượt qua kiểm tra headerRoot.

Lấy địa chỉ 0x7dbf18f679fa07d943613193e347ca72ef4642b9 làm ví dụ, địa chỉ này đã hoàn thành thành công chênh lệch giá trên chuỗi ETHW thông qua các bước sau

  1. Trước hết, hãy dựa vào khả năng của tiền giấy để rút tiền từ trao đổi mainnet.

  2. Gửi tiền vào chuỗi Đa giác thông qua chức năng gửi tiền của Cầu đa giác;

  3. Mạng chính ETH gọi chức năng thoát của Cầu đa giác để rút tiền;

  4. Sao chép và trích xuất headerRoot được gửi bởi người đề xuất mạng chính ETH;

  5. Phát lại thông báo chữ ký của người đề xuất được trích xuất ở bước trước trong ETHW;

  6. tiêu đề cấp đầu tiên

Tại sao chuyện này đang xảy ra?

Từ hai ví dụ đã phân tích ở trên, không khó để nhận thấy rằng hai giao thức này đã gặp phải các cuộc tấn công lặp lại trên ETHW do bản thân các giao thức này không có khả năng bảo vệ chống lặp lại tốt, dẫn đến tài sản tương ứng với các giao thức bị rỗng trên chuỗi phân nhánh . Tuy nhiên, do bản thân hai cây cầu không hỗ trợ chuỗi fork ETHW nên người dùng không phải chịu bất kỳ tổn thất nào. Nhưng điều chúng ta phải cân nhắc là tại sao hai cây cầu này không bổ sung các biện pháp bảo vệ phát lại khi bắt đầu thiết kế? Trên thực tế, lý do rất đơn giản, bởi vì bất kể là OmniBridge hay Polygon Bridge, các kịch bản ứng dụng mà họ thiết kế đều rất đơn lẻ và chúng chỉ được sử dụng để chuyển tài sản đến chuỗi tương ứng do họ chỉ định, không có kế hoạch đa dạng hóa. triển khai chuỗi, do đó không có bảo vệ lặp lại. Không có tác động bảo mật nào đối với chính giao thức.

Ngược lại, người dùng trên ETHW, vì các cầu nối này không hỗ trợ các kịch bản đa chuỗi, nên nếu người dùng hoạt động trên chuỗi phân nhánh ETHW, họ sẽ phải chịu các cuộc tấn công phát lại tin nhắn trên mạng chính ETH.

Lấy UniswapV2 làm ví dụ, hiện tại trong hợp đồng nhóm của UnswapV2, có chức năng cho phép và trong hàm này có biến PERMIT_TYPEHASH, hàm này chứa biến 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);
    }

Biến này lần đầu tiên được xác định trong EIP712, chứa chainId, bao gồm khả năng ngăn chặn lặp lại đối với các tình huống đa chuỗi khi bắt đầu thiết kế, nhưng theo logic của hợp đồng nhóm uniswapV2, biến này như sau:

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

tiêu đề cấp đầu tiên

Các biện pháp phòng ngừa khi bắt đầu thiết kế giao thức

tiêu đề cấp đầu tiên

tiêu đề phụ

Tác động đến người dùng

tiêu đề phụ

Ý nghĩa đối với trao đổi và người giám sát

tóm tắt

tóm tắt

Với sự phát triển của các kịch bản đa chuỗi, các cuộc tấn công lặp lại đã dần trở thành một phương thức tấn công chủ đạo từ cấp độ lý thuyết. Các nhà phát triển nên xem xét cẩn thận thiết kế giao thức. Khi thiết kế cơ chế chữ ký tin nhắn, hãy thêm các yếu tố như chainId vào nội dung chữ ký có thể. Và làm theo các phương pháp hay nhất có liên quan để ngăn chặn việc mất tài sản của người dùng.

Bài viết gốc, tác giả:Cobo Labs。Tuyển dụng: Nhân viên kinh doanh phần mềm theo dự án report@odaily.email;Vi phạm quy định của pháp luật.

Odaily nhắc nhở, mời đông đảo độc giả xây dựng quan niệm đúng đắn về tiền tệ và khái niệm đầu tư, nhìn nhận hợp lý về blockchain, nâng cao nhận thức về rủi ro; Đối với manh mối phạm tội phát hiện, có thể tích cực tố cáo phản ánh với cơ quan hữu quan.

Đọc nhiều nhất
Lựa chọn của người biên tập