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.