الانغماس في برنامج التشغيل: المبدأ العام للانعكاس باستخدام مثال مهمة NeoQUEST-2019


مثل كل المبرمجين ، فأنت تحب الكود. أنت وله أفضل الأصدقاء. لكن عاجلاً أم آجلاً في الحياة ستأتي لحظة كهذه عندما لا يكون هناك كود معك. نعم ، من الصعب تصديق ذلك ، لكن سيكون هناك فجوة كبيرة بينكما: أنت في الخارج ، وهو عميق في الداخل. من اليأس ، يجب عليك ، مثل الجميع ، أن تذهب إلى الجانب الآخر. إلى جانب الهندسة العكسية.

باستخدام مثال المهمة رقم 2 من المرحلة الإلكترونية لـ NeoQUEST-2019 ، سنقوم بتحليل المبدأ العام لنظام التشغيل العكسي Windows. بالطبع ، يتم تبسيط المثال تمامًا ، لكن جوهر العملية لا يتغير عن هذا - والسؤال الوحيد هو مقدار الشفرة التي يجب عرضها. مسلحين بالتجربة والحظ ، لنبدأ!

دانو


وفقًا للأسطورة ، حصلنا على ملفين: تفريغ حركة المرور وملف ثنائي أنشأ نفس حركة المرور. أولاً ، ألقِ نظرة على التفريغ باستخدام Wireshark:


يحتوي التفريغ على دفق حزم UDP ، يحتوي كل منها على 6 بايتات من البيانات. هذه البيانات ، للوهلة الأولى ، هي مجموعة عشوائية من البايتات - لا يمكن إخراج أي شيء من حركة المرور. لذلك ، نوجه انتباهنا إلى binar ، والذي يجب أن يخبرك بكيفية فك تشفير كل شيء.
افتحه في المؤسسة الدولية للتنمية:


يبدو أننا نواجه نوعا من السائق. تشير الدالات ذات البادئة WSK إلى Winsock Kernel ، واجهة برمجة شبكة وضع kernel في Windows. على MSDN ، يمكنك رؤية وصف للبنيات والوظائف المستخدمة في WSK.

للراحة ، يمكنك تحميل Windows Driver Kit 8 (وضع kernel) - wdk8_km (أو أي مكتبة أحدث) في المؤسسة الدولية للتنمية لاستخدام الأنواع المحددة هناك:


الحذر ، عكس!


كالعادة ، ابدأ من نقطة الدخول:


دعنا نذهب في النظام. أولاً ، تتم تهيئة Wsk ، ويتم إنشاء مأخذ توصيل وربطه - ولن نصف هذه الوظائف بالتفصيل ، فهي لا تحمل أي معلومات مفيدة لنا.

تقوم الدالة sub_140001608 بتعيين 4 متغيرات عمومية. دعنا نسميها الأحرف الأولى. في واحد منهم ، تتم كتابة قيمة على العنوان 0xFFFFF78000000320. غوغل هذا العنوان قليلا ، يمكننا أن نفترض أنه يسجل عدد علامات التجزئة من توقيت النظام من لحظة قيام النظام بالتمهيد. الآن ، دعونا اسم المتغير TickCount.


تقوم EntryPoint بعد ذلك بإعداد وظائف لمعالجة حزم IRP (حزمة طلب الإدخال / الإخراج). يمكنك قراءة المزيد عنها على MSDN. بالنسبة لجميع أنواع الطلبات ، يتم تعريف وظيفة تقوم ببساطة بتمرير الحزمة إلى برنامج التشغيل التالي في المجموعة.


ولكن بالنسبة للنوع IRP_MJ_READ (3) ، يتم تحديد وظيفة منفصلة ؛ دعنا نسميها IrpRead.



في ذلك ، بدوره ، يتم تثبيت completRoutine.


يملأ CompleteRoutine البنية غير المعروفة بالبيانات الواردة من IRP ويضعها في القائمة. حتى الآن ، لا نعرف ما هو داخل الحزمة - سنعود إلى هذه الوظيفة لاحقًا.
نحن ننظر أبعد من ذلك في EntryPoint. بعد تعريف معالجات IRP ، تسمى الدالة sub_1400012F8. دعونا ننظر من الداخل ونلاحظ على الفور أن الجهاز (IoCreateDevice) تم إنشاؤه فيه.


استدعاء وظيفة AddDevice. إذا كانت الأنواع صحيحة ، فسنرى أن اسم الجهاز هو "\\ Device \\ KeyboardClass0". لذلك سائقنا يتفاعل مع لوحة المفاتيح. غوغل حول IRP_MJ_READ في سياق لوحة المفاتيح ، يمكنك أن تجد أن بنية KEYBOARD_INPUT_DATA تنتقل في حزم. دعنا نعود إلى CompleteRoutine ونرى ما نوع البيانات التي يمر بها.


لا تقوم IDA هنا بتحليل البنية جيدًا ، ولكن عن طريق الإزاحة والمكالمات الإضافية يمكنك أن تفهم أنها تتكون من ListEntry و KeyData (رمز المسح الضوئي للمفتاح يتم تخزينه هنا) و KeyFlags.
بعد AddDevice ، يتم استدعاء الدالة sub_140001274 في EntryPoint. انها تخلق تيار جديد.


دعونا نرى ما يحدث في ThreadFunc.


إنها تحصل على القيمة من القائمة وتقوم بمعالجتها. انتبه فورًا إلى الوظيفة sub_140001A18.


يقوم بتمرير البيانات التي تمت معالجتها إلى إدخال الدالة sub_140001A68 ، مع مؤشر إلى WskSocket والرقم 0x89E0FEA928230002. بعد تحليل رقم المعلمة بالبايت (0x89 = 137 ، 0xE0 = 224 ، 0xFE = 243 ، 0xA9 = 169 ، 0x2328 = 9000) ، نحصل على نفس العنوان والمنفذ من تفريغ حركة المرور بالضبط: 169.243.224.137:9000. من المنطقي أن نفترض أن هذه الوظيفة ترسل حزمة شبكة إلى العنوان والمنفذ المحددين - لن نفكر فيها بالتفصيل.
دعونا نرى كيف تتم معالجة البيانات قبل الإرسال.

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



بعد إنشاء الرقم ، فإنه يحل محل قيمة المتغير الذي كنا نسميه سابقًا TickCount. يتم تعيين متغيرات الصيغة في InitVars. إذا عدنا إلى المكالمة إلى هذه الوظيفة ، فسنكتشف قيم هذه المتغيرات ، ونتيجة لذلك سنحصل على الصيغة التالية:

(54773 + 7141 * prev_value)٪ 259200

هذا هو مولد الرقم العشوائي الخطي متطابق الزائفة . تتم تهيئة في InitVars باستخدام TickCount. لكل رقم لاحق ، يعمل الرقم السابق كقيمة أولية (يُرجع المولد قيمة مزدوجة البايت ، ويستخدم نفس الشيء في التوليد اللاحق).


بعد المكافئ بعدد عشوائي من قيمتين يتم إرسالهما من لوحة المفاتيح ، يتم استدعاء وظيفة تشكل وحدتي بايت المتبقيين من الرسالة. إنه ينتج ببساطة xor من معلمتين مشفرة بالفعل وبعض القيمة الثابتة. من غير المحتمل أن يؤدي ذلك إلى فك تشفير البيانات بطريقة أو بأخرى ، لذلك لا تحمل آخر وحدتي بايت من الرسالة بالنسبة لنا أي معلومات مفيدة ، ولا يمكن اعتبارهما. ولكن ماذا تفعل مع البيانات المشفرة؟
دعونا نلقي نظرة فاحصة على ما هو مشفر بالضبط. KeyData هو رمز مسح يمكن أن يأخذ مجموعة واسعة من القيم ؛ والتخمين ليس سهلاً. لكن KeyFlags هو حقل بت:


إذا نظرت إلى جدول رموز المسح الضوئي ، فستلاحظ أن العلامة ستكون في معظم الأحيان إما 0 (المفتاح معطل) أو 1 (يتم رفع المفتاح). سيتم عرض KEY_E0 في حالات نادرة جدًا ، ولكن قد يظهر ، ولكن لمواجهة KEY_E1 تكون الفرص ضئيلة للغاية. لذلك ، يمكنك محاولة القيام بما يلي: نذهب إلى البيانات من التفريغ ، وحدد قيمة مشفرة KeyFlags ، وجعل ما يعادل 0 ، وإنشاء اثنين من PSCs المتعاقبة. أولاً ، KeyData هي بايت واحد ، ويمكننا التحقق من صحة MSS التي تم إنشاؤها بواسطة البايتات العالية. وثانياً ، فإن KeyFlags المشفرة التالية ، عند إجراء مكافئ مع PSC الصحيح ، ستتخذ نفس قيم البت. إذا تبين أن هذا خطأ ، فسنفترض أن KeyFlags التي نظرنا إليها في الأصل كانت 1 ، إلخ.
دعونا نحاول تنفيذ خوارزمية لدينا. سوف نستخدم بيثون لهذا:

تنفيذ الخوارزمية
#  -   keymap = […] # ,   Wireshark traffic_dump = […] #  def bxnor(a, b): return ((~a & 0xffff) | b) & (a | (~b & 0xffff)) #   def brgen(a): return ((7141 * a + 54773) % 259200) & 0xffff def decode(): #     for i in range(0, len(traffic_dump) - 1): #   KeyFlags probe = traffic_dump[i][1] #   - scancode = traffic_dump[i+1][0] #    KeyFlags tester = traffic_dump[i+1][1] fail = True #     (  KEY_E1) for flag in range(4): rnd_flag = bxnor(flag, probe) rnd_sc = brgen(rnd_flag) next_flag = bxnor(tester, brgen(rnd_sc)) #   KeyFlags if next_flag in range(4): sc = bxnor(rnd_sc, scancode) if sc < len(keymap): sym = keymap[sc] if next_flag % 2 == 0: print(sym, end='') fail = False break #   -      KeyFlags   if fail: print('Something went wrong on {} pair'.format(i)) return print() if __name__ == "__main__": decode() 


قم بتشغيل البرنامج النصي الخاص بنا على البيانات الواردة من التفريغ:


وفي حركة المرور المشفرة نجد خطنا المرغوب فيه!

NQ2019DABE17518674F97DBA393415E9727982FC52C202549E6C1740BC0933C694B3DE


قريبا سيكون هناك مقالات مع تحليل المهام المتبقية ، لا تفوت!

ملحوظة: نذكرك أن كل من أكمل مهمة واحدة على الأقل في NeoQUEST-2019 يحق له الحصول على جائزة! تحقق من بريدك للحصول على خطاب ، وإذا لم يأتِ لك ، فاكتب إلى support@neoquest.ru !

Source: https://habr.com/ru/post/ar446462/


All Articles