Comment utiliser Slither pour trouver des bogues dans les contrats intelligents
Comment utiliser Slither
Le but de ce tutoriel est de montrer comment utiliser Slither pour trouver automatiquement des bogues dans les contrats intelligents.
- Installation
- Utilisation de la ligne de commande
- Introduction à l'analyse statique : brève introduction à l'analyse statique
- API : description de l'API Python
Installation
Slither requiert Python >= 3.6. Il peut être installé via pip ou en utilisant docker.
Slither via pip :
pip3 install --user slither-analyzerSlither via docker :
docker pull trailofbits/eth-security-toolboxdocker run -it -v "$PWD":/home/trufflecon trailofbits/eth-security-toolboxLa dernière commande exécute eth-security-toolbox dans un docker qui a accès à votre répertoire actuel. Vous pouvez modifier les fichiers depuis votre hôte, et exécuter les outils sur les fichiers depuis le docker
À l'intérieur de docker, exécutez :
solc-select 0.5.11cd /home/trufflecon/Exécuter un script
Pour exécuter un script python avec python 3 :
python3 script.pyLigne de commande
Ligne de commande ou scripts définis par l'utilisateur. Slither est livré avec un ensemble de détecteurs prédéfinis qui trouvent de nombreux bogues courants. Appeler Slither depuis la ligne de commande exécutera tous les détecteurs, aucune connaissance détaillée de l'analyse statique n'est requise :
slither project_pathsEn plus des détecteurs, Slither dispose de fonctionnalités de révision de code via ses printersopens in a new tab et ses toolsopens in a new tab.
Utilisez crytic.ioopens in a new tab pour accéder à des détecteurs privés et à l'intégration GitHub.
Analyse statique
Les capacités et la conception du framework d'analyse statique Slither ont été décrites dans des articles de blog (1opens in a new tab, 2opens in a new tab) et un article académiqueopens in a new tab.
L'analyse statique se présente sous différentes formes. Vous savez probablement que les compilateurs comme clangopens in a new tab et gccopens in a new tab s'appuient sur ces techniques de recherche, mais elles sous-tendent également (Inferopens in a new tab, CodeClimateopens in a new tab, FindBugsopens in a new tab et des outils basés sur des méthodes formelles comme Frama-Copens in a new tab et Polyspaceopens in a new tab).
Nous n'allons pas passer en revue de manière exhaustive les techniques d'analyse statique et les chercheurs 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 le code.
Représentation du code
Contrairement à une analyse dynamique, qui raisonne sur un seul chemin d'exécution, l'analyse statique raisonne sur tous les chemins à la fois. Pour ce faire, elle s'appuie sur une représentation différente du code. Les deux plus courantes sont l'arbre de syntaxe abstraite (AST) et le graphe de flot de contrôle (CFG).
Arbres de syntaxe abstraits (AST)
Les AST sont utilisés chaque fois que le compilateur analyse le code. C'est probablement la structure la plus élémentaire sur laquelle une analyse statique peut être effectuée.
En bref, un AST est un arbre structuré où, généralement, chaque feuille contient une variable ou une constante et les nœuds internes sont des opérandes ou des opérations de flux de contrôle. Considérons le code suivant :
1function safeAdd(uint a, uint b) pure internal returns(uint){2 if(a + b <= a){3 revert();4 }5 return a + b;6}L'AST correspondant est présenté ci-dessous :
Slither utilise l'AST exporté par solc.
Bien que simple à construire, l'AST est une structure imbriquée. Parfois, ce n'est pas la 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 courante consiste à utiliser le patron de conception visiteur, qui parcourt l'arbre de manière récursive. Slither contient un visiteur générique dans ExpressionVisitoropens in a new tab.
Le code suivant utilise ExpressionVisitor pour détecter si l'expression contient une addition :
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 est l'expression à tester14print(f'L\'expression {expression} contient une addition : {visitor.result()}')Afficher toutGraphe de flot de contrôle (CFG)
La deuxième représentation de code la plus courante est le graphe de flot de contrôle (CFG). Comme son nom l'indique, il s'agit d'une représentation basée sur un graphe qui expose tous les chemins d'exécution. Chaque nœud contient une ou plusieurs instructions. Les arêtes du graphe représentent les opérations de flux de contrôle (if/then/else, boucle, etc.). Le CFG de notre exemple précédent est :
Le CFG est la représentation sur laquelle la plupart des analyses sont construites.
Il existe de nombreuses autres représentations du code. Chaque représentation a ses avantages et ses inconvénients en fonction de l'analyse que vous souhaitez effectuer.
Analyse
Les types d'analyses les plus simples que vous pouvez effectuer avec Slither sont les analyses syntaxiques.
Analyse syntaxique
Slither peut naviguer à travers les différents composants du code et leur représentation pour trouver des incohérences et des défauts en utilisant une approche de type reconnaissance de formes.
Par exemple, les détecteurs suivants recherchent des problèmes liés à la syntaxe :
-
Masquage de variable d'étatopens in a new tab : itère sur toutes les variables d'état et vérifie si l'une d'entre elles masque une variable d'un contrat hérité (state.py#L51-L62opens in a new tab)
-
Interface ERC20 incorrecteopens in a new tab : recherche les signatures de fonction ERC20 incorrectes (incorrect_erc20_interface.py#L34-L55opens in a new tab)
Analyse sémantique
Contrairement à l'analyse syntaxique, une analyse sémantique va plus loin et analyse la « signification » du code. Cette famille comprend quelques grands types d'analyses. Elles mènent à des résultats plus puissants et utiles, mais sont aussi plus complexes à écrire.
Les analyses sémantiques sont utilisées pour les détections de vulnérabilités les plus avancées.
Analyse de dépendance des données
Une variable variable_a est dite dépendante des données de variable_b s'il existe un chemin pour lequel la valeur de variable_a est influencée par variable_b.
Dans le code suivant, variable_a dépend de variable_b :
1// ...2variable_a = variable_b + 1;Slither dispose de fonctionnalités intégrées de dépendance des donnéesopens in a new tab, grâce à sa représentation intermédiaire (abordée dans une section ultérieure).
Un exemple d'utilisation de la dépendance des données se trouve dans le détecteur d'égalité stricte dangereuseopens in a new tab. Ici, Slither recherchera une comparaison d'égalité stricte à une valeur dangereuse (incorrect_strict_equality.py#L86-L87opens in a new tab), et informera l'utilisateur qu'il doit utiliser >= ou <= plutôt que ==, pour empêcher un attaquant de piéger le contrat. Entre autres, le détecteur considérera comme dangereuse la valeur de retour d'un appel à balanceOf(address) (incorrect_strict_equality.py#L63-L64opens in a new tab), et utilisera le moteur de dépendance des données pour suivre son utilisation.
Calcul de point fixe
Si votre analyse parcourt le CFG et suit les arêtes, vous êtes susceptible de voir des nœuds déjà visités. Par exemple, si une boucle est présentée comme ci-dessous :
1for(uint i; i < range; ++){2 variable_a += 13}Votre analyse devra savoir quand s'arrêter. Il y a deux stratégies principales ici : (1) itérer sur chaque nœud un nombre fini de fois, (2) calculer un soi-disant point fixe. Un point fixe signifie essentiellement que l'analyse de ce nœud ne fournit plus aucune information significative.
Un exemple d'utilisation de point fixe se trouve dans les détecteurs de réentrance : Slither explore les nœuds et recherche les appels externes, l'écriture et la lecture dans le stockage. Une fois qu'il a atteint un point fixe (reentrancy.py#L125-L131opens 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.pyopens in a new tab, reentrancy_read_before_write.pyopens in a new tab, reentrancy_eth.pyopens in a new tab).
L'écriture d'analyses utilisant un calcul de point fixe efficace requiert une bonne compréhension de la manière dont l'analyse propage ses informations.
Représentation intermédiaire
Une représentation intermédiaire (RI) est un langage destiné à être plus adapté à l'analyse statique que le langage original. Slither traduit Solidity dans sa propre RI : SlithIRopens in a new tab.
Comprendre SlithIR n'est pas nécessaire si vous voulez seulement écrire des vérifications de base. Cependant, ce sera utile si vous prévoyez d'écrire des analyses sémantiques avancées. Les printers SlithIRopens in a new tab et SSAopens in a new tab vous aideront à comprendre comment le code est traduit.
Principes de base de l'API
Slither dispose d'une API qui vous permet d'explorer les attributs de base du contrat et de ses fonctions.
Pour charger une base de code :
1from slither import Slither2slither = Slither('/chemin/vers/le/projet')3Explorer les contrats et les fonctions
Un objet Slither possède :
contracts (list(Contract): liste de contratscontracts_derived (list(Contract): liste de 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 possède :
name (str): nom du contratfunctions (list(Function)): liste des fonctionsmodifiers (list(Modifier)): liste de modificateursall_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 modificateur à partir de sa signatureget_state_variable_from_name (str): renvoie une StateVariable à partir de son nom
Un objet Function ou Modifier possède :
name (str): nom de la fonctioncontract (contract): le contrat où la fonction est déclaréenodes (list(Node)): liste des nœuds composant le CFG de la fonction/du modificateurentry_point (Node): point d'entrée du CFGvariables_read (list(Variable)): liste des variables luesvariables_written (list(Variable)): liste des variables écritesstate_variables_read (list(StateVariable)): liste des variables d'état lues (sous-ensemble devariables_read)state_variables_written (list(StateVariable)): liste des variables d'état écrites (sous-ensemble devariables_written)
Dernière mise à jour de la page : 3 février 2025

