Tutorial 2: Détecter un PE File Valide

Dans ce Tutorial, nous allons voir comment vérifier si un fichier donné est un PE File Valide.
Downloadez l'exemple.

(Un PE File est un ficher de type PE = un exécutable sous Win32. Un Programme Win32 en fait.)

Théorie:

Comment pouvez-vous vérifier si un fichier donné est PE file ? C'est difficile de répondre à cette question. Ça dépend jusqu'où vous voulez aller. Vous pouvez vérifier chaque structure de données définie dans le format du PE file ou bien vous pouvez vous satisfaire de vérifier seulement les éléments cruciaux. La plupart du temps, il est inutile de vérifier chaque structure appartenant aux fichiers. Si les structures cruciales sont valables, nous pouvons dire avec presque certitude que le fichier est un PE valide. Et nous nous contenterons de cette supposition.

La structure essentielle que nous vérifierons est le PE header lui-même. Donc nous avons besoin d'en savoir un peu plus sur sa programmation. Le PE header est en réalité une structure appelée IMAGE_NT_HEADERS. Sa définition est la suivante :

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

Signature est un dword qui contient la valeur 50h, 45h, 00h, 00h. En terme plus humain, ça représente le texte "PE" suivi par deux zéros de terminaison. Ce membre est la 'signature PE', donc nous nous en servirons pour vérifier si un fichier donné est ou n'est pas un PE valide.
FileHeader est une structure qui contient des informations sur la structure physique du PE file tels que son nombre de sections, la machine pour laquelle ce fichier est destinée et cetera.
OptionalHeader est une structure qui contient des informations sur la structure logique du PE file. Malgré qu'il soit "Optionel", il est toujours présent.

Notre but est maintenant clair. Si la valeur du membre 'signature' du IMAGE_NT_HEADERS est égale à "PE" suivi par deux zéros, alors le fichier est un fichier PE valide. En fait, justement dans le but de pouvoir comparer ça, Microsoft a défini une constante nommée IMAGE_NT_SIGNATURE que nous pouvons facilement employer.

IMAGE_DOS_SIGNATURE equ 5A4Dh
IMAGE_OS2_SIGNATURE equ 454Eh
IMAGE_OS2_SIGNATURE_LE equ 454Ch
IMAGE_VXD_SIGNATURE equ 454Ch
IMAGE_NT_SIGNATURE equ 4550h

La question est la suivante : comment pouvons-nous savoir où se situe le PE header ? La réponse est simple : le MZ header du DOS contient l'offset(de fichier) du PE header. Le MZ header est défini comme la structure IMAGE_DOS_HEADER. Vous pouvez le vérifier en regardant dans windows.inc. Le membre e_lfanew de la structure IMAGE_DOS_HEADER contient l'offset du PE header.

Les étapes sont les suivantes :

  1. Vérifiez si le fichier donné possède un MZ header (du DOS) valide en comparant le premier WORD du fichier avec la valeur IMAGE_DOS_SIGNATURE.
  2. Si le fichier possède bien un MZ Header valide, On utilise la valeur qui est dans le membre e_lfanew pour retrouver le PE header
  3. On compare le premier WORD du PE header avec la valeur IMAGE_NT_HEADER. Si les deux sont de même valeur, alors nous pouvons assurer que le fichier est un PE valide.

Exemple:

.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

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

.data
AppName db "PE tutorial no.2",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
FileValidPE db "This file is a valid PE",0
FileInValidPE db "This file is not a valid PE",0

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

.code
start proc
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:
             .if ValidPE==TRUE
                 invoke MessageBox, 0, addr FileValidPE, addr AppName, MB_OK+MB_ICONINFORMATION
             .else
                invoke MessageBox, 0, addr FileInValidPE, addr AppName, MB_OK+MB_ICONINFORMATION
             .endif
             push seh.PrevLink
             pop fs:[0]
             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
invoke ExitProcess, 0
start endp

SEHHandler proc C uses edx 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
end start

Analyse:

Le programme ouvre un fichier et vérifie si le DOS header (le MZ) est valide, s'il l'est, on vérifie alors si le PE header est valide à son tour. S'il l'est également, alors on peut certifier que le fichier est un PE File valide. Dans cet exemple, j'emploie la 'Structure Exception Handling' (SEH) pour ne pas avoir à faire de vérifier chaque erreur possible : si une erreur se produit, on est sûr que c'est parce que le fichier n'est pas un PE File valide. Windows lui-même utilise largement le SEH dans ses routines de validation de paramètres. Si le SEH vous intéresse, lisez l'article de Jeremy Gordon.

Son programme affiche une fenêtre OPENFILE (la boîte de Dialog qui sert à ouvrir d'autres fichiers) et quand l'utilisateur choisit un fichier exécutable, il ouvre ce fichier et les 'Mappe' en mémoire. Avant qu'il ne commence sa vérification, il installe un SEH :

   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

Nous commençons en assumant l'utilisation du register fs comme étant mis à zéro. On est obligé de faire comme ça parce que MASM assume l'utilisation du register fs à ERROR (par défaut). Ensuite nous stockons l'adresse du SEH Handler vu précédemment dans notre structure pour que Windows puisse l'utiliser. Nous stockons l'adresse de notre SEH Handler, l'adresse où l'exécution peut sans risque reprendre si une erreur arrive, les valeurs actuelles de esp et d'ebp pour que notre SEH Handler puisse récupérer l'état antérieur de la pile (comme en tant normale) avant que notre programme reprenne son exécution.

   mov edi, pMapping
   assume edi:ptr IMAGE_DOS_HEADER
   .if [edi].e_magic==IMAGE_DOS_SIGNATURE

Après que nous en ayons finis avec l'installation du SEH, nous passons à la vérification. Nous mettons l'adresse du premier octet du fichier cible dans edi, qui est le premier octet du MZ header (du DOS). Pour le bien de la comparaison, nous disons à l'assembleur qu'il peut assumer edi en tant que pointeur sur la structure IMAGE_DOS_HEADER (Ce qui est la vérité). Nous comparons alors le premier WORD du MZ header avec la chaîne de caractères "MZ" qui est définie comme une constante dans Windows.inc sous le nom d'IMAGE_DOS_SIGNATURE. Si la comparaison est ok, nous passons au PE header. S'il n'est pas valide, nous mettons à FALSE la valeur dans ValidPE, signifiant ainsi que le fichier n'est pas PE File valide.

      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

Pour arriver jusqu'au PE header, nous avons besoin de la valeur dans e_lfanew du MZ header. Ce paramètre contient l'offset du PE header. Ainsi nous ajoutons cette valeur à edi et nous arrivons au premier octet du PE header. C'est à cet endroit qu'une erreur peut se produire. Si le fichier n'est pas un vrai PE file, la valeur dans e_lfanew sera incorrecte et ainsi en l'utilisant on pointe vers une zone sauvage. Si nous n'employons pas le SEH, nous devons vérifier la valeur de e_lfanew avec la taille de ce sale fichier. Si tout va bien, nous comparons le premier dword du PE header avec la chaîne de caratères "PE". De nouveau il y a une constante bien pratique du nom de IMAGE_NT_SIGNATURE que nous pouvons utiliser. Si le résultat de la comparaison est TRUE, nous assumons que notre fichier est un PE File valide.
Si la valeur dans e_lfanew est incorrecte, une erreur peut se produire et notre SEH Handler reprendra le contrôle. Il rétablit simplement le pointer de pile, et reprend l'exécution à l'offset sauveqardé précédemment à l'endroit du label FinalExit.

FinalExit:
   .if ValidPE==TRUE
      invoke MessageBox, 0, addr FileValidPE, addr AppName, MB_OK+MB_ICONINFORMATION
   .else
      invoke MessageBox, 0, addr FileInValidPE, addr AppName, MB_OK+MB_ICONINFORMATION
   .endif

Le code ci-dessus est d'une simplicité en lui-même. Il vérifie la valeur dans ValidPE et affiche un message à l'utilisateur en conséquence.

   push seh.PrevLink
   pop fs:[0]

Quand le SEH n'est plus employé, nous le dissocions de la chaîne du SEH.


[Iczelion's Win32 Assembly Homepage]


Traduit par Morgatte