|
| 1 | +--- |
| 2 | +title: S07. 坏随机数 |
| 3 | +tags: |
| 4 | + - solidity |
| 5 | + - security |
| 6 | + - random |
| 7 | +--- |
| 8 | + |
| 9 | +# WTF Solidity 合约安全: S07. 坏随机数 |
| 10 | + |
| 11 | +我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 |
| 12 | + |
| 13 | +推特:[@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) |
| 14 | + |
| 15 | +社区:[Discord](https://discord.wtf.academy)|[微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[官网 wtf.academy](https://wtf.academy) |
| 16 | + |
| 17 | +所有代码和教程开源在github: [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity) |
| 18 | + |
| 19 | +----- |
| 20 | + |
| 21 | +这一讲,我们将介绍智能合约的坏随机数(Bad Randomness)漏洞和预防方法,这个漏洞经常在 NFT 和 GameFi 中出现,包括 Meebits,Loots,Wolf Game等。 |
| 22 | + |
| 23 | +## 伪随机数 |
| 24 | + |
| 25 | +很多以太坊上的应用都需要用到随机数,例如`NFT`随机抽取`tokenId`、抽盲盒、`gamefi`战斗中随机分胜负等等。但是由于以太坊上所有数据都是公开透明(`public`)且确定性(`deterministic`)的,它没有其他编程语言一样给开发者提供生成随机数的方法,例如`random()`。很多项目方不得不使用链上的伪随机数生成方法,例如 `blockhash()` 和 `keccak256()` 方法。 |
| 26 | + |
| 27 | +坏随机数漏洞:攻击者可以事先计算这些伪随机数的结果,从而达到他们想要的目的,例如铸造任何他们想要的稀有`NFT`而非随机抽取。更多的内容可以阅读 [WTF Solidity极简教程 第39讲:伪随机数](https://github.com/AmazingAng/WTF-Solidity/tree/main/39_Random)。 |
| 28 | + |
| 29 | + |
| 30 | + |
| 31 | +## 坏随机数案例 |
| 32 | + |
| 33 | +下面我们学习一个有坏随机数漏洞的 NFT 合约: BadRandomness.sol。 |
| 34 | + |
| 35 | +```solidity |
| 36 | +contract BadRandomness is ERC721 { |
| 37 | + uint256 totalSupply; |
| 38 | +
|
| 39 | + // 构造函数,初始化NFT合集的名称、代号 |
| 40 | + constructor() ERC721("", ""){} |
| 41 | +
|
| 42 | + // 铸造函数:当输入的 luckyNumber 等于随机数时才能mint |
| 43 | + function luckyMint(uint256 luckyNumber) external { |
| 44 | + uint256 randomNumber = uint256(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp))) % 100; // get bad random number |
| 45 | + require(randomNumber == luckyNumber, "Better luck next time!"); |
| 46 | +
|
| 47 | + _mint(msg.sender, totalSupply); // mint |
| 48 | + totalSupply++; |
| 49 | + } |
| 50 | +} |
| 51 | +``` |
| 52 | + |
| 53 | +它有一个主要的铸造函数 `luckyMint()`,用户调用时输入一个 `0-99` 的数字,如果和链上生成的伪随机数 `randomNumber` 相等,即可铸造幸运 NFT。伪随机数使用 `blockhash` 和 `block.timestamp` 声称。这个漏洞在于用户可以完美预测生成的随机数并铸造NFT。 |
| 54 | + |
| 55 | +下面我们写个攻击合约 `Attack.sol`。在攻击函数 `attackMint()`中的参数为 `BadRandomness`合约地址。在函数中,我们计算了随机数 `luckyNumber`,然后将它作为参数输入到 `luckyMint()` 函数完成攻击。由于`attackMint()`和`luckyMint()`将在同一个区块中调用,`blockhash`和`block.timestamp`是相同的,利用他们生成的随机数也相同。 |
| 56 | + |
| 57 | +```solidity |
| 58 | +contract Attack { |
| 59 | + function attackMint(BadRandomness nftAddr) external { |
| 60 | + // 提前计算随机数 |
| 61 | + uint256 luckyNumber = uint256( |
| 62 | + keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)) |
| 63 | + ) % 100; |
| 64 | + // 利用 luckyNumber 攻击 |
| 65 | + nftAddr.luckyMint(luckyNumber); |
| 66 | + } |
| 67 | +} |
| 68 | +``` |
| 69 | + |
| 70 | +## `Remix` 复现 |
| 71 | + |
| 72 | +由于 Remix 自带的 Remix VM不支持 `blockhash`函数,因此你需要将合约部署到以太坊测试链上进行复现。 |
| 73 | + |
| 74 | +1. 部署 `BadRandomness` 合约。 |
| 75 | + |
| 76 | +2. 部署 `Attack` 合约。 |
| 77 | + |
| 78 | +3. 将 `BadRandomness` 合约地址作为参数传入到 `Attack` 合约的 `attackMint()` 函数并调用,完成攻击。 |
| 79 | + |
| 80 | +4. 调用 `BadRandomness` 合约的 `balanceOf` 查看`Attack` 合约NFT余额,确认攻击成功。 |
| 81 | + |
| 82 | +## 预防方法 |
| 83 | + |
| 84 | +我们通常使用预言机项目提供的链下随机数来预防这类漏洞,例如 Chainlink VRF。这类随机数从链下生成,然后上传到链上,从而保证随机数不可预测。更多介绍可以阅读 [WTF Solidity极简教程 第39讲:伪随机数](https://github.com/AmazingAng/WTF-Solidity/tree/main/39_Random)。 |
| 85 | + |
| 86 | +## 总结 |
| 87 | + |
| 88 | +这一讲我们介绍了坏随机数漏洞,并介绍了一个简单的预防方法:使用预言机项目提供的链下随机数。NFT 和 GameFi 项目方应避免使用链上伪随机数进行抽奖,以防被黑客利用。 |
| 89 | + |
0 commit comments