That Time I Got Pwned For .2 ETH

February 27, 2018

Allow me to share my pride-swallowing story…

I was up late at night working on Cranky Coin. I was working on the persistence layer and wanted to store block headers in levelDB. I finished implementing this only to realize it wasn’t going to work. You see, Python GIL threads are effective for I/O operations, not to gain performance via parallel processing. In order to achieve this, you must either compile modules in C or use multiprocessing. The problem is, separate processes don’t share the same memory space like threads do… and levelDB happens to utilize that memory for speeding up certain write operations; which is probably why they mention on their site that it’s thread-safe and not process-safe.

What a burn. I needed a break.

What do I do in my break? I decide to explore the Ethereum blockchain for newly uploaded (and verified) smart contracts. So I skim through pages of smart contracts and I come across a few lottery, dice, and roulette contracts. I wanted to see their (pseudo)random number generators and compare them to my own. I also wanted to see if their smart contracts could be improved by using the Ping Chain Oracle.

One caught my attention. I saw this in the code:

See anything wrong?

Not only is the secret number determined beforehand, each of the variables can be derived.

By looking at the last interaction (transaction) with the contract, you can see the block which includes it. There you can derive the now which is the block’s timestamp converted to UNIX epoch time. The current block number is the height of the block which includes this transaction.

Since the contract is executed by the miner even before finding the nonce or the hash, it’s common practice to grab the hash of the previous block block.number-1. Otherwise, the unknown hashes resolve to 0. Now we have…

Concatenate the two values, calculate its sha3 sum, cast it to an unsigned integer, modulo 20… and add 1 so the range is between [1..20]. Easy Peasy?

Maybe too easy. There was about 1.8 ETH in the contract address and I felt rushed to grab it before anyone else did. My first .1 ETH didn’t yield a win and I realized I mistakenly calculated the number with the current hash rather the the parent hash. I calculated it again (correctly, this time) and played another .1 ETH.

WTF? Where’s my reward? Something is fishy, so I download the contract and enter it into remix. I convert the private variables to public variables so I can get full visibility. I confirm that all my calculations are correct but WTF? Even on remix it’s not rewarding me. I kick off the debugger and see what’s going on. Ahhh.

We see that Game is a struct of two members.

Inside this method, an instance of Game is created. The EVM stores local variables in the stack except for complex types (structs, arrays, maps) which are stored in storage. The contract writer can specify the keyword memory or storage to override this behavior. In this case, the keyword was not specified so the new instance referenced storage.

The EVM also stores each of your contract-level variables (aka state variables) in storage. This is where the trouble begins. Unlike memory, storage has a virtual structure that is determined and set by the state variable declarations at the time of contract creation. Method calls may update the values (or state) of these variables but cannot alter its structure.

Since our local instance of Game references a location in this fixed structure storage, assigning a value to one of its members immediately causes a buffer overflow into the space allotted for secretNumber.

Had it been instantiated with the memory keyword:Game memory game;

or even inlined:gamesPlayed.push(Game(msg.sender, number));

The contract would have worked as expected.

Here’s a look at the overflow in action. Keep an eye on the values in Solidity State:
Before instantiating Game (secret number is 8)

After instantiating Game in storage, before updating its member

After updating the member (the overflow)

So plan B: Couldn’t we just play the value that was used to overwrite the secretNumber variable? Well, you can’t. This harmless looking boundary check enforces your value to be an unsigned integer <= 20:

require(msg.value >= betPrice && number <= 20);

In other words, you can’t win.
Very clever… but silly me. I would have caught that if I had just tested the contract locally.

Was I upset? Heck no! I smiled. I learned a valuable lesson for the price of .2 ETH. Well, I think there’s a lesson in here somewhere.
Hmm. Something along the lines of, “If it looks too easy…” bahh. Whatever.

Advertisements

Share this:

Like this:

Related

That’s a good question. I’m assuming since it’s immediately pushed onto the array of Game upon construction, the EVM knows it won’t need to stick around nor be subject to further state changes, so the `memory` is implied.