区块链安全

Meter.io 跨链桥被黑事件分析

0x01 事件介绍

2022 年 2 月 6 日,PeckShield 在推特上披露了 Meter Passport 被黑事件。此次被黑损失了约 430 万美元(包括 1391.24945169 ETH + 2.74068396 BTC)。

Meter PassportMeter 的 bridge 跨链桥。

因为在我的博客中第一次提到跨链桥(Bridge),所以来对于跨链桥做一个小小的科普。
各种公链(如 ETHSolana、BSC、Polygon、Terra 等)彼此之间在原生设计上是无法相互沟通(发送消息的)、而且这些公链上都有很多钱(原生币和代币)。那么来想一个很实际的问题,众所周知,一些网站收钱一般都是 Terra 链上的 USDT(因为咱都知道,如果是 ETH 链上的 USDT,手续费相对高很多)。那么如果我想把一些收到的 TRC20 的 USDT 转到 tornado.cash 上进行混币,TORNADO 本身是不支持 Terra 链的,但他支持 ETH 链的 USDT。所以我现在需要把 TRC20 的 usdt 换成 ETH 链上的 usdt。那么问题来了,我怎么样把 TRC20(在这个例子中 TRON network 就是来源链)的 USDT 换成 ERC20 (在这个例子中,ETH network 就是目标链)的 USDT 呢?一种方法是通过一些交易所,另一种方法是通过 Bridge。

Bridge 跨链桥这个概念容易获得感性理解,就是跨越链接不同的公链。通过 Bridege,我们可以在两种不同的区块链之间移动代币。那么当使用 bridge 移动代币时候,实际发生了什么?它不会像那些交易所做的一样、去转换或者去交换代币。而是将来源代币通过智能合约锁在来源链中,然后在目标链中,mint 出对应数额的 wrapped 目标代币。注意这里的 wrapped 目标代币是指比如 ETH 被 wrap 成为 weth、qeth 这样。因为是目标链网络上通过智能合约定义了一个目标代币合约。然后我们可以将 wrapped 目标代币在目标链上(使用 dex 等)交换为其他代币或原生币。

0x02 寻找攻击

从 PeckShield 披露的一个交易开始分析:
https://moonriver.moonscan.io/tx/0x5a87c24d0665c8f67958099d1ad22e39a03aa08d47d00b7276b8d42294ee0591

可以看到在 Moonriver 链上,攻击者(0x8d3d13cac607b7297ff61a5e1e71072758af4d01)通过 swapExactTokensForTokens 方法,通过 UniswapV2,将 2,000 BNB.bsc 代币换为了 15.283568631316749045 ETH,最后转给了攻击者自己的账户。

看起来这并非是攻击过程,而是攻击者将黑来的代币转为 ETH 和 BTC 的换币交易之一。

顺便说一句为什么攻击者将黑来的代币喜欢换成以太坊原生币?
因为攻击者一般使用 Tornado 平台洗钱,而 Tornado 平台对于以太链的支持是比较好的:

可以看到最后黑来的钱黑客怎么销赃的呢?通过查看以太链上的黑客地址的交易记录:https://etherscan.io/address/0x8d3d13cac607b7297ff61a5e1e71072758af4d01
我们可以看到的确是去 Tornado 平台上进行了混币:

那么我们继续寻找发生攻击的交易。我们查看攻击者在 Moonriver 链上的交易记录,一共就9笔。全都是在换钱,却没有交易是关于钱是怎么来的:

通过查看攻击者的代币流转记录,可以看到 BNB.bsc 是从 0 地址转入的,一般这种情况就是 mint 出来的。

因为这是一个跨链桥,上面的这个被披露的交易中,可以看到是将 BNB.bsc 换成的 eth。这里的 BNB.bsc 是由 Meter Passport 对于 BSC 上面的 BNB 进行了 wrap 之后的代币。wrap 是什么意思呢?就是比如说在 bsc 上面,所有的代币都有一个对应的 bep20 或者 erc20 的合约,但是像 bnb、ETH 这种原生代币,区块链上天然存在,它没有对应的智能合约,那么就难以实现合约之间的一些交互功能。这样通过一个智能合约对其 wrap 一层,就使其有了跟其他的代币一样的智能合约。回到这里,所以,这个跨链是从 BSC 到 Moonriver 链的。于是呢,我们自然应该去 bsc 链上寻找攻击过程。

查看了 BSC 链上的攻击者交易历史 https://bscscan.com/address/0x8d3d13cac607b7297ff61a5e1e71072758af4d01 ,一共有19笔交易:

攻击者的钱到底是哪里来的呢?通过对 19 笔交易进行分析,可以发现钱是来自于调用 deposit 方法的交易中:

0x03 攻击交易分析

以这笔交易为例:
https://bscscan.com/tx/0xc7eb98e00d21ec2025fd97b8a84af141325531c0b54aacc37633514f2fd8ffdc

先说一下关于这个 destinationChainID 的问题。参考 meter.io 的官方文档:bsc 主网对应的 chain id 是 4,Moonriver 主网对应的 chain id 是 5:

调用了 Bridge 合约的 deposit(uint8 destinationChainID, bytes32 resourceID, bytes data) 方法:

    /**
        @notice Initiates a transfer using a specified handler contract.
        @notice Only callable when Bridge is not paused.
        @param destinationChainID ID of chain deposit will be bridged to.
        @param resourceID ResourceID used to find address of handler to be used for deposit.
        @param data Additional data to be passed to specified handler.
        @notice Emits {Deposit} event.
     */
    function deposit(uint8 destinationChainID, bytes32 resourceID, bytes calldata data) external payable whenNotPaused {
        uint256 fee = _getFee(destinationChainID);

        require(msg.value == fee, "Incorrect fee supplied");

        address handler = _resourceIDToHandlerAddress[resourceID];
        require(handler != address(0), "resourceID not mapped to handler");

        uint64 depositNonce = ++_depositCounts[destinationChainID];
        _depositRecords[depositNonce][destinationChainID] = data;

        IDepositExecute depositHandler = IDepositExecute(handler);
        depositHandler.deposit(resourceID, destinationChainID, depositNonce, msg.sender, data);

        emit Deposit(destinationChainID, resourceID, depositNonce);
    }

这种链桥合约也就是 Bridge.sol 一般部署在所有连接的链上。链下中继器被配置为监听来自这些合约的 Deposit 事件并将它们中继到其他链。参考:https://github.com/ampleforth/cross-chain-ample/wiki/Chainbridge-working

可以看到,实际是调用了 depositHandler 合约的 deposit 方法:

depositHandler.deposit(resourceID, destinationChainID, depositNonce, msg.sender, data);

上面的代码最后一行触发了 Deposit 事件,在经过充值之后,就会声明一个事件出来,这个事件的作用是给链下中继器(relayer,其实就是一台服务器)作为跨链消息来用的。

那么 handler 合约的地址是多少呢?

address handler = _resourceIDToHandlerAddress[resourceID];

通过入参我们可以获取 resourceID,然后通过 Read Contract 使用 _resourceIDToHandlerAddress 方法进行查询:

于是我们获取了 handler 合约地址 https://bscscan.com/address/0x5945241BBB68B4454bB67Bd2B069e74C09AC3D51#code,然后查看其中的 deposit 方法:

可以看到:

  • 汇编代码部分是在获取 recipientAddress 的值
  • # 1640 获得了 tokenAddress 的值
  • # 1644 验证 tokenAddress 是否等于 _wtokenAddress,如果是,什么都不做,如果不是,根据是否在 _burnList 里面选择燃烧或锁定代币。
  • # 1652 初始化一个 DepositRecord 结构体并存入字典。

所以这里有一个比较巧妙的地方,如果在 #1644 的地方,我们传入的 tokenAddress == _wtokenAddress,那么实际上我们并不用在来源链上燃烧或者锁定 erc20 代币,然后就执行完了这个 handler 里面的逻辑。然后跳到上层 bridge 里面的 deposit 函数,就会触发一个 Deposit 事件,导致在这里是 bsc 的 relayer 捕捉到这个事件,就会在目标链(这里是 Moonriver )上 mint 出 wrapped 代币。

但如果现在在 handler 合约里面使用 resourceID 进行查询,tokenAddress 是 0x0:

而 _wtokenAddress 是 WBNB,不难理解,因为上面 #1644 处的逻辑之所以这么写,是因为假设了 handler 合约中有 wbnb 代币。

所以实际上 tokenAddress != _wtokenAddress?那攻击者也无法成功啊。

但是通过对 handler 合约 的 creator 地址 https://bscscan.com/address/0xbb245d5c1d504dd5295b12b90963fd26b06bc743 的跟踪,我们发现在如下交易中,对此 resourceID 对应的 tokenAddress 做了更新,更新为了 0x0:

所以在那个 bsc 的区块高度,resourceID 为 0x0000000000000000000000bb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c01 的 _tokenAddress 到底是不是等于 _wtokenAddress 呢?

此时遇到了一个问题,我翻遍了 meter deployer 地址的全部交易记录 https://bscscan.com/txs?a=0xbb245d5c1d504dd5295b12b90963fd26b06bc743 都没有找到曾经将这个 resourceID 对应的 tokenaddress 设置为为 wbnb 的交易。

看似无解,但其实有一种可能性,就是还有其他的地址有设置 resourceID 对应的 tokenaddress 的权限度。继续在 meter deployer 地址的交易记录 https://bscscan.com/txs?a=0xbb245d5c1d504dd5295b12b90963fd26b06bc743 里面找,找到了这个交易 https://bscscan.com/tx/0x4c203978f39ad63e2bb21f0c42db295a76ac91ff203fbfbedda6d8626890b349

给 0xAF9826D8c6a132479cb5640149d16313577e1B24 这个地址授予了 role。然后对于 0xAF9826D8c6a132479cb5640149d16313577e1B24 地址查看相关交易,随便查看一笔 https://bscscan.com/tx/0x81bd667c7b6cf603f04d8117c09e8967ca2829f42d0aa81785f51fade908da55,然后表面上我们看不出来对于 resourceID 对应的 tokenAddress 的改变,实则我们对此交易使用 blockSec 的交易分析工具进行分析:
https://versatile.blocksecteam.com/tx/bsc/0x81bd667c7b6cf603f04d8117c09e8967ca2829f42d0aa81785f51fade908da55

可以很明显的看到这个交易调用了 handler 合约的 setResource 方法进行了设置:

所以也就是说,攻击过程是:

  1. 攻击者通过调用 deposit 函数,传入 resourceID,导致 tokenAddress == _wtokenAddress;
  2. 导致绕过了在来源链 burn 或者 lock 代币的过程,所以不用转入代币即可触发 Deposit 事件;
  3. 致使 relayer 捕捉到了 bsc 上面的 Deposit 事件,中继到了 Moonriver 链;
  4. 导致给 Moonriver 链上直接 mint 出了很多的 wrapped BNB.bsc。(金额是由 deposit 函数的第三个参数决定的,因为实际上不用转入钱,所以金额可以随便定义)

另外攻击也涉及 meter network,参考 https://bscscan.com/tx/0xc4d7e160c7652f2db22681aa2777c5b37937bf30375c5b2c6b2bd172ae984950,chainID 为 3:

0x04 总结

总之,是一个跨链桥上非预期的输入导致了绕过了部分逻辑的漏洞。上述交易如果不好找的话也可以从 BSC archive node 进行验证。
感谢 @Rivaill,@chiachih_wu 博士,@R4v3n 的耐心讨论与不吝赐教。后续细节将持续补充。

参考文章:
https://mp.weixin.qq.com/s/yzGuD6UGopOBATYzBKDpng
https://mp.weixin.qq.com/s/-xKQV-R6ACUIundJwqO68w