重入攻击_3
这一讲,我们将介绍NFT合约的重入攻击漏洞,并攻击一个有漏洞的NFT合约,铸造100个NFT。
NFT重入风险
我们在S01 重入攻击中讲过,重入攻击是智能合约中最常见的一种攻击,攻击者通过合约漏洞(例如fallback
函数)循环调用合约,将合约中资产转走或铸造大量代币。转账NFT时并不会触发合约的fallback
或receive
函数,为什么会有重入风险呢?
这是因为NFT标准(ERC721/ERC1155)为了防止用户误把资产转入黑洞而加入了安全转账:如果转入地址为合约,则会调用该地址相应的检查函数,确保它已准备好接收NFT资产。例如 ERC721
的 safeTransferFrom()
函数会调用目标地址的 onERC721Received()
函数,而黑客可以把恶意代码嵌入其中进行攻击。
我们总结了 ERC721
和 ERC1155
有潜在重入风险的函数:
漏洞例子
下面我们学习一个有重入漏洞的NFT合约例子。这是一个ERC721
合约,每个地址可以免费铸造一个NFT,但是我们通过重入攻击可以一次铸造多个。
漏洞合约
NFTReentrancy
合约继承了ERC721
合约,它主要有 2
个状态变量,totalSupply
记录NFT的总供给,mintedAddress
记录已铸造过的地址,防止一个用户多次铸造。它主要有 2
个函数:
- 构造函数: 初始化
ERC721
NFT的名称和代号。 mint()
: 铸造函数,每个用户可以免费铸造1个NFT。注意:这个函数有重入漏洞!
1 | contract NFTReentrancy is ERC721 { |
攻击合约
NFTReentrancy
合约的重入攻击点在mint()
函数会调用ERC721
合约中的_safeMint()
,从而调用转入地址的_checkOnERC721Received()
函数。如果转入地址的_checkOnERC721Received()
包含恶意代码,就能进行攻击。
Attack
合约继承了IERC721Receiver
合约,它有 1
个状态变量nft
记录了有漏洞的NFT合约地址。它有 3
个函数:
- 构造函数: 初始化有漏洞的NFT合约地址。
attack()
: 攻击函数,调用NFT合约的mint()
函数并发起攻击。onERC721Received()
: 嵌入了恶意代码的ERC721回调函数,会重复调用mint()
函数,并铸造100个NFT。
1 | contract Attack is IERC721Receiver{ |
Remix复现
- 部署
NFTReentrancy
合约。 - 部署
Attack
合约,参数填NFTReentrancy
合约地址。 - 调用
Attack
合约的attack()
函数发起攻击。 - 调用
NFTReentrancy
合约的balanceOf()
函数查询Attack
合约的持仓,可以看到持有100
个NFT,攻击成功。
预防方法
主要有两种办法来预防重入攻击漏洞: 检查-影响-交互模式(checks-effect-interaction)和重入锁。
- 检查-影响-交互模式:它强调编写函数时,要先检查状态变量是否符合要求,紧接着更新状态变量(例如余额),最后再和别的合约交互。我们可以用这个模式修复有漏洞的
mint()
函数:
1 | function mint() payable external { |
- 重入锁:它是一种防止重入函数的修饰器(modifier)。建议直接使用OpenZeppelin提供的ReentrancyGuard
总结
这一讲,我们介绍了NFT的重入攻击漏洞,并攻击了一个有漏洞的NFT合约,铸造了100个NFT。目前主要有两种预防重入攻击的办法:检查-影响-交互模式(checks-effect-interaction)和重入锁。