Hacking an Ethereum contract

Do it yourself — reconstruct the $1,000,000 hack!

With all the stories running around about hackers stealing millions left and right, I thought it might be the time to take a closer look at one of the latest heists. And not just a look — we’ll trace through it together and do the hack ourselves , step by step.

The following requires some basic knowledge of Solidity (the most common Ethereum smart contract language) and basic tech chops.

The back story

The recently launched PoWHCoin (cached version before the hack) was based on the the premise of a true pyramid scheme, but with a twist: You earn even when it goes down, in the form of dividends that you can cash out when the pyramid goes back up.

In the first few days interest spiked, with the contract hovering at around $1,000,000 (that’s a million real US dollars, in ether).

And then: Death. 866 ether gone in a quick, simple hack.

So how did so many experts miss such a glaring hole in such a short contract? And more importantly, how can we run the hack ourselves?

The tools

To keep things simple, we will be using the online Solidity IDE , Remix . Go ahead, click the link and open it up in your browser.

Web based Solidity IDE — Remix

The IDE is pretty simple:

  1. Top left — our code
  2. Bottom left — debug area
  3. Right — command area

The original contract

You can’t hide on the chain

The original contract is still there on the blockchain, ready for review and studying. You can check out all the transactions and code here.

The original contract

The important bit for us is the “Contract Source” tab, where we can find the original Solidity code for the contract. (Debugging directly from the Ethereum Virtual Machine would have been much harder, yet not impossible.)

Let’s start off by taking all the contract code (here is a cleaned-up version) and copying it into Remix , replacing the demo “Ballot” code you probably have there by default.

Prepare for Deploy

To make things simple, we will do all our “hacking” in private. Then, once we are ready, we can send the real transactions at once and not waste time (and risk being detected!).

With the contract copied over, at the top right, select the “Run” menu.

First let’s select the environment. Since we don’t have a local Ethereum node running and we don’t want to connect to any test nets (private, yea?), we can just select “JavaScript VM” from the “Environment” drop-down.

You might notice the “Account” drop-down. The test environment automatically created a few accounts for us with 100 ether each (if only we could cash out…).

Deploy time

Our contract drop-down will only contain one option, “PonziTokenV3” — which is exactly what we want to deploy. And right below it is an inviting pink button, “Create.”

Click “Create” now.

As our contract is created, Remix automatically reads the ABI and lets us easily access the read-only parts of the contract or run generic transactions.

Being a hacker and all, this is a great time to click around on those “sellPrice”, “name”, “totalSupply”, and other function names, and see how Remix shows us their values. (These are real values read from the contract already deployed on our test blockchain.)

Calling a read-only function in Ethereum does not issue a transaction or cost wei.

Simulate suckers

Time to simulate suckers putting money into the contract, moments before we hack it all to heck.

Remember how at the top right, under the “JavaScript VM” environment we selected, there was an “Account” drop-down? Let’s pick the first account and use it as our “greedy ol’ user.”

We are going to send 50 ether to the contract, so fill in 50 in the “Value” input and DON’T FORGET to change the currency from “wei” to “ether.”

Now scroll all the way down and click “fund”.

What just happened is that we ran a real transaction. The function was “fund” and we sent 50 ether to the contract.

At the bottom left, you can see the debug log and even click “Details” to see what happened.

Note that the addresses in this tutorial and your run will be different.

If you take a peek at the “Account” drop-down at the top right now, you will notice that the account has only 49.999999 ether.

Why? We sent 50 ether to the contract, so what happened to the 0.000001? It’s payment for transaction costs to the miners (that’s all the “gas” stuff you see there in the table).

Hax0r time

So let’s see what our hacker did (and we can, since it’s all on the blockchain — forever). The address from which everything was run is here.

Step #1 — Join the game

It starts with innocently joining the Ponzi scheme. Let’s choose the second account and use it to fund 1 ether.

Step #2 — Send to someone else?

Now the tricky part — we are going to send ONE of the cool tokens we got to “a friend.” To do so, we need a friend.

Our friend is going to be account #3 in the list of accounts. The nice thing about Remix is that once you select account #3, on the right you will see a small “Copy” icon — that copies the public address into your clipboard.

We always need to remember which account we are sending the transaction FROM. So, as soon as you copy the account #3 address, switch back to account #2.

Let’s do this. Scroll down all the way to “approve”. This function allows one account to send coins to another account. This is done semi-directly. We basically move the coins to the contract and allow the target account to “withdraw” them later. (This is the safe and recommended way to move coins around.)

function approve(address _spender, uint256 _value) public { ... }

Unfortunately, Remix is a bit ugly in the way we pass parameters to functions. We can write parameters in the input box to the right of the function in the following format:

X,Y,Z…

Where X can be:

  1. A number — e.g., 100
  2. A string in double quotes — e.g., “hi”
  3. A hex in double quotes prepended by “0x” — e.g., “0xDEADBEAF”

Since we want to transfer coins from our account #2 (which we selected in the drop-down) to account #3 (whose public key we copied to the clipboard), and we want to send 2 coins, we will put in the parameters box:

“0x0x4b0897b0513fdc7c541b6d9d7e929c4e5364d2db”,2

Please note again that your addresses will be different.

Once you’ve done that, click “approve” and let the transaction run.

Step 3 — The hack

First let’s check the balance for account #3.

Paste the public key you copied before into the input field to the right of “balanceOfOld” (don’t forget the double quotes and “0x”). Running it should read your entry from the array of balances.

// amount of shares for each address (scaled number)
mapping (address => uint256) public balanceOfOld;

Since we didn’t yet “pull” the 2 coins sent to us from account #2, it makes sense that we have 0 coins.

Now the hacker used another feature of the contract: It allows you to transfer coins from one account to another (for preapproved sums, of course).

Since we had account #2 preapprove the transfer of 2 coins, we can move that sum now. The trick here is a bug in the contract. We are going to send the 1 coin given to us to the address of the contract itself!

function transferFrom(address _from, address _to, uint256 _value) public { ... }

The params are:

  1. Public address of account #2 (copied from the “Account” drop-down)
  2. Public address of our contract (copied from the contract drop-down)
  3. Amount — 1
Where to copy the address of the contract itself

Where to copy the address of the contract itself

The result will look something like this:

“0x14723a09acff6d2a60dcdf7aa4aff308fddc160c”,”0x692a70d2e424a56d2c6c27aa97d1a86395877b3a”,2

Don’t forget, we MUST run this as account #3!

Once you’ve selected account #3 from the “Account” drop-down and copied over the params, hit “transferFrom”.

Before we dive into the code, copy the address for account #3 and try it out in the “balanceOfOld” function.

No, you are not dreaming. Our 0 became…

115,792,089,237,316,195,423,570,985,008,687,907,853,269,984,665,640,564,039,457,584,007,913,129,639,935

Or in hex: 0xFFFFF…..…..FFFFFF.

What happened?

Lets trace the call to “transferFrom”:

function transferFrom(address _from, address _to, uint256 _value) public {
var _allowance = allowance[_from][msg.sender];

if (_allowance < _value) revert();

allowance[_from][msg.sender] = _allowance - _value;
transferTokens(_from, _to, _value);
}

The code in “transferFrom” checks that we have enough coins in our allowance (1) and proceeds to call another function, “transferTokens()”:

function transferTokens(address _from, address _to, uint256 _value) internal {
if (balanceOfOld[_from] < _value) revert();
if (_to == address(this)) {
sell(_value);
}
...
}

We know account #2 has enough for our 1-coin withdrawal, so the first check passes.

The second check sees that the TO address is the address of the contract itself. So, a call to “sell” is done:

function sell(uint256 amount) internal {
var numEthers = getEtherForTokens(amount);
// remove tokens
totalSupply -= amount;
balanceOfOld[msg.sender] -= amount;

// fix payouts and put the ethers in payout
var payoutDiff = (int256) (earningsPerShare * amount + (numEthers * PRECISION));
payouts[msg.sender] -= payoutDiff;
totalPayouts -= payoutDiff;
}

And here is where everything goes wrong. The “sell” function doesn’t receive the “from” or “to” parameters from before. It assumes everything was checked before and goes ahead to deduct the amount being sent from “balanceOfOld[]”.

But from which account? Well, it assumes that the account that initiated the transaction is the seller’s account — something that is not true in our case. Since we ran “transferFrom” using account #3 to send coins from account #2 to the address of the contract, our “msg.sender” is account #3.

And what was the balance of that account? That’s right — ZERO.

When you take an unsigned number like uint256 and do 0 minus 1, you get the dreaded Integer Underflow.

balanceOfOld[Account #2] -= 1 // Regular positive value
balanceOfOld[Account #3] -= 1 // 0xFFF…FFF

We have the largest integer value possible of coins

— we are rich!

Getting the money out

Now that we have this huge balance on our hands, let’s empty the cash register.

The first order of business is to transfer some of our endless balance to the contract address.

As account #3, we will call the “transfer” function and give it the address of the contract and the sum to transfer — in our case, 50 ether (or 50,000,000,000,000,000,000 wei).

The params will look something like this:

“0x692a70d2e424a56d2c6c27aa97d1a86395877b3a”,”0x2B5E3AF16B1880000"

Go ahead, hit “transfer”.

Now the ether

Internal transfers approved (we sent the coins back to the contract), let’s get the ether itself.

We do this by calling “withdraw” (the parameter doesn’t matter).

Make sure you are still account #3, and hit “withdraw”.

Verify

Now scroll to the top and check the “Account” drop-down. Almost 150 Ether? Woohoo!

Summary

Writing contracts in Solidity is hard. In fact, it’s super hard and very dangerous. This simple tale of a $1,000,000 contract being live for days without anyone finding a tiny bug (until the hacker did!) is a serious lesson to us Solidity writers everywhere.

As a developer it’s easy to imagine the flow of this contract being developed, the assumptions assumed, and the bug going unnoticed.

Keep safe and prosper

@BorisDinkevich @ 500Tech.com

--

--

--

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Recapitulation of Itchiro Games AMA event held at AMA LOVERS CLUB

ProjectX Announces Partnership with Kyros Ventures

KeplerSwap and DeFi 2.0

Blockchain Ecosystem

Let’s Break ofMyths of Blockchain

MyWish — the first platform offers smart contracts on Binance Smart Chain

Blockchain Potential in the Energy Sector

Ganache — Truffle Framework

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store

Boris Dinkevich

More from Medium

Solidity Basic Vulnerability — Code Execution Vulnerability

Damn Vulnerable DeFi Challenge #2 Solution  — Naive receiver

$100 Million in $PYR Stolen from Vulcan Forged Marketplace💰

A Journey to Exploit the MOST Vulnerable Contracts in the DeFi Universe