Solving Ethernaut 19 — Alien Codex

By akohad Oct14,2022

[ad_1]

Photo by Kevin Ku on Unsplash

The trick to defeating this challenge, is understanding how static variables and dynamic arrays are stored.

The objective is simple enough — claim the contract by overwriting the “owner” address.

The first thing you might notice is that the “AlienCodex” contract inherits from another contract called “Ownable”, but the code for this contract is not available. The “Ownable” contract here is an early version of OpenZeppelin’s implementation, but for our purposes all we need to know is that this contract will store the “owner” address in a storage slot.

Reminder on storage mechanics in smart contracts

All persisted state in a contract — meaning all variables and all dynamic arrays — are stored in a long list of 32 byte slots. Each slot has an index starting from 0 and goes all the way to index 2²⁵⁶ -1. Each variable that is defined next each other can be packed into a storage slot together, if they can fit together into 32 bytes.

As in previous challenges you need to figure out a way to manipulate the storage slots in an unintended way.

At first glance this might not seem so straightforward, but let’s try to figure out how the variables are stored in our contract. In the browser console you can try looking at the contract’s first storage slot by writing:

await web3.eth.getStorageAt(contract.address, 0);

The result you get back should look something like this:

The value is a 32 byte long hexadecimal string, with the last 40 characters being set to an address. This is actually the owners address, because even though we cannot see the variable in the code provided by the ethernaut challenge, the variable is defined in the “Ownable” contract. Variables are also stored according to the inheritance hierarchy, so that a contract inheriting from another contract has it’s variables stored after the contract it inherits from. In our case it means that the owner address variable is stored in slot 0, because “AlienCodex” inherits from “Ownable”.

You can try checking out the more storage slots by, but you will find that all storage slots beyond slot 0 will return 0 in hexadecimal. Let’s look at the code and figure out a way forward.

bool public contact;  
bytes32[] public codex;
modifier contacted() {
assert(contact);
_;
}

The first thing we need to do is change the contact variable to true, otherwise we will not be able to call the other methods. To do so is straightforward. We just need call this method:

function make_contact() public {    contact = true;  }

Again by using the console in the browser:

await contract.make_contact()

If you check storage slot 0 after the call succeeded, you might notice something peculiar:

There is now a 1 in front of the address in our storage slot! This 1 is actually the bool variable contact that we just set to true in the last call. The reason is that a bool only takes up 1 byte, and it got packed together with the address variable to save storage space for the contract.

Now that we can call the other functions in the contract, let’s look at what we can do to crack this contract. The important methods you should concentrate on are these:

function retract() contacted public {   
codex.length--;
}
function revise(uint i, bytes32 _content) contacted public {
codex[i] = _content;
}

The “retract” method allows us to shrink the dynamic array “codex” by 1, and “revise” allows us indexed insertion into the array. Before we can continue, we need to understand how dynamic arrays behave in the storage slots. Because dynamic arrays stores data that is of an indeterminate size, the EVM cannot just pack it sequentially together like static variables. Instead the array data will be stored at a storage slot determined by hashing the storage slot index with the keccak256 hashing function. In the console you can do it so:

web3.utils.soliditySha3(web3.utils.encodePacked(1))

We are using slot 1 because the address and the bool could both fit into slot 0, so the array got put into slot 1. Another thing to note about dynamic array storage, is that the length of our array will be stored in this storage slot. So if you take a peek at slot 1 you will see that it will return 0, but what if we changed the size of our array? Eg. we could call the “retract” function:

await contract.retract()

Now looking at the storage slot again, we see something interesting:

The value changed to the maximum possible value for 32 bytes. By subtracting 1 from 0 the value underflows, and wraps all the way around to represent the largest possible value.

By underflowing the array length, it means that from the EVM’s perspective the array is the maximum possible length of 2²⁵⁶-1. If you remember from earlier, this number is the same as the total amount of storage slots in a contract. This means that the indices of this array actually wraps the entire contract!

If we combine everything we have learned to far, we can take advantage of our wrapped array, and insert anything we want at any storage slot!

There was a method called “revise” that allowed us to access the array through an index, and insert a new value at that index. But our target is to insert our own address at slot 0, so which index should we set to actually insert at slot 0?

This is where we need to know where our array starts storing its data, which we derived using this formula:

web3.utils.soliditySha3(web3.utils.encodePacked(1))

To calculate the correct index, we need to subtract the array’s storage slot index with the maximum possible value and add 1, which should land the array index exactly on storage slot 0. In the console we can calculate the value:

web3.utils.toBN(await web3.eth.getStorageAt(contract.address, 1)).sub(web3.utils.toBN(web3.utils.soliditySha3(web3.utils.encodePacked(1)))).add(web3.utils.toBN(1))

If we use this value as our index, we can insert anything we want in storage slot 0.

await contract.revise(web3.utils.encodePacked("35707666377435648211887908874984608119992236509074197713628505308453184860938"), web3.utils.padLeft("insert your wallet address", 64))

We have to pad the input to match the 32 bytes required for the function parameter, and we can finally check if it worked by looking at storage slot 0:

If it worked you will see your own address, which makes you the owner now!

Don’t forget to submit to complete the challenge.

New to trading? Try crypto trading bots or copy trading

[ad_2]

Source link

By akohad

Related Post

Leave a Reply

Your email address will not be published. Required fields are marked *