The first of a multi-part series on smart contract exploits featuring Capture the Ether.
by Caleb Lau
Contract Exploits – Featuring Capture the Ether – Lotteries
A few months back a series of Solidity (and blockchain in general) related challenges were released as a game, where one would need to hack smart contracts and drain the ethers held within to win. There are 17 challenges in total with differing difficulties, with 3 warmup challenges for those unfamiliar with how Solidity works. This series of writeup will share my approach solving these challenges, and of course, if you are a Solidity developer and have yet to try these challenges, I would highly recommend you to give it a shot, prior to reading the approaches contained within.
The website where these challenges could be found: https://capturetheether.com/challenges/
And the author of these challenges is the very brilliant smarx, catch him on his twitter handle @smarx.
This writeup will skip straight into the Lotteries section, showing that keeping secrets or creating random values on a public, deterministic distributed system could be challenging. This article also assumes some knowledge of Solidity and the dev tools surrounding it.
Without further ado – Huge spoilers ahead!
1. Guess the Number
Source code as below.
pragma solidity ^0.4.21; contract GuessTheNumberChallenge { uint8 answer = 42; function GuessTheNumberChallenge() public payable { require(msg.value == 1 ether); } function isComplete() public view returns (bool) { return address(this).balance == 0; } function guess(uint8 n) public payable { require(msg.value == 1 ether); if (n == answer) { msg.sender.transfer(2 ether); } } }
To win, we will need to call function guess()
, passing the answer through variable n
, also sending 1 ether along with the transaction. Since we have access to the source code, we could evidently see that the answer is 42! Calling function guess()
with parameter n = 42
will solve the challenge.
While this is simple enough, I do believe the author is trying to imply something further. Note that uint8 answer = 42
does not have an access modifier, which then defaults it to private. If the source codes aren’t publicly published, it wouldn’t be as apparent, as what we will have access to is only the bytecode:
This may throw off some to think the variable is inaccessible entirely, which unfortunately, is false. Even when declared private, we can still access the state storage of the contract simply by running the below command and looking for the relevant storage index:
2. Guess the Secret Number
Source code as below.
pragma solidity ^0.4.21; contract GuessTheSecretNumberChallenge { bytes32 answerHash = 0xdb81b4d58595fbbbb592d3661a34cdca14d7ab379441400cbfa1b78bc447c365; function GuessTheSecretNumberChallenge() public payable { require(msg.value == 1 ether); } function isComplete() public view returns (bool) { return address(this).balance == 0; } function guess(uint8 n) public payable { require(msg.value == 1 ether); if (keccak256(n) == answerHash) { msg.sender.transfer(2 ether); } } }
Maybe plaintext is a terrible idea. Let’s hash the answer. So now we need to call function guess()
, and likewise pass a number through variable n
, which gets keccak256 hashed and compared with state variable answerHash, being 0xdb81b4d58595fbbbb592d3661a34cdca14d7ab379441400cbfa1b78bc447c365
.
Except that variable n
is declared uint8. Which is 8 bits, with a minimum integer of 0 and a maximum of 255, a probability space of 256 to test. Not so much to run through really, so let’s quickly cook up something in Remix…
And we get 170 as the answer! Likewise sending the answer with 1 ether, we will complete this challenge.
Note that as long as you have a keccak256 implementation/library which you can access, any other programming language should give you the same result, though bearing in mind the data type, e.g. most online converters will treat inputs as string which will be hashed differently. For example, using web3.sha3
:
3. Guess the Random Number
Source code as below.
pragma solidity ^0.4.21; contract GuessTheRandomNumberChallenge { uint8 answer; function GuessTheRandomNumberChallenge() public payable { require(msg.value == 1 ether); answer = uint8(keccak256(block.blockhash(block.number - 1), now)); } function isComplete() public view returns (bool) { return address(this).balance == 0; } function guess(uint8 n) public payable { require(msg.value == 1 ether); if (n == answer) { msg.sender.transfer(2 ether); } } }
So now the state variable answer
is initialised using a previous blockhash and current block timestamp hashed with keccak256 and cast down to uint8. Now there are a few problems with this…
- It is trivial to obtain the blockhash being used here, and the timestamp of the block. Furthermore, it is generally not advisable to use blockhash as a random seed (see this, and this).
- The state variable
answer
only has a probability space of 0 to 255, maximally requiring 256ETH to iterate through in a brute force fashion (but we don’t need to). - As mentioned in Guess the Number, private variables can still be accessed.
We could obtain the blockhash easily by finding out which blocknumber the contract is deployed, minus 1, and retrieve that block’s blockhash from Etherscan. Timestamp is the Unix timestamp of the blocknumber where the contract is deployed. Keccak256 these two values and cast it to uint8, and we have our answer.
Or, the easier way is just to access the state variable index.
4. Guess the New Number
Source code as below.
pragma solidity ^0.4.21; contract GuessTheNewNumberChallenge { function GuessTheNewNumberChallenge() public payable { require(msg.value == 1 ether); } function isComplete() public view returns (bool) { return address(this).balance == 0; } function guess(uint8 n) public payable { require(msg.value == 1 ether); uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now)); if (n == answer) { msg.sender.transfer(2 ether); } } }
This is pretty much similar to “Guess the Random Number”, difference being that the variable answer
is initialised each time a guess is made. On the surface it looks like some level of randomness is introduced as it is unlikely for us to know what the blockhash is; Except that in reality we don’t even need to know what the blockhash is to exploit this case.
To elaborate further, recall that it is possible to use a smart contract to call another smart contract, with each transaction sent completing its execution within the same block even when the transaction involves execution across multiple smart contracts. To exploit this contract then, should we build a separate smart contract which calls function guess()
using the same mechanism to initialise variable answer
, we will land with the exact same value, as the execution is done within the same block, using the same blockhash and timestamp.
Now that we know this, let’s fire up Remix. Remember that our solution needs to transfer 1 ether to the contract we wish to exploit, so our function needs to be payable, allowing us to send 1 ether during execution.
Hmm. We deployed the contract but why when we try to execute the exploit, Remix complains about the transaction being likely to fail?
Let’s take a step back and review this again. When we use a smart contract to call for function guess()
, assuming the input is correct, GuessTheNewNumber contract would then return ethers, which in this case, to the msg.sender
. Recall that msg.sender
refers to the sender of the transaction in which case would be the contract address we are using for the exploit (compared to tx.origin
, which refers to the original sender). So now, we see two problems our current solution has:
- Does not allow for a fallback payable function.
- Even if specify a fallback payable function, we want the ethers back so we need to somehow tell the contract to send it back to us.
A fallback function is simply a function which executes by default if no other functions matches the function signature being called. Examples are when ethers are sent as a normal transaction to a contract hence triggering the payable fallback function to execute, or when a function is called but the function does not exist within the contract. To get the ethers back to us, we could specify a function which transfers the ethers back, or use selfdestruct(msg.sender)
. Note that this isn’t coded into the fallback function as there isn’t enough gas forwarded from the transfer operation.
Therefore the exploit code should really look like:
Running exec()
and subsequently destroy()
will get us our ethers back.
5. Predict the Future
Source code as below.
pragma solidity ^0.4.21; contract PredictTheFutureChallenge { address guesser; uint8 guess; uint256 settlementBlockNumber; function PredictTheFutureChallenge() public payable { require(msg.value == 1 ether); } function isComplete() public view returns (bool) { return address(this).balance == 0; } function lockInGuess(uint8 n) public payable { require(guesser == 0); require(msg.value == 1 ether); guesser = msg.sender; guess = n; settlementBlockNumber = block.number + 1; } function settle() public { require(msg.sender == guesser); require(block.number > settlementBlockNumber); uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now)) % 10; guesser = 0; if (guess == answer) { msg.sender.transfer(2 ether); } } }
For this challenge, we need to commit our guess beforehand. Committing the guess will log both the guess
value, and the guesser
’s address. There is a variable settlementBlockNumber
which acts as a conditional so settle()
can only be called on subsequent blocks. When we call the function settle()
, an evaluation runs to see if the committed guess matches with an uint8 cast of a keccak256 hash created using the previous blockhash and current block timestamp, mod 10. Which means the probability space is 0 to 9, and any guess within this range will have a 10% chance of being correct.
Using what we have learnt from the pass challenges, our exploit sequence should then look a bit like this:
- Create another contract, which primarily should have two functions: Commits a guess to the PredictTheFuture contract; Evaluate if our guess is correct per the line
uint8(keccak256(block.blockhash(block.number - 1), now)) % 10
- Commit our guess.
- After committing, start evaluating if our guess is correct.
- If our guess is correct, the contract will proceed to call
settle()
on the PredictTheFuture contract.
We will deploy an exploit contract, where we can lock in our guess, and using libraries like Web3JS (I will be using Nethereum) we can call the exploit contract once every block/couple of blocks to execute the exploit. Alternatively, we could call the exploit contract function manually via Remix every couple of blocks as well.
Let’s lock in 7 as our guess.
Load up VS2017 and create a function to call our exploit contract. Let it do its thing while we browse for cute cat pictures.
Once our exploit contract obtains the correct guess, the function should complete and stop.
Now we just need to call killcontract()
on our exploit contract to selfdestruct and retrieve the ethers, complete this challenge, and move on to the last one in the Lotteries section.
6. Predict the Block Hash
Source code as below.
pragma solidity ^0.4.21; contract PredictTheBlockHashChallenge { address guesser; bytes32 guess; uint256 settlementBlockNumber; function PredictTheBlockHashChallenge() public payable { require(msg.value == 1 ether); } function isComplete() public view returns (bool) { return address(this).balance == 0; } function lockInGuess(bytes32 hash) public payable { require(guesser == 0); require(msg.value == 1 ether); guesser = msg.sender; guess = hash; settlementBlockNumber = block.number + 1; } function settle() public { require(msg.sender == guesser); require(block.number > settlementBlockNumber); bytes32 answer = block.blockhash(settlementBlockNumber); guesser = 0; if (guess == answer) { msg.sender.transfer(2 ether); } } }
This one is interesting. Like the previous challenge, we need to submit our guess, and call the function settle()
on subsequent blocks to see if our guess is correct. The main difference being when we submit our guess, we are also specifying that the solution hash is to be the hash of the next block (block.number + 1
), and being a 256 bit space it would be impossible to brute force.
However, the undoing of this contract is that it relies on blockhash, and retrieval of blockhash is only good up to 256 past blocks (for client implementation efficiency purposes), which retrieval past 256 blocks simply returns 0x0000000000000000000000000000000000000000000000000000000000000000
This is mentioned in the Ethereum yellow paper, and also in the Solidity docs:
Putting this together, exploiting this contract is simple enough. Submit a guess of 0x0000000000000000000000000000000000000000000000000000000000000000
, wait for 256 blocks to pass (about an hour, or an hour and a half for good measure), then execute function settle()
to complete the challenge.
Conclusion
This wraps up the first of this multi-part series. Evidently, keeping data private and randomness on a deterministic distributed system is difficult. A few key takeaways:
- Be constantly aware that all data and transactions on a public blockchain is public for everyone to see.
- Be constantly aware that another smart contract can call your smart contract for some level of automation and state-sharing, therefore your smart contract should be designed to be safe enough to be called by both externally owned accounts and contract accounts.
- Avoid using blockhash as a seed for random computation – It could be PART of your random entropy scheme depending on how much value your smart contract is dealing with, but do not be reliant on it. Where dealing with huge values, look into combining additional entropy sources such as addresses of participants, commit-reveal schemes, game theoretical scenarios to prevent collusion, use an oracle, etc.
- Be constantly aware of EVM’s behaviour and quirks.
The rest of the series will be published over the weeks to come, probably will publish the sections Accounts and Miscellaneous first, as I suspect I would make Math to be quite lengthy. Let’s see. In the meantime, I would encourage you to try your hand on the rest of the challenges – All the best!
Update (3rd September 2018) – Part 2 now available: https://celebrusadvisory.com/smart-contract-exploits-part-2/
Leave A Comment