Tracking Down The Biggest Hack in Ethereum History — Part 2: The Exploit That Stole $50 MILLION USD

in #cryptocurrency6 years ago (edited)

That’s when it all went wrong.

In this part I’ll be explaining the exploit that allowed the DAO hack to even exist. The previous part can be found here.

Note that you don’t need to read this part to understand the rest of the articles, but if you’re interested in how the hack happened, then read this. This article is geared toward people who know basic programming but have little to no experience with Solidity, but if you don’t know how to program you can still follow along.

Some Background Info

On Ethereum, you shouldn’t think of a smart contract as a contract, you should think of it as a normal human account, but with some code attached to it that reacts when anything happens. All smart contracts have addresses that you can send money to, and they can react when you do.

Most of the time, when you interact with smart contracts, you are sending Ether with some data, so that the contract knows why you are sending Ether (note that you can also send data without sending any Ether). But when you send Ether without sending any data, it triggers a designated function called the fallback function.

The Exploit

Here’s an example of a vulnerable contract that we will run an attack on. It is basically a contract that allows anyone to deposit and withdraw Ether into.

  pragma solidity ^0.4.19;

   contract Bank {

      mapping (address => uint) balances;             // Each address has a balance attached to it.

      function deposit() payable {                    // Use this function in order to deposit Ether.
          //Adds the Ether sent to the user's balance.
          balances[msg.sender] = balances[msg.sender] + msg.value; 
      }



      // XXXXXXX         Vulnerable function!! \/         XXXXXXX



      function withdrawAll() {                        // Use this to withdraw all your funds
          uint amountToWithdraw = balances[msg.sender]; // Figures out the amount to withdraw, which
                                                // is the amount the user has in his account.
                                                
          if (msg.sender.call.value(amountToWithdraw)() == false) { // Tries to send the user the
              revert();                               // amount he wishes to withdraw, but if it
          }                                           // fails, stop!
    
          balances[msg.sender] = 0;                   // Sets the users balance to 0, because he
                                                // just withdrew all of his money.
      }

  }

That’s the entire contract, but really all that matters for this attack is the function withdrawAll():

  1 function withdrawAll() {                        
  2      uint amountToWithdraw = balances[msg.sender]; 
  3                                              
  4                                              
  5       if (msg.sender.call.value(amountToWithdraw)() == false) { 
  6           revert();                               
  7       }                                           
  8 
  9       balances[msg.sender] = 0;                   
  10                                              
  11 }

Basically this function takes all the funds in the user’s balance and sends them to the user. How does it work?

Well, in line 2, it figures out how much it wants to withdraw by taking the user’s balance (remember that we are withdrawing all the funds). Then in line 5, it gives the user the money, and if it doesn’t succeed, in line 6 it reverts everything and stops the function. Finally, in line 9 it sets the user’s balance to 0.

This seems pretty solid and secure, right?

Well it actually has a massive vulnerability!

Yep, that seemingly secure function has a vulnerability that can drain all the funds in the contract.

First we have to create another contract called Attacker, which we will use to attack the bank.

 1  contract Attacker {
 2    address bankAddress;
 3
 4     // In here would be functions that:
 5     // Would set the bank's address,
 6     // would deposit funds in the attacker's account, etc.
 7
 8      function startAttack() payable {
 9          bankAddress.call(bytes4(sha3("withdrawAll()")));
10      }
11
12      function() {
13         if(bankAddress.call(bytes4(sha3("withdrawAll()"))) == false) {
14              return;
15          }
16     }
17   }

Let’s go through how the attack would work.

In line 2 we declare a variable called bankAddress. This would be set to the address of the Bank smart contract that we want to attack.
Note that before we try to attack the contract must have some money in the bank.
In line 8 we have the function called startAttack(). This starts the attack against the bank and basically sends a transaction to the bank where we try and withdraw our entire balance.

  1 function withdrawAll() {                        
  2      uint amountToWithdraw = balances[msg.sender]; 
  3                                              
  4                                              
  5       if (msg.sender.call.value(amountToWithdraw)() == false) { 
  6           revert();                               
  7       }                                           
  8 
  9       balances[msg.sender] = 0;                   
  10                                              
  11 }

Now we are at line 2 in the withdrawAll() function. That all works just as it should and so we get to line 5, where the function sends the amount to withdraw to the Attacker contract. But the Attacker contract has a fallback function, located at line 12, so that fallback function is called since there was no extra data sent to the Attacker contract.

In the fallback function, we then call withdrawAll() again. But wait! The Bank contract still hasn’t set the Attacker’s balance to 0, so the bank still thinks it has money in its account, and it sends the money again! Already the attacker has recieved double the amount it had in its account, and the cycle just keeps repeating.

Soon, all the money in the Bank contract will have been given to the Attacker contract!

This is a simplified example of how the DAO hack happened. Combining this vulnerability with a few more minor ones, the attacker was able to drain 1/3 of the DAO’s Ether into another DAO (in fact a child one).

Note that this could be completely fixed by moving one line of code around in the withdrawAll() function:

  1 function withdrawAll() {                        
  2      uint amountToWithdraw = balances[msg.sender]; 
  3      balances[msg.sender] = 0;                                      
  4                                              
  5       if (msg.sender.call.value(amountToWithdraw)() == false) { 
  6           revert();                               
  7       }                                           
  8      
  10                                              
  11 }

All that needed to be changed was that you have to set the user’s balance to 0 before you send them the money. That would stop the entire exploit because once the withdrawAll() function was called again, it wouldn’t send the user another copy of its balance, it would just send them 0 Ether, because their balance had been set to 0 Ether.

In the next part in this series, I will explain what happened on the night of the DAO hack.

Follow me @shredz7 and make sure to upvote! Resteem if you think your followers will like this. Comment on your thoughts on this exploit! 👍

Sort:  

Great stuff. I remember when Ethereum first split into the "Ethereum" we know today and "Ethereum Classic"
Ethereum Classic still doing quiet well.
Many are calling it the truly Decentralized Ethereum ! :)

Yep I will cover Ethereum Classic in another part in this series, but here's a sneak peek for the next one.

It's called "The Night of the DAO Hack"...

This post has received a 0.43 % upvote from @booster thanks to: @shredz7.

Coin Marketplace

STEEM 0.20
TRX 0.14
JST 0.030
BTC 68845.40
ETH 3281.32
USDT 1.00
SBD 2.65