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.
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.
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:
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.
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:
play()" calls to.
selfdestruct()with it to send the recovered ETH back to your wallet.
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
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.
Then, visit the attacking contract's transaction
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(), answer the following for your lab notebook:
targetin the Lottery contract? Write it out using only keccak256, abi.encodedPacked, and
msg.sender(hint: it's not your wallet address)?
guessequal to the expression for
target. What is the value for
_seedthat will make this equality hold? This value is what the attacking contract needs to call
msg.sendercorrespond 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
_seedto use to call
It is difficult to get random numbers correct on a blockchain and you've shown you can identify contracts that rely on random numbers that aren't random and exploit them using other contracts.