Créer EXE

L'auto-isolement est le moment idéal pour commencer quelque chose qui demande beaucoup de temps et d'efforts. J'ai donc décidé de faire ce que j'ai toujours voulu: écrire mon propre compilateur.



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










, , .



, (Offset) .



, 0x00000000, 64 (0x40 16- ), 0x00000040 ..

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.



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) — (, , ..)

.



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 — (, , , , .)



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)



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:






All Articles