Guide de l'audit automatique des contrats intelligents. Partie 1: Se préparer pour un audit

Présentation


Notre entreprise est engagée dans l'audit de sécurité des contrats intelligents, et le problème de l'utilisation d'outils automatisés est très aigu. Dans quelle mesure peuvent-ils aider à identifier les endroits suspects, lesquels devraient être utilisés, ce qu'ils peuvent faire et quelles sont les spécificités du travail dans ce domaine? Ces problèmes et les problèmes connexes font l'objet de cet article. Et le matériel sera des tentatives de travailler avec de vrais contrats avec l'aide des représentants les plus intéressants et des recettes pour lancer ce logiciel extrêmement hétéroclite et extrêmement intéressant. Au début, je voulais faire un article, mais après un certain temps, la quantité d'informations est devenue trop importante, il a donc été décidé de faire une série d'articles, un pour chaque auto-analyseur. La liste à partir de laquelle nous prendrons les outils est présentée, par exemple, ici , mais si d'autres outils intéressants apparaissent lors de l'écriture, je les décrirai avec plaisir et les testerai.


Je dois dire que les tâches d'audit étaient extrêmement intéressantes, car Jusqu'à présent, les développeurs n'ont pas accordé beaucoup d'attention aux aspects économiques des algorithmes et à l'optimisation interne. Et l'audit des contrats intelligents a ajouté plusieurs vecteurs d'attaque intéressants qui doivent être pris en compte lors de la recherche d'erreurs. En outre, il s'est avéré que de nombreux outils de test automatique sont apparus: analyseurs statiques, analyseurs de bytecode, fuzzers, parsers et de nombreux autres bons logiciels.


Le but de l'article: promouvoir la distribution de code de contrat sécurisé et permettre aux développeurs de se débarrasser rapidement et facilement des bugs stupides, qui sont souvent les plus ennuyeux. Lorsque le protocole lui-même est complètement fiable et résout un problème grave, la présence d'une erreur stupide oubliée lors de la phase de test peut sérieusement ruiner la vie du projet. Par conséquent, apprenons à utiliser, au minimum, des outils qui permettent à «peu de sang» de se débarrasser de problèmes bien connus.


Pour l'avenir, je dois dire que les bogues critiques les plus courants que nous avons rencontrés lors des audits sont toujours des problèmes d'implémentation logiques et non des vulnérabilités typiques telles que les droits d'accès, le débordement d'entiers, la réentrance. Un audit complet et complet des solutions est impossible sans des développeurs expérimentés qui sont capables d'auditer la logique de haut niveau des contrats, leur cycle de vie, les aspects du fonctionnement réel et la conformité à la tâche, et pas seulement les modèles d'attaque typiques. C'est une logique de haut niveau qui devient souvent une source de bugs critiques.


Mais les avertissements, les trous typiques et les erreurs laissés par négligence qui ne devraient pas être manqués sont le destin des analyseurs automatiques, ils devraient être capables de faire face à ces tâches mieux que les gens. C'est cette thèse qui sera testée.


Caractéristiques de l'audit de code de contrat intelligent


L'audit intelligent des codes de contrat est un domaine assez spécifique. Malgré sa petite taille, le contrat intelligent Ethereum est un programme à part entière qui peut organiser des branches complexes, des boucles, des arbres de décision et même pour automatiser des transactions apparemment simples, il faut réfléchir à toutes les branches possibles à chaque étape. De ce point de vue, le développement de la blockchain est extrêmement bas niveau, très exigeant en ressources et fait très penser au développement de systèmes et de logiciels embarqués en C / C ++ et langages assembleur. C'est pourquoi nous aimons voir lors des entretiens les développeurs d'algorithmes de bas niveau, la pile réseau, les services très chargés, tous ceux qui s'occupaient d'optimisation de bas niveau et d'audit de code.


Du point de vue du développeur, Solidity est également assez spécifique, bien qu'il soit facile à lire par presque n'importe quel programmeur et dans les premières étapes et semble extrêmement simple. Le code Solidity est assez facile à lire, il est familier à tout développeur qui connaît la syntaxe C / C ++ et la POO, comme JavaScript.


Ici, la simplicité du code est la clé de la survie, rien de lourd ne fonctionne, donc tout l'arsenal de développement de bas niveau est utilisé dans le travail - algorithmes qui permettent une utilisation efficace des ressources, économiser de la mémoire: arbres Merkle, filtres Bloom, chargement des ressources «paresseux», boucles de déroulement, collecte manuelle des ordures et bien plus.
Une petite quantité de code source et le bytecode résultant.


Un contrat intelligent distinct est limité en volume de bytecode, chaque octet coûte une certaine quantité de gaz, et le maximum est limité par le haut, vous pouvez donc pousser environ 10 Ko dans la blockchain (pour le moment), cela ne fonctionnera plus. Voici un bon article sur le coût d'un contrat de déploiement et le coût du gaz . Par conséquent, beaucoup ne peut pas être poussé. Si vous exagérez, alors plusieurs milliers de lignes de code «moyen» est le maximum. Plusieurs dizaines de méthodes, le manque d'agrégation et la logique généralement complexe sont extrêmement caractéristiques des contrats. Tout ce qui ne convient pas vous oblige à sélectionner le code dans des bibliothèques distinctes, à modifier et à compliquer la procédure pour le mettre sur le réseau. Les développeurs Solidity peuvent être heureux de regrouper un tas de code dans un seul contrat, mais ils doivent simplement organiser correctement leurs systèmes de contrats en créant des bibliothèques de classes distinctes avec leur propre stockage. Et il est commode de décomposer ces "classes" séparées dans des fichiers séparés, et donc, lire le code des contrats est plutôt sympa, tout est bien structuré depuis le début - ça ne marchera pas autrement. À titre d'exemple, je recommande de regarder comment ERC721 est fabriqué en openzeppelin-solidity .


Gaz, gaz, gaz


Gas introduit une couche supplémentaire de logique dans l'exécution du code de contrat qui nécessite un audit. De plus, contrairement au code traditionnel, la même section de code peut dépenser différentes quantités de gaz. Un tableau des opcodes EVM et leur coût est utile pour comprendre les restrictions de gaz.


Pour démontrer pourquoi vous devez consacrer beaucoup de temps à l'évaluation du gaz, considérez ce morceau de pseudo-code (bien sûr, irréaliste; tirer dans la boucle avec de l'éther est une mauvaise idée):


//          function fixSomeAccountAction(uint _actionId) public onlyValidator { // … events[msg.sender].push(_actionId); } //   ,           function receivePaymentForSavedActions() { // ... for (uint256 i = 0; i < events[msg.sender].length; i++) { //  actionId   uint actionId = events[msg.sender][i]; //      action uint payment = getPriceByEventId(actionId); if (payment > 0) { paymentAccumulators[msg.sender] += payment; } emit LogEventPaymentForAction(msg.sender, actionId, payment); // … // delete “events[msg.sender][i]” from array } } 

le fait est que le cycle dans le contrat est exécuté des événements [msg.sender] .length fois, et chaque itération est une entrée dans la blockchain (transfer () et emit ()). Si la longueur du tableau est petite, alors le cycle remplit ses dix fois, en distribuant le paiement pour chaque action. Mais, si le tableau d'événements [msg.sender] est grand, il y aura de nombreuses itérations et le gaz épuisé atteindra la limite maximale de gaz codée en dur (~ 8 000 000). La transaction tombera et ne fonctionnera plus, car il n'y a aucun moyen de réduire la longueur du tableau d'événements [msg.sender] dans le contrat. Si le cycle ne calcule pas seulement une valeur unitaire, mais écrit dans la blockchain (par exemple, certains frais sont payés, paiements pour des actions), alors le nombre d'itérations autorisé est assez significativement limité. Jugez par vous-même - limite: 8 000 000, enregistrant une nouvelle valeur de 256 bits: 20 000. vous ne pouvez enregistrer ou mettre à jour des métadonnées que pour quelques centaines d'adresses 256 bits avec certaines métadonnées. Une autre partie amusante est l'écriture d'une nouvelle valeur: 20 000 et une mise à jour d'une valeur existante: 5 000, donc même avec le même environnement exact de votre contrat lorsque vous effectuez un transfert jetons vers une adresse qui a déjà des jetons, vous dépensez 4 fois moins de gaz (5 000 vs 20 000) sur un enregistrement.


Par conséquent, ne soyez pas surpris que la question du gaz dans les contrats intelligents soit si étroitement liée à la sécurité des contrats, car la situation où les fonds sont définitivement bloqués dans le contrat d'un point de vue pratique diffère peu de la situation où ils ont été volés. Le fait que l'instruction ADD coûte 3 gaz et SSTORE (économie de stockage): 20000 signifie que la ressource la plus chère dans la blockchain est le stockage, et les tâches d'optimisation du code de contrat ont beaucoup en commun avec les tâches de développement de bas niveau en C et ASM pour embarqué systèmes, où le stockage est également une ressource très limitée.


Belle blockchain


Il s'agit d'un paragraphe très positif sur la raison pour laquelle la blockchain est si bonne d'un point de vue de la sécurité uniquement pour l'auditeur. Le déterminisme de l'exécution du code du contrat est la clé d'un débogage et d'une lecture réussis des bogues et des vulnérabilités. Techniquement, tout appel à un code de contrat peut être reproduit sur n'importe quelle plate-forme avec un peu de précision, cela permet aux tests de fonctionner partout et d'être extrêmement faciles à prendre en charge, et l'enquête sur les incidents est fiable et indéniable. Maintenant, nous savons toujours qui, quand, quelle fonction a été appelée, avec quels paramètres, quel code l'a traitée et quel a été le résultat. Tout cela est complètement déterminé, c'est-à-dire joue n'importe où, même dans JS sur une page Web. Si nous parlons d'Ethereum, tout scénario de test est extrêmement facile à écrire en JavaScript pratique, y compris les paramètres de fuzzing, et fonctionne très bien partout où il y a Node.js.


Tous ces beaux mots, cependant, ne devraient pas relâcher les développeurs, car, comme mentionné ci-dessus, les erreurs les plus graves sont logiques, et pour eux le déterminisme de l'exécution est une propriété orthogonale.


L'environnement de montage du contrat


Pour écrire l'article, j'ai pris un ancien contrat expérimental pour réserver une maison du concepteur Smartz: https://github.com/smartzplatform/constructor-eth-booking . Le contrat vous permet de créer un enregistrement de l'objet (appartement ou chambre d'hôtel), de définir le prix et la date de livraison, après quoi le contrat attend le paiement et, s'il est reçu, fixe l'acte de réservation, en gardant les fonds sur le solde jusqu'à ce que le client entre dans la chambre et ne confirmera pas l'entrée. À ce stade, le propriétaire de la chambre reçoit le paiement. Le contrat est essentiellement une machine d'état, dont les états et les transitions peuvent être consultés dans Booking.sol. Nous l'avons fait assez rapidement, l'avons changé au cours du processus de développement et n'avons pas réussi à faire un grand nombre de tests, il est loin d'une nouvelle version du compilateur et d'une logique interne plus ou moins riche. Voyons donc comment les analyseurs y font face, quelles erreurs ils trouveront et, si nécessaire, nous ajoutons les nôtres.


Travailler avec différentes versions de solc


Différents analyseurs devront être utilisés de différentes manières - certains sont lancés à partir du docker, d'autres utilisent un bytecode compilé prêt à l'emploi, et l'auditeur lui-même doit également gérer non pas quelques-uns, mais des dizaines de premiers contrats avec différentes versions du compilateur. Par conséquent, vous devez être en mesure de « solc » différentes versions de solc différemment, à la fois dans le système hôte, à l'intérieur de l'image du docker et à l'intérieur de la truffe, donc je vais vous donner ces quelques options de hack sale:


1 façon: à l'intérieur de la truffe


Pour cela, aucune astuce n'est nécessaire, car à partir de la version 5.0.0 de truffe, vous pouvez spécifier la version du compilateur directement dans truffle.js, comme dans ce diff .


Truffle va maintenant télécharger le compilateur requis et l'exécuter. Un grand merci à l'équipe pour cela, Solidity est une langue jeune, les changements de langue sont sérieux et le passage d'une version à une autre n'est pas acceptable pour l'auditeur - de cette façon, vous pouvez introduire de nouvelles erreurs et masquer les anciennes.


Méthode 2: remplacement de / usr / bin / solc dans le conteneur Docker de l'analyseur
Si l'analyseur est distribué sous la forme d'un Dockerfile, vous pouvez le remplacer lors de l'assemblage d'une image Docker en ajoutant une ligne au Dockerfile qui obtient la version souhaitée solc directement de l'image, qui la tire du réseau et remplace / usr / bin / solc:


 COPY --from=ethereum/solc:0.4.19 /usr/bin/solc /usr/bin 

3 voies: remplacement de / usr / bin / solc


La façon la plus sale sur le front, s'il n'y a aucune sortie, vous pouvez vilainement remplacer le binaire / usr / bin / solc par un script comme celui-ci (n'oubliez pas de sauvegarder le fichier d'origine):


 #!/bin/bash # run Solidity compiler of given version, pass all parameters # you can run “SOLC_DOCKER_VERSION=0.4.20 solc --version” SOLC_DOCKER_VERSION="${SOLC_DOCKER_VERSION:-0.4.24}" docker run \ --entrypoint "" \ --tmpfs /tmp \ -v $(pwd):/project \ -v $(pwd)/node_modules:/project/node_modules \ -w /project \ ethereum/solc:$SOLC_DOCKER_VERSION \ /usr/bin/solc \ "$@" 

Il télécharge et met en cache l'image docker avec la version correcte de solc , va dans le répertoire courant et exécute /usr/bin/solc avec les paramètres passés. Ce n'est pas un très bon moyen, mais peut-être que pour certaines tâches, cela vous conviendra.


Code d'aplatissement


Voyons maintenant la source. Bien sûr, en théorie, les auto-analyseurs (en particulier pour l'analyse de source statique) devraient collecter un contrat, récupérer toutes les dépendances, tout combiner en un seul monolithe et l'analyser. Mais, comme je l'ai déjà dit, les changements de version en version peuvent être sérieux, et je suis constamment tombé sur la nécessité de déposer un répertoire supplémentaire dans le docker, de le configurer dans le chemin d'accès, et tout cela afin qu'il tire correctement les importations nécessaires. Certains analyseurs comprennent tout, le second n'est donc pas une option universelle, afin de ne pas souffrir du lancement de répertoires supplémentaires, il est plus pratique pour les analyseurs qui mangent un seul fichier de tout fusionner en un seul fichier et de l'analyser uniquement.


Pour ce faire, utilisez l' aplatisseur de truffes ordinaire .


Il s'agit d'un module npm standard, il est utilisé très simplement:


 truffle-flattener contracts/Booking.sol > contracts/flattened.sol 

: https://github.com/trailofbits/slither
Si vous avez besoin de personnaliser l'aplatissement, vous pouvez écrire votre propre aplatisseur, par exemple, avant d'utiliser l'option basée sur python: https://github.com/mixbytes/solidity-flattener


Commençons l'analyse.


Sur l'exemple du même vieil homme https://github.com/smartzplatform/constructor-eth-booking nous continuons l'analyse. Le contrat indique l'ancienne version du compilateur «0.4.20», et j'ai intentionnellement pris l'ancien contrat pour résoudre les problèmes avec le compilateur. La situation est aggravée par le fait qu'un auto-analyseur, par exemple, étudiant le bytecode, peut dépendre de cette version de solc, et ici les différences de versions peuvent affecter considérablement les résultats ou même tout casser. même si vous faites tout casher en utilisant les dernières versions, vous pouvez toujours exécuter un analyseur qui a été réglé sur la version précédente du compilateur.
Compilation et exécution de tests


Pour commencer, tirez simplement le projet du github et essayez de compiler.:


 git clone https://github.com/smartzplatform/constructor-eth-booking.git cd constructor-eth-booking npm install truffle compile 

Vous avez sûrement des problèmes avec la version du compilateur. De plus, les analyseurs automatiques ont également ces problèmes, utilisez donc n'importe quel moyen pour obtenir le compilateur 0.4.20 et construire le projet. Je viens d'enregistrer la version nécessaire du compilateur dans truffle.js et tout a été assemblé comme décrit ci-dessus.


Exécuter également


 truffle-flattener contracts/Booking.sol > contracts/flattened.sol 

comme indiqué dans le paragraphe sur l'aplatissement, ce sont les contracts/flattened.sol nous donnerons pour analyse à différents analyseurs
Conclusion de l'introduction


Maintenant, ayant solc et la possibilité d'utiliser solc une version arbitraire, vous pouvez commencer à analyser. Je vais omettre les problèmes avec l'exécution de la truffe et des tests, il y a beaucoup de documentation sur ce problème, triez-le vous-même. Bien sûr, les tests doivent s'exécuter et s'exécuter avec succès. De plus, afin de vérifier la logique, l'auditeur doit souvent ajouter ses propres tests, en vérifiant les endroits potentiellement fuyants, par exemple, en vérifiant la fonctionnalité du contrat aux limites des tableaux, en couvrant toutes les variables avec des tests, même celles strictement destinées au stockage de données, etc. Il existe de nombreuses recommandations, outre que ce n'est que le produit que notre entreprise fournit au marché, l'étude de la logique est donc une tâche purement humaine.


Nous irons vers des analyseurs qui sont intéressants de notre point de vue, essaierons d'y glisser notre contrat, et nous y introduirons artificiellement des vulnérabilités afin d'évaluer la façon dont les auto-analyseurs réagiront. Le prochain article sera consacré à l'analyseur Slither, et en général, le plan d'action est approximativement le suivant:


Partie 1. Introduction. Compilation, aplatissement, versions de Solidity (cet article)
Partie 2. Slither
Partie 3. Mythril
Partie 4. Manticore
Partie 5. Echidna
Partie 6. Outil inconnu 1
Partie 7. Outil inconnu 2


Cet ensemble d'analyseurs a été obtenu car il est important pour l'auditeur de pouvoir utiliser différents types d'analyses - statiques et dynamiques, et ils nécessitent des approches complètement différentes. Notre tâche consiste à apprendre à utiliser les outils de base dans chaque type d'analyse et à comprendre lequel utiliser quand.


Peut-être au cours d'une étude détaillée, de nouveaux candidats apparaîtront pour examen, ou l'ordre des articles changera, alors restez à l'écoute. Pour passer à la partie suivante, "cliquez ici"

Source: https://habr.com/ru/post/fr438336/


All Articles