一、前言
本文繼續(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í)行。

在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ù)。
然而該方法同樣存在一些弊端:

由于區(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ì)的題解可以看我之前的文章:
四、私有種子作為哈希
為了增加隨機(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))});。

使用這個函數(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)行訪問測試:

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

六、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價格獲得累積獎金。