漏洞原理、攻击复现与链下随机数防护)
WTF-Solidity 合约安全实战S07 坏随机数Bad Randomness漏洞原理、攻击复现与链下随机数防护【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity导读本文基于 WTF-Solidity 合约安全系列 S07 讲系统讲解以太坊智能合约中坏随机数Bad Randomness漏洞的形成原因、攻击手法与防护方案。你将通过仓库中的 BadRandomness.sol 漏洞合约与配套攻击合约完整复现预测随机数、铸造指定稀有 NFT的实战攻击流程并掌握使用 Chainlink VRF 等链下随机数方案进行修复的正确姿势可直接迁移到 NFT 抽奖、盲盒、GameFi 等高频随机数场景中。一、为什么以太坊上不存在真随机数NFT 随机抽取tokenId、盲盒抽奖、GameFi 战斗随机判定胜负……大量链上应用都需要随机数。但以太坊以及绝大多数 EVM 兼容链有一个天然特性所有链上数据公开透明public且执行确定性deterministic同一笔交易在任何节点上重放都会得到完全一致的结果。这决定了它无法像 C、Java 等传统语言那样向开发者提供random()之类的系统随机数 API——因为真正的随机数会破坏状态一致性导致节点之间无法达成共识。于是项目方只能退而求其次使用链上伪随机数把若干链上全局变量作为种子seed再交给哈希函数进行混淆得到看起来随机的结果。最常见的组合是uint256 randomNumber uint256(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp))) % 100;这条表达式正是本讲漏洞合约的核心逻辑它的随机性完全依赖两个全局变量种子来源含义是否可预测 / 可操纵blockhash(block.number - 1)上一个区块的哈希完全公开且只保留最近 256 个区块的哈希任何人都能查到block.timestamp当前区块的时间戳公开且可由矿工/验证者在出块时在一定范围内调整哈希函数keccak256本身确实具备灵敏性和均一性——输入微变、输出剧变、结果均匀分布。但它混淆的是公开已知的输入混淆不等于不可预测只要种子是公开的哈希结果就是可以被任何人离线复算的。二、坏随机数漏洞的本质可预测性在 WTF-Solidity 第 39 讲链上随机数 中同样给出了一个典型的链上伪随机数生成函数getRandomOnchain()把blockhash(block.number-1)、msg.sender、block.timestamp一起打包进keccak256()function getRandomOnchain() public view returns(uint256){ bytes32 randomBytes keccak256(abi.encodePacked(blockhash(block.number-1), msg.sender, block.timestamp)); return uint256(randomBytes); }其中也明确指出了这种方案的两个致命弱点可预测block.timestamp、msg.sender、blockhash全部公开使用者可以提前算出生成的随机数然后挑对自己有利的时机执行合约可操纵矿工可以操纵blockhash和block.timestamp让生成的随机数符合自己的利益。在以太坊上交易在被打包进区块之前发送方可以预判自己交易所在区块的block.number与block.timestamp即使不精确也只需等待/挑选一个合适的区块因此基于这类种子生成的随机数对攻击者而言几乎是明牌。所谓坏随机数漏洞就是攻击者可以事先计算这些伪随机数的结果从而达成自己想要的任何目的——例如铸造任何他们想要的稀有 NFT而不是随机抽取。下图形象地展示了这一漏洞的荒谬之处用户以为自己在掷骰子但攻击者0xAA能复述出完全相同的随机数代码并给出与系统随机数一致的正确答案这种漏洞在 NFT 和 GameFi 项目中反复出现Meebits、Loots、Wolf Game 等知名项目都曾因此被攻击攻击者可以精准铸造最稀有的 NFT而非随机抽取。三、漏洞合约源码剖析BadRandomness.sol仓库中的 BadRandomness.sol 实现了一个存在坏随机数漏洞的 NFT 合约。它直接继承了 WTF-Solidity 自带的 ERC721 实现位于34_ERC721目录实现了IERC721、IERC721Metadata、IERC165标准接口因此天然具备balanceOf、ownerOf、_mint等标准能力。// SPDX-License-Identifier: MIT // By 0xAA pragma solidity ^0.8.34; import ../34_ERC721/ERC721.sol; contract BadRandomness is ERC721 { uint256 totalSupply; // 构造函数初始化NFT合集的名称、代号 constructor() ERC721(, ){} // 铸造函数当输入的 luckyNumber 等于随机数时才能mint function luckyMint(uint256 luckyNumber) external { uint256 randomNumber uint256(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp))) % 100; // get bad random number require(randomNumber luckyNumber, Better luck next time!); _mint(msg.sender, totalSupply); // mint totalSupply; } }逐行拆解这个合约的核心逻辑继承与构造函数BadRandomness is ERC721构造函数调用父合约ERC721(, )初始化名称与代号教程示例中留空可自行替换为实际名称。状态变量totalSupply记录已铸造的 NFT 数量同时充当下一个tokenId从 0 递增。luckyMint(uint256 luckyNumber)铸造函数用户调用时输入一个0-99的数字合约用blockhash(block.number - 1)与block.timestamp计算链上伪随机数取模% 100得到一个0-99的值若randomNumber luckyNumber则require通过铸造 NFT 给调用者否则 revert并回滚交易报错信息为Better luck next time!。从设计上看这像是一个 1/100 概率的抽奖铸造用户需要恰好猜中随机数才能拿到 NFT。但这个随机数根本不需要猜——它就是公开算式的确定输出。漏洞就藏在这行伪随机数生成代码里。四、攻击合约 Attack.sol同一区块内预言随机数漏洞的利用方式极其简单由于attackMint()与luckyMint()在同一个区块内先后执行两个函数读取到的blockhash(block.number - 1)与block.timestamp完全一致因此它们各自计算出的随机数必然相同。攻击者只需要在调用铸造函数之前把同一套算式离线复算一遍把算出的luckyNumber作为参数传入即可 100% 通过校验。仓库中 BadRandomness.sol 的下半部分就是这个攻击合约contract Attack { function attackMint(BadRandomness nftAddr) external { // 提前计算随机数 uint256 luckyNumber uint256( keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)) ) % 100; // 利用 luckyNumber 攻击 nftAddr.luckyMint(luckyNumber); } }攻击流程拆解复算随机数attackMint()内用与受害者合约完全相同的算式keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)) % 100计算出luckyNumber发起攻击交易将算出的luckyNumber作为参数调用nftAddr.luckyMint(luckyNumber)攻击生效因为攻击者的调用与合约内部计算处于同一笔交易、同一个区块内两者读到的blockhash与block.timestamp相同随机数必然相等require必然通过NFT 成功铸造到msg.sender即攻击合约名下。从源码结构可以推断Attack合约没有继承任何接口仅通过地址参数BadRandomness nftAddr与目标合约交互属于典型的外部调用型攻击合约。攻击者可以把attackMint包装进自己的自动化脚本批量对不同区块、不同幸运数字的时机发起攻击或者直接只在对已有利的区块出手。五、Remix 实战复现从部署到攻击成功复现坏随机数攻击需要特别留意环境限制Remix 自带的 Remix VM 不支持blockhash函数在 Remix VM 中blockhash()无法按预期返回真实区块哈希因此必须将合约部署到以太坊测试链如 Sepolia 测试网上进行复现。完整复现步骤如下部署BadRandomness合约在 Remix 中编译并部署 BadRandomness.sol部署时构造函数参数留空ERC721(, )部署Attack合约编译并部署同一文件中的Attack合约发起攻击将BadRandomness合约地址作为参数传入Attack合约的attackMint()函数并调用完成攻击此时这一笔交易内已经完成了算数 铸造的全部动作验证攻击结果调用BadRandomness合约的balanceOf传入Attack合约地址查看其 NFT 余额。若余额为1即攻击合约持有一个 tokenId 为 0 的 NFT说明攻击成功——攻击者无需任何运气就拿到了幸运 NFT。提示攻击本质上是先算后调因此还可以把luckyMint的期望值打印出来与balanceOf结果对照进一步验证随机数可预测性。六、预防方法让随机数不可预测坏随机数漏洞的根因是随机数种子来自链上公开数据。修复思路有两个方向一是把随机数生成移到链下让攻击者无法在交易发出前获知结果二是采用不可被单方操纵、可验证的链上随机性协议。6.1 链下随机数 预言机Chainlink VRF目前最主流的方案是使用预言机项目提供的链下随机数例如Chainlink VRF可验证随机函数随机数在链下生成、附带可验证的密码学证明后上传到链上任何一方都无法在提交前预测结果也无法事后篡改。WTF-Solidity 第 39 讲 Random.sol 提供了完整的落地示例——一个同时支持链上伪随机铸造不安全与VRF 随机铸造安全的 NFT 合约两者对比可以直观感受安全方案的差异。使用 Chainlink VRF V2 的标准流程详见 第 39 讲文档申请 Subscription 并转入 LINK 代币在 Chainlink VRF 官网创建订阅Subscription测试网 LINK 可通过水龙头领取合约继承VRFConsumerBaseV2在构造函数中初始化VRFCoordinatorV2Interface与Subscription Id不同链需要填入对应的 Coordinator 地址、Key Hash 等参数调用requestRandomWords()申请随机数将keyHash、subId、requestConfirmations、callbackGasLimit、numWords等参数提交给 VRF Coordinator注意合约部署后必须先把合约添加到 Subscription 的 Consumers 中才能成功发起申请Chainlink 节点链下生成随机数与数字签名并上链VRF 合约验证签名有效性回调fulfillRandomWords()消费随机数VRF 合约验证通过后自动调用用户合约的回调函数把随机数发送回来消耗随机数的业务逻辑如铸造 NFT必须写在这个回调里。其中RandomNumberConsumer示例合约中的核心参数如下Sepolia 测试网为例不同网络需查询官方 supported-networks 文档替换参数含义示例值SepoliavrfCoordinatorVRF Coordinator 合约地址0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625keyHashVRF 唯一标识符gas price 档位0x474e34a077df58807dbe9c96d3c009b23b3c6d0cce433e59bbf5b34f823bc56crequestConfirmations最小确认块数数字越大安全性越高3一般可填12callbackGasLimit回调函数 gas 上限200_000上限 2,500,000numWords一次请求获得的随机数个数3上限 500需要特别注意的架构差异申请随机数requestRandomWords与接收随机数fulfillRandomWords是两笔不同的交易前者由用户合约发起后者由 VRF 合约发起两者可能相隔几分钟。因此在 VRF 回调里消费随机数时不能直接使用msg.sender此时调用者是 VRF 合约而非用户——Random.sol 的做法是用requestToSender映射记录requestId 用户地址回调时再取出来执行_mint(sender, tokenId)。这是把 VRF 接入业务合约时最容易踩的坑。6.2 其他思路可验证的链上随机性除了预言机之外行业中也存在以 DAO 模式提供链上 真随机服务的 RNG 方案如 RANDAO。但无论采用哪种方案核心原则不变随机数必须对提交方不可预测、对结果不可篡改。6.3 预防要点小结NFT 与 GameFi 项目方应避免直接使用blockhash、block.timestamp、msg.sender等链上公开变量拼接哈希生成随机数进行抽奖对随机性要求不高的场景也要认识到链上伪随机数只能防普通用户防不住同一区块内复算的攻击者涉及资产分配如抽盲盒、抽稀有度的场景优先采用 Chainlink VRF 等链下可验证随机数并把消耗随机数的逻辑放在fulfillRandomWords()回调中。七、总结本讲围绕坏随机数Bad Randomness漏洞完成了从原理到复现再到防御的闭环漏洞根源以太坊链上数据公开且确定性blockhash()block.timestamp拼接哈希生成的是可预测的伪随机数攻击本质攻击者在同一区块内复算同一算式即可 100% 预测幸运数字从 BadRandomness.sol 中的luckyMint()漏洞可见一斑实战复现部署漏洞合约与攻击合约调用attackMint()即可在测试链上拿到本应 1/100 概率才能获得的 NFT正确防御使用 Chainlink VRF 等链下预言机随机数保证随机数不可预测、不可篡改参考 第 39 讲 Random.sol 的安全实现模式。对任何涉及随机数分配的合约而言随机二字必须以密码学为背书而不是以区块元数据为赌注。这也是审计 NFT 与 GameFi 合约时最优先检查的漏洞类型之一。【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考