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àVàVà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
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.
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;
Mạng chính ETH gọi chức năng thoát của Cầu đa giác để rút tiền;
Sao chép và trích xuất headerRoot được gửi bởi người đề xuất mạng chính ETH;
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;
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.