Ethernaut: Levels 7 to 9

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

goto:
Vault
King

Level 7 - Force

Target: make the balance of the contract greater than zero.

Contract

pragma solidity ^0.5.0;

contract Force {/*

                   MEOW ?
         /\_/\   /
    ____/ o o \
  /~____  =ø= /
 (______)__m_m)

*/}

Solidity Concept: selfdestruct

3 methods exist to receive Ethers:

  1. Message calls and payable functions
    • addr.call{value: x}(''): returns success condition and return data, forwards all available gas, adjustable
    • <address payable>.transfer(uint256 amount): reverts on failure, forwards 2300 gas stipend, not adjustable
    • <address payable>.send(uint256 amount) returns (bool): returns false on failure, forwards 2300 gas stipend, not adjustable function receive() payable external {}
  2. contract designated as recipient for mining rewards
  3. selfdestruct(address payable recipient): destroy the current contract, sending its funds to the given Address and end execution.

Hack

As the contract to hack has no payable function to receive ether, we send ether to it by selfdestructing another contract, designating the victim contract as the recipient.

Takeaways

By selfdestructing a contract, it is possible to send ether to another contract even if it doesn't have any payable functions.
This is dangerous as it can result in losing ether: sending ETH to a contract without withdraw function, or to a removed contract.

Level 8 - Vault

Contract

Target: Unlock vault.

pragma solidity ^0.5.0;

contract Vault {
  bool public locked;
  bytes32 private password;

  constructor(bytes32 _password) public {
    locked = true;
    password = _password;
  }

  function unlock(bytes32 _password) public {
    if (password == _password) {
      locked = false;
    }
  }
}

Weakness

The unlock function relies on a password with a private visibility. There is no real privacy on Ethereum which is public blockchain. The private visibility parameter is misleading as all data can still be read. Indeed the contract is available, so an attacker can know in which storage slot a variable is stored in and access its value manually using getStorageAt.

Solidity Concepts: storage

Storage: storage vs memory

  • storage: persistent data between function calls and transactions.
    • Key-value store that maps 256-bit words to 256-bit words.
    • Not possible to enumerate storage from within a contract
    • Costly to read, and even more to initialise and modify storage. Because of this cost, you should minimize what you store in persistent storage to what the contract needs to run. Store data like derived calculations, caching, and aggregates outside of the contract.
    • A contract can neither read nor write to any storage apart from its own.
  • memory ~ RAM: not persistent. A contract obtains a freshly cleared instance of memory for each message call

Layout

Data stored is storage in laid out in slots according to these rules:

  • Each slot allows 32 bytes = 256 bits
  • Slots start at index 0
  • Variables are indexed in the order they’re defined in contract contract Sample { uint256 first; // slot 0 uint256 second; // slot 1 }
  • Bytes are being used starting from the right of the slot
  • If a variable takes under < 256 bits to represent, leftover space will be shared with following variables if they fit in this same slot.
  • If a variable does not fit the remaining part of a storage slot, it is moved to the next storage slot.
  • Structs and arrays (non elementary types) always start a new slot and occupy whole slots (but items inside a struct or array are packed tightly according to these rules).
  • Constants don’t use this type of storage. (They don't occupy storage slots)

Storage layout image

Read storage: web3.eth.getStorageAt

Knowing a contract's address and the storage slot position a variable is stored in, it is possible to read its value value using the getStorageAt function of web3.js.

Hack

  1. Read contract to find out in slot password is stored in:
    • locked bool takes 1 bit of the first slot index 0
    • password is 32 bytes long. It can fit on the first slot so it goes on next slot at index 1
  2. Read storage at index 1
  3. Pass this value to the unlock function ## Takeaways
  4. Nothing is private in the EVMhttps://solidity.readthedocs.io/en/v0.6.2/security-considerations.html#private-information-and-randomness: addresses, timestamps, state changes and storage are publicly visible.
  5. Even if a contract set a storage variable as private, it is possible to read its value with getStorageAt
  6. When necessary to store sensitive value onchain, hash it first (e.g with sha256)

Level 9 - King

Target: Prevent losing kingship when submitting your instance.

Contract

pragma solidity ^0.5.0;

contract King {

  address payable king;
  uint public prize;
  address payable public owner;

  constructor() public payable {
    owner = msg.sender;  
    king = msg.sender;
    prize = msg.value;
  }

  function() external payable {
    require(msg.value >= prize || msg.sender == owner);
    king.transfer(msg.value);
    king = msg.sender;
    prize = msg.value;
  }

  function _king() public view returns (address payable) {
    return king;
  }
}

Weakness

The contract uses transfer instead of a withdraw pattern to send Ether.

Solidity Concepts: sending and receiving Eth

  • Neither contracts nor “external accounts” are currently able to prevent that someone sends them Ether. Contracts can react on and reject a regular transfer
  • If a contract receives Ether (without a function being called), either the receive Ether or the fallback function is executed. If it does not have a receive nor a fallback function, the Ether will be rejected (by throwing an exception).

Hack

Upon submission, the level contract sends an Ether amount higher than prize to the contract instance contract fallback to reclaim kingship. The fallback uses transfer to send the prize value to the current king which about to be replace. Only then the king address is updated. If the current king is a contract without a fallback or receive function execution will fail before the king address can be updated.

  1. Deploy a malicious contract without neither a payable fallback nor a payable receive function
  2. Let this malicious contract become king by sending Ether to the vKing contract
  3. Submit instance

Takeaways

  • Assume any external account or contract you don't know/own is potentially malicious
  • Never assume transactions to external contracts will be successful
  • Handle failed transactions on the client side in the event you do depend on transaction success to execute other core logic.

Especially when transferring ETH:

  • Avoid using send() or transfer(). If using send() check returned value
  • Prefer a 'withdraw' pattern to send ETH

Solutions on GitHub


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

More from Gauthier Riou