Was ist eine rekursive Aufrufschwachstelle?

Was genau ist eine rekursive Aufrufschwachstelle?

Welche Maßnahmen kann ich beim Erstellen von Smart Contracts, DAOs oder DAPPs ergreifen, um sicherzustellen, dass ich nicht angreifbar bin?

Antworten (3)

Eine einfachere Erklärung

  1. Der Angreifer erstellt einen Wallet-Vertrag ( 0xc0ee9db1a9e07ca63e4ff0d5fb6f86bf68d47b89 im Angriff vom 17.06.2016) mit einer Standardeinstellung (oder einem Fallback) function (), um die Funktion von The DAO splitDAO(...)mehrmals aufzurufen. Folgendes ist eine einfache Vorgabe function ():

    function () {
       // Note that the following statement can only be called recursively
       // a limited number of times to prevent running out of gas or
       // exceeding the call stack
       call TheDAO.splitDAO(...)
    }
    
  2. Der Angreifer erstellt (oder schließt sich an) einen Split-Proposal (Nr. 59 im Angriff vom 17.06.2016) an, wobei die Empfängeradresse begin auf den oben erstellten Wallet-Vertrag gesetzt ist.

  3. Der Angreifer stimmt für den Split-Vorschlag mit Ja.

  4. Nachdem der Split-Vorschlag abgelaufen ist, ruft der Angreifer die splitDAO(...)Funktion von The DAO auf.

    a. Die splitDAO(...)Funktion ruft den Standard des Wallet-Vertrags auf, function ()als Teil des Sendens der Ether an den Empfänger.

    b. Der Standard des Wallet-Vertrags function ()ruft die DAOs splitDAO(...)erneut auf, was den Zyklus von a wiederholt. Oben.

    c. Die Standardeinstellung des Wallet-Vertrags function ()muss sicherstellen, dass kein Fehler ausgelöst wird, da die Transaktionen zurückgesetzt werden, wenn der Call-Stack oder Gas überschritten wird.


Im Folgenden finden Sie Auszüge aus dem Quellcode von The DAO, die an dieser Art von Angriff beteiligt sind:

DAO.splitDAO(...):

Das Problem im folgenden Code besteht darin, dass die Zahlung erfolgt (Anweisung withdrawRewardFor(msg.sender);), bevor die Variablen zurückgesetzt werden, die die Zahlungen verfolgen, zu deren Empfang der Empfänger berechtigt ist ( balances[msg.sender] = 0;und paidOut[msg.sender] = 0;).

    function splitDAO(
        uint _proposalID,
        address _newCurator
    ) noEther onlyTokenholders returns (bool _success) {
        ...     
        withdrawRewardFor(msg.sender); // be nice, and get his rewards
        totalSupply -= balances[msg.sender];
        balances[msg.sender] = 0;
        paidOut[msg.sender] = 0;
        return true;
    }


DAO.withdrawRewardFor(...):

    function withdrawRewardFor(address _account) noEther internal returns (bool _success) {
        if ((balanceOf(_account) * rewardAccount.accumulatedInput()) / totalSupply < paidOut[_account])
            throw;

        uint reward =
            (balanceOf(_account) * rewardAccount.accumulatedInput()) / totalSupply - paidOut[_account];
        if (!rewardAccount.payOut(_account, reward))
            throw;
        paidOut[_account] += reward;
        return true;
    }


ManagedAccount.payOut(...):

Die Anweisung _recipient.call.value(_amount)()sendet die Ether an das Konto des Empfängers, in diesem Fall wird der Standard des Wallet-Vertrags function ()aufgerufen, der es ermöglicht, die DAO.splitDAO(...)Funktion rekursiv aufzurufen.

    function payOut(address _recipient, uint _amount) returns (bool) {
        if (msg.sender != owner || msg.value > 0 || (payOwnerOnly && _recipient != owner))
            throw;
        if (_recipient.call.value(_amount)()) {
            PayOut(_recipient, _amount);
            return true;
        } else {
            return false;
        }
    }        


Siehe auch:



Weitere Hintergrundinformationen

Hier ist der ursprüngliche Blogbeitrag von Peter Vessenes, der die rekursive Aufrufschwachstelle in DAOs beschrieb: More Ethereum Attacks: Race-To-Empty is the Real Deal , mit vorgeschlagener Abhilfe für dieses Problem.

Aus dem Beitrag:

Die Schwachstelle

Hier ist ein Code; schau mal ob du das problem finden kannst.

function getBalance(address user) constant returns(uint) {  
  return userBalances[user];
}

function addToBalance() {  
  userBalances[msg.sender] += msg.amount;
}

function withdrawBalance() {  
  amountToWithdraw = userBalances[msg.sender];
  if (!(msg.sender.call.value(amountToWithdraw)())) { throw; }
  userBalances[msg.sender] = 0;
}

Hier ist das Problem: msg.sender hat möglicherweise eine Standardfunktion, die so aussieht.

function () {  
 // To be called by a vulnerable contract with a withdraw function.
 // This will double withdraw.

 vulnerableContract v;
 uint times;
 if (times == 0 && attackModeIsOn) {
   times = 1;
   v.withdraw();

  } else { times = 0; }
}

Was geschieht? Der Callstack sieht so aus:

   vulnerableContract.withdraw run 1
     attacker default function run 1
       vulnerableContract.withdraw run 2
         attacker default function run 2

Jedes Mal überprüft der Vertrag das auszahlbare Guthaben des Benutzers und sendet es aus. Der Benutzer erhält also das Doppelte seines Guthabens aus dem Vertrag.

Wenn der Code aufgelöst wird, wird das Guthaben des Benutzers auf 0 gesetzt, egal wie oft der Vertrag aufgerufen wurde.

Und die vorgeschlagenen Abhilfemaßnahmen aus dem Beitrag:

Lösungsansatz 1: Korrigieren Sie Ihre Bestellung

Der empfohlene Ansatz in den bald veröffentlichten aktualisierten Solidity-Beispielen ist die Verwendung von Code wie dem folgenden:

function withdrawBalance() {  
  amountToWithdraw = userBalances[msg.sender];
  userBalances[msg.sender] = 0;
  if (amountToWithdraw > 0) {
    if (!(msg.sender.send(amountToWithdraw))) { throw; }
  }
}

und

Abhilfeansatz 2: Mutexe

Betrachten Sie stattdessen diesen Code.

function withdrawBalance() {  
  if ( withdrawMutex[msg.sender] == true) { throw; }
  withdrawMutex[msg.sender] = true;
  amountToWithdraw = userBalances[msg.sender];
  if (amountToWithdraw > 0) {
    if (!(msg.sender.send(amountToWithdraw))) { throw; }
  }
  userBalances[msg.sender] = 0;
  withdrawMutex[msg.sender] = false;
}



Und aus dem Beitrag von Benutzer eththrowa im The DAO-Forumsbeitrag Der im MKR-Token-Vertrag entdeckte Fehler betrifft auch theDAO – würde es Benutzern ermöglichen, Belohnungen von theDAO zu stehlen, indem sie rekursiv aufrufen :

Dieser Fehler: https://www.reddit.com/r/ethereum/comments/4nmohu/from_the_maker_dao_slack_today_we_discovered_a/57 Ist auch im DAO-Code vorhanden – speziell hier in der FunktiondrawRewardFor DAO.sol:

if (!rewardAccount.payOut(_account, reward))
   throw;
paidOut[_account] += reward;
return true;

und hier in managedAccount.sol

function payOut(address _recipient, uint _amount) returns (bool) {
        if (msg.sender != owner || msg.value > 0 || (payOwnerOnly && _recipient != owner))
            throw;
        if (_recipient.call.value(_amount)()) {
            PayOut(_recipient, _amount);
            return true;
        } else {
            return false;
        }
    }

Dies würde es einem Benutzer ermöglichen, ein Vielfaches seiner Berechtigung durch rekursives Aufrufen des Vertrags zu entziehen. Seltsamerweise hat das slockit-Team diesen Fehler hier im Vorschlagsbereich entdeckt:

// we are setting this here before the CALL() value transfer to
// assure that in the case of a malicious recipient contract trying
// to call executeProposal() recursively money can't be transferred
// multiple times out of the DAO
p.proposalPassed = true;

habe es aber in der Belohnungssektion verpasst. Offensichtlich gibt es noch keine Belohnungen im DAO, also ist dies kein Thema, das heute Geld kosten könnte.



F : Welche Maßnahmen kann ich beim Erstellen von Smart Contracts, DAOs oder DAPPs ergreifen, um sicherzustellen, dass ich nicht angreifbar bin?

Testen, prüfen, testen, prüfen, ... . Wie bei jedem Softwaresystem gibt es viele potenzielle Bereiche, in denen sich Fehler einschleichen können. Und je höher der Wert ist, desto mehr Interesse haben Angreifer daran.

Aus dem Ethereum-Blog CRITICAL UPDATE Re: DAO Vulnerability :

Vertragsautoren sollten darauf achten, (1) sehr vorsichtig mit rekursiven Aufruffehlern umzugehen und auf Ratschläge der Ethereum-Community für Vertragsprogrammierung zu hören, die wahrscheinlich in der nächsten Woche zur Minderung solcher Fehler erscheinen werden, und (2) die Erstellung von Verträgen zu vermeiden, die enthalten einen Wert von mehr als 10 Millionen US-Dollar, mit Ausnahme von Sub-Token-Verträgen und anderen Systemen, deren Wert selbst durch den sozialen Konsens außerhalb der Ethereum-Plattform definiert wird und die über den Community-Konsens leicht „hart abgespalten“ werden können, wenn ein Fehler auftritt (z. B. MKR), zumindest bis die Community mehr Erfahrung mit Bug-Minderung gesammelt hat und/oder bessere Tools entwickelt wurden.

Der Reddit-Thread Können wir bitte nie wieder 100 Millionen in einen Vertrag stecken ohne formelle Korrektheitsnachweise? einen formalen Korrektheitsbeweis vorschlagen (aber es kann immer noch Fehler geben).

In den nächsten Wochen werden weitere Hinweise herauskommen - ich werde diese Antwort aktualisieren.

Einige Ressourcen:

„Der Angreifer erstellt einen geteilten Vorschlag mit der Empfängeradresse, beginnend mit dem oben erstellten Wallet-Vertrag.“ Der Satz kann nicht analysiert werden. Es ergibt keinen grammatikalischen Sinn.
Wie ist das c. The wallet contract's default function () must ensure that an error is not thrown as the transactions will be rolled back if the call stack or gas is exceeded.möglich? Wenn das Gas überschritten wird, sollte EVM keinen Fehler ausgeben, egal was passiert? @BokkyPooBah

Wenn Ihr Code im Pseudocode so aussieht:

function do:
   if (pool has mymoney = true)
     split(mymoney) 
     pool has mymoney = false

Wenn Sie diese Funktion wiederholt aufrufen, haben Sie eine Art Rennbedingung, in der Sie Ihr Geld zweimal, dreimal, ... ad infitum ausgeben dürfen .

Fix ist einfach, kehren Sie zwei Operationen um:

function do:
   if (pool has mymoney = true)
     pool= pool - mymoney // 2
     split(mymoney) //1

Sehen Sie sich diesen Commit als Beispiel für den Fix an

Werden Transaktionen auf EVM nicht atomar ausgeführt, daher sollte die Race-Condition keinen Einfluss haben? @ Roland Kofler

Eine "Schwachstelle bei rekursiven Aufrufen" ist ein mehrdeutiger Begriff, der vermieden werden sollte, da er ungenau ist und zwei Dinge bedeuten kann.

Wiedereintretender Angriff

Sie meinen wahrscheinlich "Reentrant-Schwachstelle" oder "Reentrant-Angriff", was die Antwort von @Roland beschreibt. Hinweis: Nicht alle wiedereintrittsfähigen Angriffe müssen rekursiv sein (in dem Sinne, dass bösartiger Code nicht auf die gleiche Weise wieder eintreten muss: er kann über jede extern zugängliche Funktion wieder in einen Vertrag eintreten).

http://forum.ethereum.org/discussion/1317/reentrant-contracts

https://github.com/LeastAuthority/ethereum-analyses/blob/master/GasEcon.md

Call Depth Attack (bei EIP 150 nicht mehr möglich)

In Ethereum ist auch ein „Call Depth Attack“ möglich (unter anderem mit rekursiven Aufrufen).

Wie führt der Stapeltiefenangriff dazu, dass ein send() stillschweigend fehlschlägt?

Callstack-Angriff

Wenn das jetzt jemand liest, möchte ich Sie nur wissen lassen, dass Anruftiefenangriffe nach dem EIP 150-Update nicht mehr möglich sind.