2021 年 08 月 10 日,据慢雾区消息,跨链互操作协议 Poly Network 遭受黑客攻击,慢雾安全团队第一时间介入分析,并将分析结果分享如下。
攻击背景
Poly Network 是由 Neo、Ontology、Switcheo 基金会共同作为创始成员,分布科技作为技术提供方共同发起的跨链组织。如下图,通过官方的介绍我们可以清楚的看出 Poly Network 的架构设计:用户可以在源链上发起跨链交易,交易确认后由源链 Relayer 将区块头信息同步至 Poly Chain,之后由 Poly Chain 将区块头信息同步至目标链 Relayer,目标链 Relayer 将验证信息转至目标链上,随后在目标链进行区块头验证,并执行用户预期的交易。以下是本次攻击涉及的具体地址:
攻击核心
1、源链未对发起的跨链操作的数据进行检查。
2、目标链未对解析出的目标调用合约以及调用参数进行检查。
3、EthCrossChainData 合约的 owner 为 EthCrossChainManager。
4、 bytes4(keccak256(abi.encodePacked(_method, '(bytes,bytes,uint64)')))
可以被 hash 碰撞。
攻击细节
Poly Network 会在各个链上部署智能合约以便进行跨链互操作(分析将以在以太坊部署的智能合约为例),其中 EthCrossChainManager 合约用于验证 Poly Chain 同步来的区块头以确认跨链信息的真实。EthCrossChainData 合约用于存储跨链数据,中继链验证人 (即 Keeper) 的公钥也存储在这个合约中。LockProxy 则用于资产管理。
本次攻击中,攻击者分两步来完成这次攻击,我们接下来进行详细分析:
首先攻击者通过在其他链调用 crossChain 函数构造数据发起跨链交易。
我们切入此函数进行分析:
EthCrossChainManager.crossChain
(https://github.com/polynetwork/eth-contracts/blob/master/contracts/core/cross_chain_manager/logic/EthCrossChainManager.sol#L91)
从上图我们可以清晰的看出,此函数只是用于帮助用户构造 makeTxParam 并存储了构造后的哈希以便后续验证,其并未对用户传入的跨链操作参数进行任何限制,因此攻击者完全可以通过构造任意想构造的数据而让 Relayer 毫无防备的将其同步至 Poly Chain,通过 Poly Chain 将其同步至以太坊 Relayer。
随后在以太坊上的 Relayer 通过调用 EthCrossChainManager 合约中
的 verifyHeaderAndExecuteTx 函数提交区块头信息来验证这笔跨链信息的真实性。
我们切入此函数进行分析:
EthCrossChainManager.verifyHeaderAndExecuteTx
(https://github.com/polynetwork/eth-contracts/blob/master/contracts/core/cross_chain_manager/logic/EthCrossChainManager.sol#L127)
通过上图代码我们可以看出其先对区块头进行反序列化,以解出所需要验证的具体信息。随后调用 getCurEpochConPubKeyBytes 函数从 EthCrossChainData 合约中获取 Keeper 公钥,并通过 deserializeKeepers 函数得到 Keeper 地址。
接下来将通过 ECCUtils.verifySig 验证签名是否为 Keeper,从以下代码中我们可以发现 verifySig 函数中会切出签名者的 v r s,并通过 ecrecover 接口获取签名者地址,然后调用 containMAddresses 函数循环比较签名者是否为 Keeper,只要 Keeper 签名数量符合要求即可通过检查,数量要求即为 EthCrossChainManager 合约传入的 n - ( n - 1) / 3)。
签名验证后会通过 ECCUtils.merkleProve 进行默克尔根验证,只要是正常跨链操作即可通过此项检查。随后会对交易是否重复发送进行检查并存储已验证后的数据。这里只需保证不重复提交即可。
最后,也是最关键的一步,其将通过内部调用 _executeCrossChainTx 函数执行构造的数据。
从上图我们可以看出 _executeCrossChainTx 函数未对传入的 _toContract、_method 等参数进行检查就直接以 _toContract.call 的方式执行交易。
其中通过链上数据我们可以看出 EthCrossChainData 合约的 owner 即为 EthCrossChainManager 合约,而先前我们知道中继链验证人 (即 Keeper) 的公钥存在 EthCrossChainData 合约中,且此合约存在 putCurEpochConPubKeyBytes 函数可以直接修改 Keeper 公钥。
经过以上分析,结果已经很明确了,攻击者只需在其他链通过 crossChain 正常发起跨链操作的交易,此交易目的是为了调用 EthCrossChainData 合约的 putCurEpochConPubKeyBytes 函数以修改 Keeper 角色。随后通过正常的跨链流程,Keeper 会解析用户请求的目标合约以及调用参数,构造出一个新的交易提交到以太坊上。这本质上也只是一笔正常的跨链操作,因此可以直接通过 Keeper 检查与默克尔根检查。最后成功执行修改 Keeper 的操作。
但我们注意到 putCurEpochConPubKeyBytes 函数定义为
function putCurEpochConPubKeyBytes(bytes calldata curEpochPkBytes) external returns (bool);
而 _executeCrossChainTx 函数执行的定义为
abi.encodePacked(bytes4(keccak256(abi.encodePacked(_method, '(bytes,bytes,uint64)')))
我们可以知道这两个函数的函数签名在正常情况下传入的 _method 为 putCurEpochStartHeight 肯定是完全不同的,因此通过 _toContract.call 理论上是无法调用到 putCurEpochStartHeight 函数的。但 _method 是攻击者可以控制的,其完全可以通过枚举各个字符组合以获得与调用 putCurEpochConPubKeyBytes 函数相同的函数签名,这要求其只需枚举前 4 个字节符合即可。我们也可以自己尝试枚举验证,如下所示:
可以看出前四个字节与 putCurEpochConPubKeyBytes 函数是一致的
至此我们就已还原出攻击者的攻击细节。
通过解析链上数据,我们可以发现攻击者将 Keeper 替换为了
0xA87fB85A93Ca072Cd4e5F0D4f178Bc831Df8a00B。
最后攻击者只需使用替换后的 Keeper 地址进行签名即可通过所有检查执行调用 LockProxy 合约将其管理的资产转出。
攻击流程
1、攻击者在源链精心构造一笔修改目标链 Keeper 的操作。
2、利用官方 Relayer 正常在目标链提交数据并执行替换 Keeper 操作。
3、攻击者通过替换后的 Keeper 地址对其转出资产的操作进行签名提交至 EthCrossChainManager 进行验证。
4、验证 Keeper 为攻击者已替换完的地址通过检查,执行将资产转移至攻击者指定地址。
5、获利走人。
MistTrack 分析过程
慢雾 AML 团队分析统计,本次攻击损失共计超 6.1 亿美元!
具体如下:
资金流向分析
慢雾 AML 旗下 MistTrack 反洗钱追踪系统分析发现,攻击者初始的资金来源是门罗币(XMR)。
然后在交易所里换成了 BNB/ETH/MATIC 等币种并分别提币到 3 个地址,不久后在 3 条链上发动攻击。
事件梳理
资金情况(截止到北京时间 08 月 11 日 13:00)
BSC 上:
黑客地址 1,黑客将近 1.2 亿美元(包括约 3210 万枚 BUSD 和约 8760 万枚 USDC)的流动性添加到 Curve 分叉项目 Ellipsis Finance 中,目前仍在做市无异动。
Polygon上:
资金无异动。
Ethereum 上:
1)黑客地址 3,只有一笔转出 13.37 ETH 到地址0xf8b5c45c6388c9ee12546061786026aaeaa4b682 的交易;
2)黑客在 Curve 上添加了超 9706 万美元(包括 67 万枚 DAI 和 9638 万枚 USDC)的流动性。后又撤销流动性将 9638 万枚 USDC 和 67 万枚 DAI 换成 9694 万枚 DAI,这笔资金仍停留在地址 3。目前,3343 万枚 USDT 已被 Tether 冻结。
疑难问答
注:eccm 为 EthCrossChainManager 合约的简称,eccd 为 EthCrossChainData 合约的简称。
问:为什么 keeper 能更换成功,合约代码没有进行鉴权吗?
答:eccd 合约有进行鉴权,仅允许 owner 调用 putCurEpochConPubKeyBytes 更改 keeper,因为 eccd 合约的 owner 是 eccm,所以通过 eccm 可以更改 keeper 的值。
问:为什么能签名一笔更换 keeper 的交易?
答:因为跨链要执行的数据没有判断好 toContract,所以可能原先的 keeper 以为是一笔正常的跨链交易就签名了, 但是他是一笔更换 keeper 的交易。
问:为什么能绕过代码 bytes4(keccak256(abi.encodePacked(_method, '(bytes,bytes,uint64)')))的这个限制,然后执行 putCurEpochConPubKeyBytes(bytes) 函数?
答:函数签名用的是 keccak-256 进行哈希,然后取前面的 4bytes,这种情况下是较容易被 hash 碰撞的。
问:黑客更换 keeper 的交易如何被旧的 keepers 签名?
答:keepers 是一个链中继器(Replayer),会对所有正常用户的跨链请求进行签名。当用户在BSC上发起跨链交易时,keepers 会解析用户请求的目标合约以及调用参数,构造出一个新的交易提交到以太坊上,并在以太坊上用 eccm 合约调用用户交易里包含的目标合约。黑客替换 keeper 的交易本质上也是一笔正常的跨链交易,只不过调用的目标合约是 eccd 合约,调用的参数是更换 keeper,所以能被正常签名。
总结
本次攻击主要在于 EthCrossChainData 合约的 keeper 可由 EthCrossChainManager 合约进行修改,而 EthCrossChainManager 合约的 verifyHeaderAndExecuteTx 函数又可以通过 _executeCrossChainTx 函数执行用户传入的数据。因此攻击者通过此函数传入精心构造的数据修改了 EthCrossChainData 合约的 keeper 为攻击者指定的地址,并非网传的是由于 keeper 私钥泄漏导致这一事件的发生。