Ethernaut: Levels 4 to 6

The Ethernaut is a Web3/Solidity based wargame inspired from overthewire.org, played in the Ethereum Virtual Machine. Each level is a smart contract that needs to be 'hacked'.

go to:
Token
Delegation

Level 4 - Telephone

Target: claim ownership of the contract.

Contract

pragma solidity ^0.5.0;

contract Telephone {

  address public owner;

  constructor() public {
    owner = msg.sender;
  }

  function changeOwner(address _owner) public {
    if (tx.origin != msg.sender) {
      owner = _owner;
    }
  }
}

Weakness

A conditional requirements uses tx.origin

Solidity Concepts: tx.origin vs msg.sender

  • tx.origin (address payable): sender of the transaction (full call chain)
  • msg.sender (address payable): sender of the message (current call)

In the situation where a user call a function in contract 1, that will call function of contract 2:

at execution contract 1 at execution in contract 2
msg.sender userAddress contract1Address
tx.origin userAddress userAddress

Hack

Deploy an attacker contract.
Call the changeOwner function of the original contract from the attacker contract to ensure tx.origin != msg.sender and pass the conditional requirement.

Takeaways

Don't use tx.origin

Level 5 - Token

Target: "You are given 20 tokens to start with and you will beat the level if you somehow manage to get your hands on any additional tokens. Preferably a very large amount of tokens."

Contract

pragma solidity ^0.5.0;

contract Token {

  mapping(address => uint) balances;
  uint public totalSupply;

  constructor(uint _initialSupply) public {
    balances[msg.sender] = totalSupply = _initialSupply;
  }

  function transfer(address _to, uint _value) public returns (bool) {
    require(balances[msg.sender] - _value >= 0);
    balances[msg.sender] -= _value;
    balances[_to] += _value;
    return true;
  }

  function balanceOf(address _owner) public view returns (uint balance) {
    return balances[_owner];
  }
}

Weakness

An sum operation is performed but overflow isn't checked for.

Solidity Concepts: bits storage, underflow/overflow

Ethereum’s smart contract storage slot are each 256 bits, or 32 bytes. Solidity supports both signed integers, and unsigned integers uint of up to 256 bits.
However as in many programming languages, Solidity’s integer types are not actually integers. They resemble integers when the values are small, but behave differently if the numbers are larger. For example, the following is true: uint8(255) + uint8(1) == 0. This situation is called an overflow. It occurs when an operation is performed that requires a fixed size variable to store a number (or piece of data) that is outside the range of the variable’s data type. An underflow is the converse situation: uint8(0) - uint8(1) == 255.
over/underflow image

Hack

Provoke the overflow by transferring 21 tokens to the contract.

Takeaways

Check for over/underflow manually:

if(a + c > a) {
  a = a + c;
}

Or use OpenZeppelin's math library that automatically checks for overflows in all the mathematical operators.

Level 6 - Delegation

Target: claim ownership of the contract.

Contract

pragma solidity ^0.5.0;

contract Delegate {

  address public owner;

  constructor(address _owner) public {
    owner = _owner;
  }

  function pwn() public {
    owner = msg.sender;
  }
}

contract Delegation {

  address public owner;
  Delegate delegate;

  constructor(address _delegateAddress) public {
    delegate = Delegate(_delegateAddress);
    owner = msg.sender;
  }

  function() external {
    (bool result, bytes memory data) = address(delegate).delegatecall(msg.data);
    if (result) {
      this;
    }
  }
}

Weakness

The Delegation fallback implements a delegatecall.
By sending the right msg.data we can trigger the function pwn() of the Delegate contract. Since this function is executed by a delegatecall the context will be preserved:
owner = msg.sender = address of contract that send data to the Delegation fallback (attacker contract)

Solidity Concepts: storage, call another contract's function

There are several ways to interact with other contracts from within a given contract.

If ABI available

If the ABI (like an API for smart contract) and the contract's address are known, we can simply instantiate (e.g with a contract interface) the contract and call its functions).

contract Called {
     function fun() public returns (bool);
}

contract Caller {
     Called public called; 
     constructor (Called addr) public {
         called = addr;
    }

    function call () {
      called.fun();
    }
}

ABI not available: delegatecall or call

Calling a function means injecting a specific context (arguments) to a group of commands (function) and commands are executing one by one with this context.

Bytecode

In Ethereum, a function call can be expressed by a 2 parts bytecode as long as 4 + 32 * N bytes.

  • Function Selector: first 4 bytes of function call’s bytecode. Generated by hashing target function’s name plus with the type of its arguments excluding empty space. Ethereum uses keccak-256 hashing function to create function selector: functionSelectorHash = web3.utils.keccak256('func()')
  • Function Argument: convert each value of arguments into a hex string padded to 32bytes.

If there is more than one argument, they are concatenated.
In Solidity encoding the function selector together with the arguments can be done with abi.encode, abi.encodePacked, abi.encodeWithSelector and abi.encodeWithSignature:
abi.encodeWithSignature("add(uint256,uint256)", valueForArg1, valueForArg2)

Call: doesn't preserve context.

Can be used to invoke public functions by sending data in a transaction.
contractInstance.call(bytes4(keccak256("functionName(inputType)"))
call diagram

DelegateCall: preserves context

contractInstance.delegatecall(bytes4(keccak256("functionName(inputType)"))
Delegate calls preserve current calling contract's context (storage, msg.sender, msg.value).
The calling contract using delegate calls allows the called contract to mutate its state.
delegatecall diagram
delegatecall mtating state diagram 2

Hack

  1. Compute the encoded hash that will be used for msg.data
  2. Send msg.data in a transaction to the contract fallback

Takeaways

  • Use the higher level call() function to inherit from libraries, especially when you don’t need to change contract storage and do not care about gas control.
  • When inheriting from a library intending to alter your contract’s storage, make sure to line up your storage slots with the library’s storage slots to avoid unexpected state changes..
  • Authenticate and do conditional checks on functions that invoke delegatecalls.

Solutions on GitHub


You'll only receive email when Gauthier Riou publishes a new post

More from Gauthier Riou