Capture d'écran de l'interface IDA Pro DisassemblerIDA Pro est un célèbre démonteur utilisé par les chercheurs en sécurité de l'information du monde entier depuis de nombreuses années. Chez Positive Technologies, nous utilisons également cet outil. De plus, nous avons pu développer notre propre
module processeur de désassembleur pour l'architecture de microprocesseur NIOS II , ce qui augmente la vitesse et la commodité de l'analyse de code.
Aujourd'hui, je vais raconter l'histoire de ce projet et montrer ce qui s'est finalement passé.
Contexte
Tout a commencé en 2016, lorsque nous avons dû développer notre propre module processeur pour analyser le firmware en une seule tâche. Le développement a été effectué à partir de zéro sur le manuel
Nios II Classic Processor Reference Guide , qui était alors le plus pertinent. Au total, ce travail a pris environ deux semaines.
Le module processeur a été développé pour la version IDA 6.9. Pour la vitesse, IDA Python a été choisi. À l'endroit où résident les modules du processeur - le sous-répertoire procs dans le répertoire d'installation IDA Pro - il y a trois modules Python: msp430, ebc, spu. Vous pouvez y voir comment le module est organisé et comment la fonctionnalité de démontage de base peut être implémentée:
- analyser les instructions et les opérandes,
- leur simplification et leur affichage,
- créer des décalages, des références croisées, ainsi que le code et les données auxquels ils se réfèrent
- traitement des constructions de commutateurs,
- gérer les manipulations avec la pile et les variables de pile.
Environ une telle fonctionnalité a été mise en œuvre à l'époque. Heureusement, l'outil a été utile dans le processus de travail sur une autre tâche, au cours de laquelle, un an plus tard, il a été activement utilisé et affiné.
J'ai décidé de partager l'expérience de la création du module processeur avec la communauté lors des PHDays 8. La présentation a suscité un intérêt (le reportage vidéo a été
publié sur le site PHDays), même le créateur d'IDA Pro Ilfak Gilfanov était présent. L'une de ses questions était de savoir si la prise en charge d'IDA Pro version 7 était implémentée. À ce moment-là, elle n'était pas là, mais après la représentation, j'ai promis de faire une version appropriée du module. C'est là que le plaisir a commencé.
Maintenant, le dernier
manuel d'Intel , qui a été utilisé pour vérifier et vérifier les erreurs. J'ai considérablement révisé le module, ajouté un certain nombre de nouvelles fonctionnalités, y compris la résolution de ces problèmes qui ne pouvaient pas être vaincus auparavant. Bien sûr, j'ai ajouté la prise en charge de la 7e version d'IDA Pro. Voici ce qui s'est passé.
Modèle logiciel NIOS II
NIOS II est un processeur logiciel développé pour les FPGA Altera (qui fait désormais partie d'Intel). Du point de vue des programmes, il présente les caractéristiques suivantes: ordre des octets du petit endian, espace d'adressage 32 bits, jeu d'instructions 32 bits, c'est-à-dire que 4 octets, 32 registres généraux et 32 registres spéciaux sont utilisés pour coder chaque commande.
Démontage et références de code
Nous avons donc ouvert un nouveau fichier dans IDA Pro, avec le firmware du processeur NIOS II. Après avoir installé le module, nous le verrons dans la liste des processeurs IDA Pro. Le choix du processeur est indiqué sur la figure.

Supposons que le module n'ait pas encore implémenté même une analyse de base des commandes. Étant donné que chaque commande prend 4 octets, nous regroupons les octets en quatre, puis tout ressemblera à ceci.

Après avoir implémenté la fonctionnalité de base des instructions de décodage et des opérandes, les afficher à l'écran et analyser les instructions de transfert de contrôle, l'octet défini dans l'exemple ci-dessus est converti en le code suivant.

Comme le montre l'exemple, des références croisées sont également générées à partir des commandes de transfert de contrôle (dans ce cas, vous pouvez voir le saut conditionnel et l'appel de procédure).
Une des propriétés utiles qui peuvent être implémentées dans les modules de processeur est les commentaires de commande. Si vous désactivez la sortie des valeurs d'octets et activez la sortie des commentaires, la même section de code ressemblera déjà à ceci.

Ici, si vous avez rencontré pour la première fois le code assembleur d'une nouvelle architecture, à l'aide de commentaires, vous pouvez comprendre ce qui se passe. De plus, les exemples de code seront sous la même forme - avec des commentaires, afin de ne pas regarder le manuel NIOS II, mais de comprendre immédiatement ce qui se passe dans la section de code, qui est donnée à titre d'exemple.
Pseudo-instructions et simplification des commandes
Certaines commandes NIOS II sont de pseudo instructions. Il n'y a pas d'opcodes séparés pour ces équipes, et elles sont elles-mêmes modélisées comme des cas spéciaux d'autres équipes. Dans le processus de démontage, une simplification des instructions est effectuée - le remplacement de certaines combinaisons par des pseudo-instructions. Les pseudo-instructions dans NIOS II peuvent généralement être divisées en quatre types:
- lorsque l'une des sources est nulle (r0) et peut être retirée de la considération,
- lorsque l'équipe a une valeur négative et que l'équipe est remplacée par le contraire,
- lorsque la condition est inversée,
- lorsque le décalage 32 bits est entré dans deux équipes en plusieurs parties (le plus jeune et le plus âgé) et qu'il est remplacé par une commande.
Les deux premiers types ont été mis en œuvre, car le remplacement de la condition ne donne rien de spécial, et les décalages 32 bits ont plus d'options que celles présentées dans le manuel.
Par exemple, pour la première vue, considérez le code.

On voit que l'utilisation du registre zéro dans les calculs se retrouve souvent ici. Si vous regardez attentivement cet exemple, vous remarquerez que toutes les commandes, à l'exception du transfert de contrôle, sont des options permettant de saisir simplement des valeurs dans des registres spécifiques.
Après avoir implémenté le traitement des pseudo instructions, nous obtenons la même section de code, mais maintenant il semble plus lisible, et au lieu de variations des commandes or et add, nous obtenons des variations de la commande mov.

Variables de pile
L'architecture NIOS II prend en charge la pile et, en plus du pointeur de pile sp, il existe également un pointeur vers la trame de pile fp. Prenons un exemple d'une petite procédure qui utilise une pile.

De toute évidence, l'espace est réservé aux variables locales sur la pile. On peut supposer que le registre ra est stocké dans la variable de pile puis restauré à partir de celle-ci.
Après avoir ajouté des fonctionnalités au module qui suit les modifications du pointeur de pile et crée des variables de pile, le même exemple ressemblera à ceci.

Maintenant, le code semble un peu plus clair, et vous pouvez déjà nommer les variables de pile et analyser leur fonction en suivant les références croisées. La fonction dans l'exemple est de type __fastcall et ses arguments dans les registres r4 et r5 sont poussés sur la pile pour appeler une sous-procédure de type _stdcall.
Numéros 32 bits et décalages
La particularité de NIOS II est qu'en une seule opération, c'est-à-dire lors de l'exécution d'une seule commande, il est possible d'enregistrer au plus une valeur directe de 2 octets (16 bits). D'autre part, les registres du processeur et l'espace d'adressage sont de 32 bits, c'est-à-dire que pour l'adressage, 4 octets doivent être entrés dans le registre.
Pour résoudre ce problème, des déplacements en deux parties sont utilisés. Un mécanisme similaire est utilisé dans les processeurs de PowerPC: l'offset se compose de deux parties, la plus ancienne et la plus jeune, et est entré dans le registre par deux commandes. Dans PowerPC, c'est comme suit.

Dans cette approche, des liaisons croisées sont formées à partir des deux équipes, bien qu'en fait, l'adresse soit configurée dans la deuxième commande. Cela peut parfois être gênant lors du comptage du nombre de références croisées.
Les propriétés de décalage pour la partie la plus ancienne utilisent le type non standard HIGHA16, parfois le type HIGH16 est utilisé, pour la partie la plus jeune - LOW16.

Il n'y a rien de compliqué dans le calcul des nombres en deux parties 32 bits. Des difficultés surviennent dans la formation d'opérandes comme compensations pour deux équipes distinctes. Tout ce traitement incombe au module processeur. Il n'y a aucun exemple de comment implémenter cela (en particulier en Python) dans le SDK IDA.
Dans le rapport sur les PHDays, les biais étaient un problème non résolu. Pour résoudre le problème, nous avons triché: décalage 32 bits uniquement à partir de la partie la plus jeune - sur la base. La base est calculée comme la partie la plus ancienne, décalée vers la gauche de 16 bits.

Avec cette approche, une référence croisée est formée uniquement avec la commande pour entrer la partie inférieure du décalage 32 bits.
La base est visible dans les propriétés de décalage et la propriété est marquée afin de la considérer comme un nombre, de sorte qu'un grand nombre de références croisées à l'adresse elle-même ne sont pas formées, que nous prenons comme base.

Dans le code de NIOS II, le mécanisme suivant est trouvé pour entrer des nombres 32 bits dans le registre. Tout d'abord, la partie la plus ancienne du décalage est entrée dans le registre avec la commande movhi. Puis la partie la plus jeune le rejoint. Cela peut être fait de trois manières (par des commandes): ajouter addi, soustraire subi, OR logique ori.
Par exemple, dans la section suivante du code, les registres sont définis sur des nombres de 32 bits, qui sont ensuite entrés dans des registres - arguments avant d'appeler la fonction.

Après avoir ajouté le calcul de décalage, nous obtenons la représentation suivante de ce bloc de code.

Le décalage 32 bits résultant s'affiche à côté de la commande pour entrer dans sa partie inférieure. Cet exemple est assez illustratif, et nous pourrions même facilement calculer tous les nombres 32 bits dans l'esprit en ajoutant simplement les parties mineures et les plus élevées. À en juger par les valeurs, il ne s'agit probablement pas de biais.
Considérez le cas où la soustraction est utilisée lors de l'entrée dans la partie la plus jeune. Dans cet exemple, il ne sera pas possible de déterminer les nombres finaux 32 bits (décalages) lors du déplacement.

Après avoir appliqué le calcul des nombres 32 bits, nous obtenons le formulaire suivant.

Ici, nous voyons que maintenant, si l'adresse est dans l'espace d'adressage, un décalage est formé dessus, et la valeur qui a été formée à la suite de la connexion des parties junior et senior n'est plus affichée à côté. Ici, ils ont obtenu un décalage de la ligne "22/10/08". Pour que le reste des décalages pointe vers des adresses valides, augmentons un peu le segment.

Après avoir augmenté le segment, nous obtenons que maintenant tous les nombres calculés sur 32 bits sont des décalages et indiquent des adresses valides.
Il a été mentionné ci-dessus qu'il existe une autre option pour calculer les décalages lorsqu'une commande logique OU est utilisée. Voici un exemple de code dans lequel deux décalages sont calculés de cette manière.

Celui qui est évalué dans le registre r8 est ensuite poussé sur la pile.
Après la conversion, on peut voir que dans ce cas les registres sont configurés aux adresses du début des procédures, c'est-à-dire que l'adresse de la procédure est poussée sur la pile.

Lecture et écriture par rapport à la base
Avant cela, nous avons considéré les cas où un nombre 32 bits entré à l'aide de deux commandes pouvait être juste un nombre et également un décalage. Dans l'exemple suivant, la base est entrée dans la partie supérieure du registre, puis la lecture ou l'écriture se produit par rapport à elle.

Après avoir traité de telles situations, nous obtenons le décalage des variables à partir des commandes de lecture et d'écriture elles-mêmes. De plus, selon la dimension de l'opération, la taille de la variable elle-même est fixée.

Constructions de commutation
Les constructions de commutateur trouvées dans les fichiers binaires peuvent faciliter l'analyse. Par exemple, par le nombre de cas de sélection dans la construction du commutateur, vous pouvez localiser le commutateur responsable du traitement d'un certain protocole ou système de commande. Par conséquent, la tâche se pose de reconnaître le commutateur lui-même et ses paramètres. Considérez la section de code suivante.

Le thread d'exécution s'arrête à la transition du registre jmp r2. De plus, il y a des blocs de code vers lesquels il y a des liens à partir des données, et à la fin de chaque bloc, il y a un saut vers la même étiquette. De toute évidence, il s'agit d'une construction de commutateur et ces blocs individuels en gèrent des cas spécifiques. Ci-dessus, vous pouvez également voir la vérification du nombre de cas et le saut par défaut.
Après avoir ajouté le traitement des commutateurs, ce code ressemblera à ceci.

Maintenant, le saut lui-même est indiqué, l'adresse de la table avec les décalages, le nombre de cas, ainsi que chaque cas avec le numéro correspondant.
Le tableau lui-même avec les décalages des options est le suivant. Pour économiser de l'espace, les cinq premiers éléments sont donnés.

En effet, le traitement du switch consiste à parcourir le code et à rechercher tous ses composants. Autrement dit, un schéma d'organisation du commutateur est décrit. Parfois, il peut y avoir des exceptions dans les régimes. Cela peut être la raison pour laquelle les commutateurs apparemment clairs ne sont pas reconnus dans les modules de processeur existants. Il s'avère que le véritable commutateur ne relève tout simplement pas du schéma défini à l'intérieur du module processeur. Il y a encore des options possibles lorsque le circuit semble être là, mais il y a d'autres équipes à l'intérieur qui ne sont pas impliquées dans le circuit, ou les équipes principales sont réorganisées, ou il est interrompu par des transitions.
Le module processeur NIOS II reconnaît un commutateur avec de telles instructions «étrangères» entre les commandes principales, ainsi qu'avec les emplacements réarrangés des commandes principales et avec des coupures de circuit. Un chemin de retour est utilisé le long du chemin d'exécution, en tenant compte des transitions possibles qui coupent le circuit, avec l'installation de variables internes qui signalent différents états du module de reconnaissance. Par conséquent, environ 10 options d'organisation de commutateur différentes trouvées dans le micrologiciel sont reconnues.
Instruction personnalisée
Il y a une fonctionnalité intéressante dans l'architecture NIOS II - l'instruction personnalisée. Il donne accès à 256 instructions définies par l'utilisateur qui sont possibles dans l'architecture NIOS II. Dans son travail, en plus des registres à usage général, l'instruction personnalisée peut accéder à un ensemble spécial de 32 registres personnalisés. Après avoir implémenté la logique d'analyse de la commande personnalisée, nous obtenons le formulaire suivant.

Vous pouvez remarquer que les deux dernières instructions ont le même numéro d'instruction et semblent effectuer les mêmes actions.
Selon les instructions personnalisées, il existe un
manuel séparé . Selon lui, l'une des options les plus complètes et les plus à jour pour le jeu d'instructions personnalisé est le jeu d'instructions NIOS II Floating Point Hardware 2 Component (FPH2) pour travailler avec la virgule flottante. Après avoir implémenté l'analyse des commandes FPH2, l'exemple ressemblera à ceci.

À partir des mnémoniques des deux dernières équipes, nous nous assurons qu'elles effectuent vraiment la même action - la commande fadds.
Transitions par valeur de registre
Dans le firmware étudié, une situation est souvent rencontrée lorsqu'un saut est effectué en fonction de la valeur du registre, dans lequel un décalage de 32 bits, qui détermine la place du saut, est entré auparavant.
Prenons un morceau de code.

Dans la dernière ligne, il y a un saut dans la valeur du registre, alors qu'il est clair qu'avant l'adresse de la procédure est entrée dans le registre, qui commence dans la première ligne de l'exemple. Dans ce cas, il est évident que le saut est fait à son début.
Après avoir ajouté la fonctionnalité de reconnaissance des sauts, le formulaire suivant est obtenu.

À côté de la commande jmp r8, l'adresse où le saut se produit s'il était possible de calculer s'affiche. Une référence croisée est également formée entre l'équipe et l'adresse où le saut a lieu. Dans ce cas, le lien est visible sur la première ligne, le saut lui-même s'effectue à partir de la dernière ligne.
Valeur du registre GP (pointeur global), sauvegarde et chargement
Il est courant d'utiliser un pointeur global configuré sur une adresse et les variables sont adressées par rapport à celle-ci. NIOS II utilise le registre gp (global pointer) pour stocker le pointeur global. À un moment donné, en règle générale, dans les procédures d'initialisation du micrologiciel, la valeur d'adresse est entrée dans le registre gp. Le module processeur gère cette situation; Pour illustrer cela, voici des exemples de code et la fenêtre de sortie IDA Pro lorsque les messages de débogage sont activés dans le module processeur.
Dans cet exemple, le module processeur recherche et calcule la valeur du registre gp dans la nouvelle base de données. Lors de la fermeture de la base de données idb, la valeur gp est stockée dans la base de données.

Lors du chargement d'une base de données idb existante et si la valeur gp a déjà été trouvée, elle est chargée à partir de la base de données, comme indiqué dans le message de débogage dans l'exemple suivant.

Lire et écrire sur gp
Les opérations courantes sont la lecture et l'écriture avec un décalage par rapport au registre gp. Par exemple, dans l'exemple suivant, trois lectures et un enregistrement de ce type sont effectués.

Puisque nous avons déjà obtenu la valeur de l'adresse qui est stockée dans le registre gp, nous pouvons traiter ce type de lecture et d'écriture.
Après avoir ajouté un traitement pour les situations de lecture et d'écriture par rapport au registre gp, nous obtenons une image plus pratique.

Ici, vous pouvez voir quelles variables sont accessibles, suivre leur utilisation et identifier leur objectif.
Adressage par rapport à gp
Il existe une autre utilisation du registre gp pour l'adressage des variables.

Par exemple, nous voyons ici que les registres sont configurés par rapport au registre gp pour certaines variables ou zones de données.
Après avoir ajouté une fonctionnalité qui reconnaît de telles situations, se convertit en décalages et ajoute des références croisées, nous obtenons le formulaire suivant.

Ici, vous pouvez déjà voir quelles zones relatives aux registres gp sont configurées, et il devient plus clair ce qui se passe.
Adressage par rapport à sp
De même, dans l'exemple suivant, les registres sont réglés sur certaines zones de mémoire, cette fois par rapport au registre sp - pointeur vers la pile.

De toute évidence, les registres sont réglés sur certaines variables locales. De telles situations - la définition d'arguments dans des tampons locaux avant les appels de procédure - sont assez courantes.
Après avoir ajouté le traitement (conversion des valeurs directes en décalages), nous obtenons le formulaire suivant.

Maintenant, il devient clair qu'après l'appel de procédure, les valeurs sont chargées à partir des variables dont les adresses ont été passées en paramètre avant l'appel de fonction.
Renvois du code aux champs de structure
La définition de structures et leur utilisation dans IDA Pro peuvent faciliter l'analyse de code.

En regardant cette partie du code, nous pouvons comprendre que le champ field_8 est incrémenté et, éventuellement, est un compteur de l'occurrence d'un événement. Si les champs de lecture et d'écriture sont séparés dans le code à une grande distance, les références croisées peuvent aider.
Considérez la structure elle-même.
Bien que l'accès aux domaines des structures soit, comme nous le voyons, il n'y a pas de références croisées du code aux éléments des structures.Après que de telles situations soient traitées, pour notre cas, tout ressemblera à ceci.
Il existe désormais des références croisées pour structurer les champs d'équipes spécifiques qui travaillent avec ces champs. Des références croisées avant et arrière sont créées, et vous pouvez suivre par diverses procédures où les valeurs des champs de structure sont lues et où elles sont entrées.Écarts entre manuel et réalité
Dans le manuel, lors du décodage de certaines commandes, certains bits doivent prendre des valeurs strictement définies. Par exemple, pour une commande de retour à partir d'une exception eret, les bits 22 à 26 doivent être 0x1E.
Voici un exemple de cette commande à partir d'un firmware.
En ouvrant un autre firmware dans un endroit avec un contexte similaire, nous rencontrons une situation différente.
Ces octets n'ont pas été automatiquement convertis en commande, bien qu'il y ait traitement de toutes les commandes. A en juger par l'environnement, et même une adresse similaire, il devrait s'agir de la même équipe. Examinons attentivement les octets. Il s'agit de la même commande eret, à l'exception du fait que les bits 22 à 26 ne sont pas égaux à 0x1E, mais égaux à zéro.Nous devons corriger un peu l'analyse de cette commande. Maintenant, il ne correspond pas tout à fait au manuel, mais il correspond à la réalité.
Prise en charge IDA 7
Depuis IDA 7.0, l'API fournie par Python IDA pour les scripts normaux a beaucoup changé. Quant aux modules processeurs, les changements sont colossaux. Malgré cela, le module processeur NIOS II a pu être refait pour la version 7, et il a fonctionné avec succès.
Seul moment incompréhensible: lors du chargement d'un nouveau fichier binaire sous NIOS II dans IDA 7, l'analyse automatique initiale présente dans IDA 6.9 ne se produit pas.Conclusion
En plus de la fonctionnalité de démontage de base, dont des exemples sont dans le SDK, le module processeur implémente de nombreuses fonctionnalités différentes qui facilitent le travail de l'explorateur de code. Il est clair que tout cela peut être fait manuellement, mais, par exemple, quand il y a des milliers et des dizaines de milliers de décalages de différents types sur un fichier binaire avec un firmware de quelques mégaoctets, pourquoi y consacrer du temps? Laissez le module processeur le faire pour nous. Après tout, comment sont les fonctionnalités agréables d'une navigation rapide à travers le code étudié en utilisant des références croisées! Cela fait de l'IDA un outil aussi pratique et agréable que nous le connaissons.Publié par Anton Dorfman, Positive Technologies