Solving CaptureTheEther - Part 2 of 5 - Lotteries

Overview

This is the second post of a total of five, for the CaptureTheEther CTF writeups. This time, the focus will be on the Lotteries category of challenges. Most levels here will require the player to code and deploy new contracts to the network, and interact with them.

These levels have in common that they try to implement some form of randomness in the contracts. The EVM is a deterministic state machine, so that all of the nodes (or miners) who process and verify the transactions obtain the same result upon execution of a function in the same context. This makes it hard to find sufficiently secure sources of randomness during execution time, unless an oracle is used. And even in that case, extreme care should be taken to prove that the random numbers are truly random.

Something to take into consideration when solving these challenges, is that they were made some time ago, and Solidity has had significant changes in this period. For example, the constructor of the contract has the same name as the contract itself, instead of being a special function named constructor(). Always check the changelogs and the breaking changes section of Solidity documentation for each new compiler version.

Let's dive into the challenges, and explore most common flaws regarding randomness.

Info

All my solutions to CaptureTheEther were coded some time ago, while I was still new at Solidity and EVM. All of them work, however they certainly don't meet any good coding recommendation whatsoever.

It was part of my learning path, and I don't intend to update them as it serves me (and hopefully you too!) as a remainder that learning is a never-ending process that rewards you when you look back and see your real progress.

The complete code for these solutions is at my CTE-Solutions github repo.


Guess the number


The first of the series, and hence the easiest one.

Contract analysis

The code shows a GuessTheNumberChallenge contract with the following functions implemented:

  • A payable constructor GuessTheNumberChallenge() that takes 1 ether from the deployer (that is, the player).
  • The isComplete() function that checks if the level was correctly solved. This function is called by CaptureTheEther when the Check solution button is pressed.
  • A guess(uint8) payable function that takes 1 ether from the player and checks if the value guessed is correct. In case it is, it gives back 2 ethers to the player, hopefully emptying the contract balance.

Finally, there is a public storage variable uint8 answer initialized to the value 42.

Vulnerability / Attack vector

No real vulnerability is present, but the number to be guessed is easily found in plain sight in the contract code.

Solution

The guess(uint8) function compares the number passed as argument with the answer variable, and transfers 2 ethers to the caller if the values match. As there is nothing hidden and the variable is initialized in the code, the call to guess(uint8) should pass 42 as the argument to the function.

However, if someone calls guess(uint8) with an incorrect value, the contract would lock the ether sent. In that case, to empty the contract and meet the completeness condition for the level, you will need to call guess(uint8) with the correct value more than once until the contract balance is zero.

Conclusions

The most important takeaway here is that hardcoding values in the contract has no randomness at all. This might sound like a bad joke, but even in the case where the attacker has no access to the source code, the value can be publicly seen in the blockchain and in the EVM bytecode for the deployed contract.


Guess the secret number


This is similar to the previous challenge, but it cranks up the difficulty a bit. Now the answer isn't easily visible, or is it?

Contract analysis

The GuessTheSecretNumberChallenge contract is almost identical to GuessTheNumberChallenge from the previous level. The differences are:

  • The answer variable was replaced by answerHash, and now the value stored is a Keccak256 hash of the correct answer value.
  • The guess(uint8) function now checks for the hash of the value, and then compares it to the hashed answer.

Vulnerability / Attack vector

Still the same. The answer has no randomness as it is the hash of a constant value.

Solution

This level will require some coding. The secret number is unknown, and the goal is to find it knowing its hash. Keccak256 hashes are not reversible, that is, knowing the hash of some data, there is no way other than bruteforce to get back the original data.

So, bruteforcing it is. The search space is small enough because the argument has to be a uint8, so the level will be solved in almost no time. The code I used was written in Python, but you can use any language you feel comfortable with, as the cracking will be done off-chain.

You will need hexbytes, numpy and sha3 libraries to run the code. The secret number was 170.

1from hexbytes import *
2import numpy as np
3from sha3 import keccak_256
4
5for i in range(0,256):
6    hash = "0x" + keccak_256(np.uint8(i)).hexdigest()
7    if hash == '0xdb81b4d58595fbbbb592d3661a34cdca14d7ab379441400cbfa1b78bc447c365':
8        print (i, hash)

Conclusions

Even if the value is hidden as a hash, the constraints on the valid values for the variable type and the possibility to bruteforce the hash off-chain made the challenge easy to solve.


Guess the random number


In this challenge, the secret number is not a constant anymore, and the value comes from a couple fairly random sources according to the level description.

Contract analysis

Again, the GuessTheRandomNumberChallenge contract is very similar to the previous ones. Let's see what's different:

  • The answer variable is not initialized with a constant value, so it defaults to 0.
  • The constructor assigns a value to answer that comes from the hash of the previous block hash, and the current block timestamp.

Vulnerability / Attack vector

As stated before, there is no real randomness in using previous block hashes because the value is known - after all, the block has already been mined. The block timestamp looks like a random value, but in fact is not a true source of randomness, since the miner has the ability to set it.

Solution

The goal is to calculate the value stored in answer and pass it to guess(uint8).

The constructor is executed when the contract is deployed, so one way to solve this level would be to deploy, use Etherscan or any other block explorer to check the values for the previous block hash and block timestamp for the deployment block, and calculate the correct value of answer off-chain.

However, there's one important concept in blockchains that we are missing: all data in the blockchain is public, and anyone can access it. Reading Solidity storage layout it can be concluded that the answer variable is in the contract storage area, and since it is the first variable, it's in slot 0.

This slot can be read in at least two different ways:

  • Using the web3 library in Python
  • Using Etherscan to read contract state

I went with the first approach, and the code I used follows. Note that for this to work, you will need an Infura valid project ID set as environment variable.

1from web3.auto.infura.ropsten import w3
2w3.eth.get_storage_at(DEPLOYED_CONTRACT_ADDRESS, 0)

The Etherscan way of solving the challenge is searching for the contract address, then looking at the deployment transaction, and then in the State tab, check the storage slot 0.

Once the slot has been read and the number is known, pass it as the argument to guess(uint8) and finish the level.

Conclusions

This contract tried to hide the value harder than the previous ones, but the developer forgot that there's no private data on the blockchain.

Also, the number was constant after the contract was deployed. The only place that modifies the answer variable is the constructor, so it can't be changed anymore.


Guess the new number


This is getting interesting, the value is not constant anymore, it's generated when a function is called.

No off-chain solutions this time, so this challenge will be remembered as the first CaptureTheEther level that required deploying a contract.

Contract analysis

Let's see what's different from the previous contract:

  • The answer variable no longer exists.
  • The constructor doesn't calculate anything anymore, it just takes the ether from the player.
  • The guess(uint8) function calculates the random number when it is called. The calculation is exactly the same as in the previous level.

Vulnerability / Attack vector

As stated before, there is no true randomness, the value can still be calculated and passed to the function.

Solution

The main difference between this level and the past ones, is that you have to guess the value in the same block where the guess(uint8) function is called. This is because now (deprecated in modern Solidity versions, replaced with block.timestamp) holds the current block timestamp, a value set by the miner who processes the transactions in the block.

You can't accurately predict the value, so there's no practical way of solving the level other than reading block.timestamp, calculating the expected value for the secret number on-chain, and then calling the guess(uint8) function of the target contract.

To interact on-chain with this target contract, you will have to deploy an attacker contract of your own. Instead of calling guess(uint8) from Remix, the idea is to call it from the attacker contract once the correct value has been calculated. The block.timestamp is the same for all transactions in the same block, so the answer value calculated is the same value that the guess(uint8) function is expecting.

The relevant part of the attacker contract I used:

 1contract CTE_GuessTheNewNumberSolve {
 2    address private targetContract = DEPLOYED_CONTRACT_ADDRESS;
 3    GuessTheNewNumberChallenge target;
 4
 5    constructor() public payable {
 6        target = GuessTheNewNumberChallenge(targetContract);
 7    }
 8
 9    function solveChallenge() public payable {
10        require(msg.value == 1 ether);
11        uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), block.timestamp));
12        target.guess.value(msg.value)(answer);
13    }
14}

Conclusions

This contract tried to hide the value harder than the previous ones, but the developer forgot that there's no private data on the blockchain.

Also, the number was constant after the contract was deployed. The only place that modifies the answer variable is the constructor, so it can't be changed anymore.


Predict the future


Sounds like an easy task, right? To solve this level, you need to know what will happen some time in the future.

However, since this is a game and people already solved it, it shouldn't be an impossible task. Let's take a look at the contract, and then code an attack.

Don't forget the hint: it is indeed possible to solve this challenge without losing any ether.

Contract analysis

This time, the guessing mechanics have changed. There are two functions that need to be called to make a guess: lockInGuess(uint8 n) and settle().

1function lockInGuess(uint8 n) public payable {
2    require(guesser == 0);
3    require(msg.value == 1 ether);
4
5    guesser = msg.sender;
6    guess = n;
7    settlementBlockNumber = block.number + 1;
8}

By calling lockInGuess(uint8) the player locks in a guess, that is, a bet is placed with 1 ether value to the number n. Due to the require in line 2, you can't place another guess if you've not settled the first one. Finally, the line 7 saves the next block number to settlementBlockNumber.

 1function settle() public {
 2    require(msg.sender == guesser);
 3    require(block.number > settlementBlockNumber);
 4
 5    uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now)) % 10;
 6
 7    guesser = 0;
 8    if (guess == answer) {
 9        msg.sender.transfer(2 ether);
10    }
11}

Now, when settle() is called, the require in line 3 makes sure the guess wasn't made in the current or previous block, so it had to be locked in at least 2 blocks back. Even if we can calculate the value of answer, we can't update the guess already locked.

As the problem definition states, the % 10 in line 5 means that there are 10 only possible values for answer, and this makes it a little easier to solve the level.

Vulnerability / Attack vector

There's no automated exploit, so the solution involves some bruteforcing.

Solution

There's no other solution than locking in a value and calling the settle() function later, to try our luck. However, the goal is not to lose significant amounts of ether (the hint said any ether but that wasn't taking into account the gas cost of the transactions).

As in the previous level, it's possible to compute the value of answer for the current state of the blockchain and miner's timestamp. If the value equals the one that was locked in, we go ahead and call settle(). However, if it's not equal, the execution can be reversed to avoid losing 1 ether.

The relevant part of the attacker contract I used:

 1contract CTE_PredictTheFutureSolve {
 2    address private targetContract = DEPLOYED_CONTRACT_ADDRESS;
 3    PredictTheFutureChallenge target;
 4    
 5    constructor() public payable {
 6        target = PredictTheFutureChallenge(targetContract);
 7    }
 8    
 9    function makeGuess() public payable {
10        require(msg.value == 1 ether);
11        target.lockInGuess.value(msg.value)(0);
12    }
13    
14    function solveChallenge() public payable {
15        require(0 == uint8(keccak256(block.blockhash(block.number - 1), now)) % 10);
16        target.settle();
17    }
18}

Statistically, one of every 10 executions should guess the correct value and solve the challenge. In my solution, I locked in the value zero and called solveChallenge() repeatedly until it didn't revert.

Conclusions

The challenge could be solved in a reasonable time because the answer could hold only 10 different values. This made the bruteforce success rate high enough.

However, the key point in this level and all the others of this category, is that randomness is hard to implement securely.


Predict the block hash


If predicting one out of ten values in the future wasn't enough, now the task is to predict a full block hash. That means you have to predict 256 bits, compared to the ~4 bits of the previous level.

This is impossible. Let's see what other options we have.

Contract analysis

This one is similar to Predict the future. The main differences are that lockInGuess(bytes32) now takes a hash instead of a number, and settle() checks for the equality between the guess and the block hash of the settlementBlockNumber block.

Vulnerability / Attack vector

Given the impossibility of the task at hand, the solution lies in Solidity documentation. The Global variables section of the Cheatsheet states:

blockhash(uint blockNumber) returns (bytes32): hash of the given block - only works for 256 most recent blocks

Digging deeper, you can access the hashes of the most 256 recent blocks, all other values will return zero.

Solution

To solve this level and finish the Lotteries category, the procedure is simple enough: lock in a zero value as guess, and wait at least 256 blocks before calling settle(). Given that the block time is around 15 seconds, you have a little bit more than an hour to solve the Math challenges before coming back to this one.

Conclusions

What seemed like an impossible task was easily solved by reading the documentation. That should be the moral of this challenge. Never assume behaviour, always double check your assumptions.

Congratulations for finishing Lotteries! See you in Math.

Posts in this Series

comments powered by Disqus