Chérie, j'ai agrandi l'EXE !
- Ou comment rajouter une section dans un EXE au format PE -
By Nody
Petite Intro.
Salut, tout le monde ! A première vue, les PE semblent intéresser du monde et bein on va un peu développer le sujet !
On va faire un exemple simple : ajouter une MessageBoxA sur un EXE (celui que vous voulez, chez moi ça sera sur le bloc-notes Windows) et on va le faire en 2 étapes : la première étape théorique et la deuxième plus pratique.
Etape 1 : La Théorie Il faut tout d'abord connaitre quelques informations sur le PE-Header ... (en-tête
PE pour les anglophobes) et on va voir ça en disséquant le Bloc-Notes de Windows (Window$ pour les
windophobes). Voici les 1ers octets du Bloc-Notes : 4D 5A 90 00 03 00 00 00 04 00 00 00 FF FF 00 00 B8 00 00 00 00 00 00 00 40 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 80 00 00 00 0E 1F BA 0E 00 B4 09 CD 21 B8
01 4C CD 21 54 68 69 73 20 70 72 6F 67 72 61 6D 20 63 61 6E 6E 6F 74 20 62 65 20 72 75 6E 20 69 6E 20 44 4F 53
20 6D 6F 64 65 2E 0D 0D 0A 24 00 00 00 00 00 00 00 50 45 00 00 En rouge, c'est le dword qui contient l'adresse du début du PE-Header (il
est toujours situé en 0x3C). Donc ici le PE Header commence en 0x00000080 (et pas en 0x80000000 !), je l'ai
mis en bleu pour le situer et il commence toujours par "PE". Entre deux, il y a du code qui ne nous intéresse
pas (comme la compatibilité avec le DOS, etc...) donc on ne va pas s'attarder dessus. PE-Header Voyons de plus près le fameux PE-Header ... (quoique je développe
seulement les parties dont on aura besoin) 50 45 00 00 4C 01 05 00 65 91 46 35 00 00 00 00 00 00 00 00 E0 00 0E
01 0B 01 03 0A 00 40 00 00 00 74 00 00 00 00 00 00 CC 10 00 00 00 10 00 00 00 50 00 00 00 00 40 00 00 10 00 00 00 10 00 00 04 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00 00
E0 00 00 00 04 00 00 89 18 01 00 02 00 00 00 00 00 10 00 00 10 00 00 00 00 10
00 00 10 00 00 00 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 00 60 00 00 8C 00 00 00 00 70 00 00 8C 55 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 D0 00 00 3C 09 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 E0 62
00 00 40 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 Le voilà le beau PE-Header qui est long de 0xF8 (248) octets. Pour la suite,
je suppose que le PE-Header commence à l'offset X. J'ai résumé avec des zolies couleurs les
valeurs contenues dans le PE-Header : Signature : Ça commence par les lettres 'PE', original non ?
(location : X + 0, ici 0x4550) Nombre de Sections de l'EXE : on développera les sections plus
tard... (location : X + 6, ici 0x0005) Entry Point du programme : c'est là que l'exécution du
programme va démarrer (ce n'est évident pas une constante) Attention, c'est une R.V.A. ! (location : X + 0x28,
ici 0x000010CC) ImageBase : c'est à cette adresse que l'image du programme
va être chargé en mémoire. (location : X + 0x34, ici : 0x00400000) Les Alignements des sections et du fichier : 2 dword développés
ICI. (location : X + 0x38 pour les sections, X + 0x3C pour le fichier, ici : 0x00001000 et 0x00001000) Taille de l'Image : taille que va prendre l'image du fichier une
fois chargé en mémoire. (location : X + 0x50, ici : 0x0000E000) Le reste du PE-Header n'est pas important pour les modifications qu'on aura à
lui faire. R.V.A. : Relative Virtual Address. En fait quand vous avez une R.V.A., vous n'avez pas la "vraie" adresse
en mémoire mais vous devez lui ajouter l'ImageBase pour avoir la vraie adresse. Exemple : ici la R.V.A.
de l'EntryPoint est 0x000010CC, en fait l'EntryPoint se situe en 0x000010CC + 0x00400000 (ImageBase) = 0x004010CC.
Vous pouvez vérifier avec le Symbol Loader de SoftIce que le Bloc-Notes démarre à cette adresse
(j'ai oublié de vous dire que j'avais Windows 98 donc les valeurs indiquées peuvent varier MAIS le
principe reste le même !). Il y a 2 types d'Alignement : celui du fichier sur le disque dur et celui une fois
chargé en mémoire. Ici l'alignement est de 4096 octets (décimal) pour les deux. Si une section
fait 2000 octets de long est qu'elle commence à l'offset Y, la prochaine section ne trouvera pas à
Y + 2000 mais à Y + 4096. De même, si la section fait 6000 octets de long, la prochaine se trouvera
à Y + 4096*2 (8192). Donc, si l'alignement n'est pas le même pour le fichier sur disque dur et pour
le fichier chargé en mémoire, les adresses ne seront pas les mêmes quand vous éditez
le fichier avec l'éditeur héxa que celles que vous veraient avec SoftIce !! Ici, le problème
ne se pose pas car les 2 sont alignés à 4096 octets. Aussi bien, Si l'alignement du fichier avait
été de 512 octets, on peut l'aligner sur 4096 octets car 4096 est un multiple de 512. Section Header Le Section Header contient les informations relatives aux différentes sections
de l'EXE. Voici le Section Header du Bloc-Notes : 2E 74 65 78 74 00 00 00 9C 3E 00 00 00 10 00 00 00 40 00 00 00 10 00
00 00 00 00 00 00 00 00 00 00 00 00 00 20 00 00 60 2E 64 61 74 61 00 00 00 4C 08 00 00 00 50 00 00 00 10 00 00 00 50 00 00 00 00 00 00 00 00 00 00 00
00 00 00 40 00 00 C0 2E 69
64 61 74 61 00 00 E8 0D 00 00 00 60 00 00 00 10 00 00 00 60 00 00 00 00 00 00 00 00 00 00 00 00 00 00 40 00 00
40 2E 72 73 72 63 00 00 00 00 60 00 00 00 70 00 00 00 60 00 00 00 70 00 00 00
00 00 00 00 00 00 00 00 00 00 00 40 00 00 40 2E 72 65 6C 6F 63
00 00 9C 0A 00 00 00 D0 00 00 00 10 00 00 00 D0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 40 00 00 42 Voici une partie du Section Header (je n'ai pas recopié les 2000 zéros
qui suivent ...) qui contient 5 sous-section header relatif aux 5 sections de l'EXE (d'où les 5 couleurs). Etudions de plus près la première section : 2E 74 65 78 74 00 00 00
9C 3E 00 00 00 10 00 00
00 40 00 00 00 10 00 00 00 00 00 00 00
00 00 00 00 00 00 00 20 00 00 60 (j'espère que les couleurs ne perturbent personne :-)
) Nom de la Section : 8 octets, ici il s'agit de ".text" R.V.A. Virtual Address : adresse de la section quand elle est chargée
en mémoire. (ici 0x00001000) Taille de la section : taille de la section quand elle n'est pas
chargée en mémoire ! Cette valeur est aligné sur l'alignement du fichier. (ici 0x00004000) Adresse de l'image : adresse de la section quand le fichier n'est
pas chargé en mémoire. (ici : 0x00001000) Caractéristiques de la section : sur 32 bits : bit 5 = la section contient du code (1) ou pas (0) Etape 2 : à l'action ! Oops ! Comment on trouve l'API MessageBoxA ?? Virogen a écrit une jolie routine capable de trouver la location
de Kernel32 et de ses API (donc de l'API LoadLibraryA et GetProcAddress qui nous donnerons l'adresse de MessageBoxA
ou de n'importe quel autre DLL/API) Je vous propose d'écrire un programme qui place une MessageBoxA dans l'EXE
(au format PE) de votre choix. Modifs du PE-Header et du Section Header pour rajouter une section : Il faut tout d'abord créer une nouvelle entrée dans le Section Header
: Nom de la section : .Nody Ce qui donne : 2E 4E 6F 64 79 00 00 00 00 00 00 00 ?? ?? ?? ?? 00 10 00 00 ?? ??
?? ?? 00 00 00 00 00 00 00 00 00 00 00 00 40 00 00 C0 Il n'y a pas grand chose à modifier dans le PE-Header : il faudra incrémenter
le nombre de sections, et ajouter la taille de notre section dans la Taille de l'Image du programme. J'oubliais
: il faut aussi mettre l'EntryPoint sur le début de notre section. Routines de Virogen : Ça, c'était la 1ère routine pour trouver l'offset
de kernel32 ... Voici la 2ème routine qui trouve l'adresse des API en fouillant
dans l'Export Table du prog (ici le prog est kernel32.dll): VgImport proc VgImport endp Virogen doit certainement mieux expliquer sa routine que moi, c'est
pourquoi j'ai laissé les commentaires d'origine . En gros, il cherche le PE Header, puis il pointe vers
l'Export Table et recherche le nom de l'API qu'on a entré, puis il chope l'offset de l'API qu'il place dans
EAX en sortie de procédure. A nous de jouer ... Voici la routine qu'on va devoir copier dans l'EXE à modifier
: ;Recherche des adresses des API LoadLibraryA, GetProcAddress et
FreeLibraryA : ; Les variables et les constantes : Déroulement des opérations Parlons un peu du programme qui va se charger de copier les données
... Voici ce qu'il va devoir faire :
00
bit 6 = la section contient des données (1) ou pas (0)
bit 7 = données non initialisées donc des zéros partout (1) ou alors des données initialisées
ie des constantes du programme (0)
bit 29 = accès en exécution de la section (1)
bit 30 = accès en lecture de la section (1)
bit 31 = accès en écriture de la section (1)
Virtual Address : inconnue pour le moment
Taille de la section : 4096 octets seront suffisants
Adresse de l'image : inconnue pour le moment
Caractéristiques : code exécutable, mais qui contiendra quand même des données =>
accès en exécution, en lecture et en écriture.pushad ; sauve les registres
call Get_Delta
Get_Delta:
pop ebp ; ebp = offset courant
sub ebp, offset Get_Delta ; ebp = "décalage" des offsets
mov dword ptr [ebp+offset Delta_Number], ebp ; qu'on sauve
mov eax, dword ptr [esp+(8*4)] ; eax pointe dans kernel32
and eax, 0FFFF0000h ; on vire les 4 derniers chiffres de l'offset
Base_Loop:
cmp word ptr [eax], 'ZM' ; recherche le début de kernel32
je Kernel32_Base ; c'est bon on a l'offset de kernel32
sub eax, 10000h
jmp Base_Loop
Kernel32_Base: ; on continue ...
mov edx,ebx ; edx=base
cmp word ptr [ebx],'ZM'
jnz ret_error
movzx eax,word ptr [ebx+3ch]
add ebx,eax ; ebx->PE Header
cmp word ptr [ebx],'EP'
jnz ret_error
mov ebx,[ebx+120] ; ebx->RVA of Export Table
add ebx,edx ; ebx->Export table
or edi,edi ; was pAPIName supplied?
jz ImportByOrdinal
mov esi,[ebx+32] ; esi->RVA of Address Name Table
add esi,edx ; esi->Address Name Table
mov ecx,[ebx+24] ; ecx=number of API Names
push ebx
xor ebx,ebx ; use ebx as counter
SearchLoop:
lodsd ; eax->RVA of API Name
add eax,edx ; eax->API Name
push esi edi
xchg edi,esi ; esi->pAPINamse
xchg edi,eax ; edi->API Name
strcmpi:
lodsb
or al,al ; end of string?
jnz not_zero
cmp byte ptr [edi],0
jz found_api ; if so make sure other string ends
jmp didnt_find_api
not_zero:
cmp byte ptr [edi],al
jnz didnt_find_api
inc edi
jmp strcmpi
didnt_find_api:
mov al,1 ; 1==continue search to next api
found_api:
pop edi esi
or al,al ; if 0 then we found the api
jz EndLoop
inc ebx
loop SearchLoop
EndLoop:
xchg eax,ebx ; eax=index
pop ebx ; ebx->Export Table
mov esi,[ebx+36] ; esi->RVA of Ordinal Table
add esi,edx ; esi->Ordinal Table
inc eax
shl eax, 1
add esi,eax ; esi->Ordinal number
movzx ecx,word ptr [esi]; ecx=Ordinal number
; entry:
; ecx=ordinal
ImportByOrdinal:
sub ecx,dword ptr [ebx+16] ; subtract ordinal base
mov esi,[ebx+28] ; esi->RVA of Address table
add esi,edx ; esi->Address Table
xchg eax,ecx ; eax=ordinal #
shl eax, 2
add esi,eax ; esi->Address
mov eax,dword ptr [esi] ; eax->RVA of API
add eax,edx ; eax->API
mov ebx, edx
ret
ret_error:
xor eax,eax
mov ebx, edx
ret
Recherche de Kernel32 :
RoutineDebut :
pushad
call Get_Delta
Get_Delta:
pop ebp
sub ebp, offset Get_Delta
mov dword ptr [ebp+offset Delta_Number], ebp
mov eax, dword ptr [esp+(8*4)]
and eax, 0FFFF0000h
Base_Loop:
cmp word ptr [eax], 'ZM'
je Kernel32_Base
sub eax, 10000h
jmp Base_Loop
Kernel32_Base:
mov ebx, eax
mov dword ptr [ebp+offset Kernel32Base], eax
lea edi, dword ptr [ebp+offset LoadLibrary]
call VgImport
mov dword ptr [ebp + LLAdresse], eax
lea edi, dword ptr [ebp+offset GetProcAddress]
call VgImport
mov dword ptr [ebp + GPAdresse], eax
lea edi, dword ptr [ebp+offset FreeLibrary]
call VgImport
mov dword ptr [ebp + FLAdresse], eax
; Recherche de l'API MessageBoxA via LoadLibraryA et GetProcAddress
mov ebx, dword ptr [ebp+offset LLAdresse]
mov esi, offset API_USER32
add esi, ebp
push esi
call ebx ; call LoadLibraryA
mov dword ptr [ebp+offset ModuleHandle], eax
mov ebx, dword ptr [ebp+offset GPAdresse]
mov esi, offset MessageBox
add esi, ebp
push esi
push eax
call ebx ; call GetProcAddress
; Affichage de la MessageBoxA :
push 0
mov esi, offset Titre
add esi, ebp
push esi
mov esi, offset Message
add esi, ebp
push esi
push 0
call eax ; call MessageBoxA
mov eax, dword ptr [ebp+offset ModuleHandle]
push eax
mov eax, dword ptr [ebp+offset FLAdresse]
call eax ; call FreeLibrary
Quit:
popad
; Retour au programme "normal" :
call The_End
The_End:
pop eax
sub eax, offset The_End
mov eax, dword ptr [eax+offset Entry_Point]
jmp eax
Entry_Point dd 0
Delta_Number dd 0
Kernel32Base dd 0
LoadLibrary db "LoadLibraryA",0
LLAdresse dd 0
FreeLibrary db "FreeLibrary",0
FLAdresse dd 0
GetProcAddress db 'GetProcAddress',0
GPAdresse dd 0
API_USER32 db 'user32.dll',0
MessageBox db 'MessageBoxA',0
Titre db 'Chérie, j''ai agrandi l''EXE v0.01',0
Message db 'Salut !',0 ; rien trouver de mieux !
ModuleHandle dd 0
VgImport proc
; ENTREE : EDI = pAPIName
; EBX = ModuleBase
; ECX = Ordinal
;
; ModuleBase=Base of the module you want to import the API from. (pour nous, c'est kernel32.dll)
; pAPIName =pointer to the ASCIIz name of the API, or NULL if you
; are importing by ordinal. (on utilise cette fonction)
; Ordinal =Ordinal number if you wish to import by ordinal, use
; 0 if pAPIName is supplied. (on utilise pas)
;
; SORTIE : EAX = offset de l'API
; EAX = 0 si erreur
;
mov edx,ebx
cmp word ptr [ebx],'ZM'
jnz ret_error
movzx eax,word ptr [ebx+3ch]
add ebx,eax
cmp word ptr [ebx],'EP'
jnz ret_error
mov ebx,[ebx+120]
add ebx,edx
or edi,edi
jz ImportByOrdinal
mov esi,[ebx+32]
add esi,edx
mov ecx,[ebx+24]
push ebx
xor ebx,ebx
SearchLoop:
lodsd
add eax,edx
push esi edi
xchg edi,esi
xchg edi,eax
strcmpi:
lodsb
or al,al
jnz not_zero
cmp byte ptr [edi],0
jz found_api
jmp didnt_find_api
not_zero:
cmp byte ptr [edi],al
jnz didnt_find_api
inc edi
jmp strcmpi
didnt_find_api:
mov al,1
found_api:
pop edi esi
or al,al
jz EndLoop
inc ebx
loop SearchLoop
EndLoop:
xchg eax,ebx
pop ebx
mov esi,[ebx+36]
add esi,edx
inc eax
shl eax, 1
add esi,eax
movzx ecx,word ptr [esi]
ImportByOrdinal:
sub ecx,dword ptr [ebx+16]
mov esi,[ebx+28]
add esi,edx
xchg eax,ecx
shl eax, 2
add esi,eax
mov eax,dword ptr [esi]
add eax,edx
mov ebx, edx
ret
ret_error:
xor eax,eax
mov ebx, edx
ret
VgImport endp
RoutineFin :
1/ Ouverture du fichier avec CreateFileA dont la structure ressemble
à :
---------------------------------------------------------
Entete :
* Offset_PE_Header
---------------------------------------------------------
PE-Header :
* Nombre de Sections : n
* EntryPoint : Offset_EntryPoint
* Taille de l'Image : TailleImage
---------------------------------------------------------
Section Header
* Entrée Section 1
* Entrée Section 2
* [...]
* Entrée Section n
---------------------------------------------------------
Section 1
---------------------------------------------------------
Section 2
---------------------------------------------------------
[...]
---------------------------------------------------------
Section n
---------------------------------------------------------
2/ Lecture du PE-Header dans un buffer (PEHeaderBuffer) et vérification
qu'il s'agit bien d'un PE (signature "PE")
3/ Récupération de la taille du fichier, récupération de l'offset du PE-Header, du
nombre de section grace aux API SetFilePointer et ReadFile
4/ Modifications des données du Buffer PEHeaderBuffer :
Nombre de Sections = n + 1
5/ Lecture de l'Entrée de la Section n dans SectionHeaderBuffer
:
Offset_Section_n = Offset_PE_Header + 0F8h (taille du PE-Header) + (n-1) * 28h (chaque entrée dans le Section
Header fait 28h octets)
6/ Ajustement du RVA de notre section :
RVA_Notre_Section = RVA_Section_n + 1000h - Reste(Taille_Section_n divisé par 1000h) <- respect de l'alignement
de 4096 octets du fichier !!
7/ Ajustement de l'Offset de notre section :
Offset_Notre_Section = Taille_Du_Fichier (et oui car on écrit nos octets à la fin ! Ne pas confondre
RVA de notre section qui est l'adresse de notre section quandd elle est chargée en mémoire et l'Offset
(ou Raw Offset) qui est l'adresse de la section dans le fichier)
8/ Ecriture de notre section :
Sauvegarde de l'ancienne Entry_Point dans la routine
Allocations de mémoire de 4096 octets, copie des routines en mémoire, et copie des 4096 octets à
la fin du fichier (4096 octets pour respecter l'alignement du fichier)
Libération de la mémoire
9/ Modification du PE_Header
Entry_Point = RVA_Notre_Section
Taille_Image = Taille_Image + 1000h
Ecriture de PEHeaderBuffer à la place de l'ancien PE-Header
10/ Ajout d'une entrée dans le SectionHeader
On pointe après l'entrée de la section n
On copie le buffer SectionHeader
11/ Fermeture du fichier qui ressemble maintenant à ça
:
---------------------------------------------------------
Entete :
* Offset_PE_Header
---------------------------------------------------------
PE-Header :
* Nombre de Sections : n+1
* EntryPoint : Offset_Section_n+1
* Taille de l'Image : TailleImage + 1000h
---------------------------------------------------------
Section Header
* Entrée Section 1
* Entrée Section 2
* [...]
* Entrée Section n
* Entrée Section n+1 (la notre !)
---------------------------------------------------------
Section 1
---------------------------------------------------------
Section 2
---------------------------------------------------------
[...]
---------------------------------------------------------
Section n
---------------------------------------------------------
Section n+1 : (la notre !)
* Code
* jmp Ancien_Entry_Point
---------------------------------------------------------
Vous retrouverez les sources commentées du programme dans le Zip...Ce tut est le 1er d'une série sur les PE, alors à bientôt pour de nouvelles aventures !
Contact : Nody