Weiter zum Hauptinhalt

So verwenden Sie Slither, um Bugs in Smart Contracts zu finden

solidity
smart contracts
security
testing
Fortgeschritten
Trailofbits
9. Juni 2020
7 Minuten Lesedauer

So verwenden Sie Slither

Das Ziel dieses Tutorials ist es zu zeigen, wie Sie Slither verwenden, um automatisch Fehler in Smart Contracts zu finden.

Installation

Slither erfordert Python >= 3.6. Die Installation kann über pip oder Docker erfolgen.

Slither über pip:

pip3 install --user slither-analyzer

Slither über Docker:

docker pull trailofbits/eth-security-toolbox
docker run -it -v "$PWD":/home/trufflecon trailofbits/eth-security-toolbox

Der letzte Befehl führt die eth-security-toolbox in einem Docker aus, der Zugriff auf dein aktuelles Verzeichnis hat. Du kannst die Dateien von deinem Host aus ändern und die Tools für die Dateien aus dem Docker ausführen

Führe im Docker aus:

solc-select 0.5.11
cd /home/trufflecon/

Ausführen eines Skripts

So führst du ein Python-Skript mit Python 3 aus:

python3 script.py

Befehlszeile

Befehlszeile versus benutzerdefinierte Skripte. Slither wird mit einer Reihe von vordefinierten Detektoren geliefert, die viele häufige Fehler finden. Wenn Sie Slither von der Befehlszeile aus aufrufen, werden alle Detektoren ausgeführt, ohne dass detaillierte Kenntnisse der statischen Analyse erforderlich sind:

slither project_paths

Zusätzlich zu den Detektoren verfügt Slither über Code-Review-Funktionen durch seine Printersopens in a new tab und Toolsopens in a new tab.

Verwenden Sie crytic.ioopens in a new tab, um Zugang zu privaten Detektoren und zur GitHub-Integration zu erhalten.

Statische Analyse

Die Fähigkeiten und das Design des statischen Analyse-Frameworks von Slither wurden in Blog-Beiträgen (1opens in a new tab, 2opens in a new tab) und einem wissenschaftlichen Artikelopens in a new tab beschrieben.

Statische Analyse gibt es in verschiedenen Varianten. Ihnen ist wahrscheinlich bewusst, dass Compiler wie clangopens in a new tab und gccopens in a new tab auf diesen Forschungstechniken basieren, aber sie bilden auch die Grundlage für (Inferopens in a new tab, CodeClimateopens in a new tab, FindBugsopens in a new tab und auf formalen Methoden basierende Werkzeuge wie Frama-Copens in a new tab und Polyspaceopens in a new tab).

Wir werden hier nicht erschöpfend auf statische Analysetechniken und Forscher eingehen. Stattdessen konzentrieren wir uns darauf, was nötig ist, um zu verstehen, wie Slither funktioniert, damit Sie es effektiver nutzen können, um Fehler zu finden und den Code zu verstehen.

Code-Darstellung

Im Gegensatz zu einer dynamischen Analyse, die einen einzelnen Ausführungspfad betrachtet, betrachtet die statische Analyse alle Pfade auf einmal. Dazu stützt es sich auf eine andere Code-Darstellung. Die beiden häufigsten sind der abstrakte Syntaxbaum (AST) und der Kontrollflussgraph (CFG).

Abstrakte Syntaxbäume (AST)

ASTs werden jedes Mal verwendet, wenn der Compiler Code parst. Es ist wahrscheinlich die grundlegendste Struktur, auf der statische Analysen durchgeführt werden können.

Kurz gesagt, ein AST ist ein strukturierter Baum, in dem normalerweise jedes Blatt eine Variable oder eine Konstante enthält und interne Knoten Operanden oder Kontrollflussoperationen sind. Betrachten Sie den folgenden Code:

1function safeAdd(uint a, uint b) pure internal returns(uint){
2 if(a + b <= a){
3 revert();
4 }
5 return a + b;
6}

Der entsprechende AST ist hier dargestellt:

AST

Slither verwendet den von solc exportierten AST.

Obwohl der AST einfach zu erstellen ist, handelt es sich um eine verschachtelte Struktur. Manchmal ist dies nicht am einfachsten zu analysieren. Um beispielsweise die vom Ausdruck a + b <= a verwendeten Operationen zu identifizieren, müssen Sie zuerst <= und dann + analysieren. Ein gängiger Ansatz ist die Verwendung des sogenannten Visitor-Patterns, das rekursiv durch den Baum navigiert. Slither enthält einen generischen Visitor in ExpressionVisitoropens in a new tab.

Der folgende Code verwendet ExpressionVisitor, um zu erkennen, ob der Ausdruck eine Addition enthält:

1from slither.visitors.expression.expression import ExpressionVisitor
2from slither.core.expressions.binary_operation import BinaryOperationType
3
4class HasAddition(ExpressionVisitor):
5
6 def result(self):
7 return self._result
8
9 def _post_binary_operation(self, expression):
10 if expression.type == BinaryOperationType.ADDITION:
11 self._result = True
12
13visitor = HasAddition(expression) # expression ist der zu testende Ausdruck
14print(f'Der Ausdruck {expression} enthält eine Addition: {visitor.result()}')
Alles anzeigen

Kontrollflussgraph (CFG)

Die zweithäufigste Code-Darstellung ist der Kontrollflussgraph (CFG). Wie der Name schon sagt, handelt es sich um eine graphbasierte Darstellung, die alle Ausführungspfade aufzeigt. Jeder Knoten enthält eine oder mehrere Anweisungen. Kanten im Graphen stellen die Kontrollflussoperationen dar (if/then/else, Schleife usw.). Der CFG unseres vorherigen Beispiels lautet:

CFG

Der CFG ist die Darstellung, auf der die meisten Analysen aufbauen.

Es gibt viele andere Code-Darstellungen. Jede Darstellung hat Vor- und Nachteile, je nachdem, welche Analyse Sie durchführen möchten.

Analyse

Die einfachste Art von Analysen, die Sie mit Slither durchführen können, sind syntaktische Analysen.

Syntaktische Analyse

Slither kann durch die verschiedenen Komponenten des Codes und deren Darstellung navigieren, um Inkonsistenzen und Fehler mit einem Ansatz zu finden, der dem Pattern-Matching ähnelt.

Die folgenden Detektoren suchen beispielsweise nach syntaxbezogenen Problemen:

Semantische Analyse

Im Gegensatz zur Syntaxanalyse geht eine semantische Analyse tiefer und analysiert die "Bedeutung" des Codes. Diese Familie umfasst einige allgemeine Arten von Analysen. Sie führen zu aussagekräftigeren und nützlicheren Ergebnissen, sind aber auch komplexer zu schreiben.

Semantische Analysen werden für die fortschrittlichsten Schwachstellenerkennungen verwendet.

Datenabhängigkeitsanalyse

Eine Variable variable_a gilt als datenabhängig von variable_b, wenn es einen Pfad gibt, auf dem der Wert von variable_a durch variable_b beeinflusst wird.

Im folgenden Code ist variable_a von variable_b abhängig:

1// ...
2variable_a = variable_b + 1;

Slither verfügt über integrierte Datenabhängigkeitsopens in a new tab-Fähigkeiten, dank seiner Zwischendarstellung (die in einem späteren Abschnitt besprochen wird).

Ein Beispiel für die Verwendung der Datenabhängigkeit findet sich im Detektor für gefährliche strikte Gleichheitopens in a new tab. Hier sucht Slither nach einem strikten Gleichheitsvergleich mit einem gefährlichen Wert (incorrect_strict_equality.py#L86-L87opens in a new tab) und informiert den Benutzer, dass er >= oder <= anstelle von == verwenden sollte, um zu verhindern, dass ein Angreifer den Vertrag in eine Falle lockt. Unter anderem wird der Detektor den Rückgabewert eines Aufrufs von balanceOf(address) (incorrect_strict_equality.py#L63-L64opens in a new tab) als gefährlich einstufen und die Datenabhängigkeits-Engine verwenden, um seine Verwendung zu verfolgen.

Fixpunktberechnung

Wenn Ihre Analyse durch den CFG navigiert und den Kanten folgt, werden Sie wahrscheinlich bereits besuchte Knoten sehen. Wenn zum Beispiel eine Schleife wie unten dargestellt ist:

1for(uint i; i < range; ++){
2 variable_a += 1
3}

Ihre Analyse muss wissen, wann sie anhalten muss. Hier gibt es zwei Hauptstrategien: (1) jeden Knoten eine endliche Anzahl von Malen durchlaufen, (2) einen sogenannten Fixpunkt berechnen. Ein Fixpunkt bedeutet im Grunde, dass die Analyse dieses Knotens keine aussagekräftigen Informationen mehr liefert.

Ein Beispiel für die Verwendung eines Fixpunkts findet sich in den Reentrancy-Detektoren: Slither untersucht die Knoten und sucht nach externen Aufrufen sowie Schreib- und Lesezugriffen auf den Speicher. Sobald ein Fixpunkt erreicht ist (reentrancy.py#L125-L131opens in a new tab), wird die Untersuchung gestoppt und die Ergebnisse werden anhand verschiedener Reentrancy-Muster (reentrancy_benign.pyopens in a new tab, reentrancy_read_before_write.pyopens in a new tab, reentrancy_eth.pyopens in a new tab) analysiert, um festzustellen, ob eine Reentrancy vorliegt.

Das Schreiben von Analysen mit effizienter Fixpunktberechnung erfordert ein gutes Verständnis dafür, wie die Analyse ihre Informationen propagiert.

Zwischendarstellung

Eine Zwischendarstellung (Intermediate Representation, IR) ist eine Sprache, die sich besser für die statische Analyse eignen soll als die ursprüngliche. Slither übersetzt Solidity in seine eigene IR: SlithIRopens in a new tab.

Das Verständnis von SlithIR ist nicht notwendig, wenn Sie nur einfache Prüfungen schreiben wollen. Es wird sich jedoch als nützlich erweisen, wenn Sie vorhaben, fortgeschrittene semantische Analysen zu schreiben. Die SlithIRopens in a new tab- und SSAopens in a new tab-Printers helfen Ihnen zu verstehen, wie der Code übersetzt wird.

API-Grundlagen

Slither hat eine API, mit der Sie die grundlegenden Attribute des Vertrags und seiner Funktionen untersuchen können.

So laden Sie eine Codebasis:

1from slither import Slither
2slither = Slither('/path/to/project')
3

Untersuchen von Verträgen und Funktionen

Ein Slither-Objekt hat:

  • contracts (list(Contract): Liste von Verträgen
  • contracts_derived (list(Contract): Liste von Verträgen, die nicht von einem anderen Vertrag geerbt werden (Teilmenge von contracts)
  • get_contract_from_name (str): Gibt einen Vertrag anhand seines Namens zurück

Ein Contract-Objekt hat:

  • name (str): Name des Vertrags
  • functions (list(Function)): Liste von Funktionen
  • modifiers (list(Modifier)): Liste von Modifikatoren
  • all_functions_called (list(Function/Modifier)): Liste aller internen Funktionen, die vom Vertrag aus erreichbar sind
  • inheritance (list(Contract)): Liste der geerbten Verträge
  • get_function_from_signature (str): Gibt eine Funktion anhand ihrer Signatur zurück
  • get_modifier_from_signature (str): Gibt einen Modifikator anhand seiner Signatur zurück
  • get_state_variable_from_name (str): Gibt eine Zustandsvariable anhand ihres Namens zurück

Ein Function- oder ein Modifier-Objekt hat:

  • name (str): Name der Funktion
  • contract (contract): der Vertrag, in dem die Funktion deklariert ist
  • nodes (list(Node)): Liste der Nodes, aus denen der CFG der Funktion/des Modifikators besteht
  • entry_point (Node): Einstiegspunkt des CFG
  • variables_read (list(Variable)): Liste der gelesenen Variablen
  • variables_written (list(Variable)): Liste der geschriebenen Variablen
  • state_variables_read (list(StateVariable)): Liste der gelesenen Zustandsvariablen (Teilmenge von variables_read)
  • state_variables_written (list(StateVariable)): Liste der geschriebenen Zustandsvariablen (Teilmenge von variables_written)

Seite zuletzt aktualisiert: 16. Februar 2026

War dieses Tutorial hilfreich?