Ethernaut Alien Codex Solution
Disclamer. This guide is no longer valid due to changes in Solidity. Look for other solutions.
Alien Codex is Ethernaut level number 20 designed by Nicole Zhu. This paper presents my solution to the level.
Spoiler alert. Do not read below if you want to solve it on your own. The text below contains a full solution.
The goal of the level is to claim ownership of the contract. AlienCodex is inherited from Ownable so in order to do that we want to override _owner
variable in the contract's storage with our address.
The entire solution can be split into two parts. First, we have to somehow set contract
variable to true to gain access to contract methods, because they are protected by contacted
modifier.
Setting Contact to True
Method make_contact
sets contact
to true, but there is a check assert(_firstContactMessage.length > 2**200);
. The obvious solution would be to pass bytes32[]
array with the length greater than 2 ** 200
. The thing is an array of size is 2 ** 200
is so big, that no machine has an amount of RAM to store it. If you try to do it anyway, it would crash your local node.
What can be done? One might ask. As always devil in the details. Solidity doesn't generate code at that moment which verifies payload sized versus declared length. In other words, when you call solidity method you pass array size and it is not checked against actual payload.
Let's abuse that.
Contract ABI specification defines how we should encode data for a contract call. In our case make_contact(bytes32[] _firstContactMessage)
we have a function with one dynamic parameter. According to specification data parameter would have the following structure:
- 4 bytes of a hash of the signature of the function
- the location of the data part of bytes32[]
- the length of the bytes32[] array
- the actual data.
Let's calculate all the values.
Hash of the signature of the function would be web3.sha3('make_contact(bytes32[])')
, which results to 0x1d3d4c0b6dd3cffa8438b3336ac6e7cd0df521df3bef5370f94efed6411c1a65
. We are going to take first 4 bytes, so 0x1d3d4c0b
our desired result.
The location of the data is offset from the hash of the signature and in our case, it would be just 32 bytes because the length in ABI specification is 32 bytes – 0x0000000000000000000000000000000000000000000000000000000000000020
.
The length of an array has to be bigger than 2**200
lets go with 0x1000000000000000000000000000000000000000000000000000000000000000
.
The actual data is going to be literally nothing because we'll break our promise and not going to supply any data for the array.
The final data would be
Finally, we can call make_contact
method to set contact
to true -
sendTransaction({to: contract.address, data: data, from: player, gas: 900000});
.
A result of await contract.contact()
should change to true
.
Overwriting owner with player variable
Since contact
is not set to true
, we can access all methods protected by contacted
modifier.
It seems that there is no way to modify _owner
variable since no code assigning it exists. But keep in mind that all state variable located on the same storage continuum and can fall victims of writing errors.
revise
function can set any storage slot to any value we provide. Exactly what we need. Unfortunately, it would fail, if we would call it with index >= length.
Method retract
doesn't have a check for int underflow. By calling it, we would change codex
length from 0
to 2**256 - 1
. EVM storage size is exactly 2**256-1
slots of 32
bytes. Essentially by setting the length of codex
to the length of EVM storage, we gain the ability to modify any slot of entire EVM storage.
We have to figure out the location of _owner
variable on storage as well as offset index to modify it with revise
method.
_owner
variable is located at 0 slot of contract's storage. Codex array length is located at 1 slot of storage. That is because of EVM optimize storage and address type takes 20 bytes, bool take 1 byte, so they both fit in one 32 bytes slot.
Now we have to calculate index i
for revise
method. Start of codex
array on the EVM storage located at keccak256(bytes32(1))
according to allocation rules. Thus our target location would be index = 2 ** 256 - uint(one);
.
After running the calculation result is 35707666377435648211887908874984608119992236509074197713628505308453184860938
.
Now we finally can call revise
method.
As a result await contract.owner();
should change to your address. Success!
Conclusion
The level shows us how to abuse contract ABI to provide fake length for a dynamic array parameter in a contract's call and how to abuse storage's dynamic array to overwrite any slot inside contract's storage with desired data.