Bugs…I hate these guys

Fifth instance in a series of articles breaking down the awesome solidity security coding challenges at The Ethernaut by the good folks of Zeppelin Solutions.

The set of problems available are very fun and illustrative of common security pitfalls in smart contracts so if you’re feeling stuck in a particular challenge and just want a peek into the how and why of the solution this and other articles I’ve published with the same nomenclature are for you.

This one is going to deal with the following challenge:

The “Privacy” Contract

The creator of this contract was careful enough to protect the sensitive areas of its storage. Unlock this contract to beat the level.

Let’s examine the contract 🔍

bool public locked = true;

uint256 public constant ID = block.timestamp;

uint8 private flattening = 10;

uint8 private denomination = 255;

uint16 private awkwardness = uint16(now);

bytes32[3] private data;

Firstly, the contract declares a bunch fo storage variables, and some spoilers, only “locked” (what we need to change) and “data” (what we need to read) really matter, the rest are there just to screw with us.

function Privacy(bytes32[3] _data) public {

data = _data;

}



function unlock(bytes16 _key) public {

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

locked = false;

}

The constructor is where the mystery data was originally passed to the contract, an option to find out more about this information would be to snoop around the input parameters for the transaction that created this contract and derive the relevant information from there.

The unlock function is our only point of interaction with this contract and here we just need to pass the right sequence of bytes16 to unlock the contract and count this as a win.

Our approach 🏃

The problem and solution is actually quite similar to a previous challenge I’ve covered, the Vault Problem. This is basically a souped version of that challenge were we are forced to know a little bit more about the way storage variables are indexed and how casting works for different types.

The first thing to know is that nothing is stopping us from snooping around a contracts storage variables (even private ones!) very easily, easing something like web3.eth.getStorageAt(/contract.address/ /var index/)

Storage variables are indexed as they’re defined, in other words in this particular case the “locked” variable that is first defined in the contract will be available web3.eth.getStorageAt(/contract.address/, 0)

And while this is true, things can get a little bit more complicated taking storage optimizations into account! The best way to illustrate this is to fetch all the available data and see what we get!

let data = [] let callbackFNConstructor = (index) => (error, contractData) => { data[index] = contractData } for(var i = 0; i < 6; i++){ web3.eth.getStorageAt(contract.address, i, callbackFNConstructor(i)) }

Running the code in the web console above will fetch us the first 6 storage variables defined in the target contract.

You should be looking at something like this:

Dat is storage data

But hey, that’s weird! There are 6 storage variables defined in this contract and our data array only has information for 4. What’s more the first variable defined is a simple boolean so we’d expect the first result to be something like:

"0x0000...001"

Instead we get way more bits than we bargained for in the first position.

This all has to do with storage optimization and it follows very simple rules.

Each index set aside for a storage variable allows 256 bits. Variables are indexed in the order they’re defined in contract. If a variable takes under < 256 bits to represent, leftover space will be shared with subsequent variables IF THEY FIT. Constants don’t use this type of storage.

Looking back at our storage variables we can see the index goes something like this.

bool public locked = true; //rightmost bits of index[0]

uint256 public constant ID = block.timestamp; //Not indexed

uint8 private flattening = 10; //rightmost bits of index[0] - 2

uint8 private denomination = 255; //rightmost bits of index[0] - 4

uint16 private awkwardness = uint16(now); //bits of index[0] -6

bytes32[3] private data; //indexes 1,2,3 occupied by each piece

With this indexing rules we can actually make sense of our data[0] result:

from the hexadecimal

01 = true

0a = 10;

ff = 255;

c6a6 = “A coerced to 16 bits ‘now’ ”

Now that we know what represents what in our fished out storage data, it’s easy to grab the relevant information for our hack: data[3] (Which is equivalent to data[2] from the contract)

Variable Coercion 🛠

function unlock(bytes16 _key) public {

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

locked = false;

}

The unlock function matches our ‘key’ with a coerced to bytes16 data[2]. Now that we know what data[2] is, it’s going to be peanuts to solve this, we just need to understand how coercion works from bytes32 =>bytes16.

If you have decent programming experience the concept of type coercion shouldn’t be a strange on but it’s basically like this:

int datumA = 5; /This is 5/

uint datumB = uint(datumA); /This is 5/

datumB can’t be negative now, and that type is relevant to use for array indexes and a bunch of other useful things. Very simple right? Let’s see another example:

int datumA = -5; /This is -5/

uint datumB = (datumA); /This is 115792089237316195423570985008687907853269984665640564039457584007913129639931/

WTF right? Well it all has to do with the bit representation of the types you are coercing. In a sense coercion breaks the fundamental rules of a statistically typed language and a sort of meta-rule for dealing with that coercion kicks in. How that plays out can vary from language to language and from type to type (Should you be able to coerce non numerics strings to uints? they’re all bits after all…)

In any case, the best way to understand the rules reigning over coercion is to read the documentation or play around with coercion till you get it (or do both)

I’ve digressed, we’re interested in bytes32 => bytes16 so we can know what bytes16 type key to send as an input parameter, and that particular coercion is pretty straightforward.

You just take the first half of the bytes32 data and that’s going to be your new bytes16 type data. Calling the unlock method with that as an input parameter should clear up the last hurdle.

Conclusion

Just like with the vault problem this challenge emphasizes just how not-private are our private variables (or anything really) in the blockchain. The victory screen includes a link to Darius’s write up on snooping contract’s storage with more examples for more complex data types which I had linked in the vault problem as well, so be sure to give it a read if you’re still interested in this topic.