الآن هو قادر على ترجمة 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
تم ، تمت كتابة أول 64 بايت. أنت الآن بحاجة إلى إضافة 64 أخرى ، وهذا ما يسمى DOS Stub (Stub). عند التشغيل من نظام DOS ، يجب إخطار المستخدم بأن البرنامج غير مصمم للتشغيل في هذا الوضع.
, , .
, (Offset) .
, 0x00000000, 64 (0x40 16- ), 0x00000040 ..
لكن بشكل عام ، هذا برنامج 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) — (, , ..)
.
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 — (, , , , .)
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)
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 بطريقة مماثلة قريبًا ، فهل ستكون هذه المقالة ممتعة؟)
الروابط:
- Intel 64 و IA-32 معماريات دليل
أوامر مطوري البرامج ودليل هندسة المعالج.
- PE (قابل للتنفيذ المحمول): On Stranger Tides
مقال ممتاز عن بنية ملف exe. - مستودع وثائق Microsoft
هنا يمكنك العثور على أي معلومات حول الرؤوس والهياكل والأنواع ووصفها