This is the level 12 of OpenZeppelin Ethernaut web3/solidity based game.
Pre-requisites
- Fixed size byte arrays
- Layout of State Variables in a Solidity contract
- Reading storage at a slot
Hack
Given contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Privacy {
bool public locked = true;
uint256 public ID = block.timestamp;
uint8 private flattening = 10;
uint8 private denomination = 255;
uint16 private awkwardness = uint16(now);
bytes32[3] private data;
constructor(bytes32[3] memory _data) public {
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
*/
}
player
has to set locked
state variable to false
.
This is similar to level 8: Vault where any state variable (irrespective of whether it is private
) can be read given it's slot number.
unlock
uses the third entry (index 2) of data
which is a bytes32
array. Let's determined data
's third entry's slot number (each slot can accommodate at most 32 bytes) according to storage rules:
-
locked
is 1 bytebool
in slot 0 -
ID
is a 32 byteuint256
. It is 1 byte extra big to be inserted in slot 0. So it goes in & totally fills slot 1 -
flattening
- a 1 byteuint8
,denomination
- a 1 byteuint8
andawkwardness
- a 2 byteuint16
totals 4 bytes. So, all three of these go into slot 2 - Array data always start a new slot, so
data
starts from slot 3. Since it isbytes32
array each value takes 32 bytes. Hence value at index 0 is stored in slot 3, index 1 is stored in slot 4 and index 2 value goes into slot 5
Alright so key is in slot 5 (index 2 / third entry). Read it.
key = await web3.eth.getStorageAt(contract.address, 5)
// Output: '0x5dd89f7b81030395311dd63330c747fe293140d92dbe7eee1df2a8c233ef8d6d'
This key
is 32 byte. But require
check in unlock
converts the data[2]
32 byte value to a byte16
before matching.
byte16(data[2])
will truncate the last 16 bytes of data[2]
and return only the first 16 bytes.
Accordingly convert key
to a 16 byte hex (with prefix - 0x
):
key = key.slice(0, 34)
// Output: 0x5dd89f7b81030395311dd63330c747fe
Call unlock
with key
:
await contract.unlock(key)
Unlocked! Verify by:
await contract.locked()
// Output: false
Learned something awesome? Consider starring the github repo 😄
and following me on twitter here 🙏
Top comments (0)