إنشاء EXE

العزلة الذاتية هي وقت رائع لبدء شيء يستغرق الكثير من الوقت والجهد. لذلك قررت أن أفعل ما كنت أرغب فيه دائمًا - كتابة المترجم الخاص بي.



الآن هو قادر على ترجمة Hello World ، لكن في هذه المقالة لا أريد أن أتحدث عن التحليل والهيكل الداخلي للمترجم ، ولكن عن جزء مهم مثل تجميع بايت بايت لملف exe.



بداية



تريد سبويلر؟ سيكون برنامجنا 2048 بايت.



عادة ، العمل مع ملفات exe هو دراسة أو تعديل هيكلها. يتم تشكيل الملفات القابلة للتنفيذ نفسها من قبل المترجمين ، وهذه العملية تبدو سحرية بعض الشيء للمطورين.



لكن الآن سنحاول إصلاحه!



لبناء برنامجنا ، نحتاج إلى أي محرر HEX (أنا شخصياً استخدمت HxD).



للبدء ، لنأخذ الرمز الكاذب:



مصدر
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)
}




يشير أول سطرين إلى الوظائف المستوردة من مكتبات WinAPI . تعرض وظيفة MessageBoxA مربع حوار مع النص الخاص بنا ، وتقوم ExitProcess بإعلام النظام بنهاية البرنامج.

ليس من المنطقي النظر في الوظيفة الرئيسية بشكل منفصل ، لأنها تستخدم الوظائف الموضحة أعلاه.



رأس DOS



أولاً ، نحتاج إلى إنشاء DOS Header الصحيح ، هذا هو رأس برامج DOS ويجب ألا يؤثر على إطلاق exe تحت Windows.



لقد لاحظت حقولًا أكثر أو أقل أهمية ، والباقي مليء بالأصفار.



بنية 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
}




الأهم من ذلك ، أن هذا الرأس يحتوي على حقل e_magic ، مما يعني أن هذا ملف قابل للتنفيذ ، و e_lfanew ، مما يشير إلى إزاحة رأس PE من بداية الملف (في ملفنا ، هذه الإزاحة هي 0x80 = 128 بايت).



رائع ، الآن بعد أن عرفنا بنية DOS Header ، دعنا نكتبها في ملفنا.



(1) رأس RAW DOS (إزاحة 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 ..

تم ، تمت كتابة أول 64 بايت. أنت الآن بحاجة إلى إضافة 64 أخرى ، وهذا ما يسمى DOS Stub (Stub). عند التشغيل من نظام DOS ، يجب إخطار المستخدم بأن البرنامج غير مصمم للتشغيل في هذا الوضع.



لكن بشكل عام ، هذا برنامج DOS صغير يطبع سطرًا ويخرج من البرنامج.

دعنا نكتب Stub إلى ملف وننظر فيه بمزيد من التفصيل.



(2) كعب RAW DOS (إزاحة 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






والآن نفس الرمز ، ولكن في شكل مفكك



Asm DOS كعب
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$" ;  




إنه يعمل على النحو التالي: أولاً ، يقوم كعب الروتين بطباعة سطر يشير إلى أنه لا يمكن بدء تشغيل البرنامج ، ثم يخرج من البرنامج برمز 1. والذي يختلف عن الإنهاء العادي (الرمز 0).



قد يختلف كود كعب الروتين قليلاً (من مترجم إلى مترجم) لقد قارنت gcc و delphi ، لكن المعنى العام هو نفسه.



من المضحك أيضًا أن ينتهي السطر الأساسي بـ \ x0D \ x0D \ x0A $. غالبًا ما يكون سبب هذا السلوك هو أن c ++ يفتح الملف في وضع النص افتراضيًا. نتيجة لذلك ، يتم استبدال الحرف \ x0A بالتسلسل \ x0D \ x0A. نتيجة لذلك ، نحصل على 3 بايت: 2 بايت من حرف الإرجاع (0x0D) والذي لا معنى له ، وواحد لتغذية السطر (0x0A). في الوضع الثنائي (std :: ios :: binary) ، لا يحدث هذا الاستبدال.



للتحقق من كتابة القيم بشكل صحيح ، سأستخدم Far مع المكون الإضافي ImpEx:







رأس NT



بعد 128 (0x80) بايت ، وصلنا إلى رأس NT (IMAGE_NT_HEADERS64) ، والذي يحتوي أيضًا على رأس PE (IMAGE_OPTIONAL_HEADER64). على الرغم من أن الاسم IMAGE_OPTIONAL_HEADER64 مطلوب ، ولكنه مختلف لبنيات x64 و x86.



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




دعونا نرى ما يتم تخزينه في هذا الهيكل:



الوصف 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) — (, , ..)

.



الآن بعد أن نظرنا إلى ما يتكون منه رأس NT ، سنكتبه أيضًا في ملف عن طريق القياس مع الآخرين في 0x80.



(3) RAW NT-Header (الإزاحة 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




نتيجة لذلك ، نحصل على هذا النوع من رؤوس IMAGE_FILE_HEADER و IMAGE_OPTIONAL_HEADER64 و IMAGE_DATA_DIRECTORY:















بعد ذلك ، نصف جميع أقسام تطبيقنا وفقًا للبنية IMAGE_SECTION_HEADER



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
}




وصف IMAGE_SECTION_HEADER
Name — 8 ,

VirtualSize —

VirtualAddress — SectionAlignment

SizeOfRawData — FileAlignment

PointerToRawData — FileAlignment

Characteristics — (, , , , .)



في حالتنا ، سيكون لدينا 3 أقسام.



لماذا يبدأ Virtual Address (VA) من 1000 ، وليس من الصفر ، لا أعرف ، لكن كل المترجمين الذين أعتبرهم يفعلون ذلك. نتيجة لذلك ، 1000 + 3 أقسام * 1000 (SectionAlignment) = 4000 ، والتي كتبناها في SizeOfImage. هذا هو الحجم الإجمالي لبرنامجنا في الذاكرة الافتراضية. ربما تستخدم لتخصيص مساحة البرنامج في الذاكرة.



 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


فك تشفير السمات:



I - بيانات مهيأة ، بيانات مهيأة

U - بيانات غير مهيأة ، ليست بيانات مهيأة

C - كود ، يحتوي على كود قابل للتنفيذ

E - تنفيذ ، يسمح بتنفيذ

R - قراءة الكود ، يسمح بقراءة البيانات من القسم

W - الكتابة ، يسمح لكتابة البيانات إلى القسم



.text (.code) - يخزن الكود القابل للتنفيذ (البرنامج نفسه) ، سمات CE

.rdata (.rodata) - يخزن بيانات للقراءة فقط ، مثل الثوابت والسلاسل وما إلى ذلك ، سمات IR

. البيانات التي يمكن قراءتها وكتابتها ، مثل المتغيرات الثابتة أو العامة. سمات IRW

.bss - يخزن البيانات غير المهيأة مثل المتغيرات الثابتة أو العامة. بالإضافة إلى ذلك ، عادةً ما يحتوي هذا القسم على حجم RAW صفر وحجم VA غير صفري ، لذلك لا يشغل مساحة في الملف. سمات URW

.idata - قسم يحتوي على وظائف مستوردة من مكتبات أخرى. سمات IR



نقطة مهمة ، يجب أن تتبع الأقسام بعضها البعض. علاوة على ذلك ، سواء في الملف أو في الذاكرة. على الأقل عندما غيرت ترتيبهم بشكل تعسفي ، توقف البرنامج عن العمل.



الآن بعد أن عرفنا الأقسام التي سيحتويها برنامجنا ، سنكتبها في ملفنا. هنا تنتهي الإزاحة عند 8 وسيبدأ التسجيل من منتصف الملف.



(4) أقسام RAW (إزاحة 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






سيكون عنوان الإدخال التالي 00000200 الذي يتوافق مع حقل SizeOfHeaders في رأس PE. إذا أضفنا قسمًا آخر ، وهذا زائد 40 بايت ، فلن تتناسب رؤوسنا مع 512 (0x200) بايت وسيتعين علينا استخدام 512 + 40 = 552 بايت محاذاتها بواسطة FileAlignment ، أي 1024 (0x400) بايت. وكل ما تبقى من 0x228 (552) إلى العنوان 0x400 يجب أن يتم ملؤه بشيء ما ، أفضل بالطبع بالأصفار.



دعنا نلقي نظرة على الشكل الذي تبدو عليه كتلة الأقسام في Far:







بعد ذلك ، سنكتب الأقسام نفسها في ملفنا ، ولكن هناك فارق بسيط واحد.



كما ترون من مثال SizeOfHeaders ، لا يمكننا فقط كتابة العنوان والانتقال إلى القسم التالي. نظرًا لأنه لتسجيل عنوان ، نحتاج إلى معرفة المدة التي ستستغرقها جميع العناوين معًا. نتيجة لذلك ، نحتاج إما إلى حساب مقدار المساحة المطلوبة مسبقًا ، أو كتابة قيم فارغة (صفرية) ، وبعد كتابة جميع الرؤوس ، نعود ونكتب حجمها الفعلي.



لذلك ، يتم تجميع البرامج في عدة مسارات. على سبيل المثال ، يأتي قسم .rdata بعد قسم .text ، بينما لا يمكننا العثور على العنوان الظاهري للمتغير في .rdata ، لأنه إذا كان قسم النص ينمو بأكثر من 0x1000 (SectionAlignment) بايت ، فسيشغل عناوين 0x2000 من النطاق. وبناءً على ذلك ، لن يكون قسم .rdata موجودًا على 0x2000 ، ولكن على 0x3000. وسنحتاج إلى العودة وإعادة حساب عناوين جميع المتغيرات في قسم النص الذي يسبق .rdata.



لكن في هذه الحالة ، لقد قمت بالفعل بحساب كل شيء ، لذلك سنقوم على الفور بتدوين كتل الكود.



قسم النص



قسم ASM. نص
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




على وجه التحديد بالنسبة لهذا البرنامج ، تعتبر الأسطر الثلاثة الأولى ، تمامًا مثل الثلاثة الأخيرة ، اختيارية.

لن يتم تنفيذ الثلاثة الأخيرة حتى ، لأن البرنامج سينتهي عند وظيفة الاستدعاء الثانية.



لكن دعنا نقول هذا ، إذا لم تكن الوظيفة الرئيسية ، بل وظيفة فرعية ، فيجب أن يتم ذلك بهذه الطريقة.



لكن الثلاثة الأولى في هذه الحالة ، على الرغم من أنها ليست مطلوبة ، مرغوبة. على سبيل المثال ، إذا لم نستخدم MessageBoxA ، ولكننا printf ، فبدون هذه الأسطر سنحصل على خطأ.



وفقًا لاتفاقية الاستدعاء لأنظمة MSDN 64 بت ، يتم تمرير المعلمات الأربعة الأولى في السجلات RCX و RDX و R8 و R9. إذا كانت مناسبة هناك ولم تكن ، على سبيل المثال ، رقم فاصلة عائمة. ويتم تمرير الباقي عبر المكدس.



من الناحية النظرية ، إذا مررنا وسيطين إلى دالة ، فيجب علينا تمريرهما عبر السجلات ونحتفظ بمكانين في المكدس من أجلهما ، حتى تتمكن الوظيفة ، إذا لزم الأمر ، من دفع السجلات إلى المكدس. كما يجب ألا نتوقع إعادة هذه السجلات إلينا في حالتها الأصلية.



لذا فإن المشكلة في وظيفة printf هي أنه إذا مررنا وسيطة واحدة فقط إليها ، فستظل تكتب فوق جميع الأماكن الأربعة في المكدس ، على الرغم من أنه يبدو أنه يجب الكتابة فوق واحد فقط ، بعدد الوسائط.



لذلك ، إذا كنت لا تريد أن يتصرف البرنامج بشكل غريب ، فاحجز دائمًا 8 بايت على الأقل * 4 وسيطات = 32 (0x20) بايت إذا قمت بتمرير وسيطة واحدة على الأقل إلى الوظيفة.



ضع في اعتبارك كتلة من التعليمات البرمجية مع استدعاءات الوظائف



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


نمرر الوسيطات



أولاً : rcx = 0

rdx = العنوان المطلق للسلسلة في الذاكرة ImageBase + Sections [". Rdata"]. VirtualAddress + إزاحة السلسلة من بداية القسم ، تتم قراءة السلسلة على أنها بايت صفر

r8 = مشابه للسلسلة السابقة

r9 = 64 (0x40) MB_ICONINFORMATION ، رمز المعلومات



وبعد ذلك هناك استدعاء لوظيفة MessageBoxA ، والتي لا يكون كل شيء بهذه البساطة. النقطة المهمة هي أن المترجمين يحاولون استخدام أقصر الأوامر الممكنة. كلما كان حجم التعليمات أصغر ، كلما تناسبت هذه التعليمات مع ذاكرة التخزين المؤقت للمعالج ، على التوالي ، سيكون هناك عدد أقل من ذاكرة التخزين المؤقت المفقودة ، والحمولات الزائدة ، وزيادة سرعة البرنامج. لمزيد من المعلومات حول الأوامر والأعمال الداخلية للمعالج ، ارجع إلى أدلة مطوري برامج Intel 64 و IA-32.



يمكننا استدعاء الوظيفة على العنوان الكامل ، لكن هذا قد يستغرق على الأقل (رمز تشغيل واحد + 8 عنوان = 9 بايت) ، ومع عنوان نسبي ، يستغرق أمر الاستدعاء 6 بايت فقط.



دعونا نلقي نظرة فاحصة على هذا السحر: rip + 0x203E ليس أكثر من استدعاء دالة على العنوان المحدد بواسطة الإزاحة الخاصة بنا.



نظرت إلى الأمام قليلاً واكتشفت عناوين التعويضات التي نحتاجها. بالنسبة لـ MessageBoxA ، فهي 0x3068 وبالنسبة إلى ExitProcess فهي 0x3098.



حان الوقت لتحويل السحر إلى علم. في كل مرة يصل فيها كود التشغيل إلى المعالج ، فإنه يحسب طوله ويضيفه إلى عنوان التعليمات الحالي (RIP). لذلك ، عندما نستخدم RIP داخل تعليمات ، يشير هذا العنوان إلى نهاية التعليمات الحالية / بداية التعليمة التالية.

بالنسبة للمكالمة الأولى ، سيشير الإزاحة إلى نهاية أمر الاتصال ، هذا هو 002A. لا تنس أنه في الذاكرة سيكون هذا العنوان في أقسام الإزاحة [". نص"]. VirtualAddress ، أي 0x1000. لذلك ، سيكون RIP لمكالمتنا هو 102 أ. العنوان الذي نحتاجه لـ MessageBoxA هو 0x3068. ضع في اعتبارك 0x3068 - 0x102A = 0x203E . بالنسبة إلى العنوان الثاني ، كل شيء هو نفسه 0x1000 + 0x0037 = 0x1037 ، 0x3098 - 0x1037 = 0x2061 .



هذه هي الإزاحات التي رأيناها في أوامر المجمّع.



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


لنكتب قسم النص إلى ملفنا ، ونضيف الأصفار إلى العنوان 0x400:



(5) قسم نص RAW (إزاحة 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 , ! .




قسم البيانات



ربما يكون هذا هو أبسط قسم. سنضع هنا سطرين فقط ، ونضيف الأصفار إلى 512 بايت.



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




(6) قسم RAW. rdata (إزاحة 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




قسم البيانات



حسنًا ، إليك القسم الأخير ، الذي يصف الوظائف المستوردة من المكتبات.



أول ما ينتظرنا هو الهيكل الجديد IMAGE_IMPORT_DESCRIPTOR



بنية IMAGE_IMPORT_DESCRIPTOR
Struct IMAGE_IMPORT_DESCRIPTOR
{
	u32 OriginalFirstThunk (INT)
	u32 TimeDateStamp
	u32 ForwarderChain
	u32 Name
	u32 FirstThunk (IAT)
}




الوصف IMAGE_IMPORT_DESCRIPTOR
OriginalFirstThunk — , Import Name Table (INT)

Name — ,

FirstThunk — , Import Address Table (IAT)



أولاً ، نحتاج إلى إضافة مكتبتين مستوردتين. اعد الاتصال:



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


[7) RAW IMAGE_IMPORT_DESCRIPTOR (إزاحة 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




نستخدم مكتبتين ، ولنقول إننا انتهينا من إدراجهما. الهيكل الأخير مليء بالأصفار.



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


لنقم الآن بإضافة أسماء المكتبات نفسها:



أسماء المكتبات
063	"user32.dll\0"
0648	"kernel32.dll\0"




(8) أسماء مكتبة RAW (إزاحة 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




بعد ذلك ، دعنا نصف مكتبة user32:



(9) RAW user32.dll (إزاحة 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




يشير حقل الاسم الخاص بالمكتبة الأولى إلى 0x303C إذا نظرنا إلى مستوى أعلى قليلاً ، فسنرى أنه في العنوان 0x063C توجد مكتبة "user32.dll \ 0".



تلميح ، تذكر أن قسم .idata يتوافق مع إزاحة الملف 0x0600 وإزاحة الذاكرة 0x3000. بالنسبة للمكتبة الأولى ، INT هو 3058 ، مما يعني أنه في الملف سيتم إزاحته 0x0658. في هذا العنوان ، نرى الإدخال 0x3078 والصفر الثاني. مما يدل على نهاية القائمة. 3078 يشير إلى 0x0678 هذه هي سلسلة RAW



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



أول 2 بايت لا تهمنا وتساوي الصفر. ثم هناك خط باسم الدالة ينتهي بصفر. أي يمكننا تمثيلها كـ "\ 0 \ 0MessageBoxA \ 0".



في هذه الحالة ، يشير IAT إلى بنية مشابهة لجدول IAT ، ولكن سيتم تحميل عناوين الوظائف فقط فيه عند بدء تشغيل البرنامج. على سبيل المثال ، الإدخال الأول 0x3068 في الذاكرة سيكون له قيمة أخرى غير 0x0668 في الملف. سيكون هناك عنوان وظيفة MessageBoxA التي تم تحميلها بواسطة النظام الذي سنشير إليه من خلال استدعاء المكالمة في كود البرنامج.



والجزء الأخير من اللغز ، النواة 32. ولا تنس إضافة أصفار إلى SectionAlignment.



(10) RAW kernel32.dll (إزاحة 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






نتحقق من أن Far كان قادرًا على تحديد الوظائف التي قمنا باستيرادها بشكل صحيح:







رائعة! كان كل شيء على ما يرام ، والآن أصبح ملفنا جاهزًا للتشغيل.

Drumroll ...



الاخير







مبروك ، لقد فعلناها!



يشغل الملف 2 كيلوبايت = رؤوس 512 بايت + 3 أقسام بحجم 512 بايت.



الرقم 512 (0x200) ليس أكثر من FileAlignment الذي حددناه في رأس برنامجنا.



بالإضافة إلى ذلك:

إذا كنت تريد التعمق أكثر ، يمكنك استبدال النقش "Hello World!" لشيء آخر ، فقط لا تنس تغيير عنوان السطر في كود البرنامج (قسم. نص). العنوان الموجود في الذاكرة هو 0x00402000 ، ولكن سيكون للملف ترتيب بايت عكسي 00 20 40 00.



أو أن المهمة أكثر تعقيدًا. قم بإضافة مكالمة MessageBox أخرى إلى الرمز. للقيام بذلك ، سيتعين عليك نسخ المكالمة السابقة وإعادة حساب العنوان النسبي فيها (0x3068 - RIP).



خاتمة



تبين أن المقالة مجعدة إلى حد ما ، وستتكون بالطبع من 3 أجزاء منفصلة: العناوين ، البرنامج ، جدول الاستيراد.



إذا قام شخص ما بتجميع exe ، فإن عملي لم يكن عبثًا.



أفكر في إنشاء ملف ELF بطريقة مماثلة قريبًا ، فهل ستكون هذه المقالة ممتعة؟)



الروابط:






All Articles