以太坊智能合约中随机数预测
作者:CQITer小编 时间:2018-07-30 21:57
作为首次币发行(ICO)的平台,以太坊已经获得了极大的普及。 但是,它不仅仅用于 ERC20 通证,轮盘,彩票和纸牌游戏都可以使用以太坊区块链实现。 与任何区块链实施一样,以太坊是不可逆的,去中心化的,透明公开的。 以太坊允许运行图灵完备程序,这些程序通常用 Solidity 编写,使其成为平台创始人所说的“世界超级计算机”。 对于计算机赌博来说,所有这些特征都是非常有益的,尤其是用户信用。

而这类应用必不可少的部分就是产生随机数,但是以太坊区块链通过打包区块来实现数据存储,数据需要非常高的确定性,这为编写伪随机数发生器(PRNG)带来了一定的难度。
二、PRNG相关漏洞类型开发者生成随机数时,一般都会使用伪随机数生成器(pseudo-random number generator),简称 `PRNG`。而有漏洞的PRNG,一般有四种类型:
1. 使用区块变量作为熵源的 PRNG
2. 基于过往区块的区块哈希的 PRNG
3. 基于过往区块和私有种子(seed)的区块哈希的 PRNG
4. 易被抢占交易(front-running)的 PRNG
下面我们来分别一一详解。
1、使用区块变量作为熵源的 PRNG(1). block.coinbase 表示当前区块的矿工地址
(2). block.difficulty 表示当前区块的挖掘难度
(3). block.gaslimit 区块内交易的最大限制燃气消耗量
(4). block.number 表示当前区块高度
(5). block.timestamp 表示当前区块挖掘时间
以上所有的区块变量都可以被矿工操纵,所以都不能用来做信息熵源。因为这些区块变量在同一区块上是共用的。攻击者通过其恶意合约调用受害者合约,那么此交易打包在同一区块中,其区块变量是一样的。
示例1:< https://etherscan.io/address/0x80ddae5251047d6ceb29765f38fed1c0013004b7>
// 如果 block.number 是偶数,则 won 输出为 true。
bool won = (block.number % 2) == 0;
示例2:< https://etherscan.io/address/0xa11e4ed59dc94e69612f3111942626ed513cb172>
var random = uint(sha3(block.timestamp)) % 2;示例3:< https://etherscan.io/address/0xcC88937F325d1C6B97da0AFDbb4cA542EFA70870>
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;
示例4 : 使用 block.difficulty
pragma solidity ^0.4.0;
contract random{
function rand() public returns(uint256) {
uint256 random = uint256(keccak256(block.difficulty,now));
return random%10;
}
}
2、基于过往区块的区块哈希的 PRNG
每一个Ethereum区块链上的区块都有认证的hash值,通过 block.blockhash() 函数可以获取此值。此函数经常被错误地使用。
block.blockhash(block.number) :基于当前区块的区块哈希
block.blockhash(block.number – 1) : 基于负一区块的区块哈希
block.blockhash() of a block that is at least 256 blocks older than the current one : 比当前区块小256个区块高度的区块哈希。
(1)block.blockhash(block.number)
通过 `block.number` 变量可以获取当前区块区块高度。但是还没执行时,这个“当前区块”是一个未来区块,即只有当一个矿工拾取一个执行合约代码的交易时,这个未来区块才变为当前区块,所以合约才可以可靠地获取此区块的区块哈希。而一些合约曲解了 `block.blockhash(block.number)` 的含义,误认为当前区块的区块哈希在运行过程中是已知的,并将之做为熵源。还有一点就是在以太坊虚拟机中(EVM),区块哈希恒为 0。

示例1:< https://etherscan.io/address/0xa65d59708838581520511d98fb8b5d1f76a96cad>
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);
}
示例2:< https://github.com/axiomzen/eth-random/issues/3>
function random(uint64 upper) public returns (uint64 randomNumber) {
_seed = uint64(sha3(sha3(block.blockhash(block.number), _seed), now));
return _seed % upper;
}
(2)block.blockhash(block.number-1)
有一些合约则基于负一高度区块区块哈希来产生伪随机数,这也是有缺陷的。攻击合约只要以相同代码执行,即可以产生到同样的伪随机数。
示例:< https://etherscan.io/address/0xF767fCA8e65d03fE16D4e38810f5E5376c3372A8>
//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;
}
(3)Blockhash of a future block
一个更好的方法是使用未来区块的区块哈希。示例应用执行的脚本逻辑示例如下:
1. 玩家发起下注,以当前区块高度下注,庄家存储此区块高度。
2. 玩家第二次调用时,调用庄家公布赢家的功能。
3. 庄家检索存储的区块高度,并以此区块高度的区块哈希来产生伪随机数。
此方法只有在十分必要的时候才能使用。因为也存在一定危险性,EVM 能存储的区块哈希为最近的 256 条。超过的话值为 0。
因此,如果第二次调用时,与第一次下注时的区块高度差超过了 256,那么此时的产生的区块哈希为 0,此时伪随机数就变成可猜测的了。
一个众所周知的案例是 SmartBillions lottery。合约对区块高度的验证不足,导致了 400 ETH 的损失
相关资料:< https://www.reddit.com/r/ethereum/comments/74d3dc/smartbillions_lottery_contract_just_got_hacked/>
(4)Blockhash with a private seed
为了增加熵值,有些合约使用一个私有种子(seed)变量。
如 Slotthereum lottery 合约:
// case of Slotthereum lottery
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;
}
}




