跨链重入攻击

本文详细探讨了跨链安全中的一种重入攻击向量,指出在构建跨链NFT合约时可能面临的安全风险。通过对危险外部调用的分析,作者提出了如何可能利用这一漏洞进行攻击的策略,并提供了针对性的解决方案。

跨链重入性

在跨链安全中,必须同时考虑多个不同实例的合约,这一范式引入了新的概念挑战。在单链上被认为安全的东西,可能在跨链协议中存在严重的安全漏洞。一些经典的例子是签名和重放攻击。

在本文中,我们将回顾在我们的一次审计中发现的一个攻击向量,该向量可以在消息传递的跨链环境中被利用。该攻击是通过人工审查发现并进行了测试。

内容:

  1. 构建跨链 NFT 合约
  2. 桥接抽象
  3. 安全库
  4. 危险的外部调用利用
  5. Solidity 中的攻击者合约示例

为了简化起见,我将提供合约的简化版本。在详细解释攻击的同时,在文章最后,我们将提供一个攻击者合约的示例,并使用 Mumbai 和 Goerli 进行测试。桥接机制也将被简化,不需要了解不同链之间的魔法是如何工作的,如果你感兴趣,我们可以在未来的文章中讨论这个问题。

构建跨链 NFT 合约

如何创建可在两个不同链上使用的跨链 NFT 收藏品,我们应该考虑哪些预防措施?

假设我们想创建一个可在两个不同链上使用的 NFT 收藏品,称为 chainA 和 chainB。

我们可以通过在两个链上部署 NFT 合约,并使用桥接维护单个实例,来非常简单地做到这一点。这确保了给定 ID 只有一个拥有者,防止创建相同 ID 的多个副本,这可能导致可替代代币的结果。我们还需要一个在支持的链之间转移代币的功能。

在这个例子中,NFT 收藏品的名称是 crossChainWarriors。如果用户在 chainA 上拥有一个战士并想将其转移到 chainB,可以通过在 chainA 上销毁它并使用桥接的魔法和安全性在 chainB 上铸造它来简单实现。

crossChainWarrior.sol

桥接抽象

一个小图示将帮助我们理解我们的现状。

正如我们在图中所见,在 chainA 执行跨链转账会触发一个事件,该事件被桥接验证者监听。然后,他们使用提取的数据调用目标合约。

安全库

让我们看看 crossChainWarriors 的 mint 函数:

一切正常,但让我们探索一下 _safeMint 的作用:

该函数的逻辑与 _mint 相同,但它执行额外检查以确保接收者能够处理 ERC721 功能。这在接收者不是 EOA 时很重要。

checkOnERC721Received 在做什么?

这是一大段代码,但我们只关注这部分:

请注意,正在执行对接收者的 外部调用。如果你在考虑重入攻击,外部调用是黑客的后门。

这个外部调用用于询问接收者他是否能够处理 ERC721,更具体地说,是否实现IERC721receiver接口。接收者必须返回 IERC721Receiver.onERC721Received.selector 以通过检查,但在此之前,合约还可以执行其他交易。

了解 ERC721 接收者接口的最简单合约是:

如下面的代码所示,在返回选择器之前,你可以做任何你想做的事情,只是不要忘记在最后返回选择器。

但这个合约真的能处理 ERC721 转移吗?

答案显然是否定的,发送到这个合约的 NFTs 将永远丢失。

简单来说,_safeMint 的想法是提供额外的检查,以保护用户不丢失他们的代币。但这个检查仅验证接收者是否了解 IERC721Receiver,并不能保证接收者能够处理 ERC721。此外,它伴随着一个不安全的外部调用的额外成本。

现在是考虑如何利用这个不安全的外部调用来损害合约的时候了。

危险的外部调用利用

让我们返回到原来的 mint 函数,专注于第三行。

现在考虑这一点:

执行可以在第 3 行被中断,允许在更新状态变量之前执行外部调用。请密切关注在此函数中修改的状态变量及其修改的位置。

tokenIds.increment()

让我们尝试黑客合约,了解为什么在进行外部调用之前更新状态变量是最佳实践。

一个自然的初步方法是再次调用 mint()

但 ERC721 的 mint 内部函数将失败,因为代币 ID 并未增加且已存在。

require(!_exists(tokenId), "ERC721: token already minted");

一旦你发现了漏洞,探索代码中所有可能的路径以损害合约。

攻击者合约可以调用哪些其他功能?

  • crossChainTransfer():

我之前告诉你要关注被外部调用后递增的 ID 计数器变量。

一个可能的攻击路径可以是:

  1. 调用 chainA 中的 mint(),设置攻击者合约为接收者
  2. 重新进入合约,调用 crossChainTransfer 将新 ID 转移到 chainB。
  3. 再次在 chainA 上调用 mint()

这将导致在不同链上存在两个相同 ID 的副本。

我们现在已经到了最后一步,这涉及编写一个攻击者合约并使用 Goerli 和 Mumbai 测试这个攻击。如果你对这一部分不感兴趣,可以跳到结论部分。

Solidity 中的攻击者合约示例

这将是我们的攻击者合约:

我们将使用以下地址:

  • 0x1491a65c4DE0fEe28f8bFEB185eD76fAB49c5a56 作为桥接验证者
  • 0x0011fCC414149402DFd8629D71b8823f52d7Ca0B 作为黑客
  • 0xb6Fa6b5Ac9C88AE7a422ac479E798978186B1B60 作为攻击者合约
  • 0x50287aFea592863C2f30eacFBca54aaC6e85BF02 作为

Goerli中的 CrossChainWarriors 合约 (chainA)

  • 0xCB0fc01dE73aa06F5A443d9F7b682d583E9Aa684 作为

Mumbai中的 CrossChainWarriors 合约 (chainB)

那么让我们看看黑客发起的攻击交易:

Goerli 交易哈希 (Txhash) 详情 | Etherscan \ \ Goerli (GTH) 对 txhash 0xc403c8694e30dc90bbfa4ae1a62ee424fb285e97e6208d68ddf23b313c5a1bec 的详细交易信息…\ \ goerli.etherscan.io

在 chainA 中,攻击似乎已经成功。 ID #1 被铸造后被销毁以进行转移,并再次被铸造。

让我们检查 CrossChainTransfer 事件:

验证者获取地址和来自事件的消息来执行链B上的交易:

消息:(96 字节)

0x0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000b6fa6b5ac9c88ae7a422ac479e798978186b1b600000000000000000000000000011fcc414149402dfd8629d71b8823f52d7ca0b

验证者在链B上的交易:

我们可以看到交易的输入数据与事件的消息相符(96 字节):

Polygon 交易哈希 (Txhash) 详情 | PolygonScan \ \ 0x39cc33a94ba95ce6d1cb539f6dd2d51b664df275f12b846bec7c55c6cb0d1a47 9 分钟前(2022 年 10 月 14 日 10:16:05 PM +UTC)0…\ \ mumbai.polygonscan.com

攻击已成功,地址 0x0011fCC414149402DFd8629D71b8823f52d7Ca0B 在两个链上都是 ID #1 的拥有者。

解决方案?

如果在外部调用之前进行代币 ID 的递增,则此攻击将不可能发生。

另一种解决方案是使用 OpenZeppelin 的 ReentrancyGuard 合约模块。

结论

我们已经看到了多链 NFT 合约的基本功能和消息传递跨链环境的攻击面,侧重于安全库执行的外部调用。在单链环境中,这种攻击将不可能发生。

良好的安全实践必须被了解和理解。始终检查你的代码,安全代码通常伴随攻击面扩展,不要依赖于库的名称。

原始报告:

https://quantumbrief.io/#trusted-by

  • 原文链接: medium.com/@mateocesaron...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
mateocesaroni_11308
mateocesaroni_11308
江湖只有他的大名,没有他的介绍。