Comment utiliser Slither pour trouver des bugs de contrat intelligent
Comment utiliser Slither
Le but de ce tutoriel est de démontrer comment utiliser Slither pour trouver automatiquement des bugs dans les contrats intelligents.
- Installation
- Utilisation des lignes de commande
- Introduction à l'analyse statique: Brève introduction à l'analyse statique
- API : Description de l'API Python
Installation
Slither nécessite Python >= 3.6. Il peut être installé via pip ou avec docker.
Slither via pip :
pip3 install --user slither-analyzer
Slither via docker :
docker pull trailofbits/eth-security-toolboxdocker run -it -v "$PWD":/home/trufflecon trailofbits/eth-security-toolbox
La dernière commande exécute eth-security-toolbox dans un docker qui a accès à votre répertoire courant. Vous pouvez changer les fichiers depuis votre hôte et exécuter les outils sur les fichiers depuis le docker
Dans docker, lancez :
solc-select 0.5.11cd /home/trufflecon/
Exécuter un script
Pour exécuter un script python avec python 3 :
python3 script.py
Ligne de commande
Ligne de commande contre scripts définis par l'utilisateur. Slither est livré avec un ensemble de détecteurs prédéfinis qui trouvent beaucoup de bogues communs. Faire appel à Slither à partir de la ligne de commande exécutera tous les détecteurs, aucune connaissance détaillée de l'analyse statique requise :
slither project_paths
En plus des détecteurs, Slither dispose de capacités de révision de code grâce à ses printers(opens in a new tab) et outils(opens in a new tab).
Utilisez crytic.io(opens in a new tab) pour avoir accès à des détecteurs privés et à l'intégration GitHub.
Analyse statique
Les capacités et la conception du cadre d'analyse statique Slither ont été décrites dans les articles de blog (1(opens in a new tab), 2(opens in a new tab)) et un document académique(opens in a new tab).
L'analyse statique existe dans différentes saveurs. Vous réalisez très probablement que les compilateurs comme clang(opens in a new tab) et gcc(opens in a new tab) dépendent de ces techniques de recherche, mais il soutient aussi (Infer(opens in a new tab), CodeClimate(opens in a new tab), FindBugs(opens in a new tab) et les outils basés sur des méthodes formelles telles que Frama-C(opens in a new tab) et Polyspace(opens in a new tab).
Nous ne passerons pas en revue de façon exhaustive les techniques d'analyse statique et le chercheur ici. Au lieu de cela, nous nous concentrerons sur ce qui est nécessaire pour comprendre le fonctionnement de Slither afin que vous puissiez l'utiliser plus efficacement pour trouver des bogues et comprendre du code.
Représentation du code
Contrairement à une analyse dynamique, qui explique les raisons d'un seul chemin d'exécution, l'analyse statique explique tous les chemins en même temps. Pour ce faire, il repose sur une représentation du code différente. Les deux plus courants sont l'arborescence de syntaxe abstraite (AST) et le graphique de flux de contrôle (CFG).
Arbres syntaxiques abstraits (AST)
AST est utilisé chaque fois que le compilateur analyse le code. C'est probablement la structure la plus basique sur laquelle une analyse statique peut être effectuée.
En un mot, un AST est un arbre structuré où, habituellement, chaque feuille contient une variable ou une constante, et les nœuds internes sont des opérandes ou des opérations de contrôle de flux. Considérez les codes suivants :
1function safeAdd(uint a, uint b) pure internal returns(uint){2 if(a + b <= a){3 revert();4 }5 return a + b;6}Copier
L'AST correspondant est affiché dans :
Slither utilise l'AST exporté par solc.
Bien que simple à construire, l'AST est une structure imbriquée. Parfois, ce n'est pas le plus simple à analyser. Par exemple, pour identifier les opérations utilisées par l'expression a + b <= a
, vous devez d'abord analyser <=
puis +
. Une approche commune est d'utiliser le design pattern « visiteur » pour naviguer dans l'arbre récursivement. Slither contient un visiteur générique dans ExpressionVisitor
(opens in a new tab).
Le code suivant utilise ExpressionVisitor
pour détecter si l'expression contient un ajout :
1from slither.visitors.expression.expression import ExpressionVisitor2from slither.core.expressions.binary_operation import BinaryOperationType34class HasAddition(ExpressionVisitor):56 def result(self):7 return self._result89 def _post_binary_operation(self, expression):10 if expression.type == BinaryOperationType.ADDITION:11 self._result = True1213visitor = HasAddition(expression) # expression correspond a l'expression qui sera testé14print(f'L’expression {expression} contient une addition: {visitor.result()}')Afficher toutCopier
Graphe de contrôle du flux (CFG)
La deuxième représentation de code la plus courante est le graphique de flux de contrôle (CFG). Comme son nom l'indique, il s'agit d'une représentation graphique qui expose tous les chemins d'exécution. Chaque nœud contient une ou plusieurs instructions. Les bords dans le graphique représentent les opérations de contrôle du flux (if/then/else, loop, etc.). Le CFG de notre exemple précédent est :
Le CFG est la représentation sur laquelle la plupart des analyses sont construites.
De nombreuses autres représentations de code existent. Chaque représentation a des avantages et des inconvénients en fonction de l'analyse que vous voulez effectuer.
Analyse
Les analyses les plus simples que vous pouvez effectuer avec Slither sont des analyses syntaxiques.
Analyse syntaxique
Slither peut naviguer à travers les différents composants du code et de leur représentation pour trouver des incohérences et des défauts en utilisant une approche similaire à la recherche de modèles.
Par exemple, les détecteurs suivants recherchent les problèmes liés à la syntaxe :
State variavle shadowing>(opens in a new tab) : itère sur toutes les variables d'état et vérifie si une variable est masquée à partir d'un contrat hérité (état. y#L51-L62(opens in a new tab))
Interface ERC20 incorrecte(opens in a new tab) : recherche des signatures de fonction ERC20 incorrectes (incorrect_erc20_interface.py#L34-L55(opens in a new tab))
Analyse sémantique
Contrairement à l'analyse syntaxique, une analyse sémantique ira plus loin et analysera le « sens » du code. Cette famille comprend quelques grands types d'analyse. Ils conduisent à des résultats plus puissants et utiles, mais sont également plus complexes à rédiger.
Les analyses sémantiques sont utilisées pour les détections de vulnérabilité les plus avancées.
Analyse des dépendances des données
Une variable variable_a
est dite dépendante des données de variable_b
s'il y a un chemin pour lequel la valeur de variable_a
est influencée par variable_b
.
Dans le code suivant, variable_a
est dépendant de variable_b
:
1// ...2variable_a = variable_b + 1;Copier
Slither est livré avec des capacités intégrées grâce à sa représentation intermédiaire (discutée dans une section ultérieure).
Un exemple d'utilisation de la dépendance des données peut être trouvé dans le dangereux détecteur strict d'égalité(opens in a new tab). Ici, Slither recherchera une comparaison stricte de l'égalité avec une valeur dangereuse (incorrect_strict_equality. y#L86-L87(opens in a new tab)), et informera l'utilisateur qu'il doit utiliser >=
ou <=
au lieu de ==
, pour empêcher un attaquant de piéger le contrat. Entre autres, le détecteur considérera comme dangereux la valeur de retour d'un appel à balanceOf(address)
(incorrect_strict_equality. y#L63-L64(opens in a new tab)), et utilisera le moteur de dépendance de données pour suivre son utilisation.
Calcul à decimales fixes
Si votre analyse navigue à travers le CFG et suit les bords, vous êtes susceptible de voir des nœuds déjà visités. Par exemple, si une boucle est présentée tel qu'illustré ci-dessous :
1for(uint i; i < range; ++){2 variable_a += 13}Copier
Votre analyse devra savoir quand s'arrêter. Il y a deux stratégies principales ici : (1) itérer sur chaque noeud un nombre limité de fois, (2) calculer un fixpoint. Un point fixe signifie que l'analyse de ce noeud ne fournit aucune information significative.
Un exemple de point fixe utilisé peut être trouvé dans les détecteurs de rétractation : Slither explore les nœuds et il est possible de chercher des appels externes, écrire et lire sur le stockage. Une fois qu'il a atteint un point fixe (réentrance. y#L125-L131(opens in a new tab)), il arrête l'exploration, et analyse les résultats pour voir si une réentrance est présente, à travers différents modèles de réentrance (reentrancy_benign. y(opens in a new tab), reentrancy_read_before_write.py(opens in a new tab), reentrancy_eth.py(opens in a new tab)).
Écrire des analyses à l'aide de calculs fixes efficaces nécessite une bonne compréhension de la manière dont l'analyse propage ses informations.
Représentation Intermédiaire
Une représentation intermédiaire (IR) est un langage destiné à être plus apte à une analyse statique que le langage original. Slither traduit Solidity à son propre IR : SlithIR(opens in a new tab).
Comprendre SlithIR n'est pas nécessaire si vous voulez seulement écrire des vérifications de base. Cependant, il sera pratique si vous prévoyez d'écrire des analyses sémantiques avancées. Les imprimantes SlithIR(opens in a new tab) et SSA(opens in a new tab) vous aideront à comprendre comment le code est traduit.
Les bases de l’API
Slither a une API qui vous permet d'explorer les attributs de base du contrat et de ses fonctions.
Pour charger le code base :
1from slither import Slither2slither = Slither('/chemin/vers/projet')3Copier
Exploration des contrats et fonctions
Un objet Slither
a :
contrats (list(Contrat)
: liste des contratscontracts_derived (list(Contrat)
: liste des contrats qui ne sont pas hérités par un autre contrat (sous-ensemble de contrats)get_contract_from_name (str)
: Renvoie un contrat à partir de son nom
Un objet Contract
a :
name(str)
: Nom du contratfonctions(list(fonction))
: Liste des fonctionsmodifiers(list(Modifier))
: Liste des fonctionsall_functions_called(list(Function/Modifier))
: Liste de toutes les fonctions internes accessibles par le contratinheritance(list(Contract))
: Liste des contrats héritésget_function_from_signature(str)
: Renvoie une fonction à partir de sa signatureget_modifier_from_signature(str)
: Renvoie un Modifier à partir de sa signatureget_state_variable_from_name(str)
: Renvoie une StateVariable à partir de son nom
Un objet Function
ou un Modifier
a :
name(str)
: Nom de la fonctioncontract (contract)
: le contrat où la fonction est déclaréenodes(list(Node))
: Liste des noeuds composant la fonction/modifier de CFGentry_point(Node)
: Point d’entrée du CFGvariables_read(list(Variable))
: Liste des variables de lecturevariables_written(list(Variable))
: Liste des variable d’écrituresstate_variables_read(list(StateVariable))
: Liste des variables d’états de lectures (sous-ensemble de lecture des variables)state_variables_read(list(StateVariable))
: Liste des variables d’états de lectures (sous-ensemble de lecture des variables)
Dernière modification: @nhsz(opens in a new tab), 15 août 2023