一、前言

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

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

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

二、随机数问题归类

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

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

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

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

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

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

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

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

  • now 该变量为当前时间戳信息。
contract test{
    event block(uint);
    function run() public{
        block(now);
    }
}

  • 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);
    }
}

  • 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);
    }

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

我们执行两次:

而通过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的内部函数获取。具体的方法我们在下文中进行讲解。

五、参考链接

点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖