Crashing RPC : analyse d'un nouveau type de vulnérabilité dans les nœuds RPC Blockchain sécurisés en mémoire

L'équipe Skyfall de CertiK a récemment découvert plusieurs vulnérabilités dans les nœuds RPC basés sur Rust dans plusieurs chaînes de blocs, notamment Aptos, StarCoin et Sui. Étant donné que les nœuds RPC sont des composants d'infrastructure critiques reliant les dApps et la blockchain sous-jacente, leur robustesse est essentielle pour un fonctionnement transparent. Les concepteurs de blockchain connaissent l'importance de services RPC stables, ils adoptent donc des langages sécurisés en mémoire tels que Rust pour éviter les vulnérabilités courantes qui peuvent détruire les nœuds RPC.

L'adoption d'un langage sécurisé pour la mémoire tel que Rust aide les nœuds RPC à éviter de nombreuses attaques basées sur les vulnérabilités de corruption de la mémoire. Cependant, grâce à un audit récent, nous avons constaté que même les implémentations Rust sécurisées pour la mémoire, si elles ne sont pas soigneusement conçues et vérifiées, peuvent être vulnérables à certaines menaces de sécurité susceptibles de perturber la disponibilité des services RPC.

Dans cet article, nous présenterons notre découverte d'une série de vulnérabilités à travers des cas pratiques.

** Rôle de nœud RPC Blockchain **

Le service d'appel de procédure à distance (RPC) de la blockchain est le composant d'infrastructure de base de la blockchain de couche 1. Il fournit aux utilisateurs un important front-end API et agit comme une passerelle vers le réseau back-end blockchain. Cependant, le service RPC blockchain est différent du service RPC traditionnel en ce sens qu'il facilite l'interaction de l'utilisateur sans authentification. La disponibilité continue du service est essentielle et toute interruption du service peut avoir un impact important sur la disponibilité de la blockchain sous-jacente.

Perspective d'audit : serveur RPC traditionnel VS serveur RPC blockchain

L'audit des serveurs RPC traditionnels se concentre principalement sur la vérification des entrées, l'autorisation/l'authentification, la falsification de requête intersite/falsification de requête côté serveur (CSRF/SSRF), les vulnérabilités d'injection (telles que l'injection SQL, l'injection de commande) et la fuite d'informations.

Cependant, la situation est différente pour les serveurs RPC blockchain. Tant que la transaction est signée, il n'est pas nécessaire d'authentifier le client demandeur au niveau de la couche RPC. En tant que frontal de la blockchain, l'un des principaux objectifs du service RPC est de garantir sa disponibilité. En cas d'échec, les utilisateurs ne peuvent pas interagir avec la blockchain, ce qui les empêche d'interroger les données en chaîne, de soumettre des transactions ou d'émettre des fonctions contractuelles.

Par conséquent, l'aspect le plus vulnérable d'un serveur RPC blockchain est la "disponibilité". Si le serveur tombe en panne, les utilisateurs perdent la possibilité d'interagir avec la blockchain. Ce qui est plus grave, c'est que certaines attaques vont se propager sur la chaîne, affecter un grand nombre de nœuds, voire conduire à la paralysie de l'ensemble du réseau.

Pourquoi la nouvelle blockchain utilisera le RPC sécurisé en mémoire

Certaines chaînes de blocs de couche 1 bien connues, telles qu'Aptos et Sui, utilisent le langage de programmation sécurisé Rust pour implémenter leurs services RPC. Grâce à sa sécurité renforcée et à ses contrôles stricts au moment de la compilation, Rust rend les programmes pratiquement immunisés contre les vulnérabilités de corruption de mémoire, telles que les débordements de pile et les vulnérabilités de déréférencement de pointeur nul et de reréférencement après libération.

Pour sécuriser davantage la base de code, les développeurs suivent strictement les meilleures pratiques, telles que ne pas introduire de code non sécurisé. Utilisez #![forbid(unsafe_code)] dans le code source pour vous assurer que le code non sécurisé est bloqué et filtré.

Exemples de développeurs de blockchain mettant en œuvre des pratiques de programmation Rust

Pour éviter le débordement d'entiers, les développeurs utilisent généralement des fonctions telles que check_add, check_sub, saturating_add, saturating_sub, etc. au lieu de simples additions et soustractions (+, -). Atténuez l'épuisement des ressources en définissant des délais d'expiration, des limites de taille de demande et des limites d'éléments de demande appropriés.

** Menaces RPC pour la sécurité de la mémoire dans la blockchain de couche 1 **

Bien qu'ils ne soient pas vulnérables à l'insécurité de la mémoire au sens traditionnel, les nœuds RPC sont exposés à des entrées facilement manipulables par des attaquants. Dans une implémentation RPC sécurisée en mémoire, plusieurs situations peuvent conduire à un déni de service. Par exemple, l'amplification de la mémoire peut épuiser la mémoire du service, tandis que les problèmes de logique peuvent introduire des boucles infinies. De plus, les conditions de concurrence peuvent constituer une menace dans laquelle des opérations simultanées peuvent avoir une séquence d'événements inattendue, laissant le système dans un état indéfini. De plus, des dépendances mal gérées et des bibliothèques tierces peuvent introduire des vulnérabilités inconnues dans le système.

Dans cet article, notre objectif est d'attirer l'attention sur des moyens plus immédiats de déclencher les protections d'exécution de Rust, provoquant l'arrêt des services.

Explicit Rust Panic : un moyen de mettre fin directement aux services RPC

Les développeurs peuvent introduire un code de panique explicite, intentionnellement ou non. Ces codes sont principalement utilisés pour gérer des conditions inattendues ou exceptionnelles. Voici quelques exemples courants :

assert!() : utilisez cette macro lorsqu'une condition doit être remplie. Si la condition affirmée échoue, le programme paniquera, indiquant qu'il y a une grave erreur dans le code.

panique!() : cette fonction est appelée lorsque le programme rencontre une erreur dont il ne peut pas se remettre et ne peut pas continuer.

inaccessible!() : utilisez cette macro lorsqu'un morceau de code ne doit pas être exécuté. Si cette macro est invoquée, cela indique une erreur logique grave.

unimplemented!() et todo!() : ces macros sont des espaces réservés pour les fonctionnalités non implémentées. Si cette valeur est atteinte, le programme plantera.

unwrap() : Cette méthode est utilisée pour les types Option ou Result. Lorsqu'une variable Err ou None est rencontrée, le programme plante.

Vulnérabilité 1 : Déclenchez l'assertion dans Move Verifier !

La blockchain Aptos utilise le vérificateur Move bytecode pour effectuer une analyse de sécurité de référence via une interprétation abstraite du bytecode. La fonction ute() fait partie de l'implémentation du trait TransferFunctions et simule l'exécution d'instructions bytecode dans des blocs de base.

La tâche de la fonction ute_inner() est d'interpréter l'instruction de bytecode actuelle et de mettre à jour l'état en conséquence. Si nous avons exécuté jusqu'à la dernière instruction du bloc de base, comme indiqué par index == last_index, la fonction appellera assert!(self.stack.is_empty()) pour s'assurer que la pile est vide. L'intention derrière ce comportement est de garantir que toutes les opérations sont équilibrées, ce qui signifie également que chaque poussée a un pop correspondant.

Dans le flux normal d'exécution, la pile est toujours équilibrée pendant l'interprétation abstraite. Ceci est garanti par le Stack Balance Checker, qui vérifie le bytecode avant de l'interpréter. Cependant, une fois que nous élargissons notre perspective au domaine des interpréteurs abstraits, nous voyons que l'hypothèse d'équilibre de la pile n'est pas toujours valide.

Correctif pour la vulnérabilité analyze_function dans AbstractInterpreter

À la base, un interpréteur abstrait émule le bytecode au niveau du bloc de base. Dans son implémentation d'origine, rencontrer une erreur pendant ute_block incite le processus d'analyse à consigner l'erreur et à poursuivre l'exécution jusqu'au bloc suivant dans le graphe de flux de contrôle. Cela peut créer une situation dans laquelle une erreur dans un bloc d'exécution peut entraîner un déséquilibre de la pile. Si l'exécution continue dans ce cas, une vérification assert! sera effectuée si la pile n'est pas vide, provoquant une panique.

Cela donne aux attaquants une opportunité d'exploiter. Un attaquant peut déclencher une erreur en concevant un bytecode spécifique dans ute_block(), puis ute() peut exécuter une assertion si la pile n'est pas vide, provoquant l'échec de la vérification de l'assertion. Cela aggravera la panique et mettra fin au service RPC, affectant sa disponibilité.

Pour éviter cela, le correctif mis en œuvre garantit que l'ensemble du processus d'analyse est arrêté lorsque la fonction ute_block rencontre pour la première fois une erreur, évitant ainsi le risque de plantages ultérieurs pouvant survenir lors de la poursuite de l'analyse en raison d'un déséquilibre de la pile dû à des erreurs. Cette modification supprime les conditions susceptibles de provoquer des paniques et contribue à améliorer la robustesse et la sécurité de l'interpréteur abstrait.

** Vulnérabilité 2 : déclenchez la panique dans StarCoin ! **

La blockchain Starcoin a son propre fork de l'implémentation Move. Dans ce référentiel Move, il y a une panique dans le constructeur du type Struct ! Si la StructDefinition fournie contient des informations de champ Native, la panique sera explicitement déclenchée ! .

Paniques explicites pour les structures initialisées dans les routines de normalisation

Ce risque potentiel existe dans le processus de redistribution des modules. Si le module publié existe déjà dans le magasin de données, la normalisation du module est requise à la fois pour le module existant et pour le module d'entrée contrôlé par l'attaquant. Au cours de ce processus, la fonction "normalized::Module::new" construit la structure du module à partir des modules d'entrée contrôlés par l'attaquant, déclenchant une "panique !".

Prérequis pour la routine de normalisation

Cette panique peut être déclenchée en soumettant une charge utile spécialement conçue par le client. Par conséquent, des acteurs malveillants peuvent perturber la disponibilité des services RPC.

Patch de panique d'initialisation de structure

Le patch Starcoin introduit un nouveau comportement pour gérer le cas natif. Maintenant, plutôt que de paniquer, il renvoie un ec vide. Cela réduit la possibilité que les utilisateurs soumettent des données provoquant des paniques.

** Implicit Rust Panic : un moyen facilement négligé de mettre fin aux services RPC **

Les paniques explicites sont facilement identifiables dans le code source, tandis que les paniques implicites sont plus susceptibles d'être ignorées par les développeurs. Des paniques implicites se produisent généralement lors de l'utilisation d'API fournies par des bibliothèques standard ou tierces. Les développeurs doivent lire et comprendre attentivement la documentation de l'API, sinon leurs programmes Rust peuvent s'arrêter de manière inattendue.

Panique implicite dans BTreeMap

Prenons l'exemple de BTreeMap de Rust STD. BTreeMap est une structure de données couramment utilisée qui organise les paires clé-valeur dans un arbre binaire trié. BTreeMap propose deux méthodes pour récupérer les valeurs par clé : get(&self, key : &Q) et index(&self, key : &Q).

La méthode get(&self, key: &Q) récupère la valeur à l'aide de la clé et renvoie une Option. L'option peut être Some(&V), si la clé existe, renvoie la référence de la valeur, si la clé n'est pas trouvée dans le BTreeMap, renvoie None.

En revanche, index(&self, clé : &Q) renvoie directement une référence à la valeur correspondant à la clé. Cependant, il comporte un gros risque : il déclenchera une panique implicite si la clé n'existe pas dans le BTreeMap. S'il n'est pas géré correctement, le programme peut se bloquer de manière inattendue, ce qui en fait une vulnérabilité potentielle.

En fait, la méthode index(&self, key: &Q) est l'implémentation sous-jacente du trait std::ops::Index. Ce trait est une opération d'index dans un contexte immuable (c'est-à-dire conteneur [index] ) fournit un sucre syntaxique pratique. Les développeurs peuvent directement utiliser btree_map [key] , appelez la méthode index(&self, key: &Q). Cependant, ils peuvent ignorer le fait que cette utilisation peut paniquer si la clé n'est pas trouvée, posant ainsi une menace implicite à la stabilité du programme.

Vulnérabilité 3 : déclenche une panique implicite dans Sui RPC

La routine de publication du module Sui permet aux utilisateurs de soumettre des charges utiles de module via RPC. Le gestionnaire RPC utilise la fonction SuiCommand::Publish pour désassembler directement le module reçu avant de transmettre la demande au réseau de vérification principal pour la vérification du bytecode.

Lors de ce désassemblage, la section code_unit du module soumis est utilisée pour construire un VMControlFlowGraph. Le processus de construction consiste à créer des blocs de base, qui sont stockés dans un BTreeMap nommé "'blocks'". Le processus comprend la création et la manipulation de la carte, où une panique implicite est déclenchée sous certaines conditions.

Voici un code simplifié :

Panique implicite lors de la création de VMControlFlowGraph

Dans ce code, un nouveau VMControlFlowGraph est créé en parcourant le code et en créant un nouveau bloc de base pour chaque unité de code. Les blocs de base sont stockés dans un bloc nommé BTreeMap.

La carte de blocs est indexée à l'aide de block[&block] dans une boucle qui itère sur la pile, qui a été initialisée avec ENTRY_BLOCK_ID. L'hypothèse ici est qu'il y a au moins un ENTRY_BLOCK_ID dans la carte de blocs.

Cependant, cette hypothèse ne tient pas toujours. Par exemple, si le code validé est vide, la "carte de bloc" sera toujours vide après le processus de "créer un bloc de base". Lorsque le code tente ultérieurement de parcourir la carte de blocs en utilisant for succ in &blocks[&block].successors , une panique implicite peut être déclenchée si la clé n'est pas trouvée. En effet, l'expression blocks[&block] est essentiellement un appel à la méthode index(), qui, comme mentionné précédemment, paniquera si la clé n'existe pas dans le BTreeMap.

Un attaquant disposant d'un accès à distance pourrait exploiter la vulnérabilité de cette fonction en soumettant une charge utile de module mal formée avec un champ code_unit vide. Cette simple requête RPC bloque l'ensemble du processus JSON-RPC. Si un attaquant continue d'envoyer de telles charges utiles malformées avec un minimum d'effort, cela entraînera une interruption prolongée du service. Dans un réseau blockchain, cela signifie que le réseau peut ne pas être en mesure de confirmer de nouvelles transactions, ce qui entraîne une situation de déni de service (DoS). La fonctionnalité du réseau et la confiance des utilisateurs dans le système seront gravement affectées.

Le correctif de Sui : supprimer le désassemblage de la routine de problème RPC

Il convient de noter que le CodeUnitVerifier dans le Move Bytecode Verifier est responsable de s'assurer que la section code_unit n'est jamais vide. Cependant, l'ordre des opérations expose les gestionnaires RPC à des vulnérabilités potentielles. En effet, le processus de validation a lieu sur le nœud Validator, qui est une étape après que le RPC a traité les modules d'entrée.

En réponse à ce problème, Sui a résolu la vulnérabilité en supprimant la fonction de désassemblage dans la routine RPC de publication du module. Il s'agit d'un moyen efficace d'empêcher les services RPC de traiter un bytecode potentiellement dangereux et non validé.

En outre, il convient de noter que d'autres méthodes RPC liées aux recherches d'objets contiennent également des capacités de désassemblage, mais elles ne sont pas vulnérables à l'utilisation de cellules de code vides. En effet, ils interrogent et désassemblent toujours les modules publiés existants. Les modules publiés doivent avoir été vérifiés, de sorte que l'hypothèse de cellules de code non vides est toujours valable lors de la création d'un VMControlFlowGraph.

Suggestions pour les développeurs

Après avoir compris les menaces à la stabilité des services RPC dans les blockchains provenant de paniques explicites et implicites, les développeurs doivent maîtriser des stratégies pour prévenir ou atténuer ces risques. Ces stratégies peuvent réduire la possibilité d'interruptions de service imprévues et augmenter la résilience du système. Par conséquent, l'équipe d'experts de CertiK propose les suggestions suivantes et les répertorie comme les meilleures pratiques pour la programmation Rust.

Rust Panic Abstraction : Dans la mesure du possible, envisagez d'utiliser la fonction catch_unwind de Rust pour intercepter les paniques et les convertir en messages d'erreur. Cela empêche l'ensemble du programme de planter et permet aux développeurs de gérer les erreurs de manière contrôlée.

Utilisez les API avec prudence : des paniques implicites se produisent généralement en raison d'une mauvaise utilisation des API fournies par des bibliothèques standard ou tierces. Par conséquent, il est crucial de bien comprendre l'API et d'apprendre à gérer les erreurs potentielles de manière appropriée. Les développeurs doivent toujours supposer que les API peuvent échouer et se préparer à de telles situations.

Gestion appropriée des erreurs : utilisez les types de résultat et d'option pour la gestion des erreurs au lieu de recourir à la panique. Le premier offre un moyen plus contrôlé de gérer les erreurs et les cas particuliers.

Ajoutez de la documentation et des commentaires : assurez-vous que votre code est bien documenté et ajoutez des commentaires aux sections critiques (y compris celles où des paniques peuvent se produire). Cela aidera les autres développeurs à comprendre les risques potentiels et à les gérer efficacement.

Résumer

Les nœuds RPC basés sur Rust jouent un rôle important dans les systèmes de blockchain tels qu'Aptos, StarCoin et Sui. Puisqu'ils sont utilisés pour connecter les DApps et la blockchain sous-jacente, leur fiabilité est essentielle au bon fonctionnement du système de blockchain. Bien que ces systèmes utilisent le langage sécurisé en mémoire Rust, il existe toujours un risque de mauvaise conception. L'équipe de recherche de CertiK a exploré ces risques avec des exemples concrets qui démontrent la nécessité d'une conception soignée et soignée dans la programmation sécurisée en mémoire.

Voir l'original
This page may contain third-party content, which is provided for information purposes only (not representations/warranties) and should not be considered as an endorsement of its views by Gate, nor as financial or professional advice. See Disclaimer for details.
  • Récompense
  • Commentaire
  • Partager
Commentaire
0/400
Aucun commentaire
  • Épingler
Trader les cryptos partout et à tout moment
qrCode
Scan pour télécharger Gate app
Communauté
Français (Afrique)
  • 简体中文
  • English
  • Tiếng Việt
  • 繁體中文
  • Español
  • Русский
  • Français (Afrique)
  • Português (Portugal)
  • Bahasa Indonesia
  • 日本語
  • بالعربية
  • Українська
  • Português (Brasil)