BNB 零元购? — Meter.io 桥被黑分析

区块链安全 2年前 (2022) admin
729 0 0

前言

2022 年 2 月 6 日,一大早醒来又有桥暴雷了,继 Qubit Finance  QBridge 被黑之后,又有一个跨链桥被黑了。为什么我这次要提 Qubit Finance 呢?原因在于,这两桥被黑的细节太相识了,所以放在了一起。和往常一样,话不多说,直接开始技术分析。

BNB 零元购? — Meter.io 桥被黑分析

技术细节分析

本次被黑的桥是用于 BSC  MOON 链的一个桥,从 ps 透露的交易来看(https://moonriver.moonscan.io/tx/0x5a87c24d0665c8f67958099d1ad22e39a03aa08d47d00b7276b8d42294ee0591),这里已经是跨链接过去到 MOON 链进行销赃的交易了。

BNB 零元购? — Meter.io 桥被黑分析

其中的 BNB.bsc  ETH 都是真实的代币。同时为了验证我们的想法,我们通过查看攻击者的交易记录,不难发现这里已经是第二案发现场了

BNB 零元购? — Meter.io 桥被黑分析

同时攻击者的代币流转记录也应证了这一点,钱无端端就变出来了 😀

BNB 零元购? — Meter.io 桥被黑分析

看过 Qubit 被黑分析的朋友都会知道,这里很明显不是第一案发现场,那么为了寻找第一案发现场,结合这个桥是 BSC  MOON 的桥,我们自然要去 BSC 上寻找踪迹。通过查询攻击者同个地址(0x8d3d13cac607b7297ff61a5e1e71072758af4d01)在 BSC 上的操作,我们不难找到攻击者的第一案发现场。

BNB 零元购? — Meter.io 桥被黑分析

如上图所示,攻击者是直接调用了 Meter.io  Deposit 函数进行充值。我们选取其中的一笔交易,竟发现攻击者什么代币都没有转  :D,妥妥的 零元购 行为啊!

BNB 零元购? — Meter.io 桥被黑分析

联想 Qubit 被黑的那次是 EOA 的问题,这次会是同样的问题吗?为了探究这个问题,我们需要深入到合约代码中进行细节的发现,由于调用的是 deposit 函数,我们直接对 deposit 函数进行分析

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);
}

通过分析代码,不难发现,其实 deposit 函数什么都没有做,具体的逻辑是在 #13 行 depositHandler  deposit 函数进行实现的。在经过充值之后,就会声明一个事件出来,这个事件的作用就很明显了,就是给 relayer 作为跨链消息来用的。

由于这个 deposit 函数的逻辑实现和 Qubit Finance 实在是太像了,我不得不顺手看了下有没有 depositEth 函数,结果还真的找到了。。

function depositETH(uint8 destinationChainID, bytes32 resourceID, bytes calldata data) external payable whenNotPaused {
uint256 fee = _getFee(destinationChainID);

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

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

uint256 value = msg.value - fee;
uint256 amount;
assembly {
amount := calldataload(0x84)
}
require (amount == value, "msg.value and data mismatched");

address wtokenAddress = IERCHandler(handler)._wtokenAddress();
require(wtokenAddress != address(0), "_wtokenAddress is 0x");
IWETH(wtokenAddress).deposit{value: value}();
IWETH(wtokenAddress).transfer(address(handler), value);

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);
}

抛开这个函数所有的中间逻辑,不难发现这两个函数实际上都是声明同一个 Deposit 事件的,那么顺着上次 Qubit 被黑的思路,是不是一下子就能想到可以通过充值 ERC20 来达到充值 ETH 的效果 :D。难道说这次又是 EOA 的问题?先不着急下结论,继续深入看 depositHandler 的逻辑实现。

function deposit(
bytes32 resourceID,
uint8 destinationChainID,
uint64 depositNonce,
address depositer,
bytes calldata data
) external override onlyBridge {
bytes memory recipientAddress;
uint256 amount;
uint256 lenRecipientAddress;

assembly {

amount := calldataload(0xC4)

recipientAddress := mload(0x40)
lenRecipientAddress := calldataload(0xE4)
mstore(0x40, add(0x20, add(recipientAddress, lenRecipientAddress)))

calldatacopy(
recipientAddress, // copy to destinationRecipientAddress
0xE4, // copy from calldata @ 0x104
sub(calldatasize(), 0xE) // copy size (calldatasize - 0x104)
)
}

address tokenAddress = _resourceIDToTokenContractAddress[resourceID];
require(_contractWhitelist[tokenAddress], "provided tokenAddress is not whitelisted");

// ether case, the weth already in handler, do nothing
if (tokenAddress != _wtokenAddress) {
if (_burnList[tokenAddress]) {
burnERC20(tokenAddress, depositer, amount);
} else {
lockERC20(tokenAddress, depositer, address(this), amount);
}
}

_depositRecords[destinationChainID][depositNonce] = DepositRecord(
tokenAddress,
uint8(lenRecipientAddress),
destinationChainID,
resourceID,
recipientAddress,
depositer,
amount
);
}

定位到 depositHandler 函数的实现,不难发现在 #30-34行有关于 token 逻辑的一些处理,其中包含了一个对 _wtokenAddress 的判断,如果不是 _wtokenAddress 的话,会根据 tokenAddress 进行 burn 或者 lock 的处理。但如果是 _wtokenAddress 的话,这个逻辑就直接不走了。那么回想上文的 Bridge 中的 deposit 逻辑,由于 deposit 函数本身没有充值逻辑的检查,那么如果我们可以充值一个 tokenAddress != _wtokenAddress 的代币,是不是就可以实现正确的 Deposit 事件声明,并且不需要转移任何代币呢?听起来是不是和攻击者的行为很像?

通过代码,不难发现 tokenAddress 是由 resourceID 进行获取的,那么为了知道对应的 tokenAddress 地址,就需要从攻击者交易的 resourceID 进行入手,通过检查对应的交易,我们拿到了对应的 resourceID 数据(0x0000000000000000000000bb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c01),然后通过合约查询到对应的 tokenAddress  0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c(大家现在查是查不到的,因为这个 ID 已经被改了 :D),正好是 _wtokenAddress 的地址,而这个地址正好是 WBNB 的地址

BNB 零元购? — Meter.io 桥被黑分析

这下就舒服了,也就是说攻击者用 deposit 函数充值了 WBNB 代币,然后直接掠过了 depositHandler 的检查。真正实现了 零元购

总结

虽然 Meter.io 的架构和 Qubit 很像,但是出问题的点却是不一样的,但同样的问题都是用非预期的函数实现了预期之外的功能。


原文始发于微信公众号(蛋蛋的区块链笔记):BNB 零元购? — Meter.io 桥被黑分析

版权声明:admin 发表于 2022年2月6日 上午8:47。
转载请注明:BNB 零元购? — Meter.io 桥被黑分析 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...