L'analyse du code binaire, c'est-à-dire du code exécuté directement par la machine, est une tâche non triviale. Dans la plupart des cas, s'il est nécessaire d'analyser le code binaire, il est d'abord restauré par désassemblage, puis décompilation en une représentation de haut niveau, puis ils analysent ce qui s'est passé.
Ici, il faut dire que le code qui a été restauré, selon la représentation textuelle, a peu de choses en commun avec le code initialement écrit par le programmeur et compilé dans un fichier exécutable. Il est impossible de récupérer exactement le fichier binaire reçu des langages de programmation compilés tels que C / C ++, Fortran, car il s'agit d'une tâche algorithmique non formalisée. En cours de conversion du code source que le programmeur a écrit dans le programme que la machine exécute, le compilateur effectue des conversions irréversibles.
Dans les années 90 du siècle dernier, il était largement admis que le compilateur, comme un hachoir à viande, moud le programme d'origine, et la tâche de le restaurer est similaire à la tâche de restaurer un bélier à partir d'une saucisse.

Cependant, pas si mal. Dans le processus d'obtention des saucisses, le mouton perd sa fonctionnalité, tandis que le programme binaire l'enregistre. Si la saucisse résultante pouvait courir et sauter, les tâches seraient similaires.

Ainsi, une fois que le programme binaire a conservé sa fonctionnalité, nous pouvons dire qu'il est possible de restaurer le code exécutable dans une représentation de haut niveau afin que la fonctionnalité du programme binaire, dont la représentation d'origine n'est pas présente, et le programme dont nous avons reçu la représentation textuelle, soient équivalents.
Par définition, deux programmes sont fonctionnellement équivalents si, sur les mêmes données d'entrée, tous deux terminent ou échouent leur exécution et, si l'exécution se termine, produisent le même résultat.
La tâche de démontage est généralement résolue en mode semi-automatique, c'est-à-dire que le spécialiste effectue la récupération manuelle à l'aide d'outils interactifs, par exemple le
démonteur interactif
IdaPro ,
Radare ou un autre outil. De plus, également en mode semi-automatique, une décompilation est effectuée.
HexRays ,
SmartDecompiler ou un autre décompilateur, qui convient pour résoudre cette tâche de décompilation, est utilisé comme outil de décompilation pour aider un spécialiste.
La restauration de la représentation textuelle originale du programme à partir du code octet peut être rendue assez précise. Pour les langages interprétés tels que Java ou les langages de la famille .NET, dont la traduction est effectuée en octet-code, la tâche de décompilation est résolue différemment. Nous ne considérons pas ce problème dans cet article.
Ainsi, vous pouvez analyser les programmes binaires par décompilation. Typiquement, une telle analyse est effectuée pour comprendre le comportement d'un programme afin de le remplacer ou de le modifier.
De la pratique de travailler avec les programmes hérités
Certains logiciels, écrits il y a 40 ans dans la famille de langage bas niveau C et Fortran, contrôlent les équipements de production de pétrole. La panne de cet équipement peut être critique pour la production, donc changer le logiciel est hautement indésirable. Cependant, au cours des dernières années, les codes sources ont été perdus.
Le nouvel employé du département de la sécurité de l'information, dont les responsabilités étaient de comprendre comment cela fonctionne, a constaté que le programme de surveillance des capteurs écrit quelque chose sur le disque avec une certaine régularité, qu'il écrit et comment ces informations peuvent être utilisées n'est pas claire. Il a également eu l'idée que la surveillance du fonctionnement des équipements peut être affichée sur un grand écran. Pour ce faire, il a fallu comprendre le fonctionnement du programme, ce qu'il écrit et dans quel format il écrit sur le disque, comment ces informations peuvent être interprétées.
Pour résoudre le problème, une technologie de décompilation a été appliquée, suivie d'une analyse du code restauré. Nous avons d'abord démonté les composants logiciels un par un, puis localisé le code responsable de l'entrée / sortie des informations et commencé progressivement à récupérer de ce code, compte tenu des dépendances. Ensuite, la logique du programme a été restaurée, ce qui a permis de répondre à toutes les questions du service de sécurité concernant le logiciel analysé.
Si vous devez analyser un programme binaire afin de restaurer la logique de son fonctionnement, restaurer partiellement ou complètement la logique de conversion des données d'entrée en données de sortie, etc., il est pratique de le faire à l'aide d'un décompilateur.
En plus de ces tâches, dans la pratique, il existe des problèmes d'analyse des programmes binaires pour les exigences de sécurité de l'information. De plus, le client ne comprend pas toujours que cette analyse prend beaucoup de temps. Il semblerait, faites la décompilation et exécutez le code résultant avec un analyseur statique. Mais à la suite d'une analyse qualitative, elle ne réussit presque jamais.
Premièrement, les vulnérabilités trouvées doivent pouvoir non seulement trouver, mais aussi expliquer. Si la vulnérabilité a été trouvée dans un programme dans un langage de haut niveau, l'analyste ou l'outil d'analyse de code y montre quels fragments du code contiennent certains défauts, dont la présence a causé la vulnérabilité. Et s'il n'y a pas de code source? Comment montrer quel code a causé la vulnérabilité?
Le décompilateur récupère le code «jonché» d'artefacts de récupération, et il est inutile de cartographier la vulnérabilité révélée à ce code, mais rien n'est clair. De plus, le code restauré est mal structuré et se prête donc mal aux outils d'analyse de code. Il est également difficile d'expliquer la vulnérabilité en termes de programme binaire, car la personne pour laquelle l'explication est faite doit être bien familiarisée avec la représentation binaire des programmes.
Deuxièmement, une analyse binaire en fonction des exigences de sécurité de l'information doit être effectuée avec une compréhension de ce qu'il faut faire du résultat résultant, car il est très difficile de corriger une vulnérabilité dans un code binaire, mais il n'y a pas de code source.
Malgré toutes les caractéristiques et les difficultés de mener une analyse statique des programmes binaires en fonction des exigences de la sécurité de l'information, il existe de nombreuses situations où une telle analyse est nécessaire. Si, pour une raison quelconque, il n’existe pas de code source et que le programme binaire exécute des fonctionnalités essentielles aux exigences de sécurité des informations, il doit être vérifié. Si des vulnérabilités sont trouvées, une telle application devrait être envoyée pour révision, si possible, ou un «shell» supplémentaire devrait être fait pour cela, ce qui permettra de contrôler le mouvement des informations sensibles.
Quand la vulnérabilité était cachée dans un fichier binaire
Si le code que le programme exécute a un niveau de criticité élevé, même si le code source du programme est dans un langage de haut niveau, il est utile d'auditer le fichier binaire. Cela aidera à éliminer les fonctionnalités que le compilateur peut introduire en effectuant des transformations d'optimisation. Ainsi, en septembre 2017, la transformation d'optimisation effectuée par le compilateur Clang a été largement discutée. Son résultat a été un appel à une
fonction qui ne devrait jamais être appelée.
#include <stdlib.h> typedef int (*Function)(); static Function Do; static int EraseAll() { return system("rm -rf /"); } void NeverCalled() { Do = EraseAll; } int main() { return Do(); }
À la suite des transformations d'optimisation, le compilateur obtiendra ce code assembleur. L'exemple a été compilé sous Linux X86 avec l'indicateur -O2.
.text .globl NeverCalled .align 16, 0x90 .type NeverCalled,@function NeverCalled: # @NeverCalled retl .Lfunc_end0: .size NeverCalled, .Lfunc_end0-NeverCalled .globl main .align 16, 0x90 .type main,@function main: # @main subl $12, %esp movl $.L.str, (%esp) calll system addl $12, %esp retl .Lfunc_end1: .size main, .Lfunc_end1-main .type .L.str,@object # @.str .section .rodata.str1.1,"aMS",@progbits,1 .L.str: .asciz "rm -rf /" .size .L.str, 9
Il y a
un comportement indéfini dans le code source. La fonction NeverCalled () est appelée en raison des transformations d'optimisation effectuées par le compilateur. Au cours du processus d'optimisation, il effectue très probablement une analyse des
allias et, par conséquent, la fonction Do () reçoit l'adresse de la fonction NeverCalled (). Et puisque la méthode main () appelle la fonction Do (), qui n'est pas définie, qui est un comportement non défini (comportement non défini), le résultat est le suivant: la fonction EraseAll () est appelée, qui exécute la commande rm -rf /.
L'exemple suivant: suite aux transformations d'optimisation du compilateur, nous avons perdu la vérification d'un pointeur sur NULL avant de le déréférencer.
#include <cstdlib> void Checker(int *P) { int deadVar = *P; if (P == 0) return; *P = 8; }
Étant donné que la ligne 3 déréférence le pointeur, le compilateur suppose que le pointeur est différent de zéro. Ensuite, la ligne 4 a été supprimée à la suite de l'optimisation
«suppression du code inaccessible» , car la comparaison est considérée comme redondante, et après cela, la ligne 3 a été supprimée par le compilateur à la suite de l'optimisation
« élimination du code mort». Il ne reste que la ligne 5. Le code assembleur résultant de la compilation de gcc 7.3 sous Linux x86 avec l'indicateur -O2 est illustré ci-dessous.
.text .p2align 4,,15 .globl _Z7CheckerPi .type _Z7CheckerPi, @function _Z7CheckerPi: movl 4(%esp), %eax movl $8, (%eax) ret
Les exemples ci-dessus de travail d'optimisation du compilateur sont le résultat d'un comportement indéfini d'UB dans le code. Cependant, c'est un code tout à fait normal que la plupart des programmeurs considéreraient comme sûr. Aujourd'hui, les programmeurs passent du temps à éliminer les comportements indéfinis dans le programme, alors qu'il y a 10 ans ils n'y prêtaient pas attention. Par conséquent, le code hérité peut contenir des vulnérabilités UB.
La plupart des analyseurs de code source statiques modernes ne détectent pas les erreurs liées à UB. Par conséquent, si le code effectue une fonction critique aux exigences de la sécurité de l'information, il est nécessaire de vérifier à la fois son code source et le code qui sera exécuté.