Tutorial 6: La Table des Importations
(Import Table)

Dans ce Tutorial nous allons voir la table des importations. Laissez-moi vous avertir d'abord, ce Tutorial est long et difficile pour ceux qui ne connaissent rien à la table des importations. Il y a des chances que vous deviez relire ce Tutorial plusieurs fois et deviez même examiner les structures relatives aux debuggers.

Downloadez l'exemple.

Théorie:

Tout d'abord, vous devez savoir ce qu'est une fonction d'importation. Une fonction d'importation est une fonction qui n'est pas directement dans le programme, mais est appelée par le programme, d'où le nom "import". Les fonctions d'importation résident en réalité dans une ou plusieurs DLLs. Seules les informations à propos de ces fonctions sont contenue dans le programme qui fait appel à la DLL dans laquelle est contenue le fonction d'importation. Ces informations sont entre autres le nom de chaque fonction et le nom de la (ou les) DLL(s) dans lesquels elles ont été placées.
Maintenant comment pouvons-nous découvrir à quel endroit dans le PE file (le programme) se situent ces informations ? Nous devons nous tourner vers le Répertoire des Données pour trouver la réponse. Je vous rafraîchi un peu la mémoire. Ci-dessous le PE header :

IMAGE_NT_HEADERS STRUCT
   Signature dd ?
   FileHeader IMAGE_FILE_HEADER <>
   OptionalHeader IMAGE_OPTIONAL_HEADER <>
IMAGE_NT_HEADERS ENDS

Le dernier membre de l'Optional header est le répertoire des données:

IMAGE_OPTIONAL_HEADER32 STRUCT
   ....
   LoaderFlags dd ?
   NumberOfRvaAndSizes dd ?
   DataDirectory IMAGE_DATA_DIRECTORY 16 dup(<>)
IMAGE_OPTIONAL_HEADER32 ENDS

Le 'répertoire des données' est un tableau, ce tableau est en faite une structure du nom d'IMAGE_DATA_DIRECTORY. Elle possède 16 membres (ou paramètres). Si vous vous rappelez de la table des sections qu'on avait comparés à un répertoire racine pour les sections du PE file, et bien vous pouvez de la même manière comparer le 'répertoire des données' à un répertoire racine des composants logiques stockés à l'intérieur de ces sections. Pour être plus précis, le 'répertoire des données' contient les emplacements et les tailles des structures de données importantes à l'intérieur du PE file. Chaque membre contient des informations sur une structure de données importante.

Membre
Info qu'il contient
0
Export symbols
1
Import symbols
2
Resources
3
Exception
4
Security
5
Base relocation
6
Debug
7
Copyright string
8
Unknown
9
Thread local storage (TLS)
10
Load configuration
11
Bound Import
12
Import Address Table
13
Delay Import
14
COM descriptor

Je n'ai des connaissances qu'uniquement sur les membres surlignés. Maintenant que vous connaissez ce que contient chaque membre du 'répertoire des données', nous pouvons regarder en détail le membre. Chaque membre du 'répertoire des données' est une structure appelée IMAGE_DATA_DIRECTORY laquelle est défie comme suit :

IMAGE_DATA_DIRECTORY STRUCT
  VirtualAddress dd ?
  isize dd ?
IMAGE_DATA_DIRECTORY ENDS

VirtualAddress est en réalité l'adresse relative virtuelle (RVA) de la structure de données (Data Structure). Par exemple, si cette structure est destinée aux Symboles d'Importation, alors ce membre contient le RVA du tableau IMAGE_IMPORT_DESCRIPTOR.
isize contient la taille en octets de la structure de données mentionnée par VirtualAddress .

Voici l'arrangement général des différentes structures de données importantes dans un PE file:

  1. A partir du DOS header, vous allez au PE header
  2. Récupérez l'adresse du 'Répertoire des Données' dans l'Optional Header.
  3. Multipliez la taille d'IMAGE_DATA_DIRECTORY par l'index du membre que vous voulez : par exemple si vous voulez savoir où les symboles d'importation se situent, vous devez multiplier la taille d'IMAGE_DATA_DIRECTORY (8 octets) par 1.
  4. Ajoutez le résultat à l'adresse du 'Répertoire des données' et vous obtenez ainsi l'adresse de la structure IMAGE_DATA_DIRECTORY qui contient les renseignements sur la 'structure de données' désirée.

Maintenant on arrête de jouer, on va pouvoir vraiment discuter de la table des importations. L'adresse de la table des importations est contenue dans le membre (paramètre) VirtualAddress du deuxième membre du Répertoire des Données. La table des importations est en réalité un tableau contenant plusieures structures IMAGE_IMPORT_DESCRIPTOR. Chaque structure contient des informations sur une DLL dont le PE File importe ses symboles. Par exemple, si le PE File importe 10 fonctions appartenant à 10 DLLs différentes, alors nous retrouverons 10 membres dans ce tableau. Le dernier membre de ce tableau qui sert à terminé le tableau ne contient que des zéros. Maintenant nous pouvons examiner la structure en détail :

IMAGE_IMPORT_DESCRIPTOR STRUCT
  union
    Characteristics dd ?
    OriginalFirstThunk dd ?
  ends
  TimeDateStamp dd ?
  ForwarderChain dd ?
  Name1 dd ?
  FirstThunk dd ?
IMAGE_IMPORT_DESCRIPTOR ENDS

Le premier membre de cette structure est une union. En réalité, l'union fournit seulement le pseudonyme d'OriginalFirstThunk, de cette façon vous pouvez appeler "Characteristics". Ce membre contient le RVA d'un tableau contenant des structures IMAGE_THUNK_DATA.
Qu'est-ce que IMAGE_THUNK_DATA ? C'est une union de taille dword. D'habitude, nous l'interprétons comme le pointer portant sur une structure IMAGE_IMPORT_BY_NAME. Remarquez qu'IMAGE_THUNK_DATA contient le pointer qui est sur une structure IMAGE_IMPORT_BY_NAME : et non pas sur la structure elle-même.
Regardez sa logique : Il y a plusieurs structures IMAGE_IMPORT_BY_NAME. Nous rassemblons dans un tableau le RVA de ces structures (IMAGE_THUNK_DATAs), et le terminons avec un 0. Ensuite nous mettons le RVA du tableau (lui-même) dans OriginalFirstThunk.
La structure IMAGE_IMPORT_BY_NAME contient les informations à propos d'une fonction d'importation. Maintenant regardons à quoi ressemble la structure IMAGE_IMPORT_BY_NAME:

IMAGE_IMPORT_BY_NAME STRUCT
  Hint dw ?
  Name1 db ?
IMAGE_IMPORT_BY_NAME ENDS

Hint contient l'index de la fonction, cet index réside dans 'la table d'exportations de la DLL'. Ce membre sert uniquement à l'utilisation du PE Loader. Ainsi le PE Loader peut rapidement retrouver la fonction à partir de la table d'exportation de la DLL. Ce n'est pas une valeur essentielle et il se peut même que certains linkers mette la valeur de ce paramètre à 0.
Name1 contient le nom de la fonction d'importation. Le nom est une chaîne de caractères ASCIIZ. Notez que la taille de Name1 est par défaut d'un octet mais en réalité c'est un paramètre de taille variable. C'est juste qu'il n'y a aucune façon de représenter un paramètre de taille variable dans une structure. Cette structure nous est gracieusement fournit, ainsi nous pouvons nous référer à la 'Structure de Données' avec des noms en guise de descriptifs.

TimeDateStamp et ForwarderChain sont des machins avancés : Nous parlerons d'eux une foie que vous aurez bien pris la main avec les autres membres.

Name1 contient le RVA du nom de la DLL, bref, le pointer qui est sur le nom de la DLL. C'est une chaîne de caractères ASCIIZ.

FirstThunk est très semblable à OriginalFirstThunk, c'est-à-dire. Il contient le RVA d'un tableau contenant des structures IMAGE_THUNK_DATA (bien qu'en fait c'est un tableau différent).
Ok, si vous êtes encore embrouillés, regardez ça : Il y a plusieurs structures IMAGE_IMPORT_BY_NAME. Vous créez deux tableaux, les remplissez ensuite avec les RVAs de ces structures IMAGE_IMPORT_BY_NAME, donc les deux tableaux contiennent exactement les mêmes valeurs (c'est-à-dire des duplicatas exactes). Maintenant vous assignez le RVA du premier tableau à OriginalFirstThunk et le RVA du deuxième tableau à FirstThunk .

OriginalFirstThunk IMAGE_IMPORT_BY_NAME FirstThunk

|

 
 
|
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
...
IMAGE_THUNK_DATA
--->
--->
--->
--->
--->
--->
Fonction 1
Fonction 2
Fonction 3
Fonction 4
...
Fonction n
<---
<---
<---
<---
<---
<---
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
...
IMAGE_THUNK_DATA

Maintenant vous devez être capables de comprendre ce que je veux dire. Ne soyez pas intimidé par le nom IMAGE_THUNK_DATA : c'est seulement un RVA dans la structure IMAGE_IMPORT_BY_NAME. Si vous remplacez le mot IMAGE_THUNK_DATA par RVA dans votre esprit, ça vous semblera peut-être plus clairement. Le nombre d'éléments dans les tableaux OriginalFirstThunk et FirstThunk dépend des Fonctions que le PE file importe de la DLL. Par exemple, si le PE file importe 10 Fonctions de kernel32.DLL, alors Name1 appartenant à la structure IMAGE_IMPORT_DESCRIPTOR contiendra le RVA de la chaîne de caractères "kernel32.DLL" et il y aura 10 IMAGE_THUNK_DATAs dans chaque tableau.
La question suivante c'est : pourquoi avons-nous besoin de deux tableaux qui sont identiques trait pour trait ? En réponse à cette question, une fois que le PE file est chargé en mémoire nous avons besoin de savoir ce qu'il regardera dans les IMAGE_THUNK_DATAs et IMAGE_IMPORT_BY_NAMEs, il déterminera les adresses des Fonctions d'importation. Ainsi il remplace les IMAGE_THUNK_DATAs dans le tableau indiqué par FirstThunk avec les adresses réelles des Fonctions. Ainsi lorsque le PE file est prêt à être exécuté, la susdite image est déjà modifiée :

OriginalFirstThunk   IMAGE_IMPORT_BY_NAME   FirstThunk

|

 
 
|
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
...
IMAGE_THUNK_DATA
--->
--->
--->
--->
--->
--->
Fonction 1
Fonction 2
Fonction 3
Fonction 4
...
Fonction n
   
 
 
 
 
 
Adresse de la Fonction 1
Adresse de la Fonction 2
Adresse de la Fonction 3
Adresse de la Fonction 4
...
Adresse de la Fonction n

Le tableau des RVAs indiqué par OriginalFirstThunk reste inchangé pour que si le besoin s'en fasse sentir il soit possible de retrouver les noms des fonctions importées, le PE loader peut toujours les retrouver.
Il y a une petite entorse à ce schéma *simple et directe*. Certaines fonctions sont uniquement exportées suivant un ordre. Ça signifie que vous n'appelez pas les fonctions par leurs noms : vous les appelez par leurs positions. Dans ce cas, il n'y aura aucune structure IMAGE_IMPORT_BY_NAME pour ces fonctions dans le 'module Caller' (le programme principale qui fait appelle à une DLL pour se servir de ses fonctions (en les important)). Au lieu de ça, l'IMAGE_THUNK_DATA pour cette fonction contiendra le n° de position de la fonction dans le WORD de poids faible et le 'bit de poids fort' (MSB ou the most significant bit) d'IMAGE_THUNK_DATA sera mis à 1. Par exemple, si une fonction est exportée uniquement par ordre et que son n° est 1234h, l'IMAGE_THUNK_DATA pour cette fonction sera 80001234h. Microsoft fournit une constante très pratique pour tester l'état du MSB d'un dword, IMAGE_ORDINAL_FLAG32. Sa valeur est de 80000000h. (=poids fort à 1)
Supposons que nous voulons inscrire les noms de TOUTES les fonctions d'importation du PE file, nous avons besoin de suivre les étapes ci-dessous :

  1. On vérifie que le fichier est un PE File valide
  2. A partir du DOS header, on va au PE header
  3. On récupère l'adresse du Répertoire des Données présent dans l'OptionalHeader
  4. On passe au 2e member du Répertoire des Données. Et on extrait la valeur de VirtualAddress
  5. On se sert de cette valeur pour aller à la première structure IMAGE_IMPORT_DESCRIPTOR
  6. On vérifie la valeur d'OriginalFirstThunk. S'il n'est pas à zéro, on passe au RVA suivant d'OriginalFirstThunk dans le tableau de RVA. Si OriginalFirstThunk est nul (zéro), on utilise la valeur dans FirstThunk à sa place. Certains linkers génèrent des PE File avec 0 dans OriginalFirstThunk. C'est considéré comme un bogue(défaut). Juste pour rester sur le droit chemin, nous vérifions d'abord la valeur dans OriginalFirstThunk.
  7. Pour chaque membre du tableau, nous comparons la valeur de ce membre avec IMAGE_ORDINAL_FLAG32. Si le MSB de ce membre est a 1, alors la fonction est exportée par ordre et nous pouvons extraire le numéro d'ordre à partir du WORD de poids faible du membre.
  8. Si le bit le plus significatif (MSB) du membre est a 0, on utilise la valeur dans le membre en tant qu'RVA dans l'IMAGE_IMPORT_BY_NAME, on saute à Hint et on est au nom de la fonction.
  9. On saute au membre suivant du tableau et on retrouve les noms avant qu'on ai atteint la fin du tableau (qui est un null de terminaison). Maintenant nous avons réussi à extraire les noms des fonctions importées à partir d'une DLL, nous allons à la DLL suivante.
  10. On saute à l'IMAGE_IMPORT_DESCRIPTOR suivant et le traitons. Faites-le avant que la fin du tableau ne soit atteinte (le tableau IMAGE_IMPORT_DESCRIPTOR est terminé par un membre dont les paramètres sont tous nuls) .

Exemple:

Cet exemple ouvre un PE file et lit puis affiche les noms de toutes les fonctions d'importation de ce fichier dans un contrôle d'édition. Il affiche aussi les valeurs dans les structures IMAGE_IMPORT_DESCRIPTOR.

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib

IDD_MAINDLG equ 101
IDC_EDIT equ 1000
IDM_OPEN equ 40001
IDM_EXIT equ 40003

DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD
ShowImportFunctions proto :DWORD
ShowTheFunctions proto :DWORD,:DWORD
AppendText proto :DWORD,:DWORD

SEH struct
PrevLink dd ? ; adresse de la structure SEH ci-dessus
CurrentHandler dd ? ; adresse du nouvel 'exception handler'
SafeOffset dd ? ; endroit où est sauvegardé l'offset pour continuer l'exécution
PrevEsp dd ? ; l'ancienne valeur dans 'esp'
PrevEbp dd ? ; l'ancienne valeur dans 'ebp'
SEH ends

.data
AppName db "PE tutorial no.6",0
ofn OPENFILENAME <>
FilterString db "Executable Files (*.exe, *.dll)",0,"*.exe;*.dll",0
             db "All Files",0,"*.*",0,0
FileOpenError db "Cannot open the file for reading",0
FileOpenMappingError db "Cannot open the file for memory mapping",0
FileMappingError db "Cannot map the file into memory",0
NotValidPE db "This file is not a valid PE",0
CRLF db 0Dh,0Ah,0
ImportDescriptor db 0Dh,0Ah,"================[ IMAGE_IMPORT_DESCRIPTOR ]=============",0
IDTemplate db "OriginalFirstThunk = %lX",0Dh,0Ah
           db "TimeDateStamp = %lX",0Dh,0Ah
           db "ForwarderChain = %lX",0Dh,0Ah
           db "Name = %s",0Dh,0Ah
           db "FirstThunk = %lX",0
NameHeader db 0Dh,0Ah,"Hint Function",0Dh,0Ah
           db "-----------------------------------------",0
NameTemplate db "%u %s",0
OrdinalTemplate db "%u (ord.)",0

.data?
buffer db 512 dup(?)
hFile dd ?
hMapping dd ?
pMapping dd ?
ValidPE dd ?

.code
start:
invoke GetModuleHandle,NULL
invoke DialogBoxParam, eax, IDD_MAINDLG,NULL,addr DlgProc, 0
invoke ExitProcess, 0

DlgProc proc hDlg:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
.if uMsg==WM_INITDIALOG
  invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_SETLIMITTEXT,0,0
.elseif uMsg==WM_CLOSE
  invoke EndDialog,hDlg,0
.elseif uMsg==WM_COMMAND
  .if lParam==0
    mov eax,wParam
    .if ax==IDM_OPEN
      invoke ShowImportFunctions,hDlg
    .else ; IDM_EXIT
      invoke SendMessage,hDlg,WM_CLOSE,0,0
    .endif
  .endif
.else
  mov eax,FALSE
  ret
.endif
mov eax,TRUE
ret
DlgProc endp

SEHHandler proc C pExcept:DWORD, pFrame:DWORD, pContext:DWORD, pDispatch:DWORD
  mov edx,pFrame
  assume edx:ptr SEH
  mov eax,pContext
  assume eax:ptr CONTEXT
  push [edx].SafeOffset
  pop [eax].regEip
  push [edx].PrevEsp
  pop [eax].regEsp
  push [edx].PrevEbp
  pop [eax].regEbp
  mov ValidPE, FALSE
  mov eax,ExceptionContinueExecution
  ret
SEHHandler endp

ShowImportFunctions proc uses edi hDlg:DWORD
  LOCAL seh:SEH
  mov ofn.lStructSize,SIZEOF
  ofn mov ofn.lpstrFilter, OFFSET FilterString
  mov ofn.lpstrFile, OFFSET buffer
  mov ofn.nMaxFile,512
  mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY
  invoke GetOpenFileName, ADDR ofn
  .if eax==TRUE
    invoke CreateFile, addr buffer, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL
    .if eax!=INVALID_HANDLE_VALUE
      mov hFile, eax
      invoke CreateFileMapping, hFile, NULL, PAGE_READONLY,0,0,0
      .if eax!=NULL
        mov hMapping, eax
        invoke MapViewOfFile,hMapping,FILE_MAP_READ,0,0,0
        .if eax!=NULL
          mov pMapping,eax
          assume fs:nothing
          push fs:[0]
          pop seh.PrevLink
          mov seh.CurrentHandler,offset SEHHandler
          mov seh.SafeOffset,offset FinalExit
          lea eax,seh
          mov fs:[0], eax
          mov seh.PrevEsp,esp
          mov seh.PrevEbp,ebp
          mov edi, pMapping
          assume edi:ptr IMAGE_DOS_HEADER
          .if [edi].e_magic==IMAGE_DOS_SIGNATURE
            add edi, [edi].e_lfanew
            assume edi:ptr IMAGE_NT_HEADERS
            .if [edi].Signature==IMAGE_NT_SIGNATURE
              mov ValidPE, TRUE
            .else
              mov ValidPE, FALSE
            .endif
          .else
            mov ValidPE,FALSE
          .endif
FinalExit:
          push seh.PrevLink
          pop fs:[0]
          .if ValidPE==TRUE
            invoke ShowTheFunctions, hDlg, edi
          .else
            invoke MessageBox,0, addr NotValidPE, addr AppName, MB_OK+MB_ICONERROR
          .endif
          invoke UnmapViewOfFile, pMapping
      .else
          invoke MessageBox, 0, addr FileMappingError, addr AppName, MB_OK+MB_ICONERROR
      .endif
      invoke CloseHandle,hMapping
    .else
      invoke MessageBox, 0, addr FileOpenMappingError, addr AppName, MB_OK+MB_ICONERROR
    .endif
    invoke CloseHandle, hFile
   .else
   invoke MessageBox, 0, addr FileOpenError, addr AppName, MB_OK+MB_ICONERROR
   .endif
 .endif
 ret
ShowImportFunctions endp

AppendText proc hDlg:DWORD,pText:DWORD
   invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_REPLACESEL,0,pText
   invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_REPLACESEL,0,addr CRLF
   invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_SETSEL,-1,0
   ret
AppendText endp

RVAToOffset PROC uses edi esi edx ecx pFileMap:DWORD,RVA:DWORD
   mov esi,pFileMap
   assume esi:ptr IMAGE_DOS_HEADER
   add esi,[esi].e_lfanew
   assume esi:ptr IMAGE_NT_HEADERS
   mov edi,RVA ; edi == RVA
   mov edx,esi
   add edx,sizeof IMAGE_NT_HEADERS
   mov cx,[esi].FileHeader.NumberOfSections
   movzx ecx,cx
   assume edx:ptr IMAGE_SECTION_HEADER
   .while ecx>0 ; check all sections
     .if edi>=[edx].VirtualAddress
       mov eax,[edx].VirtualAddress
       add eax,[edx].SizeOfRawData
       .if edi<eax ; The address is in this section
         mov eax,[edx].VirtualAddress
         sub edi,eax
         mov eax,[edx].PointerToRawData
         add eax,edi ; eax == file offset
         ret
       .endif
     .endif
     add edx,sizeof IMAGE_SECTION_HEADER
     dec ecx
   .endw
   assume edx:nothing
   assume esi:nothing
   mov eax,edi
   ret
RVAToOffset endp

ShowTheFunctions proc uses esi ecx ebx hDlg:DWORD, pNTHdr:DWORD
   LOCAL temp[512]:BYTE
   invoke SetDlgItemText,hDlg,IDC_EDIT,0
   invoke AppendText,hDlg,addr buffer
   mov edi,pNTHdr
   assume edi:ptr IMAGE_NT_HEADERS
   mov edi, [edi].OptionalHeader.DataDirectory[sizeof IMAGE_DATA_DIRECTORY].VirtualAddress
   invoke RVAToOffset,pMapping,edi
   mov edi,eax
   add edi,pMapping
   assume edi:ptr IMAGE_IMPORT_DESCRIPTOR
   .while !([edi].OriginalFirstThunk==0 && [edi].TimeDateStamp==0 && [edi].ForwarderChain==0 && [edi].Name1==0 && [edi].FirstThunk==0)
     invoke AppendText,hDlg,addr ImportDescriptor
     invoke RVAToOffset,pMapping, [edi].Name1
     mov edx,eax
     add edx,pMapping
     invoke wsprintf, addr temp, addr IDTemplate, [edi].OriginalFirstThunk,[edi].TimeDateStamp,[edi].ForwarderChain,edx,[edi].FirstThunk      invoke AppendText,hDlg,addr temp
     .if [edi].OriginalFirstThunk==0
        mov esi,[edi].FirstThunk
     .else
        mov esi,[edi].OriginalFirstThunk
     .endif
     invoke RVAToOffset,pMapping,esi
     add eax,pMapping
     mov esi,eax
     invoke AppendText,hDlg,addr NameHeader
     .while dword ptr [esi]!=0
       test dword ptr [esi],IMAGE_ORDINAL_FLAG32
       jnz ImportByOrdinal
       invoke RVAToOffset,pMapping,dword ptr [esi]
       mov edx,eax
       add edx,pMapping
       assume edx:ptr IMAGE_IMPORT_BY_NAME
       mov cx, [edx].Hint
       movzx ecx,cx
       invoke wsprintf,addr temp,addr NameTemplate,ecx,addr [edx].Name1
       jmp ShowTheText
ImportByOrdinal:
       mov edx,dword ptr [esi]
       and edx,0FFFFh
       invoke wsprintf,addr temp,addr OrdinalTemplate,edx
ShowTheText:
       invoke AppendText,hDlg,addr temp
       add esi,4
    .endw
    add edi,sizeof IMAGE_IMPORT_DESCRIPTOR
  .endw
  ret
ShowTheFunctions endp
end start

Analyse:

Le programme affiche une DialogBox de type OPENFILE (laquelle sert à sélectionner et ouvrir un fichier quand l'utilisateur clique dessus). Il vérifie que le fichier est un PE valide et appelle ensuite ShowTheFunctions.

ShowTheFunctions proc uses esi ecx ebx hDlg:DWORD, pNTHdr:DWORD
   LOCAL temp[512]:BYTE

Réservez un espace de 512 octets sur la pile pour des opérations sur des chaînes de caractères.

   invoke SetDlgItemText,hDlg,IDC_EDIT,0

Efface le texte dans le contrôle d'édition

   invoke AppendText,hDlg,addr buffer

Insère le nom du PE file dans le contrôle d'édition.AppendText envoie juste des messages EM_REPLACESEL pour ajouter du texte dans le contrôle d'édition. Notez qu'il envoie EM_SETSEL avec wParam=-1 et lParam=0 au contrôle d'édition pour déplacer le curseur en fin de texte.

   mov edi,pNTHdr
   assume edi:ptr IMAGE_NT_HEADERS
   mov edi, [edi].OptionalHeader.DataDirectory[sizeof IMAGE_DATA_DIRECTORY].VirtualAddress

Sert à récupérer le RVA des symboles d'importation. Au départ edi pointe sur le PE header. On s'en sert pour aller jusqu'au 2ème membre du tableau du Répertoire des Données et on récupère la valeur du membre VirtualAddress.

   invoke RVAToOffset,pMapping,edi
   mov edi,eax
   add edi,pMapping

Voici ici un des pièges pour les nouveaux venus à la programmation des PE. La plupart des adresses dans un PE file sont des RVAs et les RVAs n'ont de sens que lorsque le PE file est chargé en mémoire par le PE loader. Dans notre cas, nous mappons le fichier en mémoire, mais pas comme le fait le PE loader. Ainsi nous ne pouvons pas employer directement ces RVAs. D'une façon ou d'une autre nous devons convertir ces RVAs offsets pour le fichier. J'ai écris la fonction RVAToOffset dans ce but. Je ne l'analyserai pas en détail ici. Il est suffisant de dire qu'il compare le RVA qu'on lui a suggéré avec les début et fins des RVAs de toutes les sections du PE file, et utilise la valeur du paramètre PointerToRawData de la structure IMAGE_SECTION_HEADER pour convertir le RVA en un offset pour le fichier.
Pour utiliser cette fonction, vous devez lui passer deux paramètres : le pointer sur le fichier a Mapper en mémoire et le RVA que vous voulez convertir. Elle retourne l'offset(de fichier) dans eax. Dans ce susdit petit bout, nous devons additionner le 'pointer sur le fichier a Mapper en mémoire' avec l'offset (de fichier)' pour le convertir en une adresse virtuelle. Ça vous semble compliqué, huh ? :)

   assume edi:ptr IMAGE_IMPORT_DESCRIPTOR
   .while !([edi].OriginalFirstThunk==0 && [edi].TimeDateStamp==0 && [edi].ForwarderChain==0 && [edi].Name1==0 && [edi].FirstThunk==0)

edi pointe maintenant sur la première structure IMAGE_IMPORT_DESCRIPTOR. Nous lisons le tableau jusqu'à ce que nous trouvions la structure qui ne comporte que des membres nuls, elle sert à marquer la fin du tableau.

     invoke AppendText,hDlg,addr ImportDescriptor
     invoke RVAToOffset,pMapping, [edi].Name1
     mov edx,eax
     add edx,pMapping

Nous souhaitons afficher les valeurs de la structure IMAGE_IMPORT_DESCRIPTOR courante dans le contrôle d'édition. Name1 diffère des autres membres puisque il contient le RVA du nom de la dll. C'est pourquoi nous devons d'abord le convertir en une adresse virtuelle.

     invoke wsprintf, addr temp, addr IDTemplate, [edi].OriginalFirstThunk,[edi].TimeDateStamp,[edi].ForwarderChain,edx,[edi].FirstThunk      invoke AppendText,hDlg,addr temp

Affiche les valeurs de l'IMAGE_IMPORT_DESCRIPTOR courante.

     .if [edi].OriginalFirstThunk==0
        mov esi,[edi].FirstThunk
     .else
        mov esi,[edi].OriginalFirstThunk
     .endif

Ensuite nous nous préparons à parcourir le tableau IMAGE_THUNK_DATA. Normalement nous aimerions pouvoir utiliser le tableau pointé par OriginalFirstThunk. Cependant, certains mauvais linkers mettent 0 dans OriginalFirstThunk, c'est pourquoi nous devons d'abord vérifier si la valeur d'OriginalFirstThunk est zéro ou non. S'il est à 0, nous employons le tableau pointé par FirstThunk instead.

     invoke RVAToOffset,pMapping,esi
     add eax,pMapping
     mov esi,eax

De nouveau, la valeur dans OriginalFirstThunk/FirstThunk est un RVA. Nous devons la convertir en une adresse virtuelle.

     invoke AppendText,hDlg,addr NameHeader
     .while dword ptr [esi]!=0

Maintenant nous sommes prêts nous promener dans le tableau IMAGE_THUNK_DATAs pour chercher les noms des fonctions importées dans cette DLL. Nous lisons le tableau jusqu'à ce que nous trouvions une entrée qui contient 0.

       test dword ptr [esi],IMAGE_ORDINAL_FLAG32
       jnz ImportByOrdinal

La première chose que nous faisons avec l'IMAGE_THUNK_DATA c'est de le comparer à IMAGE_ORDINAL_FLAG32. Ce teste est validé si le bit le plus significatif (MSB) d'IMAGE_THUNK_DATA est 1. S'il l'est, la fonction est exportée par ordre donc nous n'avons aucun besoin de la traiter plus loin. Nous pouvons extraire son n° d'ordre à partir du WORD de poids faible d'IMAGE_THUNK_DATA puis passer au DWORD IMAGE_THUNK_DATA suivant.

       invoke RVAToOffset,pMapping,dword ptr [esi]
       mov edx,eax
       add edx,pMapping
       assume edx:ptr IMAGE_IMPORT_BY_NAME

Si le MSB d'IMAGE_THUNK_DATA est 0, il contient le RVA de la structure IMAGE_IMPORT_BY_NAME. Nous avons d'abord besoin de le convertir en une adresse virtuelle.

       mov cx, [edx].Hint
       movzx ecx,cx
       invoke wsprintf,addr temp,addr NameTemplate,ecx,addr [edx].Name1
       jmp ShowTheText

Hint est un paramètre de taille WORD. Nous devons le convertir en une valeur de taille DWORD avant de l'envoyer à l'API wsprintf. Et nous affichons ensemble dans le contrôle d'édition 'Hint' et le nom de la fonction

ImportByOrdinal:
       mov edx,dword ptr [esi]
       and edx,0FFFFh
       invoke wsprintf,addr temp,addr OrdinalTemplate,edx

Au cas où la fonction est uniquement exportée par ordre, nous mettons à zéro le WORD de poids fort et plaçons le n° d'ordre.

ShowTheText:
       invoke AppendText,hDlg,addr temp
       add esi,4

Après avoir inséré le nom/numéro de la fonction dans le contrôle d'édition, nous passons à l'IMAGE_THUNK_DATA suivante.

    .endw
    add edi,sizeof IMAGE_IMPORT_DESCRIPTOR

Quand tous les dwords IMAGE_THUNK_DATA du tableau ont été traités, nous sautons au IMAGE_IMPORT_DESCRIPTOR suivant pour traiter les fonctions d'importation des éventuelles autres DLLs.

Annexe:

Ce serait incomplet si je ne mentionnais rien sur les importations attachées (Bound Import). Pour expliquer ce que c'est, j'ai besoin de faire un petit aparté. Quand le PE loader charge le PE file (un programme) en mémoire, il examine sa table des importations et charge les DLLs qui lui serviront dans l'espace d'adresses qui lui est réservée. Ensuite il parcoure le tableau IMAGE_THUNK_DATA de long en large comme nous l'avons fait et remplace les IMAGE_THUNK_DATAs par les adresses réelles des fonctions d'importation, ça ne prend pas longtemps. Si d'une façon ou d'une autre le programmeur peut prévoir les adresses des fonctions correctement, alors le PE loader n'a pas besoin de fixer les IMAGE_THUNK_DATAs à chaque fois que le PE file est exécuté. L'importation attachée est le produit de cette idée.
Pour l'expliquée en termes simples, il existe un utilitaire nommé bind.exe qui est fournit avec les compilateurs de Microsoft tel que Visual Studio qui examine la table des importations du PE file et remplace les dwords IMAGE_THUNK_DATA par les adresses des fonctions d'importation. Lorsque le fichier est chargé, le PE loader doit vérifier si les adresses sont valides. Si les versions des DLL ne correspondent pas avec celles utilisées par le PE File ou bien si les DLLs ont besoin d'être transférées, alors le PE loader sait que les adresses de precomputed ne sont pas valides c'est pourquoi il doit parcourir le tableau indiqué par OriginalFirstThunk pour recalculer les nouvelles adresses des fonctions d'importation.
L'importation attachée n'a pas beaucoup de signification pour notre exemple parce que nous utilisons OriginalFirstThunk par défaut. Pour plus d'informations sur les importations attachées, je vous recommande de lire la doc LUEVELSMEYER's pe.txt.


[Iczelion's Win32 Assembly Homepage]


Traduit par Morgatte