This codelab will walk you through levels in the Security Innovation CTF that suffer from bad randomness, demonstrating some of the problems with writing smart contracts in Solidity securely. If you'd like more information on smart contract vulnerabilities, I would recommend visiting https://github.com/sigp/solidity-security-blog or viewing the associated screencast lectures available from class.

What you'll learn

What you'll need

This level contract requires a player to guess coin flips so that the number of correct guesses is more than the number of incorrect guesses by 10. If coin flips were truly random, it would take a long time for this to happen. In the contract, the result of a coin flip is determined by the last bit of blockhash(block.number-1) (the hash of the last block added to the blockchain). The code for this is shown below:

As the coin flip is determined from a recent block, the code assumes that it is difficult for a player invoking the play() function to know what the coinFlip result will be beforehand. This assumption, unfortunately, is incorrect. Moreover, because all contracts have programmatic access to the blockhash(block.number-1) value, a program can be written to deterministically guess the correct answer every flip.

Calling a contract from another contract is similar to calling a cross-class function in other languages. We have seen this with KillMyContract previously. There are 3 ways to do so that you can employ.

Call function using hash of signature (the most painful)

Solidity uses the first 4 bytes of call data to determine which function is being called. These 4 bytes are the first 4 bytes of the keccak256 hash of the function signature.

Using this, you can make the call within your code that invoke function as shown below:

Call function using full source

Solidity will setup and handle the call for you if you provide a contract or its interface. To do so, you can get the source code of the contract from the CTF and then copy and paste the contract code into Remix IDE. With this method, you can run the contract yourself in the local EVM to debug your exploit. In the CTF level, you can download the contract code by going to the "Source" tab or the "Mythril" tab.

After doing so, the snippet below can be used to instantiate the victim contract to call.

Call function using interface

This is the recommended approach that was used in the KillMyContract lab performed previously. Note that for this approach to work, you must:

An example of this is shown below:

Consider the first part of the attack contract below. In its constructor, the contract is initialized with two things:

The contract also has a payable fallback function in order to allow our victim to send ETH to us once we've correctly guessed the coin flip 10 times in a row.

We wish to programmatically call the victim's play() function using guesses for coin flips that will always be correct. We can actually borrow code from the original contract to calculate the coinFlip value directly in our exploit() function. The first two lines come straight from the victim to calculate the value of coinFlip. Using this, we can then call play() with the appropriate amount of ETH via.value(.1 ether) using an appropriately set boolean as a parameter to play that will always win (coinFlip == 1). The attacking contract continues to call the victim contract until it no longer has a positive balance (which will happen once it pays us its ETH). Finally, it calls selfdestruct() to send the money to the owner of the attacking contract (i.e. us). Note that exploit() is marked as a payable function. This is necessary because it needs to receive the initial 0.1 ETH that it uses to then invoke the victim's play() function.

Implement the attacking contract in Remix. Then, select the appropriate compiler version in the compiler tab:

Compile and deploy the contract on Ropsten

In order to limit who can attack a CTF level contract, the CtfFramework ensures only authorized addresses can interact with the level. This consists of your wallet address initially. However, in this level, we want our attacking contract to also be able to call into the CTF level. Thus, we need to add the address of the attacking contract to the authorized users. To do so:

Within Remix, give the exploit contract enough ETH to place the initial first guess (0.1 ETH), then push the button to call exploit().

After the transaction is complete, bring up the attacking contract's address inside of Etherscan. Click on "Internal Transactions". You should see a series of transactions for all of the calls the attacking contract makes to the victim. Take a screenshot and include it in your lab notebook.

To get credit for completion, take a screenshot of the payments that your attacking contract receives from the CTF level contract to include in your lab notebook.

Also include the level screenshot showing a 0 ETH balance.

Now that you've exploited one contract from another, it's now your turn to try. Lottery is similar to HeadsOrTails. To solve it, create a contract that programmatically calculates the winning combination to receive the balance of ETH in the victim contract.

To make the level easier, go through the D6 lecture and find the value of

bytes32 entropy = blockhash(block.number);

Given the above, in play() what is the value of target in the Lottery contract? Write it out using only keccak256, abi.encodedPacked, and msg.sender.

Then, answer this question: if you call the contract from an attacking contract what is the value of msg.sender (hint: it's not your wallet address)?

Set the expression for guess equal to the expression for target. What is the value for _seed that will make this equality hold? This value is what the attacking contract needs to call play() with.

Finally, within the attacking contract. What address does msg.sender correspond to? Is this the same address that Lottery is expecting when it calculates the target? How would you find the appropriate address to use when calculating the value of _seed to use to call play() with?

As before, include screenshots of the Etherscan transactions and the level screenshot showing a 0 ETH balance for your lab notebok. In addition, commit the attacking contract's code to BitBucket using the following convention.

It is difficult to get random numbers correct on a blockchain and you've shown you can exploit contracts that don't get it right.

What we've covered