本文详细探讨了跨链安全中的一种重入攻击向量,指出在构建跨链NFT合约时可能面临的安全风险。通过对危险外部调用的分析,作者提出了如何可能利用这一漏洞进行攻击的策略,并提供了针对性的解决方案。
在跨链安全中,必须同时考虑多个不同实例的合约,这一范式引入了新的概念挑战。在单链上被认为安全的东西,可能在跨链协议中存在严重的安全漏洞。一些经典的例子是签名和重放攻击。
在本文中,我们将回顾在我们的一次审计中发现的一个攻击向量,该向量可以在消息传递的跨链环境中被利用。该攻击是通过人工审查发现并进行了测试。
内容:
为了简化起见,我将提供合约的简化版本。在详细解释攻击的同时,在文章最后,我们将提供一个攻击者合约的示例,并使用 Mumbai 和 Goerli 进行测试。桥接机制也将被简化,不需要了解不同链之间的魔法是如何工作的,如果你感兴趣,我们可以在未来的文章中讨论这个问题。
如何创建可在两个不同链上使用的跨链 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");
一旦你发现了漏洞,探索代码中所有可能的路径以损害合约。
攻击者合约可以调用哪些其他功能?
我之前告诉你要关注被外部调用后递增的 ID 计数器变量。
一个可能的攻击路径可以是:
这将导致在不同链上存在两个相同 ID 的副本。
我们现在已经到了最后一步,这涉及编写一个攻击者合约并使用 Goerli 和 Mumbai 测试这个攻击。如果你对这一部分不感兴趣,可以跳到结论部分。
这将是我们的攻击者合约:
我们将使用以下地址:
Goerli中的 CrossChainWarriors 合约 (chainA)
Mumbai中的 CrossChainWarriors 合约 (chainB)
那么让我们看看黑客发起的攻击交易:
在 chainA 中,攻击似乎已经成功。 ID #1 被铸造后被销毁以进行转移,并再次被铸造。
让我们检查 CrossChainTransfer 事件:
验证者获取地址和来自事件的消息来执行链B上的交易:
消息:(96 字节)
0x0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000b6fa6b5ac9c88ae7a422ac479e798978186b1b600000000000000000000000000011fcc414149402dfd8629d71b8823f52d7ca0b
验证者在链B上的交易:
我们可以看到交易的输入数据与事件的消息相符(96 字节):
攻击已成功,地址 0x0011fCC414149402DFd8629D71b8823f52d7Ca0B 在两个链上都是 ID #1 的拥有者。
如果在外部调用之前进行代币 ID 的递增,则此攻击将不可能发生。
另一种解决方案是使用 OpenZeppelin 的 ReentrancyGuard 合约模块。
我们已经看到了多链 NFT 合约的基本功能和消息传递跨链环境的攻击面,侧重于安全库执行的外部调用。在单链环境中,这种攻击将不可能发生。
良好的安全实践必须被了解和理解。始终检查你的代码,安全代码通常伴随攻击面扩展,不要依赖于库的名称。
原始报告:
https://quantumbrief.io/#trusted-by
- 原文链接: medium.com/@mateocesaron...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!