Analyse statique du BIOS / UEFI ou comment obtenir un graphique de dépendance

"J'ai fini de forger hier,
J'ai trompé deux plans ... "
... chanson VS Vysotsky ...

Il y a près de 3 ans (début 2016), le souhait d'un utilisateur est apparu sur la question du projet UEFITool sur GitHub: construire un «Dependency Graph» pour les modules exécutables inclus dans BIOS / UEFI.

Même une petite discussion s'en est suivie, à la suite de laquelle il est finalement devenu clair que cette tâche n'est nullement triviale, les fonctionnalités disponibles pour sa solution ne suffisent pas, les perspectives à ce moment sont brumeuses ...

Et cette question est restée dans les limbes, avec la perspective d'une réalisation dans un avenir indéfini (mais le désir est probablement resté, et l'espoir, comme vous le savez, meurt en dernier!).

Il y a une suggestion: enfin, trouvez une solution à ce problème!

Définissez les termes


On suppose en outre que nous avons affaire à l'architecture Intel 64 et IA-32.

Afin de déterminer sans ambiguïté ce que nous avons décidé de construire, nous devrons traiter plus en détail le fonctionnement des phases individuelles du BIOS / UEFI.

Si vous regardez attentivement les types de fichiers présentés dans les volumes de firmware FFS , il s'avère que la plupart des fichiers disponibles incluent une section avec des modules exécutables.

Même si nous considérons le nouveau firmware d'ASUS ou ASRock, dans lequel vous pouvez facilement trouver jusqu'à une centaine et demi de fichiers de type EFI_FV_FILETYPE_FREEFORM contenant des images de différents formats, néanmoins, même dans ces firmwares, il y a plus de fichiers exécutables que des fichiers d'autres types.

+--------------------------------------------------------------------------+ | File Types Information | +--------------------------------------------------------------------------+ | EFI_FV_FILETYPE_RAW = 6 | | EFI_FV_FILETYPE_FREEFORM = 83 | | EFI_FV_FILETYPE_SECURITY_CORE = 1 | | EFI_FV_FILETYPE_PEI_CORE = 1 | | EFI_FV_FILETYPE_DXE_CORE = 1 | | EFI_FV_FILETYPE_PEIM = 57 | | EFI_FV_FILETYPE_DRIVER = 196 | | EFI_FV_FILETYPE_APPLICATION = 1 | | EFI_FV_FILETYPE_SMM = 60 | | EFI_FV_FILETYPE_SMM_CORE = 1 | | EFI_FV_FILETYPE_PAD = 4 | +--------------------------------------------------------------------------+ | Total Files : = 411 | +--------------------------------------------------------------------------+ 
Un exemple de la composition d'un firmware ordinaire (ordinaire).

Bien que les fichiers contenant des modules exécutables ne soient pas marqués dans ce tableau, néanmoins, ils seront (par définition) tous dans cette liste, à l'exception des fichiers avec les suffixes RAW, FREEFORM et PAD.

Les fichiers avec le suffixe "CORE" (SECURITY_CORE, PEI_CORE et DXE_CORE) sont les "noyaux" correspondants (modules de tête de la phase correspondante) qui reçoivent le contrôle d'autres phases (ou après le démarrage), SMM_CORE est une sous-phase de la phase DXE et est appelée pendant celle-ci accomplissement. L'APPLICATION ne peut être effectuée qu'à la demande de l'utilisateur; elle n'a pas de liaison spécifique aux phases.

Les types de fichiers les plus courants n'étaient pas répertoriés: PEIM (modules de phase PEI), DRIVER (modules de phase DXE) et SMM (modules de sous-phase DXE). Les modules CORE des phases PEI et DXE comprennent un répartiteur qui contrôle la séquence des modules de chargement / démarrage de la phase correspondante.

Dans l'exemple ci-dessus, il n'y a pas d'options combinées, nous ne nous en souviendrons pas: bien qu'elles se trouvent dans un vrai firmware, c'est assez rare. Ceux qui souhaitent recevoir des informations plus détaillées et détaillées sont invités à se référer aux articles CodeRush 1 , 2 , 3 . Et citer également son conseil: «Pour les fans de la documentation originale, la spécification UEFI PI est toujours disponible, tout est décrit de manière beaucoup plus détaillée.»

Chaque module de micrologiciel exécutable est un module de format PE + (Portable Executable) ou son dérivé (Terse Executable: format TE). Le module exécutable au format PE + est un ensemble de données structurées «légèrement» compressées contenant les informations nécessaires au chargeur pour mapper ce module en mémoire.

Le format PE + (structure) lui-même n'a pas de mécanisme d'interaction entre les modules PE + individuels. Chaque module exécutable après le chargement et le démarrage de l'exécution est un processus indépendant et autonome (enfin, il devrait en être ainsi!) , C'est-à-dire le module ne doit rien «supposer» sur ce qui se fait en dehors de lui.

L'organisation de l'interaction entre des modules exécutables séparés d'une phase UEFI est organisée au moyen du module CORE de la phase correspondante. Les modules exécutables individuels peuvent définir (installer) des protocoles, demander (localiser) et utiliser des protocoles déclarés par d'autres modules, définir / déclarer des événements et déclarer (notifier) ​​des gestionnaires d'événements.

Ainsi, pour chaque module de firmware exécutable, nous nous intéressons à la présence des artefacts suivants:

  1. Liste des protocoles définis par ce module. (Chaque protocole est identifié par un numéro unique - guid).
  2. Liste des protocoles que ce module utilise (essaie d'utiliser).
  3. Liste des événements que ce module annonce. (L'événement a un numéro unique - guid).
  4. Une liste des gestionnaires d'événements présents (mis en œuvre et pouvant être installés / initialisés) dans ce module.
Un graphique de dépendance statique pour une phase BIOS / UEFI donnée est considéré comme défini si, pour chaque module de phase exécutable, nous connaissons tous les artefacts répertoriés ci-dessus dans les sections 1-4. (En d'autres termes, si nous avons défini toutes les informations qui décrivent les interdépendances entre les modules).
Nous ne considérerons que l'option d'analyse statique, cela signifie que certains éléments du code qui implémentent les éléments 1-4 peuvent être inaccessibles (sont des fragments du code "mort") ou ne seront réalisables qu'avec certaines options pour les données / paramètres d'entrée.

Tout ce que nous avons considéré jusqu'à présent est basé uniquement sur la spécification BIOS / UEFI . Et pour comprendre les «relations» des modules exécutables existants du firmware en question, nous devrons approfondir leur structure, ce qui signifie que nous devons au moins partiellement les inverser (restaurer les algorithmes originaux).

Comme déjà mentionné ci-dessus, le module exécutable au format PE + n'est qu'un ensemble de structures pour le chargeur, construisant en mémoire un objet vers lequel le contrôle sera transféré, et cet objet par sa nature se compose d'instructions de processeur, ainsi que de données pour ces instructions.
Nous dirons qu'un démontage complet du module exécutable a été effectué s'il était possible de résoudre le problème de séparation des commandes et des données présentées dans ce module.
En même temps, nous n'imposerons aucune exigence sur la structure et les types de données, il suffit que pour chaque octet appartenant à l'image du module exécutable reçu par le chargeur, nous pouvons clairement dire à laquelle des deux catégories il appartient: octet de commande ou octet de données.

La tâche de démonter complètement le module exécutable lui-même n'est généralement pas triviale, de plus, dans le cas général, elle n'est pas solvable sur le plan algorithmique. Nous n'entrerons pas dans les détails de cette question, ne cassons pas les lances non plus, nous considérons cette affirmation comme un axiome.

Supposons:

  1. Nous avons déjà résolu le problème du démontage complet d'un module d'exécution BIOS / UEFI spécifique, c'est-à-dire nous avons réussi à séparer les commandes et les données.
  2. Il y a le code source du module dans le langage «C» (dans le firmware BIOS / UEFI actuel, les modules sont principalement développés uniquement dans le langage «C»).

Même dans ce cas, la simple comparaison des résultats obtenus (le texte de l'assembleur n'est qu'une représentation textuelle des instructions du processeur) avec le code source en langage "C" nécessitera presque toujours une bonne expérience / qualification, à l'exception des cas absolument dégénérés.

Une étude complète d'exemples montrant des difficultés à identifier ou à comparer les résultats de désassemblage avec le code source ne fait pas partie de nos plans actuels.
Considérons seulement un exemple lorsque, dans la liste des assembleurs, nous rencontrons la commande «Appel indirect» - un appel de procédure implicite.

Voici un exemple d'appel de procédure référencé dans une table. Un tableau contenant des liens vers diverses procédures est un cas typique d'implémentation de la présentation des interfaces d'un protocole arbitraire.

Un tel tableau ne doit pas être composé uniquement de références à des procédures, personne n'interdit de stocker des données arbitraires dans cette structure (et ceci est un exemple de structure typique en «C»).

Voici une forme d'un tel appel (au lieu du registre ecx, presque toutes les variantes de registres de processeur 32 bits sont possibles):
FF 51 18 appel dword ptr [ecx + 18h]
Ayant, après analyse, une commande similaire, il est possible de comprendre quelle procédure est appelée, une liste de ses paramètres, le type et la valeur du résultat retourné, n'est possible que si nous connaissons le type d'objet (protocole) dont l'interface est appelée par cette commande.

Si nous savons que dans l'exemple précédent le registre «ecx» contient un pointeur (l'adresse du début de la table EFI_PEI_SERVICES), nous pouvons recevoir (présenter) cette commande de la manière la plus compréhensible et «agréable» suivante:
Appel FF 51 18 [exx + EFI_PEI_SERVICES.InstallPpi]
L'obtention d'informations sur le contenu du registre participant à la commande "Appel indirect" dépasse le plus souvent les capacités d'un désassembleur "typique", dont la tâche consiste simplement à analyser et à convertir le code binaire du processeur sous une forme lisible par l'homme - une représentation textuelle de la commande de processeur correspondante.

Pour résoudre ce problème, il est souvent nécessaire d'utiliser des informations (méta) supplémentaires qui ne sont pas disponibles dans le module exécutable binaire (perdues à la suite de la compilation et de la liaison - elles sont utilisées dans les transformations d'une représentation de l'algorithme à une autre, mais le processeur n'a plus besoin d'exécuter les commandes reçues).

Si ces métadonnées sont toujours disponibles pour nous à partir de sources supplémentaires, puis en les utilisant et en effectuant une analyse supplémentaire, nous obtenons une représentation plus compréhensible (et plus précise) de la commande «Appel indirect» .

En fait, cette analyse avancée rappelle déjà plus le processus de «décompilation», bien que le résultat ne ressemble pas au code source du module dans le langage «C», néanmoins, à l'avenir, nous parlerons de ce processus comme de la décompilation de commandes qui sont «Appel indirect» ou « décompilation partielle .

Nous sommes donc prêts à déterminer les conditions suffisantes pour construire le graphe de l'interdépendance des modules de firmware exécutables pour la phase BIOS / UEFI donnée:
Pour obtenir un graphe de dépendance statique (n'importe laquelle des phases - PEI ou DXE), il suffit de démonter complètement tous les modules exécutables de la phase correspondante (au moins séparer toutes les commandes), et de décompiler les commandes «Appel indirect» présentes dans les modules désassemblés.
Il y a immédiatement beaucoup de questions sur la manière dont notre connaissance des équipes «Appel indirect» est liée aux interactions inter-modules.
Comme mentionné ci-dessus, l'ensemble du service de gestion des interactions est fourni par le module «CORE» de la phase correspondante, et les services des phases sont conçus comme des tables de services «de base».

Étant donné que les modèles d'interaction entre les modules dans les phases PEI et DXE, bien que idéologiquement (structurellement) similaires, sont techniquement toujours différents, il est proposé de passer de quelques considérations formelles à l'examen d'une construction directe spécifique d'un graphe de dépendance statique pour la phase PEI.

Nous pourrons même déterminer et formuler les conditions nécessaires et suffisantes pour la possibilité de construire un graphe de dépendance statique pour la phase PEI.

Création d'un graphique de dépendance statique pour la phase PEI


Les descriptions de la solution au problème du démontage complet des modules exécutables de la phase PEI et de la décompilation des commandes d' appel indirect présentes dans ces modules dépassent le cadre de notre histoire et n'y seront pas données - la présentation de ce matériel en volume peut dépasser la taille de cet opus.

Il est possible qu'au fil du temps, cela se produise en tant que matériau distinct, mais pour l'instant - sachez comment.

Nous notons seulement que l'utilisation de métadonnées, plus la présence d'une certaine structure pour construire du code binaire, permet en pratique de démonter complètement les modules BIOS / UEFI exécutables. La preuve formelle de ce fait n'est pas supposée maintenant ni à l'avenir. Au moins dans l'analyse / le traitement de plus de cent (100) BIOS / UEFI de divers fabricants, il n'y avait aucun exemple où un démontage complet n'était pas possible.

De plus, seuls des résultats spécifiques (avec explications: quoi, comment et combien ...).

La structure EFI_PEI_SERVICES est la structure de base de la phase PEI, qui est transmise en paramètre au point d'entrée de chaque module PEI et contient des liens vers les services de base nécessaires au fonctionnement des modules PEI.

Nous ne nous intéresserons qu'aux domaines situés au tout début de la structure:



Un fragment d'une structure réelle de type EFI_PEI_SERVICES dans le désassembleur IDA Pro.

Et voici comment cela apparaît dans le code source en langage "C" (rappelez-vous, ce n'est qu'un fragment de la structure):

 struct EFI_PEI_SERVICES { EFI_TABLE_HEADER Hdr; EFI_PEI_INSTALL_PPI InstallPpi; EFI_PEI_REINSTALL_PPI ReInstallPpi; EFI_PEI_LOCATE_PPI LocatePpi; EFI_PEI_NOTIFY_PPI NotifyPpi; //...      ... }; 

Au début de la structure EFI_PEI_SERVICES, comme dans toutes les tables de services "de base" (Tables de services), se trouve la structure EFI_TABLE_HEADER. Les valeurs présentées dans cette structure d'en-tête nous permettent d'affirmer sans équivoque que si la structure EFI_PEI_SERVICES elle-même est effectivement présente sur le fragment du désassembleur (voir le champ «Hdr.Signature»), alors au moins le modèle de cette structure!

 struct EFI_TABLE_HEADER { UINT64 Signature; UINT32 Revision; UINT32 HeaderSize; UINT32 CRC32; UINT32 Reserved; }; 

En cours de route, nous pouvons établir que le micrologiciel était en cours de développement à un moment où la version de la spécification UEFI PI était de 1,2, dont la période de pertinence était de 2009 à 2013, mais pour le moment (début 2019), la version actuelle de la spécification a déjà grandi (littéralement développée l'autre jour) à la version 1.7.

Dans le champ "Hdr.HeaderSize", vous pouvez déterminer que la longueur totale de la structure est de 78h (et ce n'est pas la longueur de l'en-tête, comme son nom l'indique, mais la longueur de la structure entière de EFI_PEI_SERVICES).

Les interfaces EFI_PEI_SERVICES sont divisées en 7 catégories / classes. Nous les énumérons simplement:

  1. Services PPI.
  2. Services en mode de démarrage.
  3. Services HOB.
  4. Services de volume du micrologiciel.
  5. Services de mémoire de l'Î.-P.-É.
  6. Services de code d'état.
  7. Réinitialiser les services.

Toute autre narration sera directement liée aux procédures appartenant à la catégorie / classe PPI Services, destinées à l'organisation de l'interaction inter-modules des modules exécutables de la phase PEI.

Et il n'y en a que quatre pour la phase PEI.

En général, il n'est pas nécessaire de deviner le but de chacune des interfaces: la fonctionnalité est complètement déterminée par le nom de l'interface, tous les détails sont dans la spécification .

Voici les prototypes de ces procédures:

 typedef EFI_STATUS (__cdecl *EFI_PEI_INSTALL_PPI)( const EFI_PEI_SERVICES **PeiServices, const EFI_PEI_PPI_DESCRIPTOR *PpiList); typedef EFI_STATUS (__cdecl *EFI_PEI_REINSTALL_PPI)( const EFI_PEI_SERVICES **PeiServices, const EFI_PEI_PPI_DESCRIPTOR *OldPpi, const EFI_PEI_PPI_DESCRIPTOR *NewPpi); typedef EFI_STATUS (__cdecl *EFI_PEI_LOCATE_PPI)( const EFI_PEI_SERVICES **PeiServices, const EFI_GUID *Guid, UINTN Instance, EFI_PEI_PPI_DESCRIPTOR **PpiDescriptor, void **Ppi); typedef EFI_STATUS (__cdecl *EFI_PEI_NOTIFY_PPI)( const EFI_PEI_SERVICES **PeiServices, const EFI_PEI_NOTIFY_DESCRIPTOR *NotifyList); 

Nous notons seulement qu'en plus des commandes "Appel indirect" qui appellent les procédures / interfaces de la classe "Services PPI", un appel explicite (direct - non tabulaire) à ces procédures est possible, ce qui se produit parfois dans les modules exécutifs, où la structure EFI_PEI_SERVICES est définie / créée.

Je vais vous dire un petit secret: curieusement, bien que ce soit la table de services «de base» pour la phase PEI, cependant, comme le montre la pratique, elle peut être définie non seulement dans le module PEI_CORE.

En réalité, il existe des firmwares dans lesquels la structure EFI_PEI_SERVICES a été définie / formée et utilisée dans plusieurs modules, et ceux-ci n'étaient en aucun cas des copies du module PEI_CORE.

Ainsi, les options de code suivantes sont possibles:

 seg000:00785F0D B8 8C A6 78+ mov eax, offset ppiList_78A68C seg000:00785F12 50 push eax ; PpiList seg000:00785F13 57 push edi ; PeiServices seg000:00785F14 89 86 40 0E+ mov [esi+0E40h], eax seg000:00785F1A E8 70 FC FF+ call InstallPpi 

Un exemple d'appel explicite à la procédure "InstallPpi".

 seg000:00787CBB 8B 4D FC mov ecx, [ebp+PeiServices] seg000:00787CBE 50 push eax ; PpiList seg000:00787CBF C7 00 10 00+ mov dword ptr [eax], 80000010h seg000:00787CC5 C7 43 3C A8+ mov dword ptr [ebx+3Ch], offset guid_78A9A8 seg000:00787CCC 8B 11 mov edx, [ecx] seg000:00787CCE 51 push ecx ; PeiServices seg000:00787CCF FF 52 18 call [edx+EFI_PEI_SERVICES.InstallPpi] 

Un exemple d'appel implicite à l'interface InstallPpi.

 FF 51 18 call dword ptr [ecx+18h] FF 51 18 call [ex+EFI_PEI_SERVICES.InstallPpi] FF 51 1 call dword ptr [ecx+1Ch] FF 51 1C call [ex+EFI_PEI_SERVICES.ReInstallPpi] FF 51 20 call dword ptr [ecx+20h] FF 51 20 call [ex+EFI_PEI_SERVICES.LocatePpi] FF 51 24 call dword ptr [ecx+24h] FF 51 24 call [ex+EFI_PEI_SERVICES.NotifyPpi] 
Exemples d'appels d'interface implicites avant et après l'authentification.

On note une caractéristique: dans le cas de la phase PEI pour l'architecture IA-32, les interfaces de la classe PPI Services ont des décalages de 18h, 1Ch, 20h et 24h.

Et maintenant, nous déclarons la déclaration suivante:
Pour construire un graphe de dépendance statique de la phase PEI, il est nécessaire et suffisant de démonter complètement tous les modules exécutables de la phase (au moins séparer toutes les commandes), et de décompiler les commandes «Appel indirect» avec les décalages 18h, 1Ch, 20h, 24h dans les modules désassemblés.
En fait, nous avons entièrement formulé un algorithme pour résoudre le problème, et dès que nous avons réussi à isoler tous les appels aux interfaces / procédures de la classe PPI Services, il ne reste plus qu'à déterminer quels paramètres sont passés à ces appels. La tâche n'est peut-être pas la plus triviale, mais, comme la pratique l'a montré, elle est complètement résoluble, nous avons toutes les données pour cela.

Et maintenant de vrais exemples de données réelles pour de vrais modules en phase PEI. Nous n'indiquons pas sciemment les résultats BIOS / UEFI de l'entreprise qui ont été obtenus, nous donnons simplement des exemples de leur apparence.

Deux exemples de descriptions de module PEIM avec des informations complètes sur l'utilisation des interfaces des services PPI dans ces modules


  -- File 04-047/0x02F/: "TcgPlatformSetupPeiPolicy" : [007CCAF0 - 007CD144] DEPENDENCY_START EFI_PEI_READ_ONLY_VARIABLE_ACCESS_PPI DEPENDENCY_END Install Protocols: [1] TCG_PLATFORM_SETUP_PEI_POLICY Locate Protocols: [2] EFI_PEI_READ_ONLY_VARIABLE_ACCESS_PPI 
 -- File 04-048/0x030/: "TcgPei" : [007CD160 - 007CF5DE] DEPENDENCY_START EFI_PEI_MASTER_BOOT_MODE_PEIM_PPI EFI_PEI_READ_ONLY_VARIABLE_ACCESS_PPI AND DEPENDENCY_END Install Protocols: [1] AMI_TCG_PLATFORM_PPI [2] EFI_PEI_TCG_PPI [2] PEI_TPM_PPI Locate Protocols: [1] EFI_PEI_TCG_PPI [1] EFI_PEI_READ_ONLY_VARIABLE_ACCESS_PPI [1] TCG_PLATFORM_SETUP_PEI_POLICY [5] PEI_TPM_PPI Notify Events: [1] AMI_TCM_CALLBACK ReInstall Protocols: [1] PEI_TPM_PPI 

Listes de protocoles par types d'interfaces dans lesquels ils ont été utilisés


Sous les spoilers se trouvent ci-dessous des exemples abrégés de listes de protocoles PPIM pour chacune des interfaces de la classe Services PPI.

Le format des listes est le suivant:
 |  numéro de série |  name_PPI |  guid_PPI |  nom_exécutable: nom d'utilisateur |

***** Installez 99 Ppi dans "Firmware"


***** Localisez 194 Ppi dans "Firmware"


***** Réinstallez 5 Ppi dans "Firmware"


***** Avertissez 29 Ppi dans "Firmware"


La liste finale de tous les guides des protocoles référencés dans un BIOS / UEFI particulier avec une légende indiquant dans quels "services PPI" ces protocoles se trouvent


Vous trouverez ci-dessous une liste de spoilers de 97 PPi-guids trouvés et explicitement utilisés dans un firmware spécifique, dont les données ont été fournies plus tôt.

Chaque élément de la liste est précédé d'une légende, qui reflète tous les types d'utilisation d'un protocole particulier.

 "D" - in DEPENDENCY section used "I" - in "InstallPpi" functions used "L" - in "LocatePpi" functions used "R" - in "ReInstallPpi" functions used "N" - in "NotifyPpi" functions used 

***** Liste Ppi dans "Firmware"




Les intervalles de liste de protocoles suivants sont à noter dans ce BIOS / UEFI:

  1. N ° 38-50.
    Définition de protocoles / événements (InstallPpi) qui ne sont utilisés par aucun module.
  2. No. 87-95.
    Essayez de demander des protocoles qui n'ont été installés par aucun module de ce micrologiciel.
  3. N ° 96-97.
    Deux événements «Notify», pour lesquels aucun module n'a pris la peine de déclarer l'interface correspondante, respectivement, bien que ces procédures soient déclarées dans des modules exécutables, elles ne fonctionneront jamais.

Conclusion


  • Des résultats similaires à ceux ci-dessus ont été obtenus pour le BIOS / UEFI de divers fabricants, c'est pourquoi tous les exemples sont anonymes.
  • En fait, des tâches plus générales d'inversion des algorithmes des modules BIOS / UEFI exécutables ont été résolues, et le graphique résultant est un résultat secondaire, une sorte de bonus supplémentaire.
  • La solution correcte de la tâche «Obtention d'un graphique de dépendance statique» pour les modules BIOS / UEFI exécutables nécessite une analyse statique du code binaire, qui comprend un démontage complet des modules exécutables et une décompilation partielle des commandes d' appel indirect de ces modules.

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


All Articles