2024-02-15
Comrades, leave me here a little, while as yet 'tis early morn:
Leave me here, and when you want me, sound upon the bugle horn.
— Alfred Tennyson, Locksley Hall
يفضل تكون قاري المقال السابق، The Hunt for a UEFI Graphics Bug.
بيوم من الأيام ببداية السنة الدراسية هاي، چنت بمختبر ++C بقاعة تختلف عن القاعة مال المحاضرة الفاتت. ذيچ چانت كل الحواسيب اللي بيها نظامها Windows 7 (32 bit) وبيها گيگتين رام، بينما الحاسبة اللي گدامي چان بيها Windows 10 (64 bit) و8 گيگات رام وi5-8400. شي لفت انتباهي انو حواسيب ذيچ القاعة چان بيها لوگو القسم كخلفية ونظامها 'نظيف' لأن مشغلين بيها Deep Freeze، بينما الجديدات الطلاب لاعبين بيها طوبة.
ابو المختبر كتب برنامج يطبع Hello World عالسبورة وگعد يعلمنا شلون نشغل برنامج Borland C++ 5.02 (بدون e) حتى نطبق بي. بس شغلته رأسا طلعتلي نافذة أكتب بيها، محتاجيت أسوي project أو أي شي.
بالأخير گال (منقول بتصرف):
لمن تسوون Run، راح تشوفون الكتابة تطلع وتختفي بسرعة
هذا البرنامج يغلق النافذة لمن ينتهي التنفيذ، مو مثل التوربو اللي منگدر نستعمله هنا لأن يحتاج حاسبة 32 بت
رحت عليه بعد ما خلصت المحاضرة:
- استاذ اذا اعدل على البورلاند واخلي النافذة تبقى، احصل درجات اضافية؟
- اي بس كون صدك
- تمام... المحاضرة الجاية يكون جاهز
(كلشي مچنت اعرف ببرمجة الوندوز، اصلا صارلي سنين ماستخدمه)
ب"التوربو" قصده Borland Turbo C++ 4.5، وهو نسخة اقدم وقليلة الدسم من البورلاند مخصصة للإستخدام المنزلي. منا وجاي راح استخدم "توربو" لهذا و"بورلاند" لذاك.
بالفعل اذا نجرب التوربو على نظام 32 بت حيتنصب عادي:
بينما اذا نجرب على نظام 64 بت:
اذا نكتب برنامج بسيط بي حنشوف انو التوربو عنده console خاص بي وهذا
يبقى مفتوح بعد ما ينتهي التنفيذ، بينما البورلاند يستعمل الconsole مال
الوندوز.
ممكن لاحظتوا انو iostream
وراها h.
وcout
ماكو قبلها ::std
و()main
مابيها return type، هذا البرنامج چان موجود قبل ما يكون اكو شي اسمه C++
standard.
زين ليش القديم يبقي النافذة والجديد لا؟ البورلاند نزل سنة 1997 فگلت يمكن الوندوز مال ذاك الوكت چان يبقيهن فمحتاجوا يسوون شي اضافي، بس جربته على Windows 95 وNT 4.0 وثنينهن يغلقوهن. لعد ليش التوربو وحد؟ لأن حاسبيه نسخة مال هواة؟
التوربو نزل سنة 1995، والنظام اللي عاشره هو Windows 3. بوكتها الوندوز مچان نظام مستقل بحد ذاته وانما چان برنامج مال DOS، أو، نظام مكمل للDOS، أو hypervisor فوگ الDOS. الموضوع معقد. المهم چان الوندوز بس يدعم البرامج الرسومية واذا ردت برامج نصية فماعندك غير الDOS (ويه قيوده). لحدما مايكروسوفت سوت library اسمها QuickWin تنطي console رسومي للبرامج النصية. بورلاند جوابها چان اسمه... EasyWin وهذا اللي دنشوفه هنا. المنافسة بين مايكروسوفت وبورلاند چانت شرسة لدرجة مايكروسوفت چانت تاخذ موظفي بورلاند بالليموزينات.
بينما Windows 95 چان عنده console (لكن مو console "حقيقي" مثل مال Windows NT وانما عبارة عن DOS virtual machine يتم تمرير الIO عن طريقها) لهذا مچان اكو حاجة لEasyWin، بس لمن گلبت بالكتيب مال البورلاند لگيته بعده موجود:
يعني اذا تريد تستخدم EasyWin، لازم تسوي project جديد وتختاره:
وراها حتطلع نافذة فارغة ولازم تختار ملف الcpp:
اني كطالب ميهمني احفظ هالخطوات، اريد برنامج يشتغل بلمح البصر وينطيني نافذة اكتب بيها كود رأسا، فهيچ "حل" مو مقبول.
اذا نباوع على الmenu bar مال البورلاند حنشوف اكو دگمة اضافية مچانت موجودة بالتوربو: Script.
شنو هاي الmodules؟
خلي نشوف محتوى وحدة منهن:
الscripting language شكلها شي جدي مو كلاوات. اكو كتيب كامل عليها، راح اخليه هو يعرف عنها:
اذا نسوي سكربت بأسم personal.spp
ونخليه ويه البقية
حيشتغل تلقائيا لمن يشتغل البرنامج:
نگدر نستدعي function من DLL خارجي بداخل السكربت:
اللغة تدعم نوع بدائي من الevents، الفكرة انو يصير call لmethod معينة (اسمها event) لمن يصير حدث معين، والevent handler يسوي override للmethod فيتنفذ بدالها لمن يصير الحدث:
الكتيب يذكر هوايه events، اكثر وحدة لفتت نظري هي
DebugeeAboutToRun
ضمن الDebugger
class.
الDebugger
class موجودة الinstance مالته بملف
debug.spp
:
ماطول هي export
نگدر نسوي import
الها من
غير سكربت:
صرنا نعرف كلشي نحتاجه حتى نشغل function من DLL معين قبل تنفيذ اي كود
من البرنامج اللي كتبه المستخدم. الfunction هاي، اللي حنسميها
()inject
، راح تخلي نافذة البرنامج تبقى بعد ما ينتهي
التنفيذ:
ممكن تگول: شلون البورلاند ديستدعي function من ملف DLL عشوائي؟
الوندوز يدعم نوعين من الdynamic linking:
Load-time dynamic linking: خلاله يكون اكو import tables تنحط بالbinary مال البرنامج اثناء الlinking، يكون بيهن عناوين لأسم كل function خارجية يستعملها البرنامج من DLL معين. اثناء التنفيذ الloader راح يستبدل عناوين الأسامي ويحط بمكانها عناوين لمكان الfunctions بالaddress space مال البرنامج.
Run-time dynamic linking (وهو اللي ديصير هنا): خلاله البرنامج
يسوي ()LoadLibrary
لملف DLL معين وراها ياخذ الaddress مال
function موجودة بي عن طريق اعطاء اسمها
ل()GetProcAddress
.
"اسم" الfunction يسموه الsymbol مالتها. ممكن يطابق اسمها بالكود بس مو شرط (مثل ما راح نشوف بعد شوية).
نجي نكتب كود اولي للDLL، حيكون 32 بت (لأن البورلاند 32 بت) وحستخدم
Visual Studio 2015. الdeclspec(dllexport)_
معناها هاي
الfunction حتكون exported ونگدر نستعملها من خارج الDLL، الموضوع يشبه
الpublic والprivate بالOOP:
اذا نجي نباع على الsymbol مال الfunction حنشوفه شي غريب:
السبب هو الC++ symbol mangling/decoration: بلغة ++C ممكن يكون اكو اكثر من function بنفس الاسم (بسبب الfunction overloading مثلا)، لهذا الsymbol لازم تنحط وياها معلومات اضافية نگدر نفك ترميزها عن طريق اي demangler:
حتى نلغي هالموضوع، مجرد نضيف "extern "C
:
اذا نحط الDLL بفولدر البورلاند (يم الEXE مالته) ونحاول ننفذ اي كود، راح نشوف الرسالة تطلع قبل ما يتنفذ اول سطر من البرنامج:
المشكلة الرسالة دتطلع اكثر من مرة، بعدين حنشوفلها حل. هسه اريد اخلي الDLL ينطيني debug messages اختيارية، صح هالشي مو ضروري للشغل (لأن اگدر احطهن وراها امسحهن) بس اريد ابقيهن واخليهن اختياريات حتى اعرف شنو المشكلة اذا صار خلل بالتنفيذ داخل المختبر:
اذا نجرب من جديد حتطلع الرسالة اكثر من مرة (مثل ما توقعنا) بس هالمرة كل ما ندوس OK البورلاند حيگول انو صار خلل:
سبب هالشي هو اختلاف الcalling convention (طريقة تمرير الparameters للfunctions)، البورلاند عباله الfunction هي stdcall، بهذا الconvention الparameters راح تندفع للstack بالعكس، يعني اذا عندك function كالاتي:
int add(int arg1, int arg2, int arg3)
هيچ حتكون الcall مالتها:
push arg3
push arg2
push arg1
call add
الناتج (الreturn value) راح يرجع بالeax register والcallee هو اللي ينظف الstack (يعني بعد الreturn راح ترجع الstack لنفس حالتها قبل ما تسوي push للparameters). هذا الconvention هو المُستخدم بالWindows API. بينما الVisual Studio ديستعمل الcdecl convention واللي هو نفس الstdcall، الفرق الوحيد ان الcaller هو اللي ينظف الstack. لهذا السبب الكود اشتغل تمام والخطأ طلع بعد انتهاء التنفيذ.
البورلاند يدعم تحديد الcalling convention:
اذا نجرب من جديد البورلاند حيشتكي ان الfunction مو موجودة:
شكله تحديد الcalling convention خلاه يبحث عن غير symbol. نگدر نستعمل
اداة WinAPIOverride حتى نشوف كل استخدامات البورلاند
ل()GetProcAddress
وبالتالي نعرف شنو الsymbol اللي ديبحث
عنها:
گام يبحث عن inject_
بدل inject
لمن حدننا
الcdecl convention. سبب هالشي هو ان البورلاند بنفسه يحط _
لمن يبني cdecl function وهو ديفترض ان الدنيا كلها ماشيه مثله.
قبل شوية گلنا البورلاند ديفترض الcalling convention هو stdcall، ليش؟ هالشي ممذكور بالكتيب بس لمن ماتحدد الconvention حيجرب كل طرق التسمية المذكورة بالجدول (عدا مال fastcall) وحيطابق الconvention حسب الاسم الصحيح:
نگدر نعتبر هالتسميات نوع من الC symbol mangling، حتى Visual Studio هم يسوي هيچ شي لمن يبني برامج 32 بت:
مكتوب هنا ان الcdecl symbols ينحط قبلها _
مثل ما يسوي
البورلاند، لعد ليش الsymbol مالتنا مابيها؟ معقولة المعلومات غلط؟ خلي
نسويها stdcall__
ونشوف شيصير:
صار الmangling مثل ما كاتبين. بعد البحث اكثر، اكتشفت كاتبين التالي
بالdocumentation
مال cdecl__
:
حتى نحل الموضوع، حننطي تسمية ثانية للsymbol، يعني alias، او مثل ما اني اسميه: symbolic link.
اذا نسوي build حيطلع خلل:
الdocumentation مال EXPORT/ بي شغلة لفتت انتباهي:
مو گلنا cdecl مابيها mangling؟ خلي نتأكد:
مو كلش دافتهم شديصير هنا، اعتقد الlinker حاسب حسابه انو ديصير mangling لسبب ما، المهم ضفت underscore للطرفين واشتغل:
الdeclspec_
بعد ماله داعي:
اصلا نگدر نشيل ال "extern "C
ونسوي alias للC++-mangled
symbol مباشرة:
لمن حددنا stdcall فوگ، الsymbol صارت inject@4_
، ال4 هي
حجم الarguments بالبايتات. بس احنا عدنا char
، مو المفروض
الحجم يكون بايت واحد؟
سبب هالشي هو الstack alignment، الVisual Studio ديسوي DWORD alignment (4 بايتات) بينما البورلاند يسوي byte alignment وهمين عباله العالم ماشيه مثله:
هذا معناه انو البورلاند دينطي بايت واحد للfunction وهي دتتوقع اربع بايتات، هالشي مديسبب مشكلة چبيرة ويه الcdecl convention لأن الcaller (البورلاند) هو المسؤول عن تنظيف الstack، فراح ينطي بايت واحد ويشيل بايت واحد. بينما لو مختارين stdcall چان البورلاند ينطي بايت والfunction تشيل 4 من الstack، يعني 3 بايتات زيادة. خلي نجرب:
زين شراح يصير اذا نخلي الfunction تشيل 10003 بايت زيادة؟
نگدر نخلي الfunction تاخذ int
(4 بايتات) بدل
char
ونحل المشكلة:
In another moment down went Alice after it, never once considering how in the world she was to get out again.
— Lewis Carroll, Alice's Adventures in Wonderland
كل برامج البورلاند تنتهي بcall الى هاي الfunction:
اللي راح تودينا لهاي الjmp:
هاي يسموها indirect jump، لأن هي مدتروح للaddress المذكور (0x004130f8) وانما راح تروح للaddress اللي موجود بالمكان المذكور:
فيعني المفروض المكان النهائي اللي راح تروحله هو 0x00 0x32 0x01 0x00،
واللي لو نحوله الى address لازم ناخذ البايتات بالمگلوب لأن المعمارية
little-endian، فراح يكون عندنا 0x00013200. هذا العنوان المفروض هو
العنوان مال ()ExitProcess
بالimport table، الوجهة النهائية
لكل برامج الوندوز.
اول شي اجه ببالي هو ان استبدل الjmp بinfinite loop عن طريق الDLL، صح مو حل مثالي بس المفروض يشتغل. قبل ما نكتب اي كود خلي نأكد نظريتنا عن طريق استبداله يدويا بالباينري، كل اللي نحتاجه هو ان نسوي relative jump الى negative offset يمثل عدد البايتات مال الjump instruction نفسها. يعني اذا الinstruction اربع بايتات فراح نرجع اربع بايتات وبالتالي تصير infinite loop. اختيار patch instruction مال Ghidra مدينطيني اللي ببالي فراح نضطر نكون الinstruction يدويا.
نجي نباع كتيب Intel:
اول instruction مذكورة حجمها بايتين، الopcode مالتها 0xeb والoperand مالتها هو signed 8-bit offset، يعني الoperand لازم يكون 2- حتى نسوي infinite loop، حتى نحول 2- الى two's complement مجرد نضيفها الى 8^2، حيكون الناتج 0xfe. الinstruction النهائية حتكون 0xeb 0xfe. الinstruction الأصلية چانت 6 بايتات، مو ضروري نسوي اي شي بخصوص الاربعة الباقيات بس في سبيل الكمالية ححط instructions متسوي شي (nop: 0x90) بمكانهن:
ردت أبدي بالكود، بس جتي ببالي فكرة أحسن.
اذا نباع على الimports مال برنامجنا حنشوف اكو User32.dll، حيكون هالأسم مألوف اذا لاعب بالايقونات بالوندوز قبل: هاي الGUI library.
خلي نشوف الimport table مالته:
الفكرة: نضيف كود يتنفذ بدل ()ExitProcess
يعرض
MessageBox، هاي الMessageBox حتبقي النافذة مفتوحة لحد ما المستخدم يغلقها
وما راح تاكل الCPU. حتى نستبدل عنوان ()ExitProcess
بعنوان
الكود مالنا لازم اول شي نعرف شنو الpid مال الEXE اللي ديتنفذ حتى نگدر
نقره ونعدل على ذاكرته. البورلاند مموفر هالمعلومة للscripting language
فمنگدر نمررها للfunction، لازم نقراها من ذاكرة البورلاند. الDLL مالتنا
ديشتغل بنفس الprocess والaddress space مال البورلاند، فذاكرة البورلاند
والpid والchild processes مالته هنه مالاتنا.
شلون نعرف الpid وين صاير؟ الفكرة بسيطة: نشغل برنامج بالبورلاند، ناخذ الpid مالته، نفحص ذاكرة البورلاند ونلگي كل القيم اللي تطابق الpid والaddresses مالها، هاي القيم هوايه منها راح تطابق الpid بالصدفة فنعيد العملية اكثر من مرة حتى نضيق نطاق البحث ونلگي الaddress الصحيح. "اوگف لحظة، هذا مو نفس الشي اللي يسووه الغشاشين حتى يزيدون نقاطهم بالألعاب؟" صحيح، ولهذا السبب البرنامج اللي حنستخدمه لهلغرض اسمه Cheat Engine. نبدي بأول فحص:
نعيد تنفيذ البرنامج ونفحص الpid الجديد:
اذا ننتظر شوية حنشوف اكو كم قيمة حتتغير من وحدها:
واذا نعيد تنفيذ البرنامج حيرجعن صحيحات:
وبعد فترة هم حيرجعن غلط. اذا نراقب الكود اللي ديستخدمهن حنعرف ذني memory structures مال وندوز، مو مال بورلاند، فنستبعدهن:
بالاخير، ما بقى إلا القليل (وهذا المطلوب):
المشكلة ذني مو عناوين ثابتة، حيتغيرن اذا نغلق البورلاند ونفتحه. نحتاج نبدي من عنوان ثابت بالباينري وراها نبدي نتبع pointer وره pointer لحد ما نوصل للpid. احنا عندنا العنوان النهائي ونريد نبقى نرجع ليوره لحد ما نوصل لعنوان ثابت:
(للسهولة، لمن أكتب struct أقصد اما struct أو struct instance)
البرنامج اكيد مديحتفظ بالpid بvariable طاير وانما حاطه بstruct او class. المعالج ماعنده مفهوم اسمه struct، اذا عندك بالكود struct كل عناصرها integers، ابسط شي ممكن يسويه الكومبايلر هو ان يحط الintegers وحدة وره اللخ بالذاكرة ويحط عنوان بداية المجموعة بregister (مثل edx) ويحتفظ بالoffsets مال كل عنصر منها، وكل ما يحاول يستخدم عنصر بinstruction معينة حينطيها الoffset والbase address. يعني اذا ديقره اول عنصر حينطي edx+0x0، ثاني عنصر edx+0x4 وهكذا. ماطول احنا عدنا العنوان النهائي مال الpid (مجرد نختار اي واحد من اللي بقوا بالأخير)، نگدر نشوف شنو الinstruction اللي دتستعمله حتى نعرف شنو الoffset وشنو الbase address:
يعني عدنا struct عنوانها 0x07fd3a14، خلي نگول اسمها
Process
، وبيها عنصر خلي نگول اسمه pid
الoffset
مالته 0x38. بديهيا، الbase address عنوان متغير لكن الoffset ثابت:
Process(0x07fd3a14) + pid(0x38) -> 3276
شلون نرجع ليوره خطوة بعد ونلگي pointer للProcess
struct
بنفسها؟ نبحث عن الbase address مالتها بالذاكرة:
حنختار اي واحد من ذوله ونحسبه الpointer مال الstruct، اللي هو بنفسه
عنصر بstruct ثانية خلي نگول اسمها Debugger
. نكرر نفس
الخطوات حتى نلگي الoffset مال الpointer والbase address مال الstruct
اللي موجود بيها:
يعني:
Debugger(0x07fd2d28) + Process_ptr(0x40) -> Process(0x07fd3a14) + pid(0x38) -> 3276
نبحث عن الbase address مال Debugger
بالذاكرة:
طلعلنا عنوانين خضر، يعني ذني offsets ثابتة بالملفات المذكورة مو
عناوين ذاكرة متغيرة. حختار مال BCWBDK32.DLL
، الحروف الچبيرة
قنعتني بي:
Debugger_ptr(BCWBDK32.DLL + 0x35008) -> Debugger(0x07fd2d28) + Process_ptr(0x40) -> Process(0x07fd3a14) + pid(0x38) -> 3276
يعني اذا نريد نوصل للpid، حنروح للbase address مال
BCWDBK32.DLL
ونضيفله 0x35008، حنلگي pointer هناك، اذا نتبعه
حنروح لبداية Debugger
(اللي هو مكان متغير بالذاكرة)، لمن
نمشي 0x40 بايت وراه حنلگي pointer ثاني، هذا حيودينا لبداية
Process
، مناك نصعد 0x38 بايت وحيطلع الpid گبالنا.
نضيف الpointer chain:
حتى نتأكد من صحة اللي سويناه، نغلق البورلاند ونفتحه من جديد وننفذ برنامج بي، المفروض الpointer chain تودينا للpid مال البرنامج:
كم ملاحظة قبل ما نباشر بكتابة الكود:
اول شي حناخذ الpid والhandle مال الprocess مالتنا (اللي هي، للتذكير، نفسها مال بورلاند):
وحدة بناء الprocesses بالWindows هي الmodules، الmodule هي اما EXE او
DLL، حتى نلگي الbase address مال BCWDBK32.DLL
لازم نفر
الmodules لحد ما نلگيه. الكود مال ()GetBaseAddress
اخذته من
النت:
صار عدنا كلشي نحتاجه حتى نتبع الpointer chain:
نجرب الكود:
المشكلة اذا الواحد ديستخدم غير نسخة من البورلاند ممكن الoffsets تختلف وتصير كارثة، الوندوز يوفر function تخلينا نتأكد من صحة الpointer قبل ما نقراه:
هاي الfunction بيها مشاكل،
كحل بديل نگدر نستعمل ()VirtualQuery
حتى نشوف اذا الpointer ضمن memory page قابلة للقراءة لو لا:
وصف اول argument صايغيه بطريقة تدوخ شوية، الزبدة انو تشتغل على اي
pointer ضمن page معينة، مو الا الbase address مالها. المعلومات حتجينا
بstruct من نوع MEMORY_BASIC_INFORMATION
. خلي نشوف شنو
الpages اللي موجوده بيها الpointers مال الpointer chain:
"وين اكو pages بهالأحجام؟" كلها 4 كيلوبايت بس Process Hacker ديعرض
الpages المتلاصقة وخصائصها متماثلة سوية. الImage معناها هاي الpage بيها
محتوى ملف الDLL (اللي هو BCWDBK32.DLL
). الpointer اللي
نريده صاير بمنطقة RW (Read-Write) لأن بديهيا هو مو ثابت وانما متغير:
الCommit معناها الpage جاهزة وقابلة للاستعمال مو مجرد محجوزة:
زين اذا اجانا pointer تايه وصار الpointer النهائي على حافة readable region؟ يعني مثلا فوگ احنا بدينا عند 0x7050000 والregion چان حجمها 256 كيلوبايت واللي وراها لزگ حجمها 128 كيلوبايت، حتى نوصل للحافة نحول للبايتات ونجمع ونطرح واحد (لأن دنحسب من الصفر):
256 * 1024 + 128 * 1024 - 1 = \mathrm{0x5ffff}
ونحطها بدل 0x35008 حتى تنضاف للbase address مال
BCWDBK32.DLL
، الfunction مالتنا راح تنطي TRUE
لأن الpointer بنفسه موجود بreadable page، بس لمن نقره القيمة اللي بي
محنقره بايت وحدة وانما اربعة (لأن الDWORD اربع بايتات)، والبايتات ال3
البقية مو readable فيصير crash:
دنسوي cast الى *char
حتى نجمع بس 3 بدل
4*3. بالنهاية حيكون عندنا:
بدل ما ندوخ نفسنا بتفاصيل عمل الfunction كل ما نستعملها، نگدر
نستبدلها بوحدة تسوي نفس الوظيفة بس فوگاها تچيك ان ptr
وptr + 3
ضمن نفس الpage:
تعرف تگدر تحسب المعادلة الفوگ بدون ورقة وقلم؟
\begin{align*} 128 * 1024 &= 2^{7} * 2^{10} \\ &= 2 * 2^{16} \\ &= 2 * 2^{4^{4}} \\ &= 2 * \mathrm{0x}10^{4} \\ &= 2 * \mathrm{0x}10000 \\ &= \mathrm{0x}20000 \\ 256 * 1024 &= 2 * (128 * 1024) \\ &= 2 * \mathrm{0x}20000 \\ &= \mathrm{0x}40000 \\ 256 * 1024 + 128 * 1024 - 1 &= \mathrm{0x}20000 + \mathrm{0x}40000 - 1 \\ &= \mathrm{0x}60000 - 1 \\ &= \mathrm{0x}5\mathrm{ffff} \end{align*}
خلي نحول الكود اللي كتبناه الى function ونفتح الprocess:
خلي نجرب شغلة:
احنا شغلنا برنامج بالبورلاند، طلعتلنا رسالة started injector.dll،
وراها غلقنا البرنامج ودسنا ok. المفروض هسه باقي الكود يفشل، بس احنا مو
بس دنحصل pid وانما handle هم. اوك خلي نگول الpid بقه بذاكرة البورلاند
حتى بعد ما غلقنا البرنامج، شلون دنحصل handle؟ هالشي معناه
()OpenProcess
دتگدر تفتح process ميته.
بالوندوز لمن تموت process حتبقى بالprocess table على هيئة زومبي طالما اكو handle الها بفد مكان، بس ما راح تبين بالTask Manager. خلي نكتب كود يچيك الexit code مال الprocess حتى نتأكد انو هيه بالفعل عايشه:
حناخذ اسم البرنامج هم، حنحتاجه بعدين:
ممكن المستخدم دينفذ برنامج GUI. يجي ويه البورلاند هوايه examples موجودة بمجلد التنصيب، هذا واحد منهن:
شلون نعرف اذا البرنامج عنده console؟ لاحظت بWindows 10 انو كل برامج
الconsole عندها conhost
كchild الها:
بس لمن چيكت Windows 7 اكتشفت انو كل الconhosts
اللي بي
صايرات children لcsrss
، فمنگدر نستغل هالسلوك:
بالنهاية استخدمت ()AttachConsole
لأن اذا فشل معناها
الprocess مابيها console، لكن اذا نجح لازم نسوي
()FreeConsole
لأن الconsole I/O مالDLL/البورلاند حيروح
لنافذة البرنامج، وبس تنغلق النافذة حيطير البورلاند وياها.
بالنسخة اللي وديتها للاستاذ چنت حاط function تاخذ اول child لبورلاند
عنده console اذا اتباع الpointer chain فشل (باستثناء ادوات الconsole
اللي تجي ويه البورلاند مثل الTurbo Debugger)، مع انو النسخة اللي داشتغل
عليها اخذتها من موقع تدريسي بالقسم نفسه فإحتمالية كونها نفس النسخة اللي
بحواسيب المختبر چانت عالية، بس گلت الاحتياط واجب. شغلة لاحظتها ان
()Process32Next
تنطيك zombie processes بWindows 10، بينما
بWindows 7 لا.
Thy grace may wing me to prevent his art,
And thou like adamant draw mine iron heart.
— John Donne, Holy Sonnet 1
ملفات الEXE تتبع صيغة الPortable Executable (PE) اللي تبدي بكم header
وراها تحتوي على sections، اكو section للكود اسمه text.
واكو
للبيانات اسمه data.
، العنوان اللي نريد نغيره موجود بsection
اسمه idata.
مخصص للimported functions (تذكر اللي حچيناه عن
الload-time dynamic linking):
الصيغة بيها هوايه تفاصيل واكو عشرات المقالات عنها، يفضل تكون قاري وحدة منهن (مثل هاي السلسلة) بس مو ضروري. الحلو بالPE ان الهياكل مالته داخل البرنامج بالقرص وبعد تحميل البرنامج للذاكرة متماثلات تقريبا، ماعدا اختلافات معدودة راح نشوف واحد منها بعد شوية. اللي راح اسويه هو ان احط برنامج مبني بالبورلاند بGhidra واخليها هي تشوفنا الطريق:
مثل ما شفنا بالمخطط الفوگ، اول شي بالPE file هو الDOS header. برامج الMS-DOS والوندوز تتشارك بنفس الامتداد (EXE) فخلوا الPE اول شي يبدي بDOS header وبرنامج DOS بسيط حتى اذا واحد من ذاك الوكت حاول يشغل برنامج وندوز بداخل MS-DOS حتطلعله رسالة بدل لا يطلعله خطأ غريب يخليه دايخ. نگدر نگول ان كل برامج الوندوز اليوم بيها شظايا من الماضي.
بالDOS header اكو member اسمه e_lfanew
، هذا الmember
يحتوي على الRVA مال الNT headers (شوف المخطط). الRVA معناها Relative
Virtual Address، اللي هو offset للbase address. بمعنى اذا
e_lfanew
قيمته 0x200 والbase address هو 0x00400000 فبداية
الNT headers حتكون عند 0x00400200. خلي نكتب كود يقره الDOS header:
الحلو ان كل الstructs اللي نحتاجها موجودة بالwindows headers، ميحتاج نعرف شي من يمنا او نقره الoffsets مال القيم مباشرة. هسه خلي نشوف شكو بالNT headers:
الOptionalHeader بي هوايه امور متهمنا:
بس بالأخير اكو شي اسمه DataDirectory، اللي هي عبارة عن array كل واحد من عناصرها عنده Size وVirtual Address (Relative):
الdocumentation يگللنا شنو ذني العناصر:
وايضا موجود الها constants بالheaders:
نكتب:
اول شي موجود بالidata.
هو الImport Directory Table (IDT)
وهي عبارة عن array مال structs حجمها 20 بايت من نوع
IMAGE_IMPORT_DESCRIPTOR
، كل وحدة من عندهن تمثل DLL معين
يستخدمه البرنامج ما عدا الأخيرة كلها مصفرة (هاي نستخدمها حتى نعرف وين
نوگف لمن نسوي loop):
شلون نعرف الstruct مصفرة بالكود؟ نچيك عنصر مهم منها، اذا مصفر فكلها مصفرة وبالتالي وصلنا للنهاية:
الP
بPIMAGE_IMPORT_DESCRIPTOR
معناها
Pointer. لمن دنسوي increment لidt_addr
احنا دنضيف عشرين
بايت مو واحد.
بعد الimport descriptors تجي الImport Lookup Table (ILT) مال اول DLL
يستخدمه البرنامج، كل واحد من عناصرها حجمه اربع بايتات ونوعه
IMAGE_THUNK_DATA
ويحتوي على RVA لstruct من نوع
IMAGE_IMPORT_BY_NAME
، العنصر الأخير ايضا صفر لنفس
السبب:
بعد الILT تجي الImport Address Table (IAT) مال اول DLL، اللي محتوياتها تكون نفس محتويات الILT لكن راح تتغير بعد تحميل البرنامج للذاكرة وايضا اخير عنصر منها صفر (ركزوا على القيمة اللي بالأزرگ، معليكم بالحچي اللي حاطته Ghidra):
وراها تجي الILT والIAT مال ثاني DLL يستخدمه البرنامج. بعدها اسم اول DLL وراه اسم ثاني DLL:
وراه تجي structs من نوع IMAGE_IMPORT_BY_NAME
(اللي الRVA
مالهن بالIAT والILT) لأول DLL، كل وحدة منهن اول شي تبدي ببايتين (الها
استعمال بالواقع لكن البورلاند دائما يخليهن صفر) وراها يجي اسم الfunction
اللي راح يستعملها البرنامج من الDLL:
وراها تجي مال ثاني DLL وهكذا...
اثناء تحميل البرنامج للذاكره، الloader راح يمشي على الIATs ويشيل
الRVA مال كل IMAGE_IMPORT_BY_NAME
(اللي تحتوي على اسم
function معينة من الDLL) وراح يستبدلها بالعنوان الحقيقي لهاي الfunction
ضمن الaddress space مال البرنامج. فبالتالي لمن البرنامج يسوي indirect
jump لعنوان من IAT (مثل ما شفنا فوگ ويه ()ExitProcess
)، هو
راح يسوي jump لعنوان الfunction الحقيقي بالذاكرة، مو للعنوان مال اسمها
(بينما الILTs راح يبقن مثل ما هنه).
بعناصر الIDT، الRVA مال اول عنصر من الIAT اسمه
FirstThunk
ومال الILT اسمه OriginalFirstThunk
.
احنا راح نمشي على الIAT والILT سويه حتى ناخذ أسم الfunction من الILT
ونطابقه ويه عنوانها اللي ناخذه من الIAT:
احنا شنو اللي نريده من كل هذا؟
نريد عنوان ()MessageBoxA
، حنسميه
MessageBoxA_addr
:
نريد عنوان ()ExitProcess
(حتى نستخدمه بالكود مالنا بعد
ما اليوزر يغلق الMessageBox)، حنسميه ExitProcess_addr
:
نريد العنوان اللي موجود بي عنوان ()ExitProcess
، حتى نشيل
عنوان ()ExitProcess
ونخلي بمكانه عنوان الكود مالنا، حنسميه
ExitProcess_addraddr
:
المشكلة ان حجم الstrings ما متوفر، فإذا نريد نقراها كلها لازم نلملمها
بايت بايت لحد ما نوصل للnull (او نقره فد 200 بايت، that also works).
احنا منحتاج نسوي هالشي، ExitProcess
وMessageBoxA
ثنينهن 11 حرف (12 اذا تحسب الnull) فبس نحتاج
نقره 11 بايت:
الوندوز يخلينا نسوي pages بغير processes، حنخصص page للstrings وpage للinstructions بداخل البرنامج الديتنفذ "يعني حتضيع 8 كيلوبايتات، خليهن بنفس الpage على الأقل ووفر 4 كيلو"، صح بس اشوف هيچي أرتب. حيكون عنوان الMessageBox هو أسم البرنامج الديتنفذ ومحتواها هو "Program terminated":
خلي نشوف الfunction prototype مال MessageBoxA
:
الcalling convention هو stdcall، فراح ندفع الparameters بالعكس
للstack، حنبدي بأخير argument اللي يحدد شكل الMessageBox، حنختار
MB_OK
(اللي هي مجرد صفر) لأن بس نريد دگمة Ok وخلص:
زين شلون ندفع صفر للstack؟ نگدر نستشير اي assembler:
الassembler انطانا 0x6a، اللي هو push imm8، يعني ياخذ operand حجمه 8 بت (بايت واحد) ويدفعه للstack (الoperand هنا هو 0x00):
بعدها راح يجي الpointer مال عنوان الMessageBox:
كل pointer بنظام 32 بت حجمه 4 بايتات، فراح نستخدم وياه الopcode مال push imm32 من الجدول الفوگ (0x68):
بعدها حتجي رسالة الMessageBox:
معمارية x86 مابيها call لعنوان مباشر بنفس الsegment، فاللي نگدر نسويه
هو ان نخزن عنوان ()MessageBoxA
بالeax register ونسوي
register-indirect call. حنستخدم الassembler حتى نشوف شنو الopcode اللي
نريده:
اذن الopcode هو 0xb8 والباقي هو الoperand. نحتاج
call eax
هم:
للتذكير، احنا الfunction اللي دنعترضها هي ()ExitProcess
اللي تاخذ argument واحد بس وهو الexit status. هالشي معناه انو لمن يبدي
الcode مالتنا يتنفذ حيكون اخر شي موجود بالstack هو الreturn address مال
الcaller وقبله الexit status. فاذن نگدر نسوي jump
ل()ExitProcess
رأسا بدون call ولا push لان كلشي تحتاجه اصلا
موجود بالstack، فبهالحالة احنا دنخلي مسار البرنامج يستمر كأن شيئا لم يكن
واذا قررت ()ExitProcess
ترجع لأي سبب كان (وهالشي مستحيل)
فهي راح ترجع للcaller الاصلي وما راح ترجعلنا.
وضع الjmp مثل الcall، هم راح نحط الaddress بregister يلا نروحله:
هسه نسوي page للcode وننقل الinstructions الها ونستبدل عنوان
()ExitProcess
بالIAT بعنوان الpage مالنا. الIAT موجود
بمنطقة Read-Only فنكتب function تسوي المنطقة Read-Write مؤقتا وراها
ترجعها بعد التعديل:
ذكرت بالبداية ان ()inject
دتتنفذ أكثر من مرة، بأول
محاولة لحل هالمشكلة مجرد استبدلت اول بايت بذاكرة البرنامج بعلامة دالة،
حتى لمن تتنفذ الfunction من جديد حنعرف هي اشتغلت قبل:
بس هالشي سبب crash:
فسويت العلامة بmember من الDOS header اسمه e_oemid
لأن
شكله مو مهم، واشتغل كلشي مثل ما نريد:
بعدها انتبهت اكو event بالكتيب اسمها DebugeeCreated
:
جربتها واشتغلت الfunction مرة وحدة قبل تشغيل البرنامج، ايضا مثل ما نريد:
حذفت الكود اللي يخلي علامة لأن بعد ماله داعي. قبل ما نجرب الكود اللي
يعدل الذاكرة لأول مرة، خلي نحط int 3
(اللي الopcode مالتها
0xcc) بالبداية، هاي الinstruction يستخدموها للbreakpoints وحتخلي
البورلاند يوقف تنفيذ البرنامج ويعرضلنا نافذة الCPU اول ما يوصل للcode
مالنا:
نلاحظ ان نافذة الinstructions اللي موجودة على اليسار فسرت كل
الinstructions اللي كتبناها بشكل صحيح، واذا نباع على الESP (stack
pointer) حنشوف انو اخر شي بالstack (عند 0x0019ff18) هو الreturn address
للfunction اللي سوت call ل()ExitProcess
وهو 0x0040bd38
وقبله (عند 0x0019ff1C) اكو صفاره تمثل الparameter مال
()ExitProcess
.
الreturn address صاير بداخل اول function فتحناها بGhidra بهالمقال:
هاي الpage مال الstrings:
وهاي الpage مال الinstructions:
اجه وقت الdemo:
البورلاند ديخلي الconsole بالمقدمة دائما، فالMessageBox رأسا دتصير خلفه. بعد أحسن.
نتأكد ان الcalls دتصير صح:
اذا return 1
:
اذا نجرب نشغل الDLL على وندوز خام راح تطلعلنا هيچ رسالة:
البرامج اللي تطلع من Visual Studio تعتمد على الVisual C++ Runtime Library، اللي اذا ما موجودة لازم المستخدم ينزلها بنفسه. حتى نوفر العناء على المستخدم، خليتها تصير statically linked بحيث كلشي يحتاجه الDLL حيكون موجود وياه.
حتى أسهل التنصيب للأستاذ، سويت Self-extracting archive (SFX) بWinrar،
اللي هو عبارة عن EXE مضغوطة الملفات بداخلة وينسخ الDLL
وpersonal.spp
للأماكن الصحيحة داخل فولدر تنصيب البورلاند:
اثناء كتابتي لهلمقال قررت أسوي installer حقيقي بإستخدام NSIS لأن ردت
أوفر الكود مال كلشي (Winrar مغلق المصدر) ولأن ردت أنطي للمستخدم خيار
تنصيب السكربت كinjector.spp
بدل personal.spp
في حال چان عنده ملف personal.spp
خاص بي او راد يخلي تشغيل
السكربت يدوي لسبب ما.
صح اختيارات injector.spp
وpersonal.spp
شكلهن checkboxes بس يتصرفن مثل الradio buttons، متگدر تختارهن سوه.
كود الDLL وكود الinstaller والEXE تگدر تلگيهن هنا، إضافة الى batch file تلزيگ يبني كلشي. تگدر تنزل البورلاند منا.
Knowledge comes, but wisdom lingers, and I linger on the shore,
And the individual withers, and the world is more and more.
— Alfred Tennyson, Locksley Hall