Ethernaut(Lvl 18): Abracadabra!. The meaning of life is ...42

Ethernaut(Lvl 18): Abracadabra!. The meaning of life is ...42

Writing very tiny contracts on the EVM

Understanding Contract Creation

When a contract is initialized, the following steps occur:

  1. A user or contract sends a transaction to the Ethereum network without specifying a recipient address. This signals to the EVM that it's a contract creation transaction, distinct from a regular send/call transaction.

  2. The EVM compiles the contract code, written in Solidity (a high-level, human-readable language), into bytecode (a low-level, machine-readable language). This bytecode consists of opcodes, which are executed sequentially in a single call stack.

    It's crucial to note that the contract creation bytecode comprises both initialization code and the contract's actual runtime code, concatenated in sequential order.

  3. During contract creation, the EVM executes the initialization code until it encounters the first STOP or RETURN instruction in the stack. This stage includes running the contract's constructor() function, resulting in the assignment of a unique contract address.

Once the initialization code is executed, only the runtime code remains in the stack. These opcodes are then copied into memory and returned to the EVM.

Subsequently, the EVM stores this surplus code in the state storage, linked to the new contract address. This runtime code is executed by the stack in all future calls to the new contract.

The Challenge

This challenge needs you to provide the Ethernaut with a Solver, a contract that responds to whatIsTheMeaningOfLife() with the right number.

The solver's code needs to be really tiny.10 opcodes at most.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract MagicNum {

  address public solver;

  constructor() {}

  function setSolver(address _solver) public {
    solver = _solver;
  }

  /*
    ____________/\\\_______/\\\\\\\\\_____        
     __________/\\\\\_____/\\\///////\\\___       
      ________/\\\/\\\____\///______\//\\\__      
       ______/\\\/\/\\\______________/\\\/___     
        ____/\\\/__\/\\\___________/\\\//_____    
         __/\\\\\\\\\\\\\\\\_____/\\\//________   
          _\///////////\\\//____/\\\/___________  
           ___________\/\\\_____/\\\\\\\\\\\\\\\_ 
            ___________\///_____\///////////////__
  */
}

MagicNumberHacker, is designed to exploit a vulnerability in a smart contract that uses the create2 opcode to deploy a new contract. The create2 opcode allows for deterministic contract creation, meaning that given the same input parameters, the same contract address will be generated. This is a powerful feature but can be exploited if not properly secured.

Here's a succinct explanation of how the attack works:

  1. Initialization: The MagicNumberHacker contract is initialized with the address of the IChallenge contract it targets. This is done in the constructor, which takes the address of the IChallenge contract as an argument.
  1. Bytecode Preparation: The contract prepares a specific bytecode (bytecode variable) that will be used to create a new contract. This bytecode is hardcoded into the contract.
  1. Salt Value: A salt value is set to 0. The salt is an important parameter in the create2 opcode. It ensures that even if the same bytecode is used, the resulting contract address will be unique if the salt is different.
  1. Contract Creation: The contract uses inline assembly to call the create2 opcode. The create2 opcode takes three arguments: the endowment (how much Ether to send to the new contract), the bytecode to deploy, and the salt. The create2 opcode is called with 0 endowment, the prepared bytecode, and the salt value. The result of this operation is the address of the newly created contract, which is stored in the solver variable.
  1. Setting the Solver: Finally, the MagicNumberHacker contract calls the setSolver function on the IChallenge contract, passing the address of the newly created contract (solver). This is the exploit: by controlling the solver address, the attacker can potentially manipulate the IChallenge contract's behavior, depending on how it's programmed to interact with the solver.

The key to this attack is the use of the create2 opcode to predictably create a new contract address. If the IChallenge contract relies on the uniqueness of the solver address or has specific expectations about the solver contract's behavior, this attack could be successful.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;


interface IChallenge{
    function setSolver(address _solver) external;
}
contract MagicNumberHacker{
    IChallenge challenge;
    constructor(address _challenge){
        challenge = IChallenge(_challenge);
    }
    function attack() public {
    bytes memory bytecode = hex"600a600c600039600a6000f3602a60005260206000f3";
    bytes32 salt = 0;
    address solver;

    assembly {
        solver := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
    }

    challenge.setSolver(solver);
}
}

To understand how the bytecode was created, a very clear and understandable example is given here .