Zunächst werde ich Ihnen auf einfache Weise erklären, was ein Reentrancy-Angriff ist und wie Sie ihn verhindern können. Anschließend werde ich tiefer in Codebeispiele eintauchen, um die Schwachstellen aufzuzeigen, den Code des Angreifers aufzuzeigen und, was am wichtigsten ist, die neuesten bewährten Methoden zum Schutz nicht nur eines, sondern aller Smart Contracts in Ihrem Projekt.

Spoiler: Wenn Sie bereits vom Modifikator nonReentrant() gehört haben, lesen Sie weiter, denn ein paar Zeilen weiter unten werden Sie den Modifikator globalNonReentrant() und das Muster checks-effects-interactions entdecken.



Im Bild oben haben wir ContractA und ContractB. Wie Sie wissen, kann ein Smart Contract mit einem anderen Smart Contract interagieren. In diesem Fall kann ContractA beispielsweise ContractB aufrufen. Die Grundidee der Reentrancy besteht also darin, dass ContractB ContractA zurückrufen kann, während ContractA noch ausgeführt wird.

Wie kann der Angreifer dies also nutzen?

Oben haben wir ContractA, der 10 Ether hat, und wir sehen, dass ContractB 1 Ether in ContractA gespeichert hat. In diesem Fall kann ContractB die Auszahlungsfunktion von ContractA verwenden und Ether an sich selbst zurücksenden, wenn es die Prüfung besteht, bei der sein Saldo größer als 0 ist, um dann seinen Gesamtsaldo auf 0 ändern zu lassen.



Sehen wir uns nun an, wie ContractB Reentrancy nutzen kann, um die Abhebungsfunktion auszunutzen und alle Ether von ContractA zu stehlen. Grundsätzlich benötigt der Angreifer zwei Funktionen: attack() und fallback().

In Solidity ist eine Fallback-Funktion eine externe Funktion ohne Namen, Parameter oder Rückgabewerte. Jeder kann eine Fallback-Funktion aufrufen, indem er: eine Funktion aufruft, die im Vertrag nicht vorhanden ist; eine Funktion aufruft, ohne die erforderlichen Daten zu übergeben; Ether ohne Daten an den Vertrag sendet.

Die Reentrancy funktioniert so (folgen wir den Pfeilen Schritt für Schritt), dass der Angreifer die Funktion attack() aufruft, die wiederum die Funktion withdraw() von ContractA aufruft. Innerhalb der Funktion wird überprüft, ob der Saldo von ContractB größer als 0 ist. Ist dies der Fall, wird die Ausführung fortgesetzt.



Da der Kontostand von ContractB größer als 0 ist, sendet er 1 Ether zurück und löst die Fallback-Funktion aus. Beachten Sie, dass ContractA zu diesem Zeitpunkt 9 Ether hat und ContractB bereits 1 Ether.



Wenn als nächstes die Fallback-Funktion ausgeführt wird, löst sie erneut die Auszahlungsfunktion von ContractA aus und prüft erneut, ob der Saldo von ContractB größer als 0 ist. Wenn Sie das Bild oben noch einmal prüfen, werden Sie feststellen, dass sein Saldo immer noch 1 Ether beträgt.



Das bedeutet, dass die Prüfung erfolgreich ist und ein weiterer Ether an ContractB gesendet wird, der die Fallback-Funktion auslöst. Beachten Sie, dass die Zeile, in der wir „balance=0“ haben, nie ausgeführt wird. Dies wird so lange fortgesetzt, bis der gesamte Ether von ContractA aufgebraucht ist.

___________

Schauen wir uns nun einen Smart Contract an, bei dem wir mit dem Solidity-Code Reentranz erkennen können.



Im EtherStore-Vertrag haben wir die Funktion deposit() , die die Guthaben des Absenders speichert und aktualisiert, und dann die Funktion withdrawAll() , die alle gespeicherten Guthaben auf einmal übernimmt. Beachten Sie bitte die Implementierung von withdrawAll() , wo zuerst mit require geprüft wird, ob das Guthaben größer als 0 ist, und gleich danach erneut der Ether gesendet wird, wobei zum Schluss die Aktualisierung des Guthabens des Absenders auf 0 bleibt.



Hier haben wir den Vertragsangriff, der die Wiedereintrittsmöglichkeit nutzt, um den EtherStore-Vertrag zu leeren. Lassen Sie uns seinen Code analysieren:

  • In seinem Konstruktor übergibt der Angreifer die EtherStore-Adresse, um eine Instanz zu erstellen und so deren Funktionen nutzen zu können.

  • Dort sehen wir die Funktion Fallback() die aufgerufen wird, wenn EtherStore Ether an diesen Vertrag sendet. Darin wird Withdraw von EtherStore aufgerufen, solange der Kontostand gleich oder größer als 1 ist.

  • Und in der Funktion „attack()“ haben wir die Logik, die EtherStore ausnutzt. Wie wir sehen, initiieren wir zunächst den Angriff, indem wir sicherstellen, dass wir genug Ether haben, und zahlen dann 1 Ether ein, um einen Kontostand größer als 0 in EtherStore zu haben und so die Prüfungen zu bestehen, bevor wir mit der Auszahlung beginnen.

Ich habe oben im Beispiel von ContractA und ContractB Schritt für Schritt erklärt, wie der Code ausgeführt wird. Lassen Sie uns nun eine Zusammenfassung darüber machen, wie es ablaufen wird. Zunächst ruft der Angreifer attack() auf, was darin withdrawAll() von EtherStore aufruft, was dann Ether an die Fallback-Funktion des Angriffsvertrags sendet. Und dort wird die Wiedereintrittsfunktion gestartet und das Guthaben des EtherStore geleert.

Wie können wir also unsere Verträge vor Reentrancy-Angriffen schützen?

Ich zeige Ihnen drei Präventionstechniken für Ihren umfassenden Schutz. Ich erkläre Ihnen, wie Sie Reentranz in einer einzelnen Funktion, Reentranz funktionsübergreifend und Reentranz vertragsübergreifend verhindern können.



Die erste Technik zum Schutz einer einzelnen Funktion ist die Verwendung eines Modifikators namens „noReentrant“.

Ein Modifikator ist ein spezieller Funktionstyp, mit dem Sie das Verhalten anderer Funktionen ändern können. Mit Modifikatoren können Sie einer Funktion zusätzliche Bedingungen oder Funktionen hinzufügen, ohne die gesamte Funktion neu schreiben zu müssen.

Was wir hier tun, ist, den Vertrag zu sperren, während die Funktion ausgeführt wird. Auf diese Weise kann die einzelne Funktion nicht erneut aufgerufen werden, da der Code der Funktion durchgegangen werden und dann die Statusvariable „gesperrt“ auf „false“ geändert werden muss, um die im Erfordernis durchgeführte Prüfung erneut zu bestehen.

___________



Die zweite Technik besteht in der Verwendung des Checks-Effects-Interactions-Musters, das unsere Verträge vor funktionsübergreifender Reentranz schützt. Können Sie im aktualisierten EtherStore-Vertrag oben erkennen, was sich geändert hat?

Um tiefer in das Check-Effects-Interaction-Muster einzutauchen, empfehle ich die Lektüre von https://fravoll.github.io/solidity-patterns/checks_effects_interactions.html 





Oben sehen wir den Vergleich zwischen dem anfälligen Code aus dem Bild links, wo der Kontostand nach dem Senden des Ethers aktualisiert wurde, der, wie oben zu sehen ist, möglicherweise nie erreicht werden könnte, und rechts, wo die balances[msg.sender] = 0 (oder Effekt) direkt nach dem require(bal > 0) (Prüfung), aber vor dem Senden von Ether (Interaktion) verschoben wurden.

Auf diese Weise stellen wir sicher, dass dieser Vertrag vor Angreifern geschützt ist, auch wenn eine andere Funktion auf „withdrawAll()“ zugreift, da der Saldo vor dem Senden des Ethers immer aktualisiert wird.

Muster erstellt von https://twitter.com/GMX_IO

Die dritte Technik, die ich Ihnen zeigen werde, ist das Erstellen des Vertrags GlobalReentrancyGuard zum Schutz vor vertragsübergreifender Wiedereintrittsmöglichkeit. Es ist wichtig zu verstehen, dass dies auf Projekte mit mehreren Verträgen anwendbar ist, die miteinander interagieren.

Die Idee hier ist die gleiche wie beim Modifikator noReentrant, den ich in der ersten Technik erklärt habe. Er greift auf den Modifikator zu, aktualisiert eine Variable, um den Vertrag zu sperren, und entsperrt ihn nicht, bis der Code beendet ist. Der große Unterschied besteht darin, dass wir eine Variable verwenden, die in einem separaten Vertrag gespeichert ist und als Ort verwendet wird, um zu überprüfen, ob die Funktion aufgerufen wurde oder nicht.







Ich habe hier ein Beispiel ohne tatsächlichen Code und nur mit Funktionsnamen als Referenz erstellt, um die Idee zu verstehen, da es meiner Erfahrung nach dabei helfen kann, die Situation besser zu visualisieren, als sie nur in Worte zu fassen.

Hier würde der Angreifer die Funktion im ScheduledTransfer-Vertrag aufrufen, die nach Erfüllung der Bedingungen den angegebenen Ether an den AttackTransfer-Vertrag senden würde, der dann die Fallback-Funktion einleiten und somit die Transaktion aus Sicht des ScheduledTransfer-Vertrags „abbrechen“ und dennoch den Ether empfangen würde. Auf diese Weise würde er einen Versuch starten, bis alle Ether aus ScheduledTransfer abgezogen sind.

Durch die Verwendung des oben erwähnten GlobalReentrancyGuard kann ein solches Angriffsszenario vermieden werden.

__________________

Twittern Sie @TheBlockChainer, um weitere tägliche Updates zu Smart Contracts, Web3-Sicherheit, Solidity, Auditing von Smart Contracts und mehr zu erhalten.

__________________