Cobo 安全团队 - ETH 硬分叉中的隐藏风险和套利机会
本文由 Cobo 区块链安全团队提供。团队成员来自知名区块链安全厂商,具有丰富的智能合约审计经验。他们在多个 DeFi 项目中发现了高风险漏洞。团队目前专注于智能合约安全、DeFi安全等方向,研究和分享前沿的区块链安全技术。
我们也希望在加密数字货币领域具有研究精神和科学方法论的终身迭代学习者能够加入我们,向行业输出思维见解和研究观点!
这是Cobo的第16篇文章
前言
随着 ETH 升级为 PoS 共识系统,原 PoW 机制的 ETH 链在部分社区(以下简称 ETHW)的支持下成功硬分叉。但是,由于一些链上协议在设计之初没有为可能的硬分叉做好准备,相应的协议在ETHW分叉链中存在一定的安全风险,其中最严重的安全风险是重放攻击。
硬分叉完成后,ETHW 主网上至少有 2 次使用重放机制的攻击,分别是重放攻击和重放攻击。本文将以这两个事件为例,分析重放攻击对分叉链的影响,以及协议应该如何防止此类攻击。
重播类型
首先,在开始分析之前,我们需要对重放攻击的类型有一个初步的了解。一般来说,我们将重放攻击分为两类,即交易重放和签名消息重放。接下来我们来说说这两种播放机制的区别。
交易重播
交易重放是指将原链中的交易原封不动地迁移到目标链的操作。它属于事务级别的重放。重放后可以正常执行交易eth分叉时间,完成交易验证。最著名的案例是平台上的攻击事件,直接导致了超过2000万个OP代币的损失。但在EIP 155实施后,由于交易本身的签名有(用于区分链本身与其他分叉链的标识符),如果重放的目标链不同,则交易本身无法重放。的。
签名消息重播
签名消息重放不同于交易重放。它用于重放使用私钥签名的消息(例如 Cobo 是最好的)。在签名消息重放中,攻击者不需要重放整个交易,而只需重放签名消息。在消息签名中,最好以 Cobo 为例,由于消息不包含任何与链相关的特殊参数,因此消息在签名后理论上可以在任何分叉链中有效。可以检查。为了避免分叉上的消息重播,可以在消息内容中加上,比如Cobo最好+()。携带特定链标识后,不同分叉链上的消息内容不同,消息签名也不同,无法直接重放和重用。
和攻击原理
让我们来分析和攻击原理。首先得出结论,这两种攻击本身并不是事务重放攻击。原因是 ETHW 使用的和 ETH 主网不同,所以无法验证直接重放交易。那么剩下的唯一选择就是消息重放了,那么我们来一一分析一下它们各自在ETHW分叉链上是如何被消息重放攻击的。
它是xDAI与ETH主网之间进行资产转移的桥梁。它主要依靠桥提交的指定跨链消息来完成跨链资产的转移。中,提交的验证消息的逻辑是这样的
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行对数据报文进行解码。从解码内容不难发现,返回的字段中包含字段,那么是不是表示签名的消息无法播放呢?我们继续分析。
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);
}
通过跟踪函数,发现在#L11行检查了函数的合法性
function _isDestinationChainIdValid(uint256 _chainId) internal returns (bool res) {
return _chainId == sourceChainId();
}
function sourceChainId() public view returns (uint256) {
return uintStorage[SOURCE_CHAIN_ID];
}
通过继续分析后面的函数逻辑,不难发现,实际的校验并没有使用evm原生的来获取链本身,而是直接使用了变量中存储的值,那么这个值显然是由管理员 所以可以认为消息本身没有链身份eth分叉时间,所以理论上可以重放签名的消息。
由于在硬分叉期间,分叉前的所有状态在两条链上都会保持不变,后续的 xDAI 团队没有额外的操作。分叉后,Omni 合约在 ETHW 和 ETH 主网上的状态不会改变,也就是说合约不会改变。基于这种情况,我们可以推断,主网上的签名也可以在 ETHW 上进行验证。然后,由于签名消息本身不包含它,攻击者可以使用签名重放在 ETHW 上提取同一合约的资产。
与 Omni 一样,它是往返于 ETH 主网的资产转移的桥梁。与 Omni 不同,它依赖区块证明来提款。逻辑如下:
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()
);
}
通过函数逻辑不难发现,合约通过两个检查来确定消息的合法性,一个是检查总和,以确保交易实际发生在子链(Chain)中。第一次检查其实是可以绕过的,因为任何人都可以通过交易数据来构造自己的,但是第二次检查是不能绕过的,因为通过查看逻辑可以发现:
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;
}
从合同中提取相应的一项。按照这个逻辑,我们看一下设置
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这行代码中,只检查了签名数据,而没有检查链本身。由于消息是由合约签名的,理论上攻击者也可以分叉链。重放消息的签名是合法的,通过调用ETHW链中的exit函数并提交相应的交易证明,可以成功撤回并通过后续的校验。
以地址b9为例,该地址通过以下步骤成功完成了ETHW链上的套利
首先,依靠钞票能力在主网交易所提币。通过链上传递的功能存钱;通过ETH主网调用的exit函数取款;复制并提取提交的ETH主网;在 ETHW 中重放上一步提取的签名消息;在 ETHW 中调用 exit 提取货币
为什么会这样?
从上面分析的两个例子不难发现,这两个协议在ETHW上遭受重放攻击是因为协议本身没有做好防重放保护,导致协议对应的资产被掏空分叉的链。但是,由于这两个桥不支持ETHW分叉链,用户并没有遭受任何损失。但我们需要考虑的是,为什么这两座桥没有设计好重放保护措施?其实原因很简单,因为他们设计的应用场景很简单,只是用来将资产转移到自己指定的对应链上,没有多链部署的计划,所以没有保护。该协议本身没有安全隐患。
相比之下,ETHW 上的用户,由于这些桥接器本身不支持多链场景,如果用户在 ETHW 分叉链上操作,就会在 ETH 主网上受到消息重放攻击。
比如在当前的矿池合约中,有一个函数,这个函数中有变量,其中包含变量。
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(
'x19x01',
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);
}
这个变量首先定义在 . 这个变量包含,在设计之初就包含了可能的多链场景的重放预防,但是根据矿池合约的逻辑,如下:
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)
)
);
}
已经在构造函数中定义了,也就是说,硬分叉后,即使链本身发生了变化,矿池合约也无法获取新的更新。如果用户将来授权了 ETHW,那么 ETHW 上的签名可以在 ETH 主网上重放。另外,类似的协议还有很多,比如特定版本下的yearn vault合约,也是采用固定的情况。用户在 ETHW 上进行交互时,还需要防范此类协议的重放风险。
协议设计之初的注意事项
对于开发者来说,在为协议本身定制消息签名机制时,应该考虑到后续可能出现的多链场景。如果路线图中存在多链部署的可能性,则应将其作为变量添加到签名消息中。同时,在验证签名时,由于硬分叉不会在分叉前改变任何状态,所以用于验证签名消息的合约变量不应设置为合约变量,而应在之前重新获取每次验证,然后对签名进行验证以确保安全。.
对用户的影响
一般情况下,如果协议不支持分叉链,应尽量不要对分叉链进行任何操作,以防止相应的签名消息在主网上重播,导致用户在主网上丢失资产。
对交易所和托管人的影响
由于很多交易所本身都支持ETHW代币,因此这些因攻击而提取的代币可能会被充值到交易所出售。但是需要注意的是,这样的攻击并不是链共识本身的问题。交易所造成的恶意增发,因此对于交易所而言,此类攻击无需额外防范
总结
随着多链场景的发展,重放攻击从理论层面逐渐成为主流攻击手段。开发人员应仔细考虑协议设计。在设计消息签名机制时,尽量加入其他因素作为签名内容。遵循相关的最佳实践以防止用户资产丢失。
Cobo是亚太地区最大的加密货币托管机构。自成立以来,已为超过500家行业顶尖机构和高净值人士提供了优质服务。在保证加密资产安全存储的前提下,也实现了加密资产的稳定收益。深受世界各地用户的信赖。Cobo专注于构建可扩展的基础设施,为机构提供安全托管、资产增值、链上交互、多类型资产跨链跨层管理等多种解决方案,为机构向Web转型提供解决方案3.0 最强大的技术底层支持和赋能。Cobo 包括 Cobo、Cobo DaaS、Cobo MaaS、Cobo StaaS、Cobo、
原文源自微信公众号(Cobo):Cobo安全团队——ETH硬分叉中的隐患与套利机会
本文由链应用发布,不代表链应用立场,转载联系作者并注明出处:https://www.xiangboz.cn/baike/4647.html