以太坊随机数安全全面分析(二)
字数 1528 2025-08-22 12:22:43
以太坊随机数安全全面分析(二)
一、前言
本文继续前文的讲解,深入分析以太坊智能合约中随机数生成的安全问题。在前文中我们介绍了区块链中由公开变量做种子而引起的安全问题,本文将详细介绍四种随机数漏洞类型及其利用方法。
二、基于区块哈希的随机数问题
2.1 基本问题描述
许多合约使用block.blockhash(block.number-1)作为生成随机数的变量。这种方法存在严重问题,因为攻击者可以调用相同的方法来预测或复制该随机数。
2.2 典型案例分析
LuckyDoubler合约示例:
uint256 constant private FACTOR = 1157920892373161954235709850086879078532699846656405640394575840079131296399;
function rand(uint max) returns (uint256 result){
uint256 factor = FACTOR * 100 / max;
uint256 lastBlockNumber = block.number - 1;
uint256 hashVal = uint256(block.blockhash(lastBlockNumber));
return uint256((uint256(hashVal) / factor)) % max;
}
问题分析:
- 使用
block.number - 1获取上一个区块编号 - 通过
block.blockhash()获取该区块的哈希值 - 使用FACTOR常数进行数学运算生成"随机数"
安全隐患:
- 区块信息是公开的,攻击者可以预先计算相同的随机数
- 在交易执行前,攻击者可以预先运行相同的计算逻辑
三、未来区块哈希值的利用
3.1 方法描述
一些合约尝试使用未来区块哈希作为随机数源,步骤如下:
- 玩家下注,合约记录下注时的区块编号
- 稍后调用合约查询中奖号码
- 合约从存储中检索保存的block.number并获取其blockhash用于生成随机数
3.2 存在的问题
-
区块哈希存储限制:
- 以太坊只能保存最近的256个区块的哈希值
- 超过256个区块的哈希值返回0
- 如果第二次调用超过256个区块,随机种子变为0,可被预测
-
著名案例:
- SmartBillions彩票被黑,损失400 ETH
- 原因是合约对block.number验证不充分
3.3 CTF示例分析
强网杯babybet题目:
function bet(var arg0) {
// ...省略部分代码...
var0 = block.blockHash(block.number + ~0x00);
var1 = var0 % 0x03;
if (var1 != arg0) {
// 处理失败逻辑
} else {
// 处理成功逻辑
}
}
攻击方法:
function process() public {
target.profit();
bytes32 guess = block.blockhash(block.number - 0x01);
uint guess1 = uint(guess) % 0x03;
target.bet(guess1);
}
四、私有种子作为哈希的安全问题
4.1 问题描述
一些合约尝试使用"私有"变量作为随机数生成的种子,例如:
bytes32 _a = block.blockhash(block.number - pointer);
for (uint i = 31; i >= 1; i--) {
if ((uint8(_a[i]) >= 48) && (uint8(_a[i]) <= 57)) {
return uint8(_a[i]) - 48;
}
}
4.2 安全隐患
- 区块链数据本质上是公开透明的
- 所谓的private变量可以通过web3.js等方法读取
- 攻击者可以获取pointer的值并预测随机数
五、私有变量查看方法
5.1 基本方法
使用web3.js的getStorageAt函数可以读取合约存储中的任何数据,包括private变量。
示例合约:
pragma solidity ^0.4.18;
contract Vault {
bool public locked;
bytes32 private password;
// ...其他代码...
}
读取方法:
web3.eth.getStorageAt("合约地址", 1, function(x, y) {
console.warn(web3.toAscii(y))
});
5.2 存储布局测试
测试合约:
pragma solidity ^0.4.10;
contract attack{
uint private a;
uint public b;
uint private c;
bool public locked;
bool private locked1;
// ...初始化代码...
}
存储布局:
- 变量按照声明顺序存储
- 布尔值true存储为1,false存储为0
- private和public变量在存储上没有区别
六、Front-running攻击
6.1 攻击原理
- 矿工根据gas价格选择交易执行顺序
- gas价格高的交易优先执行
- 攻击者可以观察待处理交易池,针对特定交易发起更高gas价格的交易
6.2 应用场景
-
彩票合约:
- 观察oracle的随机数生成交易
- 在oracle交易前插入更高gas的交易
- 提前知道随机数结果并下注
-
"Last is me!"游戏:
- 观察其他玩家的"坐下"交易
- 在倒计时即将结束时发起更高gas的交易
- 确保自己是最后一个"坐下"的玩家
6.3 著名案例
- ZeroNights ICO Hacking Contest中的题目
- Fomo3D等游戏合约中的类似问题
七、防御建议
- 避免使用区块变量(block.number, block.timestamp, blockhash)作为随机数源
- 使用链外随机数生成器(Oracle)如Chainlink VRF
- 采用承诺-揭示(commit-reveal)方案
- 对于必须使用链上随机数的情况,结合多个不可控因素
- 注意私有变量的实际可见性,不要依赖其保密性