À l'heure actuelle, il existe deux approches principales pour la recherche de vulnérabilités dans les applications: l'analyse statique et l'analyse dynamique. Les deux approches ont leurs avantages et leurs inconvénients. Le marché arrive à la conclusion que les deux approches doivent être utilisées - elles résolvent des problèmes légèrement différents avec des résultats différents. Cependant, dans certains cas, l'utilisation de l'analyse statique est limitée - par exemple, lorsqu'il n'y a pas de code source. Dans cet article, nous parlerons d'une technologie assez rare mais très utile qui vous permet de combiner les avantages des approches statiques et dynamiques - l'analyse statique du code exécutable.
Allons de loinSelon la société antivirus McAfee, les dommages mondiaux causés par la cybercriminalité en 2017 s'élevaient à environ 600 milliards de dollars, soit 0,8% du PIB mondial. Nous vivons à l'ère des technologies de l'information, dont les spécificités ont été l'intégration rapide du réseau mondial et des technologies Internet dans tous les domaines de l'activité humaine. Désormais, les cybercrimes ne sortent plus de l'ordinaire.
Les statistiques montrent une augmentation exponentielle de la cybercriminalité.
La vulnérabilité des applications est devenue un problème grave: selon le département américain de la Sécurité intérieure, plus de 90% des cyberattaques réussies sont mises en œuvre en utilisant diverses vulnérabilités dans les applications. Les méthodes d'exploitation de vulnérabilité les plus connues sont:
- Injection SQL
- débordement de tampon
- script crossite
- Utilisation d'une configuration non sécurisée.
L'analyse des logiciels (logiciels) pour la présence de capacités non déclarées (NDV) et de vulnérabilités est la principale technologie pour assurer la sécurité des applications.
En parlant de technologies classiques et bien établies pour analyser les logiciels pour les vulnérabilités et NDV (pour la conformité aux exigences de sécurité de l'information), nous pouvons distinguer:
- analyse de code statique (test de sécurité d'application statique);
- analyse de code dynamique (Dynamic Application Security Testing).
Il existe IAST (analyse interactive), cependant, il est essentiellement dynamique (dans le processus d'analyse, un agent supplémentaire observe ce qui se passe pendant l'exécution de l'application). RASP (Runtime Application Self-Defense), qui est également parfois mentionné dans un certain nombre d'outils d'analyse, est plus probablement un outil de protection.
L'analyse dynamique (méthode "Black Box") est une vérification de programme lors de son exécution. Les avantages suivants peuvent être distingués de cette approche.
- Comme les vulnérabilités se trouvent dans le programme exécutable et que l'erreur est détectée à l'aide de son fonctionnement, la génération de faux positifs est inférieure à celle de l'analyse statique.
- Aucun code source n'est nécessaire pour effectuer l'analyse.
Mais il y a aussi des inconvénients.
- Couverture incomplète du code, et il existe donc des risques de vulnérabilité manquante. Par exemple, l'analyse dynamique ne peut pas trouver de vulnérabilités associées à l'utilisation d'une cryptographie faible ou de signets comme "bombe temporaire".
- La nécessité d'exécuter l'application, ce qui dans certains cas peut être difficile. Le lancement de l'application peut nécessiter une configuration complexe et la configuration de diverses intégrations. De plus, pour que les résultats soient aussi précis que possible, il est nécessaire de reproduire «l'environnement de combat», mais il est difficile de réaliser pleinement cela sans nuire au logiciel.
L'analyse statique (la méthode «White Box») est un type de test de programme dans lequel le programme ne s'exécute pas.
Nous énumérons les avantages.
- Couverture complète du code, ce qui conduit à la recherche de plus de vulnérabilités.
- Aucune dépendance à l'environnement dans lequel le programme sera exécuté.
- La possibilité de mettre en œuvre des tests aux étapes initiales de l'écriture de code pour un module ou un programme en l'absence de fichiers exécutables. Cela vous permet d'intégrer déjà de manière flexible une solution similaire dans le SDLC (cycle de vie du développement logiciel cycle de vie du développement logiciel) au début du développement.
Le seul inconvénient de la méthode est la présence de faux positifs: la nécessité d'évaluer si l'analyseur indique une erreur réelle, ou est-il probable que ce faux positif.
Comme nous pouvons le voir, les deux méthodes d'analyse présentent à la fois des avantages et des inconvénients. Cependant, est-il possible de quelque façon que ce soit d'utiliser les avantages de ces méthodes, tout en minimisant les inconvénients? Oui, si vous appliquez l'analyse binaire - la recherche de vulnérabilités dans les fichiers exécutables par analyse statique.
Technologie d'analyse binaire ou d'analyse de fichiers exécutables
L'analyse binaire permet une analyse statique sans code source, par exemple, dans le cas de prestataires tiers. De plus, la couverture du code sera complète, contrairement à l'application de la méthode d'analyse dynamique. À l'aide de l'analyse binaire, vous pouvez vérifier les bibliothèques tierces utilisées dans le processus de développement pour lesquelles il n'y a pas de code source. De plus, à l'aide de l'analyse binaire, vous pouvez effectuer un contrôle de contrôle de la version, en comparant les résultats de l'analyse du code source du référentiel et du code exécutable du serveur de combat.
Au cours de l'analyse binaire, l'image binaire est transformée en une représentation intermédiaire (représentation interne ou modèle de code) pour une analyse plus approfondie. Après cela, des algorithmes d'analyse statique sont appliqués à la représentation interne. En conséquence, le modèle actuel est complété par les informations nécessaires à la détection supplémentaire des vulnérabilités et des NDV. A l'étape suivante, l'application des règles de recherche de vulnérabilités et de NDV.
Nous avons écrit plus sur le schéma d'analyse statique
dans un article précédent . Contrairement à l'analyse de code source, qui utilise des éléments de théorie de compilation (analyse lexicale et syntaxique) pour construire le modèle, l'analyse binaire utilise la théorie de la traduction inverse pour désassembler, décompiler et désobfusquer le modèle.
Un peu sur les termes
Nous parlons d'analyser les fichiers exécutables qui n'ont pas d'informations de débogage. Avec les informations de débogage, la tâche est grandement simplifiée, mais s'il existe des informations de débogage, le code source l'est probablement et la tâche devient non pertinente.
Dans cet article, nous appelons l'analyse de bytecode Java aussi l'analyse binaire, bien que ce ne soit pas tout à fait correct. Nous faisons cela pour simplifier le texte. Bien sûr, la tâche d'analyser le bytecode JVM est plus simple que d'analyser le code binaire C / C ++ et Objective-C / Swift. Mais le schéma d'analyse général est similaire dans le cas du bytecode et du code binaire. Les principales difficultés décrites dans l'article concernent spécifiquement l'analyse du code binaire.
La décompilation est le processus de récupération du code source à partir du code binaire. Vous pouvez parler des éléments de la traduction inverse - démontage (obtention du code assembleur à partir d'une image binaire), conversion de l'assembleur en code à trois adresses ou autre représentation, restauration des constructions du niveau du code source.
Obfuscation - transformations qui préservent la fonctionnalité du code source, mais rendent difficile la décompilation et la compréhension de l'image binaire résultante. La désobfuscation est la transformation inverse. L'obfuscation peut être appliquée à la fois au niveau du code source et au niveau du code binaire.
Comment regarder les résultats?
Commençons un peu par la fin, mais la question de visualiser les résultats de l'analyse binaire est généralement posée en premier.
Il est important pour un spécialiste analysant le code binaire de mapper les vulnérabilités et NDV au code source. Pour ce faire, au stade final, le processus de désobfuscation (démêlage) est lancé si des conversions déroutantes ont été appliquées et le code binaire a été décompilé vers la source. Autrement dit, les vulnérabilités peuvent être démontrées sur du code décompilé.
Dans le processus de décompilation, même si nous décompilons le bytecode JVM, certaines informations ne sont pas restaurées correctement, donc l'analyse elle-même a lieu sur une représentation proche du code binaire. En conséquence, la question se pose: comment, en trouvant des vulnérabilités dans le code binaire, les localiser dans la source? La solution au problème du bytecode JVM a été décrite
dans notre article sur la recherche de vulnérabilités dans le bytecode Java . La solution pour le code binaire est similaire, c'est-à-dire une question technique.
Répétons la mise en garde importante - nous parlons d'analyse de code binaire sans informations de débogage. En présence d'informations de débogage, la tâche est grandement simplifiée.
La principale question qui nous est posée à propos de l'affichage des résultats est de savoir si le code décompilé est suffisant pour comprendre et localiser la vulnérabilité.
Voici quelques réflexions à ce sujet.
- Si nous parlons du bytecode JVM, alors en général la réponse est «oui» - la qualité de décompilation pour le bytecode est excellente. Presque toujours, vous pouvez déterminer quelle est la vulnérabilité.
- Ce qui peut interférer avec la localisation qualitative de la vulnérabilité est une simple obfuscation telle que renommer des noms et des fonctions de classe. Cependant, dans la pratique, il s'avère souvent qu'il est plus important de comprendre la vulnérabilité que de déterminer dans quel fichier elle se trouve. La localisation est nécessaire lorsque quelqu'un peut corriger la vulnérabilité, mais dans ce cas, le développeur comprendra également où la vulnérabilité provient du code décompilé.
- Quand on parle d'analyse de code binaire (par exemple, C ++), bien sûr, tout est beaucoup plus compliqué. Il n'y a aucun outil qui récupère complètement le code C ++ aléatoire. Cependant, la particularité de notre cas est que nous n'avons pas besoin de compiler le code plus tard: nous avons besoin d'une qualité suffisante pour comprendre la vulnérabilité.
- Le plus souvent, vous pouvez obtenir une qualité de décompilation suffisante pour comprendre la vulnérabilité trouvée. Pour ce faire, vous devez résoudre de nombreux problèmes complexes, mais vous pouvez les résoudre (nous en parlerons brièvement ci-dessous).
- Pour C / C ++, il est encore plus difficile de localiser la vulnérabilité - les noms des caractères sont perdus de plusieurs façons au cours du processus de compilation, vous ne pouvez pas les restaurer.
- La situation dans Objective-C est légèrement meilleure - il y a des noms de fonctions et il est plus facile de localiser la vulnérabilité.
- Les problèmes d'obscurcissement sont à part. Il existe un certain nombre de transformations complexes qui peuvent compliquer la décompilation et la cartographie des vulnérabilités. En pratique, il s'avère qu'un bon décompilateur peut gérer la plupart des conversions déroutantes (rappelez-vous que nous avons besoin d'une qualité de code suffisante pour comprendre la vulnérabilité).
En conclusion - le plus souvent, il s'avère que la vulnérabilité est compréhensible et vérifiable.
Complexités et détails de l'analyse binaire
Ici, nous ne parlerons pas du bytecode: toutes les choses intéressantes à ce sujet ont déjà été dites ci-dessus. La chose la plus intéressante est l'analyse du vrai code binaire. Ici, nous parlerons de l'analyse de C / C ++, Objective-C et Swift comme exemple.
Des difficultés importantes surviennent même lors du démontage. L'étape la plus importante est la division de l'image binaire en sous-programmes. Ensuite, sélectionnez les instructions de l'assembleur dans les sous-programmes - une question technique. Nous avons écrit à ce sujet
en détail
dans un article de la revue «Issues of Cybersecurity No. 1 (14) - 2016» , que nous décrirons ici brièvement.
À titre d'exemple, nous parlerons de l'architecture x86. Les instructions qu'il contient n'ont pas de longueur fixe. Dans les images binaires, il n'y a pas de division claire en sections de code et de données: les tables d'importation, les tables de fonctions virtuelles peuvent être dans la section de code, les tables de transition peuvent être dans les intervalles entre les blocs de fonction de base dans la section de code. Par conséquent, vous devez pouvoir séparer le code des données et comprendre où les routines commencent et où se terminent les routines.
Les plus courantes sont deux méthodes pour résoudre le problème de la détermination des adresses de départ des sous-programmes. Dans la première méthode, les adresses des sous-programmes sont déterminées par le prologue standard (pour l'architecture x86 c'est push ebp; mov ebp, esp). Dans la deuxième méthode, une section de code est parcourue récursivement à partir du point d'entrée avec reconnaissance des instructions d'appel de sous-programme. Le contournement se fait en reconnaissant les instructions de branchement. Des combinaisons des méthodes décrites sont également utilisées lorsqu'un parcours récursif est démarré à partir des adresses de début trouvées par le prologue.
En pratique, il s'avère que de telles approches donnent un pourcentage assez faible de code reconnu, car toutes les fonctions n'ont pas un prologue standard, et il y a des appels et des transitions indirectes.
Les algorithmes de base peuvent être améliorés par les heuristiques suivantes.
- Sur une large base d'images de test, trouvez une liste plus précise de prologues (nouveaux prologues ou variantes de prologs standard).
- Vous pouvez trouver automatiquement des tableaux de fonctions virtuelles, et à partir d'eux pour récupérer les adresses de départ des sous-programmes.
- Les adresses de départ des sous-programmes et d'autres constructions peuvent être trouvées sur la base de sections de code binaire associées au mécanisme de gestion des exceptions.
- Vous pouvez vérifier les adresses de départ en recherchant ces adresses dans l'image et en reconnaissant les instructions d'appel.
- Pour rechercher des limites, vous pouvez effectuer une traversée récursive du sous-programme avec reconnaissance des instructions à partir de l'adresse de début. Il y a une difficulté avec les transitions indirectes et les fonctions sans retour. L'analyse de la table d'importation et la reconnaissance des constructions de commutateurs peuvent être utiles.
Une autre chose importante qui doit être faite lors de la traduction inverse, afin de rechercher normalement une vulnérabilité plus tard, est de reconnaître les fonctions standard dans une image binaire. Les fonctions standard peuvent être liées statiquement à l'image, ou même être intégrées. L'algorithme de reconnaissance principal est une recherche par signature avec des variations; pour la solution, vous pouvez proposer l'algorithme Aho-Korasik adapté. Pour collecter des signatures, vous devez pré-analyser les images de bibliothèque collectées avec différentes conditions et les sélectionner comme octets immuables.
Et ensuite
Dans la section précédente, nous avons examiné l'étape initiale de la traduction inverse d'une image binaire - le démontage. La scène, en effet, est initiale, mais déterminante. À ce stade, vous pouvez perdre une partie du code, ce qui aura alors un effet spectaculaire sur les résultats de l'analyse.
Ensuite, beaucoup de choses intéressantes se produisent. Dites brièvement sur les tâches principales. Nous ne parlerons pas en détail: soit le savoir-faire, sur lequel nous ne pouvons pas explicitement écrire ici, soit des solutions techniques et d'ingénierie peu intéressantes sont dans les détails.
- Conversion du code d'assemblage en une représentation intermédiaire sur laquelle l'analyse peut être effectuée. Vous pouvez utiliser différents bytecodes. Pour les langages C, LLVM semble être un bon choix. LLVM est activement soutenu et développé par la communauté, l'infrastructure, notamment utile pour l'analyse statique, est actuellement impressionnante. À ce stade, il y a un grand nombre de détails auxquels vous devez faire attention. Par exemple, vous devez détecter les variables qui sont adressées sur la pile afin de ne pas multiplier les entités dans la vue résultante. Vous devez configurer l'affichage optimal des jeux d'instructions d'assembleur dans les instructions de bytecode.
- Restaurer des structures de haut niveau (par exemple, boucles, branches). Plus il est possible de restaurer avec précision les constructions d'origine à partir du code assembleur, meilleure est la qualité de l'analyse. La restauration de telles constructions se fait à l'aide d'éléments de théorie des graphes sur CFG (control flow graph) et quelques autres représentations graphiques du programme.
- Réalisation d'algorithmes d'analyse statique. Il y a des détails. En général, peu importe que nous obtenions la représentation interne de la source ou du binaire - nous devons également tous construire CFG, appliquer des algorithmes d'analyse de flux de données et d'autres algorithmes typiques de la statique. Il existe certaines fonctionnalités lors de l'analyse de la vue obtenue à partir du binaire, mais elles sont plus techniques.
Conclusions
Nous avons expliqué comment effectuer une analyse statique en l'absence de code source. Selon l'expérience de la communication avec les clients, il s'avère que la technologie est très demandée. Cependant, la technologie est rare: le problème de l'analyse binaire n'est pas anodin, sa solution nécessite des algorithmes complexes de haute technologie d'analyse statique et de traduction inverse.
Cet article a été écrit en collaboration avec Anton Prokofiev, analyste Solar appScreener