以太坊随机数安全全面分析(一)

一、前言

分析了如此多的合约与攻击案例后,我发现随机数是经常出现的一个话题。在CTF题目中经常能见到随机数的预测。

以太坊作为数字货币的初始平台之一,已经在市面上进行了极广的普及。对于以太坊来说,其经常应用在ERC20、轮盘、彩票、游戏等应用中,并利用Solidity完成对合约的编写。作为区块链的应用,以太坊同样是去中心化的、透明的。所以许多赌博游戏、随机数预测等相关应用需要精心设计,否则就会产生危害。

本文详细的将以太坊中的随机数安全问题进行归类,并通过样例对各个类别的安全问题进行演示操作,方便读者进行进一步的分析解读。

二、随机数问题归类

我们在这里将随机数分类为四个大的方向。

  • 随机数使用区块中的公共变量作为随机数种子

  • 随机数使用过去的区块的区块哈希

  • 随机数结合哈希与私人设置的值作为种子

  • 随机数结合区块链机制而导致的安全问题

我将在下文中对这四类问题进行分析总结,并对合约进行演示讲解。

三、基于区块变量的随机数安全问题

根据有漏洞的合约以及常见的CTF题目,我们总结了几种被用于生产随机数的区块变量,如下:

  • now 该变量为当前时间戳信息。
contract test{
    event block(uint);
    function run() public{
        block(now);
    }
}
image.png
  • block.coinbase 代表挖当前区块的矿工地址

  • block.difficulty 表示这个区块的挖矿难度

  • block.gaslimit 表示交易中所限制的最大的gas值

  • block.number表示当前区块的高度

  • block.timestamp表示当前区块何时被挖出来的

这些区块变量可以被矿工进行计算,所以我们不能轻易的使用这些变量作为生成随机数的种子。并且,这些变量可以通过区块得到,当攻击者得到这些公共信息后,可以肆无忌惮的进行计算以达到预测随机数的效果。

下面我们看此类型的几个样例:

首先为一个轮盘类型的应用代码。

/**
 *Submitted for verification at Etherscan.io on 2016-06-28
*/

contract Lottery {
    event GetBet(uint betAmount, uint blockNumber, bool won); 

    struct Bet {
        uint betAmount;
        uint blockNumber;
        bool won;
    }

    address private organizer;
    Bet[] private bets;

    // Create a new lottery with numOfBets supported bets.
    function Lottery() {
        organizer = msg.sender;
    }
    
    // Fallback function returns ether
    function() {
        throw;
    }
    
    // Make a bet
    function makeBet() {
        // Won if block number is even
        // (note: this is a terrible source of randomness, please don't use this with real money)
        bool won = (block.number % 2) == 0; 
        
        // Record the bet with an event
        bets.push(Bet(msg.value, block.number, won));
        
        // Payout if the user won, otherwise take their money
        if(won) { 
            if(!msg.sender.send(msg.value)) {
                // Return ether to sender
                throw;
            } 
        }
    }
    
    // Get all bets that have been made
    function getBets() {
        if(msg.sender != organizer) { throw; }
        
        for (uint i = 0; i < bets.length; i++) {
            GetBet(bets[i].betAmount, bets[i].blockNumber, bets[i].won);
        }
    }
    
    // Suicide :(
    function destroy() {
        if(msg.sender != organizer) { throw; }
        
        suicide(organizer);
    }
}

该合约的关键点在makeBet()函数中。

    // Make a bet
    function makeBet() {
        // Won if block number is even
        // (note: this is a terrible source of randomness, please don't use this with real money)
        bool won = (block.number % 2) == 0; 
        
        // Record the bet with an event
        bets.push(Bet(msg.value, block.number, won));
        
        // Payout if the user won, otherwise take their money
        if(won) { 
            if(!msg.sender.send(msg.value)) {
                // Return ether to sender
                throw;
            } 
        }
    }

在该函数中,用户会在调用该函数的同时获得一个won的bool变量,该变量通过对2进行取余操作来获取是否为true或者false。当won为基数的时候,合约向参与者进行赚钱。

然而这里的block.number可以进行预测,我们可以写攻击合约,当block.number满足条件时调用函数,当不满足的时候放弃执行该函数,这样就可以做到百分百命中。

第二个例子与block.timestamp有关。

/**
 *Submitted for verification at Etherscan.io on 2017-08-20
*/

pragma solidity ^0.4.15;

/// @title Ethereum Lottery Game.

contract EtherLotto {

    // Amount of ether needed for participating in the lottery.
    uint constant TICKET_AMOUNT = 10;

    // Fixed amount fee for each lottery game.
    uint constant FEE_AMOUNT = 1;

    // Address where fee is sent.
    address public bank;

    // Public jackpot that each participant can win (minus fee).
    uint public pot;

    // Lottery constructor sets bank account from the smart-contract owner.
    function EtherLotto() {
        bank = msg.sender;
    }

    // Public function for playing lottery. Each time this function
    // is invoked, the sender has an oportunity for winning pot.
    function play() payable {

        // Participants must spend some fixed ether before playing lottery.
        assert(msg.value == TICKET_AMOUNT);

        // Increase pot for each participant.
        pot += msg.value;

        // Compute some *almost random* value for selecting winner from current transaction.
        var random = uint(sha3(block.timestamp)) % 2;

        // Distribution: 50% of participants will be winners.
        if (random == 0) {

            // Send fee to bank account.
            bank.transfer(FEE_AMOUNT);

            // Send jackpot to winner.
            msg.sender.transfer(pot - FEE_AMOUNT);

            // Restart jackpot.
            pot = 0;
        }
    }

}

简单的分析一下该合约。

该合约同样为一种游戏合约,合约中设定了固定的转账金额——TICKET_AMOUNT。该合约需要满足参与者转账设定好的金额,并当msg.value满足条件后,触发参与合约,该合约设定了随机数random并且该随机数为uint(sha3(block.timestamp)) % 2。当该随机数的结果为0时获奖,获奖一方获得pot - FEE_AMOUNT的金额,而庄家收取一定手续费。

看似简单的赌博游戏其中蕴含着一些漏洞可以操纵。block.timestamp是可以进行预测的,而参与者可以通过预测该值而达到作恶的可能。

第三个合约例子为:

/**
 *Submitted for verification at Etherscan.io on 2017-09-01
*/


contract Ethraffle_v4b {
    struct Contestant {
        address addr;
        uint raffleId;
    }

    event RaffleResult(
        uint raffleId,
        uint winningNumber,
        address winningAddress,
        address seed1,
        address seed2,
        uint seed3,
        bytes32 randHash
    );

    event TicketPurchase(
        uint raffleId,
        address contestant,
        uint number
    );

    event TicketRefund(
        uint raffleId,
        address contestant,
        uint number
    );

    // Constants
    uint public constant prize = 2.5 ether;
    uint public constant fee = 0.03 ether;
    uint public constant totalTickets = 50;
    uint public constant pricePerTicket = (prize + fee) / totalTickets; // Make sure this divides evenly
    address feeAddress;

    // Other internal variables
    bool public paused = false;
    uint public raffleId = 1;
    uint public blockNumber = block.number;
    uint nextTicket = 0;
    mapping (uint => Contestant) contestants;
    uint[] gaps;

    // Initialization
    function Ethraffle_v4b() public {
        feeAddress = msg.sender;
    }

    // Call buyTickets() when receiving Ether outside a function
    function () payable public {
        buyTickets();
    }

    function buyTickets() payable public {
        if (paused) {
            msg.sender.transfer(msg.value);
            return;
        }

        uint moneySent = msg.value;

        while (moneySent >= pricePerTicket && nextTicket < totalTickets) {
            uint currTicket = 0;
            if (gaps.length > 0) {
                currTicket = gaps[gaps.length-1];
                gaps.length--;
            } else {
                currTicket = nextTicket++;
            }

            contestants[currTicket] = Contestant(msg.sender, raffleId);
            TicketPurchase(raffleId, msg.sender, currTicket);
            moneySent -= pricePerTicket;
        }

        // Choose winner if we sold all the tickets
        if (nextTicket == totalTickets) {
            chooseWinner();
        }

        // Send back leftover money
        if (moneySent > 0) {
            msg.sender.transfer(moneySent);
        }
    }

    function chooseWinner() private {
        address seed1 = contestants[uint(block.coinbase) % totalTickets].addr;
        address seed2 = contestants[uint(msg.sender) % totalTickets].addr;
        uint seed3 = block.difficulty;
        bytes32 randHash = keccak256(seed1, seed2, seed3);

        uint winningNumber = uint(randHash) % totalTickets;
        address winningAddress = contestants[winningNumber].addr;
        RaffleResult(raffleId, winningNumber, winningAddress, seed1, seed2, seed3, randHash);

        // Start next raffle
        raffleId++;
        nextTicket = 0;
        blockNumber = block.number;

        // gaps.length = 0 isn't necessary here,
        // because buyTickets() eventually clears
        // the gaps array in the loop itself.

        // Distribute prize and fee
        winningAddress.transfer(prize);
        feeAddress.transfer(fee);
    }

    // Get your money back before the raffle occurs
    function getRefund() public {
        uint refund = 0;
        for (uint i = 0; i < totalTickets; i++) {
            if (msg.sender == contestants[i].addr && raffleId == contestants[i].raffleId) {
                refund += pricePerTicket;
                contestants[i] = Contestant(address(0), 0);
                gaps.push(i);
                TicketRefund(raffleId, msg.sender, i);
            }
        }

        if (refund > 0) {
            msg.sender.transfer(refund);
        }
    }

    // Refund everyone's money, start a new raffle, then pause it
    function endRaffle() public {
        if (msg.sender == feeAddress) {
            paused = true;

            for (uint i = 0; i < totalTickets; i++) {
                if (raffleId == contestants[i].raffleId) {
                    TicketRefund(raffleId, contestants[i].addr, i);
                    contestants[i].addr.transfer(pricePerTicket);
                }
            }

            RaffleResult(raffleId, totalTickets, address(0), address(0), address(0), 0, 0);
            raffleId++;
            nextTicket = 0;
            blockNumber = block.number;
            gaps.length = 0;
        }
    }

    function togglePause() public {
        if (msg.sender == feeAddress) {
            paused = !paused;
        }
    }

    function kill() public {
        if (msg.sender == feeAddress) {
            selfdestruct(feeAddress);
        }
    }
}

参与者参与到该合约中,合约将会将contestants数组中添加参与者地址信息,而剩下的就是需要调用chooseWinner函数来对获胜者进行挑选。

    function chooseWinner() private {
        address seed1 = contestants[uint(block.coinbase) % totalTickets].addr;
        address seed2 = contestants[uint(msg.sender) % totalTickets].addr;
        uint seed3 = block.difficulty;
        bytes32 randHash = keccak256(seed1, seed2, seed3);

        uint winningNumber = uint(randHash) % totalTickets;
        address winningAddress = contestants[winningNumber].addr;
        RaffleResult(raffleId, winningNumber, winningAddress, seed1, seed2, seed3, randHash);

        // Start next raffle
        raffleId++;
        nextTicket = 0;
        blockNumber = block.number;

        // gaps.length = 0 isn't necessary here,
        // because buyTickets() eventually clears
        // the gaps array in the loop itself.

        // Distribute prize and fee
        winningAddress.transfer(prize);
        feeAddress.transfer(fee);
    }

该函数中定义了三个随机数种子,第一个为block.coinbase——contestants[uint(block.coinbase) % totalTickets].addr;

第二个为msg.sender——contestants[uint(msg.sender) % totalTickets].addr

第三个为——block.difficulty

而此刻我们也能过看出来,这三个随机数种子均是可以通过本地来获取到的,也就是说参与者同样可以对这三个变量进行提取预测,以达到作恶的目的。

由于totalTickets是合约固定的,所以see1 2 3均可以由我们提前计算,此时我们就很容易的计算出randHash,然后计算出winningAddress。而获胜方的地址是根据位置所决定的,所以我们可以提前了解到获胜者是谁并可以提前将该位置占领。提高中奖概率。

四、基于区块哈希的随机数问题

每一个以太坊中的区块均有用于验证的哈希值,而该值可以通过block.blockhash()来进行获取。这个函数需要一个指定块的函数来传入,并可以对该块进行哈希计算。

contract test{
    event log(uint256);
    function go() public{
        log(block.number);
    }
}
image.png
  • block.blockhash(block.number) 计算当前区块的哈希值

  • block.blockhash(block.number - 1)计算上一个区块的哈希值

  • block.blockhash()

下面我们具体来看几个实例。

首先是block.blockhash(block.number)

block.number状态变量允许获取当前块的高度。 当矿工选择执行合同代码的事务时,具有此事务的未来块的block.number是已知的,因此合约可以访问其值。 但是,在EVM中执行事务的那一刻,由于显而易见的原因,尚未知道正在创建的块的blockhash,并且EVM将始终为零。

有些合约误解了表达式block.blockhash(block.number)的含义。 在这些合约中,当前块的blockhash在运行时被认为是已知的并且被用作随机数的来源。

为了方便我们对合约进行解读,我们将其中关键函数拿出来:

function deal(address player, uint8 cardNumber) internal returns (uint8) {
  uint b = block.number;
  uint timestamp = block.timestamp;
  return uint8(uint256(keccak256(block.blockhash(b), player, cardNumber, timestamp)) % 52);
}

为了便于我们观察,我们将函数稍微修改一下,

    event log(uint8);
    
    function deal(address player, uint8 cardNumber)  returns (uint8) {
        uint b = block.number;
        uint timestamp = block.timestamp;
        log(uint8(uint256(keccak256(block.blockhash(b), player, cardNumber, timestamp)) % 52));
        return uint8(uint256(keccak256(block.blockhash(b), player, cardNumber, timestamp)) % 52);
    }

这样我们就拿到了每次执行的结果。

我们执行两次:


o

而通过log我们能够给知道每次的结果,也就是说这个随机数其实是可以预测的,我们用户就可以根据预测的值进行作恶。

function random(uint64 upper) public returns (uint64 randomNumber) {
  _seed = uint64(sha3(sha3(block.blockhash(block.number), _seed), now));
  return _seed % upper;
}

同样,该函数也存在类似的情况,我们知道now是所有用户都可以获得的,而该合约使用的所有随机数种子均是可获得的。且该_seed变量可以存在于区块中,并通过web3的内部函数获取。具体的方法我们在下文中进行讲解。

五、参考链接

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,658评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,482评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,213评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,395评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,487评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,523评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,525评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,300评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,753评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,048评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,223评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,905评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,541评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,168评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,417评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,094评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,088评论 2 352

推荐阅读更多精彩内容