Đội ngũ an ninh Cobo: Những rủi ro và cơ hội chênh lệch giá ẩn chứa trong đợt phân tách cứng ETH
Tuyển chọn TechFlowTuyển chọn TechFlow
Đội ngũ an ninh Cobo: Những rủi ro và cơ hội chênh lệch giá ẩn chứa trong đợt phân tách cứng ETH
Bài viết này sẽ lấy hai sự kiện trên làm ví dụ để phân tích ảnh hưởng của tấn công lặp lại (replay attack) đối với chuỗi phân nhánh, cũng như cách các giao thức nên phòng ngừa các cuộc tấn công dạng này.
Bài viết này do đội an toàn blockchain Cobo cung cấp. Các thành viên trong đội đến từ các nhà cung cấp an toàn blockchain nổi tiếng, có kinh nghiệm phong phú trong việc kiểm toán hợp đồng thông minh và từng phát hiện ra các lỗ hổng nghiêm trọng trong nhiều dự án DeFi. Hiện tại, đội đang tập trung vào các lĩnh vực như an toàn hợp đồng thông minh, an toàn DeFi, nghiên cứu và chia sẻ các công nghệ an toàn blockchain tiên tiến.
Chúng tôi cũng mong muốn những người học hỏi liên tục với tinh thần nghiên cứu và phương pháp khoa học trong lĩnh vực tiền mã hóa có thể gia nhập hàng ngũ của chúng tôi, đóng góp suy nghĩ, phân tích và quan điểm nghiên cứu cho ngành!
Đây là bài viết thứ 16 của Cobo Global
Mở đầu
Khi ETH nâng cấp lên hệ thống đồng thuận PoS, chuỗi ETH theo cơ chế PoW ban đầu đã được hard fork thành công với sự hỗ trợ của một số cộng đồng (gọi tắt là ETHW trong bài viết). Tuy nhiên, do một số giao thức trên chuỗi ban đầu không chuẩn bị sẵn sàng cho khả năng hard fork, nên các giao thức tương ứng gặp phải những rủi ro an ninh nhất định trên chuỗi fork ETHW, trong đó nghiêm trọng nhất là tấn công replay (tái phát).
Sau khi hoàn tất hard fork, mạng chính ETHW đã xảy ra ít nhất 2 vụ tấn công sử dụng cơ chế replay, lần lượt là vụ tấn công replay trên OmniBridge và vụ tấn công replay trên Polygon Bridge. Bài viết này sẽ lấy hai sự kiện này làm ví dụ để phân tích ảnh hưởng của tấn công replay đối với chuỗi fork, cũng như cách mà các giao thức nên phòng chống loại tấn công này.
Các loại tấn công replay
Trước hết, trước khi bắt đầu phân tích, chúng ta cần hiểu sơ qua về các loại tấn công replay. Nói chung, tấn công replay được chia thành hai loại: tái phát giao dịch và tái phát tin nhắn ký. Dưới đây, chúng ta sẽ lần lượt tìm hiểu sự khác biệt giữa hai cơ chế replay này.
Tái phát giao dịch
Tái phát giao dịch đề cập đến việc sao chép nguyên trạng một giao dịch từ chuỗi gốc sang chuỗi đích — đây là hình thức replay ở tầng giao dịch. Sau khi replay, giao dịch vẫn có thể thực thi bình thường và vượt qua xác thực. Vụ việc nổi tiếng nhất là cuộc tấn công Wintermute trên Optimism, dẫn đến mất mát hơn 20 triệu token OP. Tuy nhiên, kể từ khi EIP-155 được áp dụng, chữ ký giao dịch đã bao gồm tham số chainId (một định danh dùng để phân biệt chuỗi gốc với các chuỗi fork), do đó nếu chainId của chuỗi đích khác với chuỗi gốc thì giao dịch sẽ không thể replay thành công.
Tái phát tin nhắn ký
Khác với tái phát giao dịch, tái phát tin nhắn ký liên quan đến việc tái sử dụng tin nhắn đã được ký bằng khóa riêng (ví dụ: "Cobo is the best"). Trong trường hợp này, kẻ tấn công không cần replay toàn bộ giao dịch, mà chỉ cần replay lại phần tin nhắn đã ký. Với tin nhắn như "Cobo is the best", vì nội dung không chứa bất kỳ tham số đặc thù nào liên quan đến chuỗi, nên về mặt lý thuyết, tin nhắn này sau khi ký có thể hợp lệ trên mọi chuỗi fork và vượt qua xác thực chữ ký. Để ngăn chặn việc tái sử dụng này trên các chuỗi fork, có thể thêm chainId vào nội dung tin nhắn, ví dụ: "Cobo is the best + chainId()". Khi gắn định danh chuỗi cụ thể, nội dung tin nhắn trên mỗi chuỗi fork sẽ khác nhau, dẫn đến chữ ký khác nhau, do đó không thể trực tiếp replay.
Nguyên lý tấn công của OmniBridge và Polygon Bridge
Dưới đây, chúng ta sẽ phân tích nguyên lý tấn công của OmniBridge và Polygon Bridge. Trước tiên, đưa ra kết luận: cả hai vụ tấn công này đều KHÔNG phải là dạng tái phát giao dịch, bởi vì ETHW sử dụng chainId khác với mạng chính ETH, do đó không thể replay trực tiếp giao dịch. Vậy thì lựa chọn còn lại chỉ có thể là tái phát tin nhắn. Tiếp theo, hãy cùng phân tích cách mà hai giao thức này bị tấn công replay tin nhắn trên chuỗi fork ETHW.
OmniBridge
OmniBridge là cầu nối dùng để chuyển tài sản giữa xDAI và mạng chính ETH, chủ yếu phụ thuộc vào validator được chỉ định để gửi tin nhắn xuyên chuỗi nhằm hoàn tất việc chuyển tài sản. Trong OmniBridge, logic xử lý tin nhắn xác thực do validator 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 hàm này, trước tiên tại dòng #L2 sẽ kiểm tra chữ ký xem có phải do validator được chỉ định ký hay không, sau đó tại dòng #L11 giải mã tin nhắn data. Nhìn vào nội dung giải mã, ta thấy rõ ràng có trường chainId. Liệu điều này có nghĩa là không thể replay tin nhắn ký? 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); }
Theo dõi hàm _executeMessage, ta thấy tại dòng #L11 có kiểm tra tính hợp lệ của chainId:
function _isDestinationChainIdValid(uint256 _chainId) internal returns (bool res) { return _chainId == sourceChainId(); } function sourceChainId() public view returns (uint256) { return uintStorage[SOURCE_CHAIN_ID]; }
Tiếp tục phân tích logic hàm tiếp theo, dễ dàng nhận thấy việc kiểm tra chainId không dùng opcode chainid bản địa EVM để lấy chainId thực tế của chuỗi, mà dùng giá trị lưu trong biến uintStorage — giá trị này rõ ràng do quản trị viên thiết lập. Do đó, tin nhắn bản thân không mang định danh chuỗi, về mặt lý thuyết có thể bị replay tin nhắn ký.
Do trong quá trình hard fork, toàn bộ trạng thái trước fork được giữ nguyên trên cả hai chuỗi, và nếu đội xDAI không có thao tác bổ sung nào, thì sau fork, trạng thái hợp đồng OmniBridge trên ETHW và mạng chính ETH là giống hệt nhau, tức là validator cũng không thay đổi. Từ đó suy ra, chữ ký của validator trên mạng chính cũng có thể được xác thực trên ETHW. Vì tin nhắn ký không chứa chainId, kẻ tấn công có thể lợi dụng việc replay chữ ký để rút tài sản khỏi cùng một hợp đồng trên ETHW.
Polygon Bridge
Giống như OmniBridge, Polygon Bridge là cầu nối dùng để chuyển tài sản giữa Polygon và mạng chính ETH. Khác với OmniBridge, Polygon Bridge dựa vào bằng chứng khối để rút tài sản, với 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() ); }
Qua logic hàm, dễ thấy hợp đồng sử dụng hai kiểm tra để xác minh tính hợp lệ của tin nhắn: kiểm tra transactionRoot và BlockNumber để đảm bảo giao dịch thực sự xảy ra trên chuỗi con (Polygon Chain). Kiểm tra đầu tiên có thể bị vòng qua vì bất kỳ ai cũng có thể tạo transactionRoot từ dữ liệu giao dịch. Nhưng kiểm tra thứ hai thì không thể vòng qua, vì khi xem logic _checkBlockMembershipInCheckpoint ta thấy:
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; }
Giá trị headerRoot tương ứng được trích xuất từ hợp đồng _checkpointManager. Theo logic này, ta xem nơi _checkpointManager thiết lập 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 ); //.... bỏ qua logic còn lại
Dễ thấy tại dòng #L2, dữ liệu ký chỉ kiểm tra borChainId, chứ không kiểm tra chainId bản thân chuỗi. Vì tin nhắn này do proposer được chỉ định bởi hợp đồng ký, nên về mặt lý thuyết, kẻ tấn công cũng có thể replay chữ ký của proposer trên chuỗi fork, gửi headerRoot hợp lệ, sau đó gọi hàm exit trên Polygon Bridge trong chuỗi ETHW và nộp bằng chứng Merkle để rút tài sản thành công, đồng thời vượt qua kiểm tra headerRoot.
Lấy địa chỉ 0x7dbf18f679fa07d943613193e347ca72ef4642b9 làm ví dụ, địa chỉ này đã thành công套利 trên chuỗi ETHW qua các bước sau:
-
Trước tiên, rút tiền từ sàn giao dịch trên mạng chính bằng nguồn lực tài chính.
-
Trên chuỗi Polygon, nạp tiền thông qua hàm depositFor của Polygon Bridge;
-
Gọi hàm exit của Polygon Bridge trên mạng chính ETH để rút tiền;
-
Sao chép
headerRootmà proposer trên mạng chính ETH gửi; -
Trên ETHW, replay chữ ký của proposer đã lấy ở bước trước;
-
Trên Polygon Bridge của ETHW, gọi hàm exit để rút tiền
Tại sao lại xảy ra tình trạng này?
Từ hai ví dụ phân tích trên, dễ thấy cả hai giao thức đều bị tấn công replay trên ETHW do bản thân giao thức không có biện pháp phòng chống replay, dẫn đến tài sản tương ứng bị rút sạch trên chuỗi fork. Tuy nhiên, vì hai cầu nối này vốn không hỗ trợ chuỗi fork ETHW, nên người dùng không chịu tổn thất nào. Nhưng vấn đề đặt ra là tại sao ngay từ đầu thiết kế, hai cầu nối này lại không tích hợp biện pháp bảo vệ chống replay? Lý do rất đơn giản: dù là OmniBridge hay Polygon Bridge, phạm vi ứng dụng của chúng đều rất hẹp — chỉ phục vụ việc chuyển tài sản tới chuỗi đích được chỉ định, không có kế hoạch triển khai đa chuỗi, do đó việc thiếu bảo vệ chống replay không gây ảnh hưởng an ninh cho bản thân giao thức.
Ngược lại, với người dùng trên ETHW, vì các cầu nối này vốn không hỗ trợ đa chuỗi, nếu người dùng thao tác trên chuỗi fork ETHW, ngược lại có thể bị tấn công replay 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 pool của UniswapV2 có hàm permit, chứa biến PERMIT_TYPEHASH, bên trong có 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 được định nghĩa lần đầu trong EIP712, chứa chainId, ngay từ đầu đã tính đến phòng chống replay trong kịch bản đa chuỗi. Tuy nhiên, theo logic hợp đồng pool UniswapV2 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) ) ); }
Biến DOMAIN_SEPARATOR đã được xác định trong hàm khởi tạo, nghĩa là sau hard fork, dù chainId của chuỗi đã thay đổi, hợp đồng pool vẫn không thể lấy chainId mới để cập nhật DOMAIN_SEPARATOR. Nếu người dùng tương tác cấp phép trên ETHW trong tương lai, chữ ký permit trên ETHW có thể bị replay lên mạng chính ETH. Ngoài Uniswap, nhiều giao thức tương tự cũng vậy, ví dụ như các hợp đồng yearn vault ở phiên bản cụ thể cũng dùng DOMAIN_SEPARATOR cố định. Người dùng cần cảnh giác rủi ro replay khi tương tác với các giao thức này trên ETHW.
Biện pháp phòng ngừa ngay từ thiết kế giao thức
Đối với nhà phát triển, khi thiết kế cơ chế ký tin nhắn cho giao thức, cần cân nhắc khả năng triển khai đa chuỗi trong tương lai. Nếu lộ trình có kế hoạch triển khai trên nhiều chuỗi, cần đưa chainId vào nội dung ký. Đồng thời, khi xác minh chữ ký, do hard fork không thay đổi bất kỳ trạng thái nào trước fork, chainId dùng để xác minh tin nhắn ký không nên là biến hợp đồng, mà cần được lấy lại mỗi lần xác minh để đảm bảo an toàn.
Ảnh hưởng
Ảnh hưởng đến người dùng
Người dùng thông thường, nếu giao thức không hỗ trợ chuỗi fork, nên tránh thực hiện bất kỳ thao tác nào trên chuỗi fork để phòng tránh việc tin nhắn ký bị replay lên mạng chính, gây thiệt hại tài sản cho người dùng trên mạng chính.
Ảnh hưởng đến sàn giao dịch và tổ chức lưu ký
Do nhiều sàn giao dịch hiện đã hỗ trợ token ETHW, nên các token bị rút ra do tấn công có thể được nạp vào sàn để bán tháo. Tuy nhiên, cần lưu ý rằng các cuộc tấn công này không phải do lỗi đồng thuận chuỗi gây ra tăng phát độc hại, do đó các sàn giao dịch không cần biện pháp phòng ngừa bổ sung.
Tổng kết
Cùng với sự phát triển của môi trường đa chuỗi, tấn công replay dần trở thành hình thức tấn công phổ biến từ lý thuyết. Nhà phát triển cần cân nhắc kỹ lưỡng thiết kế giao thức, khi thiết kế cơ chế ký tin nhắn, nên tích hợp các yếu tố như chainId vào nội dung ký, tuân thủ các thực hành tốt nhất liên quan, nhằm ngăn chặn tổn thất tài sản cho người dùng.
Cobo là tổ chức lưu ký tiền mã hóa lớn nhất khu vực châu Á - Thái Bình Dương, kể từ khi thành lập đã phục vụ hơn 500 tổ chức hàng đầu trong ngành và cá nhân có tài sản lớn, cung cấp dịch vụ xuất sắc. Trên cơ sở đảm bảo an toàn lưu trữ tài sản mã hóa, Cobo còn mang lại lợi nhuận ổn định, được người dùng toàn cầu tin tưởng. Cobo chuyên xây dựng hạ tầng mở rộng, cung cấp nhiều giải pháp như lưu ký an toàn, tăng trưởng tài sản, tương tác trên chuỗi và xuyên chuỗi xuyên lớp cho tổ chức quản lý nhiều loại tài sản, hỗ trợ mạnh mẽ nhất cho việc chuyển đổi Web 3.0. Các mảng kinh doanh của Cobo bao gồm Cobo Custody, Cobo DaaS, Cobo MaaS, Cobo StaaS, Cobo Ventures, Cobo DeFi Yield Fund,... đáp ứng đa dạng nhu cầu của bạn.
Chào mừng tham gia cộng đồng chính thức TechFlow
Nhóm Telegram:https://t.me/TechFlowDaily
Tài khoản Twitter chính thức:https://x.com/TechFlowPost
Tài khoản Twitter tiếng Anh:https://x.com/BlockFlow_News














