C 353: Reentrancy Attack (20 pts)

What you need:

Background

We'll make a contract with a reentrancy vulnerability, exploit it, and patch it.

This is the vulnerability that was used to steal $50 million from The DAO in 2016.

The diagram below shows how the DAO attack was performed, from quantstamp.com.

The DAO had a withdrawBalance function which was intended to allow a single Ether transfer, but a malicious contract was able to re-enter the DAO code and make multiple withdrawals, taking more Ether than they were entitled to.

Opening the Remix IDE

In Chrome, go to
https://remix.ethereum.org/

Making the EtherStore Contract

In the left pane, right-click contracts and click "New File", as shown below.

Type the name Rewards.sol and press Enter.

A "Rewards.sol" tab appears on the right side. Paste in this code, as shown below.

pragma solidity ^0.4.8;

contract Rewards {
    uint public gifts;

    function allowGifts(uint num_gifts) public { gifts = num_gifts; }

    function withdraw() public {
        uint _amount = 1 ether;
        if (gifts > 0) {
           if (!msg.sender.call.value(_amount)()) revert(); 
           gifts -= 1;
        }
    }

    function deposit() payable public {}

    function getBalance() public constant returns(uint) { 
        address a = this;
        return a.balance; 
    }    
}

contract Attacker {
    Rewards r;
    uint public count;
    event LogFallback(uint count, uint balance);

    constructor(address rewards) public payable { r = Rewards(rewards); }

    function attack() public { r.withdraw(); }

    function () payable public {
        count++;
        address a = this;
        emit LogFallback(count, a.balance);     // make log entry
        if(count < 10) r.withdraw();            // limit number of withdrawals
    }

    function getBalance() public constant returns(uint) { 
        address a = this;
        return a.balance;
    }    
}

Understanding the Vulnerability

Examine the withdraw() function, outlined in aqua in the image above.

After making sure there are gifts remaining, it executes a call command, sending 1 Ether to the address that sent the request.

The developer expected this call to simply transfer 1 Ether to the sender's account, but in fact it transfers control of execution to the sender's contract.

A crafted contract can exploit this vulnerability to re-enter the withdraw() function again and again, taking more Ether, all before it ever proceeds to the instruction that decrements the gifts variable.

The Attack contract is shown below.

The function outlined in red performs the attack. This function has no name, and is called a "fallback" function--if Ether is sent to the contract, and there is no "receive" function, the fallback function is executed instead.

The fallback function makes a log entry and calls the withdraw() function in the Rewards contract again, taking more Ether than the developer intended.

This is the Reentrancy attack.

Compiling

On the left side, click the third icon, outlined in green in the image below. The "SOLIDITY COMPILER" appears. At the bottom, click the blue "Compile Rewards.sol" button.

On the left side, the third icon now has a green check-mark on it.

Deploying the Rewards Contract

On the left side, click the fourth icon from the top, outlined in red in the image below . The "DEPLOY & RUN TRANSACTIONS" pane opens.

Use an Environment of "JavaScript VM (London)". If the account balances are not all 100 Ether, switch to "JavaScript VM (Berlin)" and back to "JavaScript VM (London)" to refresh the blockchain to its initial state.

Select a CONTRACT of "Rewards - contracts/Rewards.sol", outlined in blue in the image below.

Click the orange Deploy button.

Deploying the Attacker Contract

At the lower left, under the "Deployed Contracts" heading, you see the "REWARDS" contract.

In that line, click the copy icon, outlined in green in the image below, to copy that contract's address.

Select a CONTRACT of "Attacker - contracts/Rewards.sol", outlined in red in the image below.

Paste the address into the field next to the Deploy button.

Click the red Deploy button.

Understanding the Rewards Contract

This contract is intended to hand out a limited number of free Ether, under the control of that contract's owner.

But, as we'll see, it can be exploited to steal more Ether than intended.

Funding the Rewards Contract

At the lower left, click the > next to "REWARDS" to expand that section, as shown below.

At the top left, enter a VALUE of "20 ether", outlined in green in the image below.

At the lower left, click the red deposit button.

At the lower left, click the blue-gray getBalance button.

The REWARDS contract now has a balance of 20000000000000000000 wei, or 20 Ether, outlined in green in the image below.

Allowing 2 Gifts

Perform these actions: There are now 2 allowed gifts, outlined in green in the image below.

Withdrawing a Gift

Perform these actions: The balance has dropped to 19 Ether, and the number of gifts has fallen to 1, outlined in green in the image below.

Withdrawing a Second Gift

Perform these actions: The balance has dropped to 18 Ether, and the number of gifts has fallen to 0, outlined in green in the image below.

Attempting to Withdrawing Another Gift

Perform these actions: The balance stays at 18 Ether, because you are not allowed to take any more.

Allowing 2 More Gifts

Perform these actions: There are now 2 allowed gifts, outlined in green in the image below.

Stealing 10 Ether

At the lower left, click the > next to "ATTACKER" to expand that section, as shown below.

Click the orange attack button.

In the REWARDS section, click the blue-gray getBalance button.

The balance has fallen to 8 Ether, outlined in green in the image below.

You have taken 10 Ether!

C 353.1: Logs (10 pts)

In the lower pane, click the down arrow on the next-to-last transaction, outlined in red in the image above.

Scroll down to the logs section, where you see ten items with a count incrementing from 1 to 10, as shown below.

This is how the reentrancy attack works: one action is multiplied many times while processing a single statement in the Rewards contract, so it never checks again to see that we have exceeded the allowed amount of Ether.

The flag is covered by a green rectangle in the image below.

Understanding the Vulnerability in More Depth

As explained more fully on this quantstamp.com page, there are three general ways to send Ether to smart contracts: The DAO used address.call.value(), which allowed the attacker to use a large amount of gas, enough to execute many withdrawals.

Using one of the other methods would prevent this attack by limiting the gas.

But a simpler solution is to decrease the gifts count before sending Ether out, so even when the withdraw() method is reentered, it will refuse to grant more Ether to the attacker.

Removing the Deployed Contracts

On the left side of the Remix page, click the two X icons outlined in green in the image below.

Fixing the Vulnerability

Modify the Rewards contract as shown below.

(Note that gifts is an unsigned integer, so when it goes below zero, it becomes a very large positive number.)

Compile the contract again and deploy it as you did previously.

Fund the Rewards contract with 20 Ether and approve two gifts, as you did previously.

Perform the attack and view the logs, as you did previously.

C 353.2: Logs (10 pts)

There are now only two log entries.

The flag is covered by a green rectangle in the image below.

Alternate Fix

Simply reversing the order of lines 11 and 12, as shown below, also stops the attack.

References

Ethereum Basics

Re-entrancy not reproduceable
Re-entrancy
Reentrancy | Hack Solidity (0.6)
Re-Entrancy
Constructor error on multiple pieces of code - Remix
How to call a function from an already deployed contract?
Testing for Reentrancy attacks in remix
What is a Re-Entrancy Attack?

Posted 5-19-2021
Environment updated to Javascript VM (London) on 10-26-21
Alternate fix added 10-27-21