Buat EXE

Isolasi diri adalah waktu yang tepat untuk memulai sesuatu yang membutuhkan banyak waktu dan tenaga. Jadi saya memutuskan untuk melakukan apa yang selalu saya inginkan - menulis kompiler saya sendiri.



Sekarang dia dapat membangun Hello World, tetapi dalam artikel ini saya tidak ingin berbicara tentang parsing dan struktur internal kompiler, tetapi tentang bagian penting seperti perakitan byte-by-byte dari file exe.



Mulailah



Ingin spoiler? Program kami akan berukuran 2048 byte.



Biasanya, bekerja dengan file exe adalah mempelajari atau mengubah strukturnya. File yang dapat dieksekusi sendiri dibentuk oleh kompiler, dan proses ini tampaknya agak ajaib bagi pengembang.



Tapi sekarang kami akan mencoba memperbaikinya!



Untuk membangun program kita, kita membutuhkan editor HEX (saya pribadi menggunakan HxD).



Untuk memulai, mari kita gunakan pseudocode:



Sumber
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)
}




Dua baris pertama menunjukkan fungsi yang diimpor dari pustaka WinAPI . Fungsi MessageBoxA menampilkan kotak dialog dengan teks kita, dan ExitProcess memberi tahu sistem tentang akhir program.

Tidak masuk akal untuk mempertimbangkan fungsi utama secara terpisah, karena menggunakan fungsi yang dijelaskan di atas.



Header DOS



Pertama, kita perlu membuat Header DOS yang benar, ini adalah header untuk program DOS dan seharusnya tidak mempengaruhi peluncuran exe pada Windows.



Saya mencatat bidang yang kurang lebih penting, sisanya diisi dengan nol.



Struktur 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
}




Yang terpenting, header ini berisi bidang e_magic, yang berarti ini adalah file yang dapat dieksekusi, dan e_lfanew, yang menunjukkan offset header PE dari awal file (dalam file kami, offset ini adalah 0x80 = 128 byte).



Bagus, sekarang kita tahu struktur DOS Header, mari kita tulis ke file kita.



(1) Kepala DOS RAW (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 ..

Selesai, 64 byte pertama ditulis. Sekarang Anda perlu menambahkan 64 lagi, inilah yang disebut DOS Stub (Stub). Ketika diluncurkan dari bawah DOS, itu harus memberitahu pengguna bahwa program tidak dirancang untuk berjalan dalam mode ini.



Tetapi secara umum, ini adalah program DOS kecil yang mencetak garis dan keluar dari program.

Mari tulis Stub kita ke sebuah file dan pertimbangkan lebih detail.



(2) RAW DOS Stub (Offset 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






Dan sekarang kode yang sama, tetapi dalam bentuk yang dibongkar



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$" ;  




Cara kerjanya seperti ini: pertama, rintisan mencetak baris yang menyatakan bahwa program tidak dapat dimulai, dan kemudian keluar dari program dengan kode 1. Yang berbeda dari terminasi normal (Kode 0).



Kode rintisan mungkin sedikit berbeda (dari kompiler ke kompiler) Saya membandingkan gcc dan delphi, tetapi arti umumnya sama.



Lucu juga bahwa baris rintisan diakhiri dengan \ x0D \ x0D \ x0A $. Kemungkinan besar alasan untuk perilaku ini adalah karena c ++ membuka file dalam mode teks secara default. Akibatnya, karakter \ x0A diganti dengan urutan \ x0D \ x0A. Hasilnya, kita mendapatkan 3 byte: 2 byte carriage return (0x0D) yang tidak ada artinya, dan 1 untuk umpan baris (0x0A). Dalam mode biner (std :: ios :: binary), substitusi ini tidak terjadi.



Untuk memeriksa kebenaran penulisan nilai, saya akan menggunakan Far dengan plugin ImpEx:







Header NT



Setelah 128 (0x80) byte, kami sampai ke header NT (IMAGE_NT_HEADERS64), yang juga berisi header PE (IMAGE_OPTIONAL_HEADER64). Meskipun nama IMAGE_OPTIONAL_HEADER64 diperlukan, tetapi berbeda untuk arsitektur x64 dan x86.



IMAGE_NT_HEADERS64 struktur
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
		}
	}
}




Mari kita lihat apa yang disimpan dalam struktur ini:



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

.



Sekarang kita telah melihat apa yang terdiri dari header NT, kita juga akan menulisnya ke file dengan analogi dengan yang lain di 0x80.



(3) RAW NT-Header (Offset 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




Hasilnya, kami mendapatkan header IMAGE_FILE_HEADER, IMAGE_OPTIONAL_HEADER64, dan IMAGE_DATA_DIRECTORY seperti ini:















Selanjutnya, kami menjelaskan semua bagian aplikasi kami sesuai dengan struktur IMAGE_SECTION_HEADER



Struktur 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
}




Deskripsi IMAGE_SECTION_HEADER
Name — 8 ,

VirtualSize —

VirtualAddress — SectionAlignment

SizeOfRawData — FileAlignment

PointerToRawData — FileAlignment

Characteristics — (, , , , .)



Dalam kasus kami, kami akan memiliki 3 bagian.



Mengapa Virtual Address (VA) dimulai dari 1000, dan bukan dari awal, saya tidak tahu, tetapi semua kompiler yang saya anggap melakukan ini. Hasilnya, 1000 + 3 bagian * 1000 (SectionAlignment) = 4000 yang kami tulis di SizeOfImage. Ini adalah ukuran total program kami di memori virtual. Mungkin digunakan untuk mengalokasikan ruang untuk program di memori.



 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


Penguraian atribut:



I - Data yang diinisialisasi, data yang diinisialisasi

U - Data yang tidak diinisialisasi, bukan data yang diinisialisasi

C - Kode, berisi kode yang dapat dieksekusi

E - Jalankan, memungkinkan menjalankan

R - Baca kode , memungkinkan membaca data dari bagian

W - Tulis, memungkinkan penulisan data ke bagian



.text (.code) - menyimpan kode yang dapat dieksekusi (program itu sendiri), atribut CE

.rdata (.rodata) - menyimpan data hanya-baca, misalnya, konstanta, string, dll., atribut IR

.data - menyimpan data yang dapat dibaca dan ditulis, seperti variabel statis atau global. Atribut IRW

.bss - Menyimpan data yang tidak diinisialisasi seperti variabel statis atau global. Selain itu, bagian ini biasanya memiliki ukuran RAW nol dan Ukuran VA bukan nol, sehingga tidak menggunakan ruang file. Atribut URW

.idata - bagian yang berisi fungsi yang diimpor dari pustaka lain. Atribut IR



Poin penting, bagian harus mengikuti satu sama lain. Apalagi baik di file maupun di memory. Setidaknya ketika saya mengubah pesanan mereka secara sewenang-wenang, program berhenti berjalan.



Sekarang kita tahu bagian mana dari program kita, kita akan menuliskannya ke file kita. Di sini offset berakhir pada 8 dan perekaman akan dimulai dari tengah file.



(4) Bagian RAW (Offset 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






Alamat entri berikutnya adalah 00000200 yang sesuai dengan bidang SizeOfHeaders dari PE-Header. Jika kita menambahkan satu bagian lagi, yang ditambah 40 byte, maka header kita tidak akan cocok dengan 512 (0x200) byte dan harus menggunakan 512 + 40 = 552 byte yang diselaraskan oleh FileAlignment, yaitu 1024 (0x400) byte. Dan semua yang tersisa dari 0x228 (552) ke alamat 0x400 harus diisi dengan sesuatu, lebih baik tentu saja dengan angka nol.



Mari kita lihat seperti apa tampilan blok bagian di Jauh:







Selanjutnya, kita akan menulis bagian itu sendiri ke file kita, tetapi ada satu nuansa.



Seperti yang Anda lihat dari contoh SizeOfHeaders, kita tidak bisa begitu saja menulis header dan melanjutkan ke bagian berikutnya. Karena untuk merekam sebuah heading, kita perlu mengetahui berapa lama semua heading akan digabungkan. Akibatnya, kita perlu menghitung terlebih dahulu berapa banyak ruang yang dibutuhkan, atau menulis nilai kosong (nol), dan setelah menulis semua header, kembalikan dan tulis ukuran sebenarnya.



Oleh karena itu, program dikompilasi dalam beberapa lintasan. Misalnya, bagian .rdata muncul setelah bagian .text, sementara kami tidak dapat menemukan alamat virtual variabel di .rdata, karena jika bagian .text bertambah lebih dari 0x1000 (SectionAlignment) byte, itu akan menempati alamat 0x2000 dari jangkauan. Dan karenanya, bagian .rdata tidak lagi berada di 0x2000, tetapi di 0x3000. Dan kita perlu kembali dan menghitung ulang alamat semua variabel di bagian .text yang muncul sebelum .rdata.



Tetapi dalam hal ini, saya sudah menghitung semuanya, jadi kami akan segera menuliskan blok kode.



.Bagian teks



Asm segment .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




Khusus untuk program ini, 3 baris pertama, persis seperti 3 baris terakhir, tidak diperlukan.

3 yang terakhir bahkan tidak akan dijalankan, karena program akan keluar pada fungsi panggilan kedua.



Tetapi katakanlah ini, jika itu bukan fungsi utama, tetapi subfungsi, itu harus dilakukan dengan cara ini.



Tetapi 3 yang pertama dalam kasus ini, meskipun tidak perlu, diinginkan. Misalnya, jika kita tidak menggunakan MessageBoxA, tetapi printf, maka tanpa baris ini kita akan mendapatkan error.



Menurut konvensi pemanggilan untuk sistem MSDN 64-bit, 4 parameter pertama dilewatkan dalam register RCX, RDX, R8, R9. Jika mereka pas di sana dan tidak, misalnya, bilangan floating point. Dan sisanya melewati tumpukan.



Secara teori, jika kita meneruskan 2 argumen ke suatu fungsi, maka kita harus meneruskannya melalui register dan mencadangkan dua tempat di stack untuknya, sehingga, jika perlu, fungsi tersebut dapat mendorong register ke stack. Selain itu, kami tidak mengharapkan bahwa register ini akan dikembalikan kepada kami dalam keadaan semula.



Jadi masalah dengan fungsi printf adalah jika kita hanya memberikan 1 argumen padanya, itu masih akan menimpa semua 4 tempat di tumpukan, meskipun tampaknya harus menimpa hanya satu, dengan jumlah argumen.



Oleh karena itu, jika Anda tidak ingin program berperilaku aneh, selalu sisakan setidaknya 8 byte * 4 argumen = 32 (0x20) byte jika Anda meneruskan setidaknya 1 argumen ke fungsi tersebut.



Pertimbangkan satu blok kode dengan pemanggilan fungsi



MessageBoxA(0, 'Hello World!', 'MyApp', 64)
ExitProcess(0)


Pertama kita melewati argumen kita:



rcx = 0

rdx = alamat absolut dari string di memori ImageBase + Sections [". Rdata"]. VirtualAddress + Offset string dari awal bagian, string dibaca ke byte nol

r8 = mirip dengan sebelumnya

r9 = 64 (0x40) MB_ICONINFORMATION , ikon informasi



Dan kemudian ada panggilan ke fungsi MessageBoxA, yang semuanya tidak sesederhana itu. Intinya adalah kompiler mencoba menggunakan perintah sesingkat mungkin. Semakin kecil ukuran instruksi, semakin banyak instruksi tersebut akan masuk ke dalam cache prosesor, masing-masing, akan ada lebih sedikit cache yang meleset, kelebihan beban, dan semakin tinggi kecepatan program. Untuk informasi lebih lanjut tentang perintah dan cara kerja prosesor, lihat Panduan Pengembang Perangkat Lunak Arsitektur Intel 64 dan IA-32.



Kita dapat memanggil fungsi tersebut di alamat lengkap, tetapi itu akan membutuhkan setidaknya (1 opcode + 8 alamat = 9 byte), dan dengan alamat relatif, perintah panggilan hanya membutuhkan 6 byte.



Mari kita lihat lebih dekat keajaiban ini: rip + 0x203E tidak lebih dari pemanggilan fungsi di alamat yang ditentukan oleh offset kami.



Saya melihat sedikit ke depan dan menemukan alamat offset yang kami butuhkan. Untuk MessageBoxA adalah 0x3068 dan untuk ExitProcess adalah 0x3098.



Saatnya mengubah sihir menjadi sains. Setiap kali opcode mengenai prosesor, ia menghitung panjangnya dan menambahkannya ke alamat instruksi saat ini (RIP). Oleh karena itu, ketika kita menggunakan RIP di dalam sebuah instruksi, alamat ini menunjukkan akhir dari instruksi saat ini / awal instruksi berikutnya.

Untuk panggilan pertama, offset akan menunjukkan akhir dari perintah panggilan, ini adalah 002A. Jangan lupa bahwa dalam memori alamat ini akan berada di Bagian offset [". Text"]. VirtualAddress, mis. 0x1000. Oleh karena itu, RIP untuk panggilan kami adalah 102A. Alamat yang kita butuhkan untuk MessageBoxA ada di 0x3068. Pertimbangkan 0x3068 - 0x102A = 0x203E . Untuk alamat kedua, semuanya sama dengan 0x1000 + 0x0037 = 0x1037, 0x3098 - 0x1037 = 0x2061 .



Offset inilah yang kita lihat di perintah assembler.



0024	call QWORD PTR [rip + 0x203E]
002A	mov rcx, 0x0
0031	call QWORD PTR [rip + 0x2061]
0037	add rsp, 0x20


Mari tulis bagian .text ke file kita, dengan menambahkan angka nol ke alamat 0x400:



(5) Bagian teks RAW (Offset 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 , ! .




Bagian .Rdata



Ini mungkin bagian yang paling sederhana. Kami hanya akan meletakkan dua baris di sini, menambahkan nol menjadi 512 byte.



.rdata
0400	"Hello World!\0"
040D	"MyApp\0"




(6) Bagian RAW .rdata (Offset 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




Bagian .Idata



Nah, inilah bagian terakhir, yang menjelaskan fungsi yang diimpor dari perpustakaan.



Hal pertama yang menunggu kami adalah struktur baru IMAGE_IMPORT_DESCRIPTOR



IMAGE_IMPORT_DESCRIPTOR struktur
Struct IMAGE_IMPORT_DESCRIPTOR
{
	u32 OriginalFirstThunk (INT)
	u32 TimeDateStamp
	u32 ForwarderChain
	u32 Name
	u32 FirstThunk (IAT)
}




Deskripsi IMAGE_IMPORT_DESCRIPTOR
OriginalFirstThunk — , Import Name Table (INT)

Name — ,

FirstThunk — , Import Address Table (IAT)



Pertama, kita perlu menambahkan 2 perpustakaan yang diimpor. Penarikan:



func MessageBoxA(u32 handle, PChar text, PChar caption, u32 type) i32 ['user32.dll']
func ExitProcess(u32 code) ['kernel32.dll']


(7) RAW IMAGE_IMPORT_DESCRIPTOR (Offset 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




Kami menggunakan 2 perpustakaan, dan mengatakan bahwa kami telah selesai mendaftarnya. Struktur terakhir diisi dengan angka nol.



 INT	| Time	 | Forward  | Name   | IAT
--------+--------+----------+--------+--------
0x3058	| 0x0    | 0x0      | 0x303C | 0x3068
0x3088	| 0x0    | 0x0      | 0x3048 | 0x3098
0x0000	| 0x0    | 0x0      | 0x0000 | 0x0000


Sekarang mari tambahkan nama pustaka itu sendiri:



Nama perpustakaan
063	"user32.dll\0"
0648	"kernel32.dll\0"




(8) Nama perpustakaan 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




Selanjutnya, mari kita gambarkan pustaka user32:



(9) RAW user32.dll (Offset 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




Bidang Nama pustaka pertama menunjuk ke 0x303C jika kita melihat sedikit lebih tinggi, kita akan melihat bahwa di alamat 0x063C ada pustaka "user32.dll \ 0".



Petunjuk, ingat bahwa bagian .idata terkait dengan file offset 0x0600 dan offset memori 0x3000. Untuk library pertama, INT adalah 3058, artinya di dalam file akan di-offset 0x0658. Di alamat ini, kita melihat entri 0x3078 dan nol kedua. Menandakan akhir dari daftar. 3078 mengacu pada 0x0678 ini adalah string RAW



"00 00 4D 65 73 73 61 67 65 42 6F 78 41 00 00 00"



2 byte pertama tidak menarik bagi kami dan sama dengan nol. Dan kemudian ada baris dengan nama fungsinya, diakhiri dengan nol. Artinya, kita dapat mewakilinya sebagai "\ 0 \ 0MessageBoxA \ 0".



Dalam hal ini, IAT mengacu pada struktur yang mirip dengan tabel IAT, tetapi hanya alamat fungsi yang akan dimuat ke dalamnya saat program dimulai. Misalnya, entri pertama 0x3068 di memori akan memiliki nilai selain 0x0668 di file. Akan ada alamat fungsi MessageBoxA yang dimuat oleh sistem yang akan kita rujuk melalui panggilan panggilan dalam kode program.



Dan bagian terakhir dari teka-teki itu, kernel32. Dan jangan lupa menambahkan angka nol ke SectionAlignment.



(10) RAW kernel32.dll (Offset 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






Kami memeriksa bahwa Far dapat mengidentifikasi dengan benar fungsi mana yang kami impor:







Hebat! Semuanya baik-baik saja, jadi sekarang file kita siap dijalankan.

Drumroll…



Akhir







Selamat, kami berhasil!



File ini menempati 2 KB = Header 512 byte + 3 bagian dari 512 byte.



Angka 512 (0x200) tidak lebih dari FileAlignment, yang kami tentukan di header program kami.



Selain itu:

Jika ingin lebih dalam, Anda dapat mengganti tulisan "Hello World!" ke hal lain, jangan lupa untuk mengubah alamat baris di kode program (bagian .text). Alamat di memori adalah 0x00402000, tetapi file akan berisi urutan byte terbalik 00 20 40 00.



Atau pencariannya sedikit lebih rumit. Tambahkan panggilan MessageBox lain ke kode. Untuk melakukan ini, Anda harus menyalin panggilan sebelumnya dan menghitung ulang alamat relatif di dalamnya (0x3068 - RIP).



Kesimpulan



Artikel tersebut ternyata agak kusut, tentu saja terdiri dari 3 bagian terpisah: Headings, Program, Import Table.



Jika seseorang mengkompilasi exe mereka, maka pekerjaan saya tidak sia-sia.



Aku berpikir tentang menciptakan file ELF dengan cara yang sama segera, akan artikel tersebut menarik)?



Links:






All Articles