Création d'un plugin pour Clang Static Analyzer pour rechercher les débordements d'entiers


Auteur de l'article: 0x64rem


Entrée


Il y a un an et demi, j'ai eu l'idée de réaliser mon phaser dans le cadre de la thèse à l'université. J'ai commencé à étudier des matériaux sur les graphiques de flux de contrôle, les graphiques de flux de données, l'exécution symbolique, etc. Ensuite, une recherche d'outils, un échantillon de différentes bibliothèques (Angr, Triton, Pin, Z3). Rien de concret ne s'est finalement produit, jusqu'à cet été, je suis allé au programme Summer of Hack 2019 de Digital Security , où l'on m'a proposé l'extension de Clang Static Analyzer comme thème du projet. Il m'a semblé que ce sujet m'aiderait à mettre mes connaissances théoriques sur les tablettes, à commencer à mettre en œuvre quelque chose de substantiel et à obtenir des recommandations de mentors expérimentés. Ensuite, je vais vous expliquer comment s'est déroulé le processus d'écriture du plug-in et décrire le cours de mes réflexions pendant le mois de stage.


Analyseur statique Clang


Pour le développement, Clang propose trois options d'interface pour l'interaction:


  • LibClang est une interface C de haut niveau qui vous permet d'interagir avec AST, mais pas complètement. Une bonne option si vous avez besoin d'une interaction avec une autre langue (par exemple, l'implémentation de liaisons ) ou une interface stable.
  • Clang Plugins - bibliothèques dynamiques appelées au moment de la compilation. Vous permet de manipuler complètement l'AST.
  • LibTooling - une bibliothèque pour créer des outils séparés basés sur Clang. Donne également un accès complet à l'interaction avec AST. Le code résultant peut être exécuté en dehors de l'environnement de génération du projet vérifié.

Puisque nous allons étendre les capacités de Clang Static Analyzer, nous choisissons l'implémentation du plugin. Vous pouvez écrire du code pour le plugin en C ++ ou Python.


Pour ce dernier, il existe des classeurs qui vous permettent d'analyser le code source, d'itérer sur les nœuds de l'arborescence de syntaxe abstraite résultante, d'avoir également accès aux propriétés des nœuds et de mapper le nœud à la ligne du code source. Un tel ensemble convient à un simple contrôleur. Voir le référentiel llvm pour plus de détails.


Ma tâche nécessite une analyse détaillée du code, donc C ++ a été choisi pour le développement. Voici une introduction à l'outil.


Clang Staic Analyzer (ci-après CSA) est un outil d'analyse statique du code C / C ++ / Objective-C, basé sur l'exécution symbolique. L'analyseur peut être appelé via l'interface frontale Clang en ajoutant les indicateurs -cc1 et -analyze à la commande de génération, ou via un binaire de génération de numérisation distinct. En plus de l'analyse elle-même, CSA permet de générer des rapports html visuels.


# ,      clang' clang -cc1 --help #  CSA  №1 clang++ -cc1 -x c++ -load path/to/Checker.so -analyze -analyzer-checker=test.Me -analyzer-config $BUILD_OPTIONS Checker.cpp 

  #  CSA  №2 scan-build -load-plugin path/to/Checker.so -enable-checker test.Me $BUILD_COMMAND 

  #       DivideZero clang++ -cc1 -analyze -analyzer-checker=core.DivideZero -o reports div-by-zero-test.cpp 


CSA a une excellente bibliothèque pour analyser le code source en utilisant AST (Abstract Syntax Tree), CFG (Control Flow Graph). A partir des structures, vous pouvez voir plus loin les déclarations de variables, leurs types, l'utilisation d'opérateurs binaires et unaires, vous pouvez obtenir des expressions symboliques, etc. Mon plugin utilisera les fonctionnalités des classes AST, ce choix sera justifié davantage. Ce qui suit est une liste de classes qui a été utilisée dans la mise en œuvre du plugin, la liste aidera à obtenir une compréhension primaire des capacités de CSA:


  • Stmt - cela inclut les opérations binaires.


  • Decl - déclaration de variables.


  • Expr - stocke les parties gauche et droite des expressions, leur type.


  • ASTContext - informations sur l'arbre, le nœud actuel.


  • Gestionnaire de sources - informations sur le code réel qui correspond à la partie de l'arborescence.


  • RecursiveASTVisitor, ASTMatcher - classes pour parcourir un arbre.


    Je répète que CSA offre au développeur la possibilité d'examiner en détail la structure du code, et les classes répertoriées ci-dessus ne sont qu'une petite partie de celles disponibles. Je vous recommande vivement de parcourir la documentation de votre version de Clang si vous ne savez pas comment extraire de données; très probablement, quelque chose de convenable a déjà été écrit.



Recherche de dépassement d'entier


Pour commencer à implémenter le plugin, vous devez choisir la tâche à résoudre. Dans ce cas, le site Web llvm fournit des listes de vérificateurs potentiels ; vous pouvez également modifier les vérificateurs stables ou alpha existants. Lors de l'examen du code des vérificateurs disponibles, il est devenu clair que pour un développement plus réussi de libclang, il est préférable d'écrire votre vérificateur à partir de zéro, donc le choix a été fait à partir d'une liste d' idées non réalisées . En conséquence, l'option a été choisie pour créer un vérificateur pour la détection de dépassement d'entier. Clang a déjà des fonctionnalités pour empêcher cette vulnérabilité (les drapeaux -ftrapv, -fwrapv et similaires sont indiqués pour son utilisation), il est intégré au compilateur, et cet échappement est versé dans des avertissements, et il n'y est pas souvent recherché. Il y a toujours UBSan , mais ce sont des désinfectants, tout le monde ne les utilise pas, et cette méthode consiste à identifier les problèmes lors de l'exécution, et le plug-in CSA fonctionne au moment de la compilation, en analysant les sources.


Vient ensuite la collecte de matériel sur la vulnérabilité sélectionnée. Le débordement d'entier était autrefois simple et pas grave. En fait, la vulnérabilité est divertissante et peut avoir des conséquences impressionnantes.
Les débordements d'entiers sont un type de vulnérabilité qui pourrait entraîner des données de type entier dans le code prenant des valeurs inattendues. Débordement - si la variable est devenue plus grande que prévu, Débordement - inférieur à son type d'origine. De telles erreurs peuvent apparaître à la fois à cause du programmeur et à cause du compilateur.


En C ++, lors d'une opération de comparaison arithmétique, les valeurs entières sont converties dans le même type, le plus souvent en une plus grande en termes de profondeur de bits. Et de tels fantômes se produisent partout et constamment, ils peuvent être explicites ou implicites. Il existe plusieurs règles selon lesquelles les fantômes se produisent [1]:


  • Conversion d'un signé à un type avec un bit signé, mais plus grand: ajoutez simplement l'ordre élevé.
  • Conversion d'un entier signé en un entier non signé de même capacité: le négatif est converti en positif et prend un nouveau sens. Un exemple d'une erreur similaire dans DirectFB est CVE-2014-2977 .
  • Conversion d'un entier signé en un entier non signé d'une plus grande capacité en bits: d'abord, la capacité en bits augmentera, puis si le nombre est négatif, alors il modifiera incorrectement la valeur. Par exemple: 0xff (-1) devient 0xffffffff.
  • Un entier non signé avec un signe de même capacité binaire: un nombre peut changer la valeur, en fonction de la valeur du bit haut.
  • Un entier non signé avec un entier avec un signe de plus grande capacité: d'abord, la capacité d'un nombre non signé augmente, puis la conversion en un nombre signé.
  • Conversion descendante: les bits sont juste tronqués. Cela peut rendre les valeurs non signées négatives, etc. Un exemple d'une telle vulnérabilité en PHP .

C'est-à-dire le déclencheur de la vulnérabilité peut être une entrée utilisateur non sûre, une arithmétique incorrecte, une conversion de type incorrecte provoquée par un programmeur ou un compilateur lors de l'optimisation. L'option bombe à retardement est également possible, lorsqu'un morceau de code est inoffensif avec une version du compilateur, mais avec la sortie d'un nouvel algorithme d'optimisation «explose» et provoque un comportement inattendu. Dans l'histoire, il y a déjà eu un tel cas avec la classe SafeInt (très ironique) [5, 6.5.2].


Les débordements entiers ouvrent un large vecteur: il est possible de forcer l'exécution à prendre un chemin différent (si le débordement affecte les instructions conditionnelles), provoquer un débordement de tampon. Pour plus de clarté, vous pouvez vous familiariser avec des CVE spécifiques, voir leurs causes, leurs conséquences. Naturellement, il est préférable de rechercher un débordement d'entier dans les produits open source, afin de lire non seulement la description, mais également le code.


  • CVE-2019-3560 - Un débordement d'entier dans Fizz (un projet implémentant TLS pour Facebook) pourrait exploiter une vulnérabilité DoS à l'aide d'un paquet réseau exigu.
  • CVE-2018-14618 - Débordement de tampon dans Curl provoqué par un débordement d'entier dû à la longueur du mot de passe.
  • CVE-2018-6092 - Sur les systèmes 32 bits, une vulnérabilité dans WebAssembly pour Chrome a permis d'implémenter RCE via une page HTML spéciale.

Afin de ne pas réinventer la roue, le code de détection du débordement d'entier dans l'analyseur statique CppCheck a été pris en compte. Son approche est la suivante:


  1. Déterminez si une expression est un opérateur binaire.
  2. Si oui, vérifiez si les deux arguments sont de type entier.
  3. Déterminez la taille des types.
  4. Vérifier au moyen de calculs si la valeur peut dépasser ses limites maximales ou minimales.
    Mais à ce stade, cela n'a pas donné de clarté. Il en résulte de nombreuses histoires différentes, et à partir de cette systématisation de l'information devient plus difficile. Tout à sa place a mis la liste des CWE . Au total, il existe 9 types de dépassement d'entier alloués sur le site:
    • 190 - débordement d'entier
    • 191 - sous-dépassement d'entier
    • 192 - erreur de cohésion entière
    • 193 - un par un
    • 194 - Extension de signe inattendue
    • 195 - Erreur de conversion signée à non signée
    • 196 - Erreur de conversion non signé en signé
    • 197 - Erreur de troncature numérique
    • 198 - Utilisation d'une commande d'octets incorrecte

Nous considérons la raison de chaque option et comprenons que les débordements se produisent avec des transtypages explicites / implicites incorrects. Et parce que tous les moulages sont affichés dans la structure de l'arbre de syntaxe abstraite, nous utiliserons AST pour l'analyse. Dans la figure ci-dessous (Fig.3), on peut voir que toute opération qui provoque un transtypage dans l'arborescence est un nœud distinct et, en se promenant dans l'arborescence, nous pouvons vérifier toutes les conversions de type en fonction d'une table avec des transformations qui peuvent provoquer une erreur.


Signe gSigne lSigne eAnnuler la signature gAnnuler la signature lAnnuler la signature e
Signe+-+---
Annuler la signature+----+


Plus précisément, l'algorithme ressemble à ceci: nous faisons le tour des modèles et examinons IntegralCast (conversions entières). Si vous trouvez un nœud approprié, regardez les descendants à la recherche d'une opération binaire ou Decl (déclaration de variable). Dans le premier cas, vous devez vérifier le signe et la profondeur de bits utilisés par l'opération binaire. Dans le second cas, comparez uniquement le type de déclaration.


Implémentation de Checker


Passons à la mise en œuvre. Nous avons besoin d'un squelette pour un vérificateur, qui peut être une bibliothèque autonome, ou peut être assemblé dans le cadre de Clang. Dans le code, la différence sera petite. Si vous prévoyez déjà d'écrire votre propre plugin, je vous recommande de lire immédiatement un petit pdf: "Clang Static Analyzer: A Checker Developer's Guide" , les choses de base y sont bien décrites, bien que quelque chose ne soit plus pertinent, la bibliothèque est mise à jour régulièrement, mais vous saisir tout de suite.


Si vous souhaitez ajouter votre vérificateur à votre assemblage de clang, vous devez:


  1. Écrivez le vérificateur lui-même avec approximativement le contenu suivant:


     namespace { class SuperChecker : public Checker<check::PreStmt<BinaryOperator>> { //       ,    .       struct CheckerOpts { //       string FlagOne; int FlagTwo; }; CheckerOpts Opts; //cool code }; } void ento::registerSuperChecker(CheckerManager &mgr) { auto checker = mgr.registerChecker<SuperChecker>(); //       ,   4    //       ,  stand-alone    . AnalyzerOptions &AnOpts = mgr.getAnalyzerOptions(); SuperChecker::CheckerOpts &ChOpts = checker->Opts; ChOpts.FlagOne = AnOpts.getCheckerStringOption("Inp1", "", checker); ChOpts.FlagTwo = AnOpts.getCheckerIntegerOption("Inp2", 0, checker); // getCheckerIntegerOption:  ,  ,   } 

  2. Ensuite, dans le code source de Clang, vous devrez modifier les fichiers CMakeLists.txt et Checkers.td . Vivez ici ${llvm-source-path}/clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt
    et ici ${llvm-source-path}/clang/include/clang/StaticAnalyzer/Checkers/Checkers.td .
    Dans le premier, il vous suffit d'ajouter le nom du fichier avec le code, dans le second, vous devez ajouter une description structurelle:


      #Checkers.td def SuperChecker : Checker<"SuperChecker">, HelpText<"test checker">, Documentation<HasDocumentation>; 


Si ce n'est pas clair, alors dans le fichier Checkers.td il y a suffisamment d'exemples de comment et quoi faire.


Vous ne voudrez probablement pas reconstruire Clang, et vous aurez recours à l'option avec l'assembly de bibliothèque (so / dll). Ensuite, dans le code du vérificateur devrait être quelque chose comme ceci:


 namespace { class SuperChecker : public Checker<check::PreStmt<BinaryOperator>> { //       ,    .       struct CheckerOpts { string FlagOne; int FlagTwo; }; CheckerOpts Opts; //cool code }; } void initializationFunction(CheckerManager &mgr){ SuperChecker *checker = mgr.registerChecker<SuperChecker>(); //       ,   4    AnalyzerOptions &AnOpts = mgr.getAnalyzerOptions(); TestChecker::CheckerOpts &ChOpts = checker->Opts; ChOpts.FlagOne = AnOpts.getCheckerStringOption("Inp1", "", checker); ChOpts.FlagTwo = AnOpts.getCheckerIntegerOption("Inp2", 0, checker); // getCheckerIntegerOption:  ,  ,   } extern "C" void clang_registerCheckers (CheckerRegistry &registry) { registry.addChecker(&initializationFunction, "test.Me", "SuperChecker description", "doc_link"); } extern "C" const char clang_analyzerAPIVersionString [] = "8.0.1"; 

Ensuite, collectez votre code, vous pouvez écrire votre propre script pour l'assemblage, mais si vous avez des problèmes avec cela (comme l'auteur l'avait :)), vous pouvez alors utiliser le Makefile dans le code source de clang et créer la commande clangStaticAnalyzerCheckers d'une manière étrange.


Ensuite, appelez le vérificateur:


  • pour les dames intégrées


     clang++ -cc1 -analyze -analyzer-checker=core.DivideZero test.cpp 

  • pour externe


     clang++ -cc1 -load ${PATH_TO_CHECKER}/SuperChecker.so -analyze -analyzer-checker=test.Me -analyzer-config test.Me:UsrInp1="foo" test.Me:Inp1="bar" -analyzer-config test.Me:Inp2=123 test.cpp 

    À ce stade, nous avons déjà une sorte de résultat (Fig. 4), mais le code écrit ne peut détecter que les débordements potentiels. Et cela signifie un grand nombre de faux positifs.




Pour résoudre ce problème, nous pouvons:


  • Faire le tour du graphique d'avant en arrière et vérifier les valeurs spécifiques des variables pour les cas où nous avons un débordement potentiel.
  • Pendant la traversée AST, enregistrez immédiatement les valeurs spécifiques des variables et vérifiez-les si nécessaire.
  • Utilisez l'analyse des souillures.

Pour étayer d'autres arguments, il convient de mentionner que lors de l'analyse de Clang, tous les fichiers spécifiés dans la directive #include analysent également, en conséquence, la taille de l'AST résultant augmente. En conséquence, parmi les options proposées, une seule est rationnelle concernant une tâche spécifique:


  • Tout d'abord, cela prend beaucoup de temps. Marcher dans un arbre, rechercher et compter tout ce dont vous avez besoin prendra beaucoup de temps, il peut devenir difficile d'analyser un grand projet avec un tel code. Pour parcourir l'arborescence dans le code, nous utiliserons la classe clang::RecursiveASTVisitor , qui effectue une recherche de profondeur récursive. Une estimation du temps de cette approche sera , où V est l'ensemble des sommets et E est l'ensemble des arêtes du graphique.
  • La seconde - vous pouvez certainement stocker, mais nous ne savons pas ce dont nous aurons besoin et ce qui ne le sera pas. De plus, les structures arborescentes elles-mêmes, que nous utilisons dans l'analyse, nécessitent beaucoup de mémoire, donc dépenser de telles ressources pour autre chose est une mauvaise idée.
  • La troisième est une bonne idée, pour cette méthode, vous pouvez trouver suffisamment de recherches et d'exemples. Mais dans CSA, il n'y a pas de souillure prête. Il y a un vérificateur , qui a été ajouté plus tard à la liste des vérificateurs alpha (alpha.security.taint.TaintPropagation) dans les sources, il est décrit dans le fichier GenericTaintChecker.cpp . Le vérificateur est bon, mais ne convient que pour les fonctions d'E / S non sécurisées connues de C, il ne "marque" que les variables qui étaient des arguments ou des résultats de fonctions dangereuses. En plus des options décrites, il convient de considérer les variables globales, les champs de classe, etc., afin de restaurer correctement le modèle de "distribution".

Le temps restant pour le stage a été passé à lire GenericTaintChecker.cpp et à essayer de le refaire en fonction de vos besoins. Il n'a pas fonctionné avec succès à la fin du mandat, mais il est resté une tâche de perfectionnement déjà au-delà de la portée de la formation chez DSec. Au cours du développement, il est également devenu clair que l'identification des fonctions dangereuses est une tâche distincte, les endroits dangereux du projet ne proviennent pas toujours de certaines fonctions standard, donc un indicateur a été ajouté au vérificateur pour indiquer une liste de fonctions qui seront considérées comme «empoisonnées» / «marquées» lors de l'analyse des souillures.
De plus, une vérification a été ajoutée pour déterminer si la variable est un champ de bits. Par les outils CSA standard, la taille est déterminée par type, et si nous travaillons avec un champ de bits, alors sa taille aura la valeur du type de bit de tout le champ, et non le nombre de bits spécifié dans la déclaration de variable.


Quel est le résultat?


À l'heure actuelle, un simple vérificateur a été mis en œuvre qui ne peut avertir que des débordements d'entiers potentiels. Une classe modifiée pour l'analyse des souillures, qui a encore beaucoup de travail à faire. Après cela, vous devez utiliser SMT pour déterminer les débordements. Pour cela, le solveur Z3 SMT convient, qui a été ajouté à l'assemblage Clang dans la version 5.0.0 (à en juger par les notes de publication ). Pour utiliser le solveur, il est nécessaire que Clang soit construit avec l'option CLANG_ANALYZER_BUILD_Z3=ON , et lorsque le plug-in CSA est appelé directement, les -Xanalyzer -analyzer-constraints=z3 sont transmis.


Référentiel de résultats GitHub


Références:


  1. Howard M., Leblanc D., Viega J. "Les 24 péchés de la sécurité informatique"


  2. Comment écrire un vérificateur en 24 heures


  3. Clang Static Analyzer: A Checker Developer's Guide


  4. Manuel de développement du vérificateur CSA


  5. Dietz W. et al. Comprendre le dépassement d'entier en C / C ++


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


All Articles