Adventures in Boreland

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 وهذا اللي دنشوفه هنا. المنافسة بين مايكروسوفت وبورلاند چانت شرسة لدرجة مايكروسوفت چانت تاخذ موظفي بورلاند بالليموزينات.

Borland C++ 4.0 User's Guide, p. 420


توربو بداخل Windows 3.1

بينما Windows 95 چان عنده console (لكن مو console "حقيقي" مثل مال Windows NT وانما عبارة عن DOS virtual machine يتم تمرير الIO عن طريقها) لهذا مچان اكو حاجة لEasyWin، بس لمن گلبت بالكتيب مال البورلاند لگيته بعده موجود:

Borland C++ 5 User's Guide, p. 28

يعني اذا تريد تستخدم EasyWin، لازم تسوي project جديد وتختاره:

وراها حتطلع نافذة فارغة ولازم تختار ملف الcpp:

اني كطالب ميهمني احفظ هالخطوات، اريد برنامج يشتغل بلمح البصر وينطيني نافذة اكتب بيها كود رأسا، فهيچ "حل" مو مقبول.

اذا نباوع على الmenu bar مال البورلاند حنشوف اكو دگمة اضافية مچانت موجودة بالتوربو: Script.

شنو هاي الmodules؟

خلي نشوف محتوى وحدة منهن:

الscripting language شكلها شي جدي مو كلاوات. اكو كتيب كامل عليها، راح اخليه هو يعرف عنها:

Borland C++ 5 ObjectScripting Programmer's Guide, p. 20

اذا نسوي سكربت بأسم personal.spp ونخليه ويه البقية حيشتغل تلقائيا لمن يشتغل البرنامج:

Ibid, p. 22

نگدر نستدعي function من DLL خارجي بداخل السكربت:

Ibid, p. 65

اللغة تدعم نوع بدائي من الevents، الفكرة انو يصير call لmethod معينة (اسمها event) لمن يصير حدث معين، والevent handler يسوي override للmethod فيتنفذ بدالها لمن يصير الحدث:

Ibid, p. 73

الكتيب يذكر هوايه events، اكثر وحدة لفتت نظري هي DebugeeAboutToRun ضمن الDebugger class.

Ibid, p. 156

الDebugger class موجودة الinstance مالته بملف debug.spp:

ماطول هي export نگدر نسوي import الها من غير سكربت:

Ibid, p. 61

صرنا نعرف كلشي نحتاجه حتى نشغل function من DLL معين قبل تنفيذ اي كود من البرنامج اللي كتبه المستخدم. الfunction هاي، اللي حنسميها ()inject، راح تخلي نافذة البرنامج تبقى بعد ما ينتهي التنفيذ:


import debugger;

import "injector.dll" {
  void inject();
}

on debugger:>DebugeeAboutToRun() {
  inject();
}
personal.spp

ممكن تگول: شلون البورلاند ديستدعي function من ملف DLL عشوائي؟

الوندوز يدعم نوعين من الdynamic linking:

"اسم" الfunction يسموه الsymbol مالتها. ممكن يطابق اسمها بالكود بس مو شرط (مثل ما راح نشوف بعد شوية).

نجي نكتب كود اولي للDLL، حيكون 32 بت (لأن البورلاند 32 بت) وحستخدم Visual Studio 2015. الdeclspec(dllexport)_ معناها هاي الfunction حتكون exported ونگدر نستعملها من خارج الDLL، الموضوع يشبه الpublic والprivate بالOOP:

injector.cpp

اذا نجي نباع على الsymbol مال الfunction حنشوفه شي غريب:

السبب هو الC++ symbol mangling/decoration: بلغة ++C ممكن يكون اكو اكثر من function بنفس الاسم (بسبب الfunction overloading مثلا)، لهذا الsymbol لازم تنحط وياها معلومات اضافية نگدر نفك ترميزها عن طريق اي demangler:

حتى نلغي هالموضوع، مجرد نضيف "extern "C:

injector.cpp

اذا نحط الDLL بفولدر البورلاند (يم الEXE مالته) ونحاول ننفذ اي كود، راح نشوف الرسالة تطلع قبل ما يتنفذ اول سطر من البرنامج:

المشكلة الرسالة دتطلع اكثر من مرة، بعدين حنشوفلها حل. هسه اريد اخلي الDLL ينطيني debug messages اختيارية، صح هالشي مو ضروري للشغل (لأن اگدر احطهن وراها امسحهن) بس اريد ابقيهن واخليهن اختياريات حتى اعرف شنو المشكلة اذا صار خلل بالتنفيذ داخل المختبر:

injector.cpp


personal.spp

اذا نجرب من جديد حتطلع الرسالة اكثر من مرة (مثل ما توقعنا) بس هالمرة كل ما ندوس 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:

Borland C++ 5 ObjectScripting Programmer's Guide, p. 66


personal.spp

اذا نجرب من جديد البورلاند حيشتكي ان الfunction مو موجودة:

شكله تحديد الcalling convention خلاه يبحث عن غير symbol. نگدر نستعمل اداة WinAPIOverride حتى نشوف كل استخدامات البورلاند ل()GetProcAddress وبالتالي نعرف شنو الsymbol اللي ديبحث عنها:

گام يبحث عن inject_ بدل inject لمن حدننا الcdecl convention. سبب هالشي هو ان البورلاند بنفسه يحط _ لمن يبني cdecl function وهو ديفترض ان الدنيا كلها ماشيه مثله.

Borland C++ 5 Programmer's Guide, p. 65

قبل شوية گلنا البورلاند ديفترض الcalling convention هو stdcall، ليش؟ هالشي ممذكور بالكتيب بس لمن ماتحدد الconvention حيجرب كل طرق التسمية المذكورة بالجدول (عدا مال fastcall) وحيطابق الconvention حسب الاسم الصحيح:

نگدر نعتبر هالتسميات نوع من الC symbol mangling، حتى Visual Studio هم يسوي هيچ شي لمن يبني برامج 32 بت:

مكتوب هنا ان الcdecl symbols ينحط قبلها _ مثل ما يسوي البورلاند، لعد ليش الsymbol مالتنا مابيها؟ معقولة المعلومات غلط؟ خلي نسويها stdcall__ ونشوف شيصير:

صار الmangling مثل ما كاتبين. بعد البحث اكثر، اكتشفت كاتبين التالي بالdocumentation مال cdecl__:

حتى نحل الموضوع، حننطي تسمية ثانية للsymbol، يعني alias، او مثل ما اني اسميه: symbolic link.

injector.cpp

اذا نسوي build حيطلع خلل:

الdocumentation مال EXPORT/ بي شغلة لفتت انتباهي:

مو گلنا cdecl مابيها mangling؟ خلي نتأكد:

injector.cpp

مو كلش دافتهم شديصير هنا، اعتقد الlinker حاسب حسابه انو ديصير mangling لسبب ما، المهم ضفت underscore للطرفين واشتغل:

injector.cpp

الdeclspec_ بعد ماله داعي:

injector.cpp

اصلا نگدر نشيل ال "extern "C ونسوي alias للC++-mangled symbol مباشرة:

injector.cpp

لمن حددنا stdcall فوگ، الsymbol صارت inject@4_، ال4 هي حجم الarguments بالبايتات. بس احنا عدنا char، مو المفروض الحجم يكون بايت واحد؟

سبب هالشي هو الstack alignment، الVisual Studio ديسوي DWORD alignment (4 بايتات) بينما البورلاند يسوي byte alignment وهمين عباله العالم ماشيه مثله:

Borland C++ 5 Programmer's Guide, p. 72-73

هذا معناه انو البورلاند دينطي بايت واحد للfunction وهي دتتوقع اربع بايتات، هالشي مديسبب مشكلة چبيرة ويه الcdecl convention لأن الcaller (البورلاند) هو المسؤول عن تنظيف الstack، فراح ينطي بايت واحد ويشيل بايت واحد. بينما لو مختارين stdcall چان البورلاند ينطي بايت والfunction تشيل 4 من الstack، يعني 3 بايتات زيادة. خلي نجرب:

injector.cpp


personal.spp

زين شراح يصير اذا نخلي الfunction تشيل 10003 بايت زيادة؟

injector.cpp


"Ight Imma head out"

نگدر نخلي الfunction تاخذ int (4 بايتات) بدل char ونحل المشكلة:

personal.spp


injector.cpp

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:

Intel 64 and IA-32 Architectures Software Developer’s Manual: Instruction Set Reference, p. 663

اول 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 مالتنا (اللي هي، للتذكير، نفسها مال بورلاند):

injector.cpp

وحدة بناء الprocesses بالWindows هي الmodules، الmodule هي اما EXE او DLL، حتى نلگي الbase address مال BCWDBK32.DLL لازم نفر الmodules لحد ما نلگيه. الكود مال ()GetBaseAddress اخذته من النت:

injector.cpp

صار عدنا كلشي نحتاجه حتى نتبع الpointer chain:

injector.cpp

نجرب الكود:

المشكلة اذا الواحد ديستخدم غير نسخة من البورلاند ممكن الoffsets تختلف وتصير كارثة، الوندوز يوفر function تخلينا نتأكد من صحة الpointer قبل ما نقراه:

injector.cpp

هاي ال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 جاهزة وقابلة للاستعمال مو مجرد محجوزة:

injector.cpp

زين اذا اجانا 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:

injector.cpp

دنسوي cast الى *char حتى نجمع بس 3 بدل 4*3. بالنهاية حيكون عندنا:

injector.cpp

بدل ما ندوخ نفسنا بتفاصيل عمل الfunction كل ما نستعملها، نگدر نستبدلها بوحدة تسوي نفس الوظيفة بس فوگاها تچيك ان ptr وptr + 3 ضمن نفس الpage:

injector.cpp

تعرف تگدر تحسب المعادلة الفوگ بدون ورقة وقلم؟

\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:

injector.cpp

خلي نجرب شغلة:

احنا شغلنا برنامج بالبورلاند، طلعتلنا رسالة started injector.dll، وراها غلقنا البرنامج ودسنا ok. المفروض هسه باقي الكود يفشل، بس احنا مو بس دنحصل pid وانما handle هم. اوك خلي نگول الpid بقه بذاكرة البورلاند حتى بعد ما غلقنا البرنامج، شلون دنحصل handle؟ هالشي معناه ()OpenProcess دتگدر تفتح process ميته.

بالوندوز لمن تموت process حتبقى بالprocess table على هيئة زومبي طالما اكو handle الها بفد مكان، بس ما راح تبين بالTask Manager. خلي نكتب كود يچيك الexit code مال الprocess حتى نتأكد انو هيه بالفعل عايشه:

injector.cpp

حناخذ اسم البرنامج هم، حنحتاجه بعدين:

injector.cpp

ممكن المستخدم دينفذ برنامج GUI. يجي ويه البورلاند هوايه examples موجودة بمجلد التنصيب، هذا واحد منهن:

شلون نعرف اذا البرنامج عنده console؟ لاحظت بWindows 10 انو كل برامج الconsole عندها conhost كchild الها:

بس لمن چيكت Windows 7 اكتشفت انو كل الconhosts اللي بي صايرات children لcsrss، فمنگدر نستغل هالسلوك:

بالنهاية استخدمت ()AttachConsole لأن اذا فشل معناها الprocess مابيها console، لكن اذا نجح لازم نسوي ()FreeConsole لأن الconsole I/O مالDLL/البورلاند حيروح لنافذة البرنامج، وبس تنغلق النافذة حيطير البورلاند وياها.

injector.cpp

بالنسخة اللي وديتها للاستاذ چنت حاط 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:

injector.cpp

الحلو ان كل الstructs اللي نحتاجها موجودة بالwindows headers، ميحتاج نعرف شي من يمنا او نقره الoffsets مال القيم مباشرة. هسه خلي نشوف شكو بالNT headers:

الOptionalHeader بي هوايه امور متهمنا:

بس بالأخير اكو شي اسمه DataDirectory، اللي هي عبارة عن array كل واحد من عناصرها عنده Size وVirtual Address ‏(Relative):

الdocumentation يگللنا شنو ذني العناصر:

وايضا موجود الها constants بالheaders:

نكتب:

injector.cpp

اول شي موجود بالidata. هو الImport Directory Table (IDT) وهي عبارة عن array مال structs حجمها 20 بايت من نوع IMAGE_IMPORT_DESCRIPTOR، كل وحدة من عندهن تمثل DLL معين يستخدمه البرنامج ما عدا الأخيرة كلها مصفرة (هاي نستخدمها حتى نعرف وين نوگف لمن نسوي loop):

شلون نعرف الstruct مصفرة بالكود؟ نچيك عنصر مهم منها، اذا مصفر فكلها مصفرة وبالتالي وصلنا للنهاية:

injector.cpp

ال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:

injector.cpp

احنا شنو اللي نريده من كل هذا؟

نريد عنوان ()MessageBoxA، حنسميه MessageBoxA_addr:

نريد عنوان ()ExitProcess (حتى نستخدمه بالكود مالنا بعد ما اليوزر يغلق الMessageBox)، حنسميه ExitProcess_addr:

نريد العنوان اللي موجود بي عنوان ()ExitProcess، حتى نشيل عنوان ()ExitProcess ونخلي بمكانه عنوان الكود مالنا، حنسميه ExitProcess_addraddr:

المشكلة ان حجم الstrings ما متوفر، فإذا نريد نقراها كلها لازم نلملمها بايت بايت لحد ما نوصل للnull (او نقره فد 200 بايت، that also works). احنا منحتاج نسوي هالشي، ExitProcess وMessageBoxA ثنينهن 11 حرف (12 اذا تحسب الnull) فبس نحتاج نقره 11 بايت:

injector.cpp

الوندوز يخلينا نسوي pages بغير processes، حنخصص page للstrings وpage للinstructions بداخل البرنامج الديتنفذ "يعني حتضيع 8 كيلوبايتات، خليهن بنفس الpage على الأقل ووفر 4 كيلو"، صح بس اشوف هيچي أرتب. حيكون عنوان الMessageBox هو أسم البرنامج الديتنفذ ومحتواها هو "Program terminated":

injector.cpp

خلي نشوف الfunction prototype مال MessageBoxA:

الcalling convention هو stdcall، فراح ندفع الparameters بالعكس للstack، حنبدي بأخير argument اللي يحدد شكل الMessageBox، حنختار MB_OK (اللي هي مجرد صفر) لأن بس نريد دگمة Ok وخلص:

زين شلون ندفع صفر للstack؟ نگدر نستشير اي assembler:

الassembler انطانا 0x6a، اللي هو push imm8، يعني ياخذ operand حجمه 8 بت (بايت واحد) ويدفعه للstack (الoperand هنا هو 0x00):

Intel 64 and IA-32 Architectures Software Developer’s Manual: Instruction Set Reference, p. 1250

بعدها راح يجي الpointer مال عنوان الMessageBox:

كل pointer بنظام 32 بت حجمه 4 بايتات، فراح نستخدم وياه الopcode مال push imm32 من الجدول الفوگ (0x68):

injector.cpp

بعدها حتجي رسالة الMessageBox:

injector.cpp

معمارية x86 مابيها call لعنوان مباشر بنفس الsegment، فاللي نگدر نسويه هو ان نخزن عنوان ()MessageBoxA بالeax register ونسوي register-indirect call. حنستخدم الassembler حتى نشوف شنو الopcode اللي نريده:

اذن الopcode هو 0xb8 والباقي هو الoperand. نحتاج call eax هم:

injector.cpp

للتذكير، احنا الfunction اللي دنعترضها هي ()ExitProcess اللي تاخذ argument واحد بس وهو الexit status. هالشي معناه انو لمن يبدي الcode مالتنا يتنفذ حيكون اخر شي موجود بالstack هو الreturn address مال الcaller وقبله الexit status. فاذن نگدر نسوي jump ل()ExitProcess رأسا بدون call ولا push لان كلشي تحتاجه اصلا موجود بالstack، فبهالحالة احنا دنخلي مسار البرنامج يستمر كأن شيئا لم يكن واذا قررت ()ExitProcess ترجع لأي سبب كان (وهالشي مستحيل) فهي راح ترجع للcaller الاصلي وما راح ترجعلنا.

وضع الjmp مثل الcall، هم راح نحط الaddress بregister يلا نروحله:

injector.cpp

هسه نسوي page للcode وننقل الinstructions الها ونستبدل عنوان ()ExitProcess بالIAT بعنوان الpage مالنا. الIAT موجود بمنطقة Read-Only فنكتب function تسوي المنطقة Read-Write مؤقتا وراها ترجعها بعد التعديل:

injector.cpp

ذكرت بالبداية ان ()inject دتتنفذ أكثر من مرة، بأول محاولة لحل هالمشكلة مجرد استبدلت اول بايت بذاكرة البرنامج بعلامة دالة، حتى لمن تتنفذ الfunction من جديد حنعرف هي اشتغلت قبل:

injector.cpp

بس هالشي سبب crash:

فسويت العلامة بmember من الDOS header اسمه e_oemid لأن شكله مو مهم، واشتغل كلشي مثل ما نريد:

injector.cpp

بعدها انتبهت اكو event بالكتيب اسمها DebugeeCreated:

Borland C++ 5 ObjectScripting Programmer's Guide, p. 156

جربتها واشتغلت الfunction مرة وحدة قبل تشغيل البرنامج، ايضا مثل ما نريد:

personal.spp

حذفت الكود اللي يخلي علامة لأن بعد ماله داعي. قبل ما نجرب الكود اللي يعدل الذاكرة لأول مرة، خلي نحط int 3 (اللي الopcode مالتها 0xcc) بالبداية، هاي الinstruction يستخدموها للbreakpoints وحتخلي البورلاند يوقف تنفيذ البرنامج ويعرضلنا نافذة الCPU اول ما يوصل للcode مالنا:

personal.spp

نلاحظ ان نافذة ال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