Weiter zum Hauptinhalt

Smart Contract – Sicherheit

Letzte Änderung: , Invalid DateTime

Ethereum-Smart-Contracts sind extrem flexibel und in der Lage, sowohl große Mengen an Token (oft über $1B) zu halten, als auch unveränderliche Logik auf der Grundlage von zuvor eingesetztem Smart-Contract-Code auszuführen. Während das ein lebendiges und kreatives Ökosystem vernetzter Smart Contracts geschaffen hat, das ohne Vertrauen auskommt, ist es auch das perfekte Ökosystem, um Angreifer anzulocken, die davon profitieren, Schwachstellen in Smart Contracts und unerwartetem Verhalten in Ethereum auszunutzen. Code in Smart Contracts kann normalerweise nicht geändert werden, um Sicherheitslücken zu schließen. Assets die aus Smart Contracts gestohlen wurden, können nicht zurück geholt werden und gestohlene Assets sind sehr schwer nachzuverfolgen. Die Gesamtsumme gestohlener oder verlorener Werte infolge von Problemen mit Smart Contracts beläuft sich auf weit über 1 Milliarde Dollar. Einige der größeren Codefehlern in Smart Contracts beinhalten:

Voraussetzungen

Im Folgenden wird die Sicherheit von Smart Contracts erläutert. Als Voraussetzung sollten Sie daher mit Smart Contracts vertraut sein oder sich vertraut machen, bevor Sie sich dem Thema Sicherheit widmen.

So schreiben Sie einen sicheren Smart-Contract-Code

Bevor Sie Code im Ethereum-Mainnet einführen, ist es wichtig, ausreichende Vorsichtsmaßnahmen zu treffen, damit alle Vorgänge geschützt sind, die Werte betreffen und über den Smart Contract durchgeführt werden. In diesem Artikel werden wir einige spezielle Angriffe diskutieren, Ressourcen bereitstellen, um mehr über Angriffstypen zu erfahren. Außerdem erfahren Sie mehr über einige grundlegende Werkzeuge und Best Practices, um sicherzustellen, dass Ihre Verträge korrekt und sicher funktionieren.

Audits sind keine Garantie

Vor Jahren waren die Tools zum Schreiben, Kompilieren, Testen und Bereitstellen von Smart Contracts noch unausgereift. Das führte dazu, dass viele Projekte willkürlichen Solidity-Code schreiben konnten und ihn an einen Auditor übergaben, der den Code auf seine klare Funktionsweise und deren Sicherheit untersuchte. Im Jahr 2020 sind die Entwicklungsprozesse und Tools, die das Programmieren in Solidity unterstützen, wesentlich besser. Wenn Sie die Best Practices umsetzen, stellen Sie damit sicher, dass Ihr Projekt leichter zu verwalten ist, und erfüllen viele Sicherheitsaspekte für Ihr Projekt. Den Smart Contract am Ende einem Audit zu unterziehen, reicht als einziger Sicherheitsaspekt für Ihr Projekt nicht mehr aus. Die Sicherheit beginnt vor dem Schreiben der ersten Zeile des Smart-Contract-Codes, und zwar mit geeigneten Design- und Entwicklungsprozessen.

Prozess der Entwicklung von Smart Contracts

Mindestvoraussetzungen:

  • Der gesammte Code wird in einem Versionskontrollsystem gespeichert, wie z. B. Git.
  • Alle Codeänderungen erfolgen über Pull-Anforderungen.
  • Für alle Pull-Anforderungen gibt es mindestens einen Prüfer. Wenn Sie alleine an einem Solo-Projekt arbeiten, sollten Sie überlegen, einen weiteren Solo-Autor zu finden und Codeprüfungen gegenseitig durchzuführen.
  • Ein einziger Befehl kompiliert, verteilt und führt eine Reihe von Tests für Ihren Code aus, und zwar mithilfe einer Ethereum-Entwicklungsumgebung (siehe: Truffle).
  • Sie haben Ihren Code über grundlegende Codeanalysetools wie Mythril und Slither ausgeführt, idealerweise bevor die einzlenen Pull-Anforderungen zusammengeführt werden, um so Unterschiede in der Ausgabe zu vergleichen.
  • Solidity zeigt KEINE Compiler-Warnungen an.
  • Ihr Code ist gut dokumentiert.

Es gibt noch viele weitere Aspekte des Entwicklungsprozesses, die erläutert werden könnten, doch diese Liste ist ein guter Ausgangspunkt. Weitere Informationen und ausführliche Erklärungen finden Sie in der Checkliste für Prozessqualität von DeFiSafety(opens in a new tab). DefiSafety(opens in a new tab) ist eine inoffizielle Publikation von verschiedenen großen und öffentlichen Ethereum-dApps. Im Rahmen des DeFiSafety-Bewertungssystems wird überprüft, wie gut sich das Projekt an diese Prozessqualitätsprüfungsliste hält. Dabei werden folgende Prozesse eingehalten:

  • Sie erzeugen sichereren Code durch reproduzierbare, automatisierte Tests.
  • Auditoren können Ihr Projekt effektiver prüfen.
  • Neue Entwickler können einfacher einsteigen.
  • Bietet Entwicklern die Möglichkeit, schnell zu iterieren, testen und Feedback zu Änderungen zu erhalten.
  • So kommt es wahrscheinlich zu weniger Rückschritten in Ihrem Projekt.

Angriffe und Schwachstellen

Da Sie nun Solidity-Code unter Beachtung eines effizienten Entwicklungsprozesses schreiben, sollten Sie sich mit einigen gängigen Schwachstellen vertraut machen, um ein Bild davon zu bekommen, was schief gehen kann.

Re-entrancy (Wiedereintritt)

Re-entrancy ist eines der größten und bedeutendsten Sicherheitsprobleme, die bei der Entwicklung von Smart Contracts zu berücksichtigen sind. Da die EVM nicht mehrere Contracts gleichzeitig abwickeln kann, pausiert ein Vertrag, der einen anderen Contract aufruft, die Ausführung des Vertragsaufrufs und den Speicherzustand, bis der Aufruf zurückkehrt. An diesem Punkt läuft die Ausführung normal weiter. Durch dieses Pausieren und Neustarten kann eine Schwachstelle entstehen, die als "Re-entrancy" bekannt ist.

Im Folgenden sehen Sie eine einfache Vertragsversion, der anfällig für eine Re-entrancy ist:

1// DIESER CONTRACT ENTHÄLT ABSICHTLICHE VERWUNDBARKEITEN, NICHT VERWENDEN!
2contract Victim {
3 mapping (address => uint256) public balances;
4
5 function deposit() external payable {
6 balances[msg.sender] += msg.value;
7 }
8
9 function withdraw() external {
10 uint256 amount = balances[msg.sender];
11 (bool success, ) = msg.sender.call.value(amount)("");
12 require(success);
13 balances[msg.sender] = 0;
14 }
15}
16
Alles anzeigen
📋 Kopieren

Um es einem Benutzer zu ermöglichen, ETH auszuzahlen, sind folgende Funktionen im Smart Contract gespeichert:

  1. Lesen, wie viel Guthaben ein Benutzer hat
  2. Dem Nutzer diesen Betrag in ETH senden
  3. Das Guthaben auf 0 zurücksetzen, so dass das Guthaben des Nutzers nicht doppelt ausgezahlt werden kann

Bei einem Aufruf von einem regulären Konto (z. B. Ihrem eigenen MetaMask-Konto) funktioniert dies wie erwartet: msg.sender.call.value() sendet Ihrem Konto einfach ETH. Allerdings können Smart Contracts auch Aufrufe tätigen. Wenn ein benutzerdefinierter bösartiger Vertrag die Funktion withdraw() aufruft, sendet msg.sender.call.value() nicht nur amount von ETH, sondern ruft auch implizit den Vertrag auf, um mit der Codeausführung zu beginnen. Stellen Sie sich diesen böswilligen Vertrag vor:

1contract Attacker {
2 function beginAttack() external payable {
3 Victim(VICTIM_ADDRESS).deposit.value(1 ether)();
4 Victim(VICTIM_ADDRESS).withdraw();
5 }
6
7 function() external payable {
8 if (gasleft() > 40000) {
9 Victim(VICTIM_ADDRESS).withdraw();
10 }
11 }
12}
13
Alles anzeigen
📋 Kopieren

Der Aufruf von Attacker.beginAttack() startet einen Zyklus, der ähnlich aussieht:

10.) EOA-Aufrufe des Attacker.beginAttack() mit 1 ETH
20.) Attacker.beginAttack() hinterlegt 1 ETH in Opferaccount
3
4 1.) Angreifer -> Victim.withdraw()
5 1.) Victim reads balances[msg.sender]
6 1.) Opfer sendet ETH an den Angreifer (welcher die Standardfunktion ausführt)
7 2.) Angreifer -> Victim.withdraw()
8 2.) Victim reads balances[msg.sender]
9 2.) Opfer sendet ETH an den Angreifer (welcher die Standardfunktion ausführt)
10 3.) Angreifer -> Victim.withdraw()
11 3.) Victim reads balances[msg.sender]
12 3.) Opfer sendet ETH an den Angreifer (welcher die Standardfunktion ausführt)
13 4.) Angreifer hat nicht mehr genug Gas, kehrt zurück ohne erneut aufzurufen
14 3. saldo[msg.sender] = 0;
15 2. Salden[msg.sender] = 0; (es war bereits 0)
16 1.) Salden[msg.sender] = 0; (es war bereits 0)
17
Alles anzeigen

Über den Aufruf von Attacker.beginAttack mit 1 ETH erfolgt ein Re-entrancy-Angriff auf das Opfer. Dadurch wird mehr ETH ausgezahlt, als in den Vertrag gegeben wurde (aus den Salden anderer Benutzer, was bewirkt, dass der Smart Contract des Ofers unterbesichert (under-collaterized) wird).

So umgehen Sie Re-entrancy (der falsche Weg)

Sie könnten in Erwägung ziehen, die Re-entrancy zu unterbinden, indem Sie einfach verhindern, dass Smart Contracts mit Ihrem Code interagieren. Sie suchen stackoverflow und finden dieses Code-Snippet mit Tonnen von Upvotes:

1function isContract(address addr) internal returns (bool) {
2 uint size;
3 assembly { size := extcodesize(addr) }
4 return size > 0;
5}
6
📋 Kopieren

Das scheint sinnvoll: Vertäge haben Code und wenn der Anrufer Code hat, erlauben Sie ihm keine Einzahlung. Fügen wir diesen Aspekt also hinzu:

1// DIESER CONTRACT ENTHÄLT ABSICHTLICHE VERWUNDBARKEITEN, NICHT VERWENDEN!
2contract ContractCheckVictim {
3 mapping (address => uint256) public balances;
4
5 function isContract(address addr) internal returns (bool) {
6 uint size;
7 assembly { size := extcodesize(addr) }
8 return size > 0;
9 }
10
11 function deposit() external payable {
12 require(!isContract(msg.sender)); // <- NEW LINE
13 balances[msg.sender] += msg.value;
14 }
15
16 function withdraw() external {
17 uint256 amount = balances[msg.sender];
18 (bool success, ) = msg.sender.call.value(amount)("");
19 require(success);
20 balances[msg.sender] = 0;
21 }
22}
23
Alles anzeigen
📋 Kopieren

Wenn nun ETH eingezahlt werden soll, darf Ihre Adresse keinen Smart-Contract-Code enthalten. Das kann allerdings leicht mit dem folgenden Angreifervertrag ausgehebelt werden:

1contract ContractCheckAttacker {
2 constructor() public payable {
3 ContractCheckVictim(VICTIM_ADDRESS).deposit(1 ether); // <- New line
4 }
5
6 function beginAttack() external payable {
7 ContractCheckVictim(VICTIM_ADDRESS).withdraw();
8 }
9
10 function() external payable {
11 if (gasleft() > 40000) {
12 Victim(VICTIM_ADDRESS).withdraw();
13 }
14 }
15}
16
Alles anzeigen
📋 Kopieren

Während sich der erste Angriff auf die Vertragslogik bezogen hat, liegt nun ein Angriff auf das Verhalten bei der Bereitstellung eines Ethereum-Vertrags vor. Bei der der Erstellung hat ein Vertrag seinen Code noch nicht zurückgegeben, der an seiner Adresse eingesetzt werden soll, doch er behält die volle EVM-Kontrolle WÄHREND dieses Prozesses.

Technisch kann verhindert werden, dass Smart Contracts Ihren Code aufrufen. Verwenden Sie dafür folgende Zeile:

1require(tx.origin == msg.sender)
2
📋 Kopieren

Allerdings ist das noch immer keine gute Lösung. Einer der spannendsten Aspekte von Ethereum sind die Gestaltungsmöglichkeiten, wie sich Smart Contracts miteinander integrieren und aufeinander aufbauen. Wenn Sie die obige Zeile verwenden, beschränken Sie damit den Nutzen Ihres Projekts.

So umgehen Sie Re-entrancy (der richtige Weg)

Wenn Sie ganz einfach die Reihenfolge der Speicheraktualisierung und des externen Aufrufs ändern, verhindern Sie die Re-entrancy, die den Angriff ermöglichte. Ein Rückruf in die Auszahlung wird dem Angreifer zwar möglich, doch das ist nutzlos, da die balances bereits auf 0 gesetzt sind.

1contract NoLongerAVictim {
2 function withdraw() external {
3 uint256 amount = balances[msg.sender];
4 balances[msg.sender] = 0;
5 (bool success, ) = msg.sender.call.value(amount)("");
6 require(success);
7 }
8}
9
📋 Kopieren

Der obige Code folgt dem Designmuster "Prüfungen-Auswirkungen-Interaktionen", das zum Schutz vor Re-entrancy beiträgt. Hier erfahren Sie mehr über Prüfungen-Auswirkungen-Interaktionen(opens in a new tab).

So umgehen Sie Re-entrancy (die nukleare Option)

Jedes Mal, wenn Sie ETH an eine nicht vertrauenswürdige Adresse senden oder mit einem unbekannten Vertrag interagieren (wie beispielsweise das Aufrufen von transfer() einer von dir angegebenen Token-Adresse), eröffnet sich für Sie die Möglichkeit der RE-entrancy. Durch die Gestaltung von Smart Contracts, die weder ETH senden noch nicht vertrauenswürdige Verträge aufrufen, verhindern Sie die Möglichkeit der Re-entrancy.

Weitere Angriffstypen

Die obigen Angriffstypen beziehen sich auf Probleme mit Smart-Contract-Code (Re-entrancy) und Ethereum-Kuriositäten (Codeausführung innerhalb der Vertragskonstruktoren, bevor Code unter der Vertragsadresse verfügbar ist). Doch es gibt viele weitere Angriffstypen, sie Sie kennen sollten:

  • Front-running (Vorweglaufen)
  • ETH-Sendungsablehnung
  • Ganzzahlüberlauf/-unterlauf

Weiterführende Informationen:

Sicherheitstools

Es ist unumgänglich, dass Sie die Sicherheitsgrundlagen von Ethereum verstehen und mit einem professionellen Prüfer zusammenarbeiten. Doch für die Überprüfung Ihres Codes gibt es viele Tools, die helfen, mögliche Probleme in Ihrem Code zu beleuchten.

Smart Contract – Sicherheit

Slither – In Python 3 geschriebener statischer Analyserahmen für Solidity

MythX – Sicherheitsanalyse-API für Ethereum-Smart Contracts

Mythril – Sicherheitsanalyse für EVM-Bytecode

Manticore – Eine Befehlszeilenschnittstelle, die ein symbolisches Ausführungswerkzeug für Smart Contracts und Binärdaten einsetzt

Securify – Sicherheitsscanner für Ethereum-Smart Contracts

ERC20 Verifier – Ein Verifizierungstool zur Prüfung der Übereinstimmung eines Vertrags mit dem ERC20-Standard

Formale Verifizierung

Informationen zur formalen Verifizierung

Tools einsetzen

Zwei der beliebtesten Tools für eine Smart-Contract-Sicherheitsanalyse sind:

Beide Tools sind nützlich, um Ihren Code zu analysieren und Fehler zu melden. Für jedes Tool gibt es eine [commercial] gehostete Version, sie sind aber auch für den lokalen Betrieb verfügbar. Das folgende Beispiel ist ein schnelles Beispiel für die Ausführung von Slither, das in einem praktischen Docker-Image trailofbits/eth-security-toolbox zur Verfügung gestellt wird. Sofern noch nicht geschehen, sollten Sie Node.js(opens in a new tab) installieren.

$ mkdir test-slither
$ curl https://gist.githubusercontent.com/epheph/460e6ff4f02c4ac582794a41e1f103bf/raw/9e761af793d4414c39370f063a46a3f71686b579/gistfile1.txt > bad-contract.sol
$ docker run -v `pwd`:/share -it --rm trailofbits/eth-security-toolbox
docker$ cd /share
docker$ solc-select 0.5.11
docker$ slither bad-contract.sol

Folgende Ausgabe wird erstellt:

ethsec@1435b241ca60:/share$ slither bad-contract.sol
INFO:Detectors:
Reentrancy in Victim.withdraw() (bad-contract.sol#11-16):
External calls:
- (success) = msg.sender.call.value(amount)() (bad-contract.sol#13)
State variables written after the call(s):
- balances[msg.sender] = 0 (bad-contract.sol#15)
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilities
INFO:Detectors:
Low level call in Victim.withdraw() (bad-contract.sol#11-16):
- (success) = msg.sender.call.value(amount)() (bad-contract.sol#13)
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#low-level-calls
INFO:Slither:bad-contract.sol analyzed (1 contracts with 46 detectors), 2 result(s) found
INFO:Slither:Use https://crytic.io/ to get access to additional detectors and GitHub integration
Alles anzeigen

Slither hat hier das Potenzial für eine Re-entrancy erkannt, identifiziert die Schlüsselzeilen, in denen das Problem auftreten könnte, und stellt einen Link für weitere Informationen zum Problem bereit:

Referenz: https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilities(opens in a new tab)

So werden Sie schnell über mögliche Probleme mit Ihrem Code informiert. Wie alle automatisierten Testwerkzeuge ist Slither nicht perfekt und es irrt sich manchmal, indem es zu viel rückmeldet. Es kann vor einer möglichen Re-entrancy warnen, selbst wenn keine ausbeutbare Schwachstelle vorhanden ist. Oft ist die Überprüfung des UNTERSCHIEDS in der Slither-Ausgabe zwischen Codeänderungen extrem aufschlussreich, denn es hilft bei der Entdeckung von Schwachstellen, die viel früher eingeführt wurden, anstatt zu warten, bis Ihr Projektcode vollständig ist.

Weiterführende Informationen

Leitfaden für Best Practices zur Sicherheit von Smart Contracts

Standard für die Sicherheitsüberprüfung von Smart Contracts (SCSVS)

Kennen Sie eine Community-Ressource, die Ihnen geholfen hat? Bearbeiten Sie diese Seite und fügen Sie sie hinzu.

War dieser Artikel hilfreich?