3

That Time I Got Pwned For .2 ETH

 2 years ago
source link: https://cranklin.wordpress.com/2018/02/27/that-time-i-got-pwned-for-2-eth/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

That Time I Got Pwned For .2 ETH

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:

pwned1.png

See anything wrong?

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

secretNumber = uint8(sha3(now, block.blockhash(block.number-1))) % 20 + 1;

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.

pwned4.png

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…

uint8(sha3([known value], block.blockhash([known value]-1))) % 20 + 1;

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.

pwned2.png

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.

pwned3.png

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)
pwned5.png

After instantiating Game in storage, before updating its member
pwned7.png

After updating the member (the overflow)
pwned8.png

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.


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK