Maintenant, il est capable de compiler Hello World, mais dans cet article, je ne veux pas parler de l'analyse et de la structure interne du compilateur, mais d'une partie aussi importante que l'assemblage octet par octet du fichier exe.
DĂ©but
Envie d'un spoiler? Notre programme sera de 2048 octets.
Habituellement, travailler avec des fichiers exe consiste à étudier ou à modifier leur structure. Les fichiers exécutables eux-mêmes sont formés par les compilateurs, et ce processus semble un peu magique pour les développeurs.
Mais maintenant, nous allons essayer de le réparer!
Pour construire notre programme, nous avons besoin de n'importe quel éditeur HEX (j'ai personnellement utilisé HxD).
Prenons le pseudocode pour commencer:
La source
func MessageBoxA(u32 handle, PChar text, PChar caption, u32 type) i32 ['user32.dll']
func ExitProcess(u32 code) ['kernel32.dll']
func main()
{
MessageBoxA(0, 'Hello World!', 'MyApp', 64)
ExitProcess(0)
}
Les deux premières lignes indiquent les fonctions importées des bibliothèques WinAPI . La fonction MessageBoxA affiche une boîte de dialogue avec notre texte et ExitProcess informe le système de la fin du programme.
Cela n'a aucun sens de considérer la fonction principale séparément, car elle utilise les fonctions décrites ci-dessus.
En-tĂŞte DOS
Tout d'abord, nous devons générer l'en-tête DOS correct, c'est un en-tête pour les programmes DOS et ne devrait pas affecter le lancement d'exe sous Windows.
J'ai noté des champs plus ou moins importants, le reste est rempli de zéros.
Structure IMAGE_DOS_HEADER
Struct IMAGE_DOS_HEADER
{
u16 e_magic // 0x5A4D "MZ"
u16 e_cblp // 0x0080 128
u16 e_cp // 0x0001 1
u16 e_crlc
u16 e_cparhdr // 0x0004 4
u16 e_minalloc // 0x0010 16
u16 e_maxalloc // 0xFFFF 65535
u16 e_ss
u16 e_sp // 0x0140 320
u16 e_csum
u16 e_ip
u16 e_cs
u16 e_lfarlc // 0x0040 64
u16 e_ovno
u16[4] e_res
u16 e_oemid
u16 e_oeminfo
u16[10] e_res2
u32 e_lfanew // 0x0080 128
}
Plus important encore, cet en-tête contient le champ e_magic, ce qui signifie qu'il s'agit d'un fichier exécutable, et e_lfanew, qui indique le décalage de l'en-tête PE depuis le début du fichier (dans notre fichier, ce décalage est de 0x80 = 128 octets).
GĂ©nial, maintenant que nous connaissons la structure de l'en-tĂŞte DOS, Ă©crivons-la dans notre fichier.
(1) En-tĂŞte RAW DOS (Offset 0x00000000)
4D 5A 80 00 01 00 00 00 04 00 10 00 FF FF 00 00
40 01 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
Terminé, les 64 premiers octets ont été écrits. Maintenant, vous devez en ajouter 64 de plus, c'est le soi-disant DOS Stub (Stub). Lorsqu'il est lancé sous DOS, il doit informer l'utilisateur que le programme n'est pas conçu pour s'exécuter dans ce mode.
, , .
, (Offset) .
, 0x00000000, 64 (0x40 16- ), 0x00000040 ..
Mais en général, il s'agit d'un petit programme DOS qui imprime une ligne et quitte le programme.
Écrivons notre Stub dans un fichier et considérons-le plus en détail.
(2) Stub RAW DOS (décalage 0x00000040)
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 0A 24 00 00 00 00 00 00 00 00
Et maintenant le même code, mais sous forme démontée
Asm DOS Stub
0000 push cs ; Code Segment(CS) ( )
0001 pop ds ; Data Segment(DS) = CS
0002 mov dx, 0x0E ; DS+DX, $( )
0005 mov ah, 0x09 ; ( )
0007 int 0x21 ; 0x21
0009 mov ax, 0x4C01 ; 0x4C ( )
; 0x01 ()
000c int 0x21 ; 0x21
000e "This program cannot be run in DOS mode.\x0D\x0A$" ;
Cela fonctionne comme ceci: d'abord, le stub imprime une ligne indiquant que le programme ne peut pas être démarré, puis quitte le programme avec le code 1. Ce qui est différent de l'arrêt normal (Code 0).
Le code de stub peut différer légèrement (d'un compilateur à l'autre) J'ai comparé gcc et delphi, mais la signification générale est la même.
C'est aussi drôle que la ligne de stub se termine par \ x0D \ x0D \ x0A $. La raison la plus probable de ce comportement est que c ++ ouvre le fichier en mode texte par défaut. En conséquence, le caractère \ x0A est remplacé par la séquence \ x0D \ x0A. En conséquence, nous obtenons 3 octets: 2 octets de retour chariot (0x0D) qui n'a pas de sens, et 1 pour le saut de ligne (0x0A). En mode binaire (std :: ios :: binary), cette substitution ne se produit pas.
Pour vérifier l'exactitude de l'écriture des valeurs, j'utiliserai Far avec le plugin ImpEx:
En-tĂŞte NT
Après 128 (0x80) octets, nous sommes arrivés à l'en-tête NT (IMAGE_NT_HEADERS64), qui contient également l'en-tête PE (IMAGE_OPTIONAL_HEADER64). Malgré le nom IMAGE_OPTIONAL_HEADER64 est obligatoire, mais différent pour les architectures x64 et x86.
Structure IMAGE_NT_HEADERS64
Struct IMAGE_NT_HEADERS64
{
u32 Signature // 0x4550 "PE"
Struct IMAGE_FILE_HEADER
{
u16 Machine // 0x8664 x86-64
u16 NumberOfSections // 0x03
u32 TimeDateStamp //
u32 PointerToSymbolTable
u32 NumberOfSymbols
u16 SizeOfOptionalHeader // IMAGE_OPTIONAL_HEADER64 ()
u16 Characteristics // 0x2F
}
Struct IMAGE_OPTIONAL_HEADER64
{
u16 Magic // 0x020B PE64
u8 MajorLinkerVersion
u8 MinorLinkerVersion
u32 SizeOfCode
u32 SizeOfInitializedData
u32 SizeOfUninitializedData
u32 AddressOfEntryPoint // 0x1000
u32 BaseOfCode // 0x1000
u64 ImageBase // 0x400000
u32 SectionAlignment // 0x1000 (4096 )
u32 FileAlignment // 0x200
u16 MajorOperatingSystemVersion // 0x05 Windows XP
u16 MinorOperatingSystemVersion // 0x02 Windows XP
u16 MajorImageVersion
u16 MinorImageVersion
u16 MajorSubsystemVersion // 0x05 Windows XP
u16 MinorSubsystemVersion // 0x02 Windows XP
u32 Win32VersionValue
u32 SizeOfImage // 0x4000
u32 SizeOfHeaders // 0x200 (512 )
u32 CheckSum
u16 Subsystem // 0x02 (GUI) 0x03 (Console)
u16 DllCharacteristics
u64 SizeOfStackReserve // 0x100000
u64 SizeOfStackCommit // 0x1000
u64 SizeOfHeapReserve // 0x100000
u64 SizeOfHeapCommit // 0x1000
u32 LoaderFlags
u32 NumberOfRvaAndSizes // 0x16
Struct IMAGE_DATA_DIRECTORY [16]
{
u32 VirtualAddress
u32 Size
}
}
}
Voyons ce qui est stocké dans cette structure:
Description IMAGE_NT_HEADERS64
Signature — PE
IMAGE_FILE_HEADER x86 x64.
Machine — x64
NumberOfSections — ( )
TimeDateStamp —
SizeOfOptionalHeader — IMAGE_OPTIONAL_HEADER64, IMAGE_OPTIONAL_HEADER32.
Characteristics — , , (EXECUTABLE_IMAGE) 2 RAM (LARGE_ADDRESS_AWARE), ( ) (RELOCS_STRIPPED | LINE_NUMS_STRIPPED | LOCAL_SYMS_STRIPPED).
SizeOfCode — ( .text)
SizeOfInitializedData — ( .rodata)
SizeOfUninitializedData — ( .bss)
BaseOfCode —
SectionAlignment —
FileAlignment —
SizeOfImage —
SizeOfHeaders — (IMAGE_DOS_HEADER, DOS Stub, IMAGE_NT_HEADERS64, IMAGE_SECTION_HEADER[IMAGE_FILE_HEADER.NumberOfSections]) FileAlignment
Subsystem — GUI Console
MajorOperatingSystemVersion, MinorOperatingSystemVersion, MajorSubsystemVersion, MinorSubsystemVersion — exe, . 5.2 Windows XP (x64).
SizeOfStackReserve — . 1 , 1. Rust , C++ .
SizeOfStackCommit — 4 . .
SizeOfHeapReserve — . 1 .
SizeOfHeapCommit — 4 . SizeOfStackCommit, .
IMAGE_DATA_DIRECTORY — . , , 16 . .
, , . :
Export(0) — . DLL. .
Import(1) — DLL. VirtualAddress = 0x3000 Size = 0xB8. , .
Resource(2) — (, , ..)
.
IMAGE_FILE_HEADER x86 x64.
Machine — x64
NumberOfSections — ( )
TimeDateStamp —
SizeOfOptionalHeader — IMAGE_OPTIONAL_HEADER64, IMAGE_OPTIONAL_HEADER32.
Characteristics — , , (EXECUTABLE_IMAGE) 2 RAM (LARGE_ADDRESS_AWARE), ( ) (RELOCS_STRIPPED | LINE_NUMS_STRIPPED | LOCAL_SYMS_STRIPPED).
SizeOfCode — ( .text)
SizeOfInitializedData — ( .rodata)
SizeOfUninitializedData — ( .bss)
BaseOfCode —
SectionAlignment —
FileAlignment —
SizeOfImage —
SizeOfHeaders — (IMAGE_DOS_HEADER, DOS Stub, IMAGE_NT_HEADERS64, IMAGE_SECTION_HEADER[IMAGE_FILE_HEADER.NumberOfSections]) FileAlignment
Subsystem — GUI Console
MajorOperatingSystemVersion, MinorOperatingSystemVersion, MajorSubsystemVersion, MinorSubsystemVersion — exe, . 5.2 Windows XP (x64).
SizeOfStackReserve — . 1 , 1. Rust , C++ .
SizeOfStackCommit — 4 . .
SizeOfHeapReserve — . 1 .
SizeOfHeapCommit — 4 . SizeOfStackCommit, .
IMAGE_DATA_DIRECTORY — . , , 16 . .
, , . :
Export(0) — . DLL. .
Import(1) — DLL. VirtualAddress = 0x3000 Size = 0xB8. , .
Resource(2) — (, , ..)
.
Maintenant que nous avons regardé en quoi consiste l'en-tête NT, nous allons également l'écrire dans un fichier par analogie avec les autres à 0x80.
(3) En-tête RAW NT (décalage 0x00000080)
50 45 00 00 64 86 03 00 F4 70 E8 5E 00 00 00 00
00 00 00 00 F0 00 2F 00 0B 02 00 00 3D 00 00 00
13 00 00 00 00 00 00 00 00 10 00 00 00 10 00 00
00 00 40 00 00 00 00 00 00 10 00 00 00 02 00 00
05 00 02 00 00 00 00 00 05 00 02 00 00 00 00 00
00 40 00 00 00 02 00 00 00 00 00 00 02 00 00 00
00 00 10 00 00 00 00 00 00 10 00 00 00 00 00 00
00 00 10 00 00 00 00 00 00 10 00 00 00 00 00 00
00 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00
00 30 00 00 B8 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 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 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
En conséquence, nous obtenons ce type d'en-têtes IMAGE_FILE_HEADER, IMAGE_OPTIONAL_HEADER64 et IMAGE_DATA_DIRECTORY:
Ensuite, nous décrivons toutes les sections de notre application selon la structure IMAGE_SECTION_HEADER
Structure IMAGE_SECTION_HEADER
Struct IMAGE_SECTION_HEADER
{
i8[8] Name
u32 VirtualSize
u32 VirtualAddress
u32 SizeOfRawData
u32 PointerToRawData
u32 PointerToRelocations
u32 PointerToLinenumbers
u16 NumberOfRelocations
u16 NumberOfLinenumbers
u32 Characteristics
}
Description de IMAGE_SECTION_HEADER
Name — 8 ,
VirtualSize —
VirtualAddress — SectionAlignment
SizeOfRawData — FileAlignment
PointerToRawData — FileAlignment
Characteristics — (, , , , .)
VirtualSize —
VirtualAddress — SectionAlignment
SizeOfRawData — FileAlignment
PointerToRawData — FileAlignment
Characteristics — (, , , , .)
Dans notre cas, nous aurons 3 sections.
Pourquoi Virtual Address (VA) commence à partir de 1000, et non à partir de zéro, je ne sais pas, mais tous les compilateurs que j'ai envisagés le font. En conséquence, 1000 + 3 sections * 1000 (SectionAlignment) = 4000 que nous avons écrit dans SizeOfImage. Il s'agit de la taille totale de notre programme en mémoire virtuelle. Probablement utilisé pour allouer de l'espace pour un programme en mémoire.
Name | RAW Addr | RAW Size | VA | VA Size | Attr
--------+---------------+---------------+-------+---------+--------
.text | 200 | 200 | 1000 | 3D | CER
.rdata | 400 | 200 | 2000 | 13 | I R
.idata | 600 | 200 | 3000 | B8 | I R
DĂ©codage des attributs:
I - Données initialisées,
U - Données non initialisées,
C - Code , pas de données initialisées , contient l'exécutable
E - Exécuter le code, permet d'exécuter
R - Lire le code , permet de lire les données de la section
W - Écrire, permet d'écrire des données dans la section
.text (.code) - stocke le code exécutable (le programme lui-même), les attributs CE
.rdata (.rodata) - stocke les données en lecture seule, telles que les constantes, les chaînes, etc., les attributs IR
.data - stocke les données qui peuvent être lues et écrites, telles que les variables statiques ou globales. Attributs IRW
.bss - Stocke des données non initialisées telles que des variables statiques ou globales. De plus, cette section a généralement une taille RAW nulle et une taille VA différente de zéro, elle ne prend donc pas de place dans le fichier.
Attributs URW .idata - une section contenant des fonctions importées d'autres bibliothèques. Attributs IR
Point important, les sections doivent se suivre. De plus, à la fois dans le fichier et en mémoire. Au moins, lorsque j'ai changé leur ordre de manière arbitraire, le programme s'est arrêté de fonctionner.
Maintenant que nous savons quelles sections notre programme contiendra, nous les écrirons dans notre fichier. Ici, le décalage se termine à 8 et l'enregistrement démarre à partir du milieu du fichier.
(4) Sections RAW (décalage 0x00000188)
2E 74 65 78 74 00 00 00
3D 00 00 00 00 10 00 00 00 02 00 00 00 02 00 00
00 00 00 00 00 00 00 00 00 00 00 00 20 00 00 60
2E 72 64 61 74 61 00 00 13 00 00 00 00 20 00 00
00 02 00 00 00 04 00 00 00 00 00 00 00 00 00 00
00 00 00 00 40 00 00 40 2E 69 64 61 74 61 00 00
B8 00 00 00 00 30 00 00 00 02 00 00 00 06 00 00
00 00 00 00 00 00 00 00 00 00 00 00 40 00 00 40
L'adresse d'entrée suivante sera 00000200 qui correspond au champ SizeOfHeaders de l'en-tête PE. Si nous ajoutions une section supplémentaire, et cela correspond à 40 octets, nos en-têtes ne rentreraient pas dans 512 (0x200) octets et devraient utiliser 512 + 40 = 552 octets alignés par FileAlignment, soit 1024 (0x400) octets. Et tout ce qui reste de 0x228 (552) à l'adresse 0x400 doit être rempli avec quelque chose, mieux bien sûr avec des zéros.
Voyons Ă quoi ressemble un bloc de sections dans Far:
Ensuite, nous allons Ă©crire les sections elles-mĂŞmes dans notre fichier, mais il y a une nuance.
Comme vous pouvez le voir dans l'exemple SizeOfHeaders, nous ne pouvons pas simplement écrire l'en-tête et passer à la section suivante. Puisque pour enregistrer une rubrique, nous devons savoir combien de temps toutes les rubriques prendront ensemble. En conséquence, nous devons soit calculer à l'avance l'espace nécessaire, soit écrire des valeurs vides (zéro), et après avoir écrit tous les en-têtes, retourner et noter leur taille réelle.
Par conséquent, les programmes sont compilés en plusieurs passes. Par exemple, la section .rdata vient après la section .text, alors que nous ne pouvons pas trouver l'adresse virtuelle de la variable dans le .rdata, car si la section .text augmente de plus de 0x1000 (SectionAlignment) octets, elle occupera les adresses 0x2000 de la plage. Et en conséquence, la section .rdata ne sera plus située à 0x2000, mais à 0x3000. Et nous devrons revenir en arrière et recalculer les adresses de toutes les variables dans la section .text qui précède .rdata.
Mais dans ce cas, j'ai déjà tout calculé, nous allons donc immédiatement écrire les blocs de code.
.Section de texte
Segment asm .text
0000 push rbp
0001 mov rbp, rsp
0004 sub rsp, 0x20
0008 mov rcx, 0x0
000F mov rdx, 0x402000
0016 mov r8, 0x40200D
001D mov r9, 0x40
0024 call QWORD PTR [rip + 0x203E]
002A mov rcx, 0x0
0031 call QWORD PTR [rip + 0x2061]
0037 add rsp, 0x20
003B pop rbp
003C ret
Spécifiquement pour ce programme, les 3 premières lignes, exactement comme les 3 dernières, ne sont pas nécessaires.
Les 3 derniers ne seront même pas exécutés, puisque le programme se terminera à la deuxième fonction d'appel.
Mais disons ceci, si ce n'était pas la fonction principale, mais une sous-fonction, cela devrait être fait de cette façon.
Mais les 3 premiers dans ce cas, bien que non nécessaires, sont souhaitables. Par exemple, si nous n'utilisions pas MessageBoxA, mais printf, alors sans ces lignes, nous recevrions une erreur.
Selon la convention d'appel pour les systèmes MSDN 64 bits, les 4 premiers paramètres sont passés dans les registres RCX, RDX, R8, R9. S'ils tiennent là et ne sont pas, par exemple, un nombre à virgule flottante. Et le reste est passé à travers la pile.
En théorie, si nous passons 2 arguments à une fonction, alors nous devons les passer par des registres et leur réserver deux places dans la pile, afin que, si nécessaire, la fonction puisse pousser les registres sur la pile. De plus, nous ne devons pas nous attendre à ce que ces registres nous soient retournés dans leur état d'origine.
Donc, le problème avec la fonction printf est que si nous lui passons seulement 1 argument, elle écrasera toujours les 4 places de la pile, bien qu'elle semble devoir en écraser une seule, du nombre d'arguments.
Par conséquent, si vous ne souhaitez pas que le programme se comporte de manière étrange, réservez toujours au moins 8 octets * 4 arguments = 32 (0x20) octets si vous passez au moins 1 argument à la fonction.
Considérons un bloc de code avec des appels de fonction
MessageBoxA(0, 'Hello World!', 'MyApp', 64)
ExitProcess(0)
Nous passons d'abord nos arguments:
rcx = 0
rdx = l'adresse absolue de la chaîne en mémoire ImageBase + Sections [". Rdata"]. VirtualAddress + Offset de la chaîne depuis le début de la section, la chaîne est lue jusqu'à l'octet zéro
r8 = similaire au précédent
r9 = 64 (0x40) MB_ICONINFORMATION , icĂ´ne d'information
Et puis il y a un appel à la fonction MessageBoxA, avec laquelle tout n'est pas si simple. Le fait est que les compilateurs essaient d'utiliser les commandes les plus courtes possibles. Plus la taille des instructions est petite, plus ces instructions entreront dans le cache du processeur, respectivement, il y aura moins d'erreurs de cache, de surcharges et plus la vitesse du programme sera élevée. Pour plus d'informations sur les commandes et le fonctionnement interne du processeur, reportez-vous aux manuels du développeur de logiciels des architectures Intel 64 et IA-32.
Nous pourrions appeler la fonction à l'adresse complète, mais cela prendrait au moins (1 opcode + 8 adresse = 9 octets), et avec une adresse relative, la commande d'appel ne prend que 6 octets.
Regardons de plus près cette magie: rip + 0x203E n'est rien de plus qu'un appel de fonction à l'adresse spécifiée par notre offset.
J'ai regardé un peu plus loin et j'ai trouvé les adresses des compensations dont nous avons besoin. Pour MessageBoxA, il s'agit de 0x3068 et pour ExitProcess, de 0x3098.
Il est temps de transformer la magie en science. Chaque fois qu'un opcode frappe le processeur, il calcule sa longueur et l'ajoute à l'adresse d'instruction courante (RIP). Par conséquent, lorsque nous utilisons RIP dans une instruction, cette adresse indique la fin de l'instruction en cours / le début de la suivante.
Pour le premier appel, l'offset indiquera la fin de la commande d'appel, c'est 002A. N'oubliez pas qu'en mémoire cette adresse sera à l'offset Sections [". Text"]. VirtualAddress, i.e. 0x1000. Par conséquent, le RIP de notre appel sera 102A. L'adresse dont nous avons besoin pour MessageBoxA est à 0x3068. Considérez 0x3068 - 0x102A = 0x203E . Pour la deuxième adresse, tout est identique à 0x1000 + 0x0037 = 0x1037, 0x3098 - 0x1037 = 0x2061 .
Ce sont ces décalages que nous avons vus dans les commandes de l'assembleur.
0024 call QWORD PTR [rip + 0x203E]
002A mov rcx, 0x0
0031 call QWORD PTR [rip + 0x2061]
0037 add rsp, 0x20
Écrivons la section .text dans notre fichier, en ajoutant des zéros à l'adresse 0x400:
(5) Section .text RAW (DĂ©calage 0x00000200-0x00000400)
55 48 89 E5 48 83 EC 20 48 C7 C1 00 00 00 00 48
C7 C2 00 20 40 00 49 C7 C0 0D 20 40 00 49 C7 C1
40 00 00 00 FF 15 3E 20 00 00 48 C7 C1 00 00 00
00 FF 15 61 20 00 00 48 83 C4 20 5D C3 00 00 00
........
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
4 . FileAlignment. 0x000003F0, 0x00000400, . 1024 , ! .
Section .Rdata
C'est peut-être la section la plus simple. Nous allons simplement mettre deux lignes ici, en ajoutant des zéros à 512 octets.
.rdata
0400 "Hello World!\0"
040D "MyApp\0"
(6) Section RAW .rdata (DĂ©calage 0x00000400-0x00000600)
48 65 6C 6C 6F 20 57 6F 72 6C 64 21 00 4D 79 41
70 70 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
Section .Idata
Eh bien, voici la dernière section, qui décrit les fonctions importées des bibliothèques.
La première chose qui nous attend est la nouvelle structure IMAGE_IMPORT_DESCRIPTOR
Structure IMAGE_IMPORT_DESCRIPTOR
Struct IMAGE_IMPORT_DESCRIPTOR
{
u32 OriginalFirstThunk (INT)
u32 TimeDateStamp
u32 ForwarderChain
u32 Name
u32 FirstThunk (IAT)
}
Description IMAGE_IMPORT_DESCRIPTOR
OriginalFirstThunk — , Import Name Table (INT)
Name — ,
FirstThunk — , Import Address Table (IAT)
Name — ,
FirstThunk — , Import Address Table (IAT)
Tout d'abord, nous devons ajouter 2 bibliothèques importées. Rappel:
func MessageBoxA(u32 handle, PChar text, PChar caption, u32 type) i32 ['user32.dll']
func ExitProcess(u32 code) ['kernel32.dll']
(7) RAW IMAGE_IMPORT_DESCRIPTOR (décalage 0x00000600)
58 30 00 00 00 00 00 00 00 00 00 00 3C 30 00 00
68 30 00 00 88 30 00 00 00 00 00 00 00 00 00 00
48 30 00 00 98 30 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
Nous utilisons 2 bibliothèques, et dire que nous avons fini de les lister. La dernière structure est remplie de zéros.
INT | Time | Forward | Name | IAT
--------+--------+----------+--------+--------
0x3058 | 0x0 | 0x0 | 0x303C | 0x3068
0x3088 | 0x0 | 0x0 | 0x3048 | 0x3098
0x0000 | 0x0 | 0x0 | 0x0000 | 0x0000
Ajoutons maintenant les noms des bibliothèques elles-mêmes:
Noms de bibliothèque
063 "user32.dll\0"
0648 "kernel32.dll\0"
(8) Noms de bibliothèque RAW (Offset 0x0000063C)
75 73 65 72
33 32 2E 64 6C 6C 00 00 6B 65 72 6E 65 6C 33 32
2E 64 6C 6C 00 00 00 00
Ensuite, décrivons la bibliothèque user32:
(9) RAW user32.dll (décalage 0x00000658)
78 30 00 00 00 00 00 00
00 00 00 00 00 00 00 00 78 30 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 4D 65 73 73 61 67
65 42 6F 78 41 00 00 00
Le champ Nom de la première librairie pointe vers 0x303C si on regarde un peu plus haut, on verra qu'à l'adresse 0x063C il y a une librairie "user32.dll \ 0".
Astuce, rappelez-vous que la section .idata correspond au décalage de fichier 0x0600 et au décalage de mémoire 0x3000. Pour la première bibliothèque, INT est 3058, ce qui signifie que dans le fichier, il sera offset 0x0658. À cette adresse, nous voyons l'entrée 0x3078 et le deuxième zéro. Signifiant la fin de la liste. 3078 fait référence à 0x0678 c'est la chaîne RAW
"00 00 4D 65 73 73 61 67 65 42 6F 78 41 00 00 00"
Les 2 premiers octets ne nous intéressent pas et sont égaux à zéro. Et puis il y a une ligne avec le nom de la fonction, se terminant par zéro. Autrement dit, nous pouvons le représenter comme "\ 0 \ 0MessageBoxA \ 0".
Dans ce cas, l'IAT fait référence à une structure similaire à la table IAT, mais seules les adresses de fonction y seront chargées au démarrage du programme. Par exemple, la première entrée 0x3068 en mémoire aura une valeur autre que 0x0668 dans le fichier. Il y aura l'adresse de la fonction MessageBoxA chargée par le système à laquelle nous ferons référence par l'appel d'appel dans le code du programme.
Et la dernière pièce du puzzle, le kernel32. Et n'oubliez pas d'ajouter des zéros à SectionAlignment.
(10) RAW kernel32.dll (décalage 0x00000688-0x00000800)
A8 30 00 00 00 00 00 00
00 00 00 00 00 00 00 00 A8 30 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 45 78 69 74 50 72
6F 63 65 73 73 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
Nous vérifions que Far a pu identifier correctement les fonctions que nous avons importées:
Excellent! Tout allait bien, donc maintenant notre fichier est prĂŞt Ă fonctionner.
Roulement de tambour…
Le final
FĂ©licitations, nous l'avons fait!
Le fichier occupe 2 Ko = en-tĂŞtes 512 octets + 3 sections de 512 octets.
Le nombre 512 (0x200) n'est rien de plus que le FileAlignment que nous avons spécifié dans l'en-tête de notre programme.
De plus:
si vous voulez aller un peu plus loin, vous pouvez remplacer l'inscription "Hello World!" à autre chose, n'oubliez pas de changer l'adresse de la ligne dans le code du programme (section .text). L'adresse en mémoire est 0x00402000, mais le fichier aura l'ordre d'octet inversé 00 20 40 00.
Ou la quête est un peu plus compliquée. Ajoutez un autre appel MessageBox au code. Pour ce faire, vous devrez copier l'appel précédent et recalculer l'adresse relative (0x3068 - RIP) qu'il contient.
Conclusion
L'article s'est avéré plutôt froissé, il se composerait bien sûr de 3 parties distinctes: en-têtes, programme, tableau d'importation.
Si quelqu'un a compilé son exe, mon travail n'a pas été vain.
Je pense bientôt créer un fichier ELF de la même manière, un tel article serait-il intéressant?)
Liens:
- Intel 64 et IA-32 Architectures Software Developer Manuals Manuals
Commands and Processor Architecture Guide.
- PE (Portable Executable): On Stranger Tides
Excellent article sur la structure des fichiers exe. - Référentiel de documentation Microsoft
Vous trouverez ici toutes les informations sur les en-tĂŞtes, les structures, les types et leur description