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

2019-07-12 约 1175 字 预计阅读 6 分钟

声明:本文 【以太坊随机数安全全面分析(一)】 由作者 Pinging 于 2019-07-12 09:00:00 首发 先知社区 曾经 浏览数 98 次

感谢 Pinging 的辛苦付出!

一、前言

分析了如此多的合约与攻击案例后,我发现随机数是经常出现的一个话题。在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的内部函数获取。具体的方法我们在下文中进行讲解。

五、参考链接

关键词:[‘安全技术’, ‘区块链安全’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now