While blockchain protocols are typically secure, the decentralized apps (dapps) utilizing them may still contain a host of vulnerabilities. In addition to the bugs that exist in traditional software, blockchain smart contracts include numerous areas of which a hacker can take advantage, if you’re not careful.
Capture the Ether is a game to help you learn about those vulnerabilities. The goal of the game is straightforward – exploit vulnerabilities in Ethereum smart contracts to capture ether and complete the challenges.
Just as we did with the vulnerable FumbleChain, our team of experts applied our blockchain penetration testing methodologies to Capture the Ether, completing every challenge it threw at us. Here’s what we found.
WARNING: The remainder of this article contains solutions to numerous Capture the Ether challenges. If you’re planning on completing Capture the Ether yourself, you may want to do so before finishing this article.
Part 1: Finding Vulnerabilities in Lotteries
In part one of our Capture the Ether series, we show you how to apply static code analysis to discover common vulnerabilities in blockchain lotteries. Gambling and gaming platforms are some of the most prolific dapps on the Ethereum blockchain. Because many of them include lottery aspects, it’s no surprise that Capture the Ether begins here.
In each of the following lottery challenges, our goal is to correctly guess the winning number as we place our bet. Let’s dig in.
Challenge 1: Guess the Number
As you’ll notice throughout these challenges, our first step is to always look through the smart contract code to gain a better understanding of its logistics. A glance through the code in the first challenge reveals a glaring mistake. Can you spot it?
The smart contract developer hardcoded 42 as the winning lottery number, and that value never changes. You can see this error in the line:
No matter how many times you run the lottery, the winning number will always be 42. To take advantage of this mistake, place your bet on 42, run the contract, and win your ether.
Challenge 2: Guess the Secret Number
Learning from challenge one’s mistake, the lottery creators decide to store the winning number as a hash value (instead of the number itself) in challenge two.
At first glance, this challenge seems quite tricky. Brute-forcing the hash to expose the winning number would take a significant amount of time, more time than we have. However, looking at the guess function, we spot a clue.
Our guesses can only be a uint8 value type. Looking through the Ethereum documentation, you can find that uint8 is an unsigned integer that’s eight bits long. Therefore, it’s a number between 0 and 255.
At this point, we could place small bets on each of the numbers to discover the winner. But why waste our ether?
Instead, let’s write a smart contract to figure it out. Starting at 0, the contract hashes each integer up through 255 as a 32-byte hash and checks it against the one from the lottery contract. Once there’s a match, it returns the associated integer.
With the winning number on hand, you can place your guess and win the ether.
Challenge 3: Guess the Random Number
Now, the lottery creators have realized that hard-coding a winning number into the lottery contract isn’t a wise idea. Instead, the lottery contract from challenge three creates a random winning number when it’s deployed. Take a look at the contract below.
When you execute the contract, it calculates the winning number according to the following function:
Let’s break down piece by piece how this function calculates answer (the winning number).
First, it subtracts one from the smart contract’s block number (block.number – 1) and then hashes that block (block.blockhash()). Next, it concatenates that hash with the time of contract deployment (now). The now variable is represented by seconds in Unix Epochs.
Finally, the contract hashes the concatenation with Keccak256 and stores the result as a uint8 value. It converts to uint8 by merely storing the last byte as the answer value.
The block number and time of deployment are public information on the block explorer. So once we find them, we just need to plug those figures into the answer equation to calculate the winning number. There’s an even simpler solution, however.
Looking back in the contract code, notice how the developers set answer as a public, global variable. Therefore, you can search for the contract’s state changes on Etherscan to disclose the winning number. On to the next one.
Challenge 4: Guess the New Number
For the fourth challenge, the lottery developers get a little trickier. In challenge three, the lottery contract generated the winning number when it was deployed. For this challenge, however, the winning number isn’t decided until after we make our guess. This change causes our exploitation efforts to become slightly more involved. Here’s the new contract:
To understand how the winning number is chosen, look at the guess function this time.
Fortunately, the contract developers utilize almost the exact method to create answer as they did in the previous challenge. In this lottery, though, the contract inputs the block number and time of your guess’s contract rather than the initial lottery contract.
To take advantage of this vulnerability, you need to create an exploit contract:
Our contract, CTERandWin, performs the same computations as the lottery contract and then submits the output as our guess. Because the exploit contract and lottery contract reside in the same block, the guess is always correct.
After completing the challenge, we still have one issue to overcome, though. Because the exploit contract placed the bet, it receives the ether reward, not us. However, you can run the contract’s destroy() function to kill the contract and receive the winnings.
Challenge 5: Predict the Future
The fifth challenge, once again, requires us to create an exploit contract. In this lottery, you’re first required to guess a number between 0 and 9. Then, at a later point in time, you need to call a settle() function that reveals the winning number. If the two numbers match, you win the lottery.
As with the previous lotteries, let’s examine the lottery contract to see how it determines the winning number. You can see that it calculates the answer according to the equation below. Look familiar?
Again, the equation is nearly the same as the previous challenge. This time, however, we mod the 8-bit unsigned integer by 10 to give us a number 0 through 9. Knowing this calculation, we can make an exploit contract similar to the one from the fourth challenge.
First, we set our guess to 3. The winning number continuously changes with each new block, so the guess doesn’t ultimately matter. The timing of when we settle is the critical part. We need to make sure that we settle the bet in a block with a winning number that matches our guess.
Another critical component to notice is our predict() function. This function effectively checks if our guess (lockedguess) matches the winning number. If they match, then the contract calls the settle() function of the original lottery contract.
Our exploit contract looks great, but we still need to send it ether to place bets on our behalf. The lockguess() function handles this task. After you deploy the exploit contract, call the lockguess() function and include your ether bet with the transaction. The contract will then place your bet on the lottery contract, choosing 3 as your guess.
Following the lockguess() call, simply call predict() until you win the lottery. Thanks to our check, the transaction will fail if the numbers don’t match. Once they do, the transaction will complete successfully and the contract receives the reward. Finally, call the destroy() function and collect your ether.
Challenge 6: Predict the Block Hash
In the final lottery challenge, the creators step their security up a notch. Now, instead of guessing a number between 0 and 9, we need to predict a complete 256-bit block hash correctly. Find the lottery contract below.
As always, let’s begin with a read-through of the code, specifically the settle() function.
Two things stick out in this function. First, it produces answer as follows:
So, through the block.blockhash() function, it’s effectively setting the answer to the hash of the block that contains our guess (settlementBlockNumber).
We also see that there’s only one requirement to settle; we need to do so in a block that is greater than the one in which we guess.
To make things more understandable, here’s a simplified version of the process:
We place our bet and guess at block 123.
Our guess is an attempt to predict the hash of block 123.
The network mines and hashes block 123.
We settle in some block after block 123.
The lottery contract sets the winning number to the hash of block 123.
If our guess is the same as that winning number, we receive the ether.
At this point, our odds are looking a little grim. Our exploit contract worked previously because we only had ten numbers to pick from (0-9). Due to the ridiculously high number of hash possibilities, the previous exploit contract isn’t a viable option.
However, we may be able to find an exploit through the structure of the Solidity code. Take a look at the blockhash() function description.
Here, we see that the function only works for the 256 most recent blocks. If you try to call the function on one that occurred, say, 300 blocks ago, it returns zero. There’s our ticket.
All we need to do is place our bet and set our guess to zero. We know that once 256 blocks pass, block.blockhash(settlementBlockNumber) will return zero. Therefore, that’s our winning number.
So, wait for at least 256 blocks to process. You may be waiting for over an hour, so feel free to grab a coffee, watch some TV, whatever. Once you confirm that 256 blocks have passed, settle your bet, and capture your ether.
We’re Not Done Yet…
Be on the lookout for part two of our Capture the Ether serious in the next week or so. We’re going to cover common math vulnerabilities that affect token sales, a multitude of funds, and even time-based token lock-ups. You won’t want to miss it.