以太坊隨機(jī)數(shù)安全全面分析(二)

一、前言

本文繼續(xù)前文的講解。在前文中我們介紹了區(qū)塊鏈中由公開變量做種子而引起的安全問題;有些合約使用區(qū)塊哈希作為變量并將其放入合約函數(shù)中作為某種讀博游戲的判定依舊。

由于這些隨機(jī)數(shù)并非真正的“隨機(jī)”,所以其安全隱患也是巨大的。本文我們繼續(xù)介紹四種隨機(jī)數(shù)漏洞類型。

二、基于區(qū)塊哈希的隨機(jī)數(shù)問題

block.blockhash(block.number-1)。

許多合約使用blockhash作為生產(chǎn)隨機(jī)數(shù)的變量,并傳入上一個區(qū)塊編號。這種方法同樣存在問題,攻擊者可以調(diào)用相同的方法來生成該隨機(jī)數(shù)。

例如下面一個合約:

/**
 *Submitted for verification at Etherscan.io on 2016-04-23
*/

contract LuckyDoubler {
//##########################################################
//#### LuckyDoubler: A doubler with random payout order ####
//#### Deposit 1 ETHER to participate                   ####
//##########################################################
//COPYRIGHT 2016 KATATSUKI ALL RIGHTS RESERVED
//No part of this source code may be reproduced, distributed,
//modified or transmitted in any form or by any means without
//the prior written permission of the creator.
    event log(uint256);
    address private owner;
    
    //Stored variables
    uint private balance = 0;
    uint private fee = 5;
    uint private multiplier = 125;

    mapping (address => User) private users;
    Entry[] private entries;
    uint[] private unpaidEntries;
    
    //Set owner on contract creation
    function LuckyDoubler() {
        owner = msg.sender;
    }

    modifier onlyowner { if (msg.sender == owner) _ }
    
    struct User {
        address id;
        uint deposits;
        uint payoutsReceived;
    }
    
    struct Entry {
        address entryAddress;
        uint deposit;
        uint payout;
        bool paid;
    }

    //Fallback function
    function() {
        init();
    }
    
    function init() private{
        
        if (msg.value < 1 ether) {
             msg.sender.send(msg.value);
            return;
        }
        
        join();
    }
    
    function join() private {
        
        //Limit deposits to 1ETH
        uint dValue = 1 ether;
        
        if (msg.value > 1 ether) {
            
            msg.sender.send(msg.value - 1 ether);   
            dValue = 1 ether;
        }
      
        //Add new users to the users array
        if (users[msg.sender].id == address(0))
        {
            users[msg.sender].id = msg.sender;
            users[msg.sender].deposits = 0;
            users[msg.sender].payoutsReceived = 0;
        }
        
        //Add new entry to the entries array
        entries.push(Entry(msg.sender, dValue, (dValue * (multiplier) / 100), false));
        users[msg.sender].deposits++;
        unpaidEntries.push(entries.length -1);
        
        //Collect fees and update contract balance
        balance += (dValue * (100 - fee)) / 100;
        
        uint index = unpaidEntries.length > 1 ? rand(unpaidEntries.length) : 0;
        Entry theEntry = entries[unpaidEntries[index]];
        
        //Pay pending entries if the new balance allows for it
        if (balance > theEntry.payout) {
            
            uint payout = theEntry.payout;
            
            theEntry.entryAddress.send(payout);
            theEntry.paid = true;
            users[theEntry.entryAddress].payoutsReceived++;

            balance -= payout;
            
            if (index < unpaidEntries.length - 1)
                unpaidEntries[index] = unpaidEntries[unpaidEntries.length - 1];
           
            unpaidEntries.length--;
            
        }
        
        //Collect money from fees and possible leftovers from errors (actual balance untouched)
        uint fees = this.balance - balance;
        if (fees > 0)
        {
                owner.send(fees);
        }      
       
    }
    
    //Generate random number between 0 & max
    uint256 constant private FACTOR =  1157920892373161954235709850086879078532699846656405640394575840079131296399;
    function rand(uint max)  returns (uint256 result){
        uint256 factor = FACTOR * 100 / max;
        uint256 lastBlockNumber = block.number - 1;
        uint256 hashVal = uint256(block.blockhash(lastBlockNumber));
        log(hashVal);
        return uint256((uint256(hashVal) / factor)) % max;
    }
    
    
    //Contract management
    function changeOwner(address newOwner) onlyowner {
        owner = newOwner;
    }
    
    function changeMultiplier(uint multi) onlyowner {
        if (multi < 110 || multi > 150) throw;
        
        multiplier = multi;
    }
    
    function changeFee(uint newFee) onlyowner {
        if (fee > 5) 
            throw;
        fee = newFee;
    }
    
    
    //JSON functions
    function multiplierFactor() constant returns (uint factor, string info) {
        factor = multiplier;
        info = 'The current multiplier applied to all deposits. Min 110%, max 150%.'; 
    }
    
    function currentFee() constant returns (uint feePercentage, string info) {
        feePercentage = fee;
        info = 'The fee percentage applied to all deposits. It can change to speed payouts (max 5%).';
    }
    
    function totalEntries() constant returns (uint count, string info) {
        count = entries.length;
        info = 'The number of deposits.';
    }
    
    function userStats(address user) constant returns (uint deposits, uint payouts, string info)
    {
        if (users[user].id != address(0x0))
        {
            deposits = users[user].deposits;
            payouts = users[user].payoutsReceived;
            info = 'Users stats: total deposits, payouts received.';
        }
    }
    
    function entryDetails(uint index) constant returns (address user, uint payout, bool paid, string info)
    {
        if (index < entries.length) {
            user = entries[index].entryAddress;
            payout = entries[index].payout / 1 finney;
            paid = entries[index].paid;
            info = 'Entry info: user address, expected payout in Finneys, payout status.';
        }
    }
    
}

該合約重點部分為:

//Generate random number between 0 & max
uint256 constant private FACTOR =  1157920892373161954235709850086879078532699846656405640394575840079131296399;
function rand(uint max) constant private returns (uint256 result){
  uint256 factor = FACTOR * 100 / max;
  uint256 lastBlockNumber = block.number - 1;
  uint256 hashVal = uint256(block.blockhash(lastBlockNumber));
  return uint256((uint256(hashVal) / factor)) % max;
}

該函數(shù)為隨機(jī)數(shù)生成函數(shù),該函數(shù)設(shè)置了一個初始參數(shù)FACTOR用于對隨機(jī)數(shù)進(jìn)行初步處理。那么該隨機(jī)數(shù)是如何生成的呢?

這里合約中使用lastBlockNumber = block.number - 1以及uint256(block.blockhash(lastBlockNumber))來生成了hashVal這個參數(shù),然而該變量的隨機(jī)數(shù)種子是由區(qū)塊已知的部分組成的。

我們部署合約并執(zhí)行。

image.png

在log中看到了相關(guān)信息,同樣我們可以在執(zhí)行該函數(shù)前進(jìn)行預(yù)操作以便能夠預(yù)測隨機(jī)數(shù)。

而我們在后面會對該類型的ctf題目進(jìn)行一些簡單的講述。

三、未來區(qū)塊的哈希值

最后的方法是使用未來區(qū)塊哈希來進(jìn)行隨機(jī)數(shù)預(yù)測,方法步驟如下:

  • 玩家進(jìn)行下注,合約對玩家的下注信息進(jìn)行記錄并存在區(qū)塊的編號

  • 第二步調(diào)用合約,玩家詢問合約中獎號碼的具體值

  • 合約從存儲中檢索保存的block.number并獲取其blockhash,然后用于生成偽隨機(jī)數(shù)。

然而該方法同樣存在一些弊端:

image.png

由于區(qū)塊的擴(kuò)展性問題,所以該方法不能保存所有的區(qū)塊哈希,稚嫩保存最近的256個區(qū)塊的哈希值,其他的值均為0.

所以當(dāng)?shù)诙握{(diào)用的值超過了256數(shù)量,此時哈希變?yōu)椴豢捎?,并且隨機(jī)種子均為0 ,同樣產(chǎn)生威脅。

被利用的這個弱點的最著名的案例是SmartBillions彩票的黑客攻擊。 合約對block.number的驗證不充分,導(dǎo)致400 ETH丟失。

而在CTF中同樣有類似的題目,例如今年強網(wǎng)杯的babybet題目就需要我們進(jìn)行隨機(jī)數(shù)預(yù)測。

    function bet(var arg0) {
        var var0 = 0x00;
        memory[var0:var0 + 0x20] = msg.sender;
        memory[0x20:0x40] = var0;
        var var1 = var0;
    
        if (0x0a > storage[keccak256(memory[var1:var1 + 0x40])]) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x01;
    
        if (0x02 <= storage[keccak256(memory[0x00:0x40])]) { revert(memory[0x00:0x00]); }
    
        memory[0x00:0x20] = msg.sender;
        memory[0x20:0x40] = 0x00;
        var temp0 = keccak256(memory[0x00:0x40]);
        storage[temp0] = storage[temp0] + ~0x09;
        var0 = block.blockHash(block.number + ~0x00);
        var1 = var0 % 0x03;
    
        if (var1 != arg0) {
            memory[0x00:0x20] = msg.sender;
            memory[0x20:0x40] = 0x01;
            storage[keccak256(memory[0x00:0x40])] = 0x02;
            return;
        } else {
            memory[0x00:0x20] = msg.sender;
            memory[0x20:0x40] = 0x00;
            var temp1 = keccak256(memory[0x00:0x40]);
            storage[temp1] = storage[temp1] + 0x03e8;
            memory[0x00:0x20] = msg.sender;
            memory[0x20:0x40] = 0x01;
            storage[keccak256(memory[0x00:0x40])] = 0x02;
            return;
        }
    }
    

而我們寫腳本就可以對其進(jìn)行攻擊。

    function process() public {
        target.profit();
        bytes32 guess = block.blockhash(block.number - 0x01);
        uint guess1 = uint(guess) % 0x03;
        target.bet(guess1);

    }

詳細(xì)的題解可以看我之前的文章:

https://xz.aliyun.com/t/5372

四、私有種子作為哈希

為了增加隨機(jī)數(shù)的隨機(jī)性,一些合約使用了被認(rèn)為“私有”的種子作為隨機(jī)數(shù)生成的參數(shù)。例如:

bytes32 _a = block.blockhash(block.number - pointer);
for (uint i = 31; i >= 1; i--) {
  if ((uint8(_a[i]) >= 48) && (uint8(_a[i]) <= 57)) {
    return uint8(_a[i]) - 48;
  }
}

在該代碼中,合約定義了pointer指針作為私有變量,合約創(chuàng)建者本意想隱藏該私有變量,讓用戶參與者無法獲取到該種子的具體數(shù)字。在每次游戲之后,1到9之間的中獎號碼被分配給該變量,然后在檢索blockhash時將其用作當(dāng)前block.number的偏移量。

然而我們知道,區(qū)塊鏈的規(guī)則是將數(shù)據(jù)公開透明,所以這些所謂的私有變量可以通過web3的方法進(jìn)行獲取,下面我們對該方法進(jìn)行詳細(xì)的講解。

五、私有變量查看方法

查看合約制定內(nèi)存的方法,雖然合約定義的內(nèi)容是private類型,但是我們?nèi)匀豢梢阅玫健?/p>

文檔詳情:https://web3js.readthedocs.io/en/1.0/web3-eth.html

在合約中,我們可以這樣進(jìn)行查看:

pragma solidity ^0.4.18;

contract Vault {
  bool public locked;
  bytes32 private password;

  function Vault(bytes32 _password) public {
    locked = true;
    password = _password;
  }

  function unlock(bytes32 _password) public {
    if (password == _password) {
      locked = false;
    }
  }
}

若要查看在內(nèi)存中第一個位置的函數(shù)值,那么使用web3.eth.getStorageAt("0x80994df46e168050262d1a63129592fc6247b4ed", 1, function(x, y) {console.warn(web3.toAscii(y))});。

image.png

使用這個函數(shù)是只能夠查看private變量嗎?

我們進(jìn)行測試。

測試代碼:

pragma solidity ^0.4.10;


contract attack{
    
    uint private a;
    
    uint public b;
    
    uint private c;
    
    bool public locked;
    bool private locked1;
    
    function attack(){
        a = 1;
        b = 2;
        c = 3;
        locked = true;
        locked1 = false;
    }
    
}

然后我們對所有的變量進(jìn)行訪問測試:

image.png

這里我們發(fā)現(xiàn)對于布爾變量,我們能夠得到true為1,false為0 。

image.png

六、Front-running

為了獲得最大獎勵,礦工根據(jù)每筆交易使用的累計gas選擇交易來創(chuàng)建新區(qū)塊。區(qū)塊中交易執(zhí)行的順序由gas價格決定。gas價格最高的交易將首先執(zhí)行。因此,通過操縱gas價格,可以在當(dāng)前區(qū)塊中的所有其他交易之前執(zhí)行期望的交易。當(dāng)合同的執(zhí)行流程取決于其在一個區(qū)塊中的位置時,這可能構(gòu)成安全問題 - 通常稱為Front-running。

彩票使用外部函數(shù)來獲得偽隨機(jī)數(shù),用于確定每輪投注中的投注者中的獲勝者。這些號碼是未加密的。攻擊者可能會觀察待處理事務(wù)池并等待來自oracle的號碼。一旦oracle的交易出現(xiàn)在交易池中,攻擊者就會以更高的汽油價格下注。攻擊者的交易最后一輪,但由于天然氣價格較高,實際上是在oracle交易之前執(zhí)行,使得攻擊者獲勝。ZeroNights ICO Hacking Contest中有這樣的任務(wù)。

另一個易于前線合約的例子是名為“Last is me!”的游戲。每當(dāng)玩家購買一張票時,該玩家就會聲稱最后一個座位并且計時器開始倒計時。如果沒有人在一定數(shù)量的街區(qū)內(nèi)購買機(jī)票,最后一個“坐下”的玩家將獲得累積獎金。當(dāng)該輪即將結(jié)束時,攻擊者可以觀察交易池以進(jìn)行其他參賽者的交易,并通過更高的gas價格獲得累積獎金。

七、參考鏈接

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 一、前言 分析了如此多的合約與攻擊案例后,我發(fā)現(xiàn)隨機(jī)數(shù)是經(jīng)常出現(xiàn)的一個話題。在CTF題目中經(jīng)常能見到隨機(jī)數(shù)的預(yù)測。...
    CPinging閱讀 811評論 0 0
  • 以太坊(Ethereum ):下一代智能合約和去中心化應(yīng)用平臺 翻譯:巨蟹 、少平 譯者注:中文讀者可以到以太坊愛...
    車圣閱讀 3,924評論 1 7
  • 【中文版】以太坊白皮書 翻譯:少平、 Seven當(dāng)中本聰在 2009 年 1 月啟動比特幣區(qū)塊鏈時,他同時向世界引...
    __Seven__閱讀 4,450評論 0 10
  • 雖然處于起步階段,但是 Solidity 已被廣泛采用,并被用于編譯我們今天看到的許多以太坊智能合約中的字節(jié)碼。相...
    筆名輝哥閱讀 1,321評論 0 53
  • 我難得偷得半日閑,只想放空大腦,什么也不想,什么也不干,好好享受一把午后的閑暇時光。但天氣十分炎熱,坐在家里后背的...
    氫氣球在飄閱讀 294評論 0 0

友情鏈接更多精彩內(nèi)容