المؤلفون: دكتوراه تشيرنوف إيه. ( monsieur_cher ) والدكتوراه تروشينا كي.كيف ، باستخدام أكثر الافتراضات العامة استنادًا إلى معرفة بنيات المعالج الحديثة ، يمكنك استعادة بنية البرنامج من صورة ثنائية لمعمارية غير معروفة ، ثم استعادة الخوارزميات وأكثر من ذلك بكثير؟في هذه المقالة سوف نتحدث عن مهمة واحدة مثيرة للاهتمام تم وضعها أمامنا قبل عدة سنوات. طلب العميل التعامل مع البرامج الثابتة الثنائية للجهاز الذي كان يدير عملية فعلية معينة. كان بحاجة إلى خوارزمية تحكم في شكل برنامج C مترجم ، بالإضافة إلى صيغ مع شرح لكيفية عملها ولماذا. وفقًا للعميل ، كان هذا ضروريًا لضمان التوافق مع المعدات "القديمة" في النظام الجديد. الطريقة التي تعاملنا بها في نهاية المطاف مع الفيزياء ، في إطار سلسلة المقالات هذه ، أغفلناها ، لكننا سننظر في عملية استعادة الخوارزمية بالتفصيل.
يتطلب الاستخدام شبه الكامل لأجهزة التحكم الدقيقة القابلة للبرمجة في الأجهزة الجماعية (IOT أو SmartHome Internet of Things) الانتباه إلى التحليل الثنائي للرمز المضمن ، أو بمعنى آخر ، التحليل الثنائي للبرامج الثابتة للجهاز.
قد يكون للتحليل الثنائي للبرامج الثابتة للجهاز الأهداف التالية:
- تحليل رمز الثغرات الأمنية التي تسمح بالوصول غير المصرح به إلى الجهاز أو إلى البيانات المرسلة أو المعالجة بواسطة هذا الجهاز.
- تحليل الكود للميزات غير الموثقة ، مما يؤدي ، على سبيل المثال ، إلى تسرب المعلومات.
- تحليل الشفرة لاستعادة بروتوكولات واجهات التفاعل مع الأجهزة لضمان توافق هذا الجهاز مع الآخرين.
يمكن اعتبار مهمة تحليل الشفرة الثنائية المذكورة أعلاه كحالة خاصة لمهمة التحليل الثنائي لضمان توافق الجهاز.
تحليل تنسيق الملف الثنائي
إذا كان في عالم أنظمة التشغيل "الحقيقية" ، فإن تنسيقات الملفات القابلة للتنفيذ موحدة ، ثم في عالم البرامج المدمجة ، يمكن لكل بائع استخدام حل الملكية الخاص به. لذلك ، يجب أن يبدأ تحليل ملف البرنامج الثابت الثنائي بتحليل تنسيق الملف الثنائي.
في بداية العمل ، كان الموقف بالنسبة لنا كما يلي: تلقينا ملفًا معينًا مع البرنامج الثابت دون أي مستندات مرفقة. لم تكن هناك معلومات حول تنسيق ملف البرنامج الثابت ، ولا عن بنية المتحكم الدقيق.
تحول ملف البرنامج الثابت إلى ملف نصي. يحتوي على سطور من النموذج التالي:
:04013000260F970CF8 :10020000004D000B043F000B34AD010C002FFE4D30 :02023000FD0BC1 :1004000018001A0000001E0008005E000200190052
بعد أن نظرنا بعناية في مجموعة هذه الخطوط ، أدركنا أن هذا هو تنسيق Intel HEX قياسي تمامًا لوحدات التحكم الصغيرة. يتكون الملف من سجلات ، يشير كل منها إلى نوعه وموقعه الذاكرة وبياناته واختباره. في حد ذاته ، يعني استخدام تنسيق Intel Hex أن الملف ليس مشفرًا على الأرجح وأنه صورة لبرنامج موجود في الذاكرة.
على الرغم من أن تنسيق Intel Hex يدعم ما يصل إلى 32 بت من الذاكرة عنونة ، كان هناك فقط عناوين ذاكرة 16 بت في ملفنا. لذلك ، من السهل إنشاء ملف ثنائي لصورة ذاكرة من ملف نصي يتم فيه بالفعل وضع سجلات من ملف الاختبار الأصلي في العناوين المحددة. من الأسهل فحص هذا الملف الثنائي باستخدام الأدوات المساعدة لتحليل الملفات الثنائية ، ومن السهل كتابة الأدوات المساعدة الخاصة بك بدلاً من اختبار Intel HEX. أكد ملف ذاكرة الصورة الثنائية أنه لم يتم تشفير الملف ، حيث تم العثور على مجموعة متنوعة من الأسطر ذات مغزى في أماكن مختلفة في الكود.
ومع ذلك ، فإن هذا لم يجيب على السؤال عن الهيكلية لهذا الملف.

لقد حصلنا على ملف يحتوي على صورة لذاكرة متحكم 16 بت أو 8 بت. وأي نوع من متحكم ليست واضحة. أخذنا IDA Pro وحاولنا تفكيك الملف بكل المتغيرات الممكنة للمعالجات المدعومة. ولا شيء لم يتم إنشاء أي من معالجات IDA Pro المدعومة: إما أن القائمة لم يتم إنشاؤها أو كانت تحتوي على هراء واضح. ربما كان برنامجًا لأحد معالجات IDA Pro المدعومة ، لكننا ارتكبنا خطأ ما. على سبيل المثال ، كنت بحاجة فقط إلى معالجة إضافية لملف الصورة. في أي حال ، كان من الممكن هنا تعليق العمل وطلب معلومات إضافية حول الملف الثنائي.
جميع المعالجات هي نفسها تقريبا.
لكنها أصبحت مثيرة للاهتمام بالنسبة لنا ، وما يمكننا فهمه من البرنامج الثنائي ، حتى لو كان المعالج الذي تم تجميعه من أجله غير معروف. الجواب هو "لا شيء" - غير مهتم ، حتى لو تمكنا من فهم بعض الشيء ، فهو أفضل من لا شيء. من الواضح ، يمكن أن توفر السلاسل النصية معلومات حول البرنامج ، لكننا نهدف إلى المزيد - لفهم شيء ما من بنية البرنامج.
أبنية المعالج المختلفة - عدد كبير. لقد أدى تطور الحوسبة إلى إنشاء أكثر البنى غرابة مثل أجهزة الكمبيوتر الثلاثية. ومع ذلك ، فإن المعالجات الدقيقة و ميكروكنترولر الموجودة حاليا ، على الأقل منها كتلة ، تشبه بشكل ملحوظ مع بعضها البعض.
فيما يلي قائمة بالمبادئ الأساسية الشائعة للمعالجات الدقيقة الحديثة.
التنفيذ المستمر للتعليمات. ينفذ المعالج التعليمات بالتسلسل في الذاكرة. هناك إرشادات خاصة بالقفزة الشرطية وغير المشروطة والاتصال والعودة من الروتين الفرعي ، والتي تتيح لك مقاطعة التحديد المتسلسل للتعليمات من الذاكرة والمتابعة إلى تنفيذ تعليمة أخرى. ومع ذلك ، تفترض بقية التعليمات تنفيذ متسلسل ، وبالتالي لا تحتوي على عنوان التعليمة التالية.
الترميز الثنائي. بالإضافة إلى حقيقة أن المعالج يعالج البيانات في نموذج ثنائي ، فإن تعليمات المعالج التي تشكل البرنامج القابل للتنفيذ يتم ترميزها بالتنسيق الثنائي ، أي الحقول التي يتم تخزين معلمات التعليمات فيها ، على سبيل المثال ، الإزاحات أو أرقام التسجيل ، تحتل عددًا صحيحًا من البتات. يمكن للمرء أن يتخيل نظريا أنه ، على الرغم من الترميز الثنائي للبيانات والبرامج ، سيتم معالجتها في المعالج في بعض أنظمة الأرقام الأخرى ، لكننا لسنا على علم بمثل هذا الغرابة.
المبادئ التالية ، بشكل عام ، ليست مبادئ معمارية أساسية ، ولكنها تُطبق عمليًا على المستوى العالمي ، خاصةً لرمز الجهاز غير المكتوب يدويًا بلغة التجميع ، ولكن يتم إنشاؤه بواسطة مترجم لغة عالي المستوى.
البرمجة الإجرائية. ينقسم البرنامج إلى وحدات هيكلية ، والتي يمكن تسميتها بطريقة مختلفة: الإجراءات ، الوظائف ، البرامج الفرعية ، إلخ. يمكن أن تقوم البرامج الفرعية باستدعاء البرامج الفرعية الأخرى ، وتمرير المعلمات إليها واستعادة نتيجة التنفيذ. من المهم أن يحتوي البرنامج الفرعي على نقطة إدخال واحدة ، أي أن جميع البرامج الفرعية التي تستدعي النقطة الواحدة تذهب إلى نفس عنوان نقطة الدخول.
عادةً ما تحتوي الروتين على نقطة خروج واحدة تعيد التحكم إلى نقطة الاتصال ، ولكن هذا أقل أهمية من طلب نقطة دخول واحدة لكل روتين. وعادة ما يتم الحصول على هذا الرمز عن طريق تجميع البرنامج. يمكن أن يقوم مُحسِّن وقت الارتباط بتدمير هذه البنية جزئيًا لتقليل حجم البرنامج ، كما أن حجم البرنامج مهم للأنظمة المدمجة. علاوة على ذلك ، يمكن تدمير هذا الهيكل عن طريق شفرة التعتيم.
يمكن تنظيم تداخل مكالمات الروتين الفرعي باستخدام المكدس ، والذي لا يزال من الممكن استخدامه لتمرير الوسائط إلى الروتين الفرعي وتخزين المتغيرات المحلية ، ولكن على المستوى الحالي لتطوير البنية ، هذه المعلومات سابقة لأوانها.
كيف يمكن تطبيق هذه المبادئ على التحليل الأولي للرمز الثنائي؟
نحن نفترض أن هناك تعليمة RET (إرجاع من روتين فرعي) في نظام أوامر المعالج. تحتوي هذه التعليمات على نوع من التمثيل الثنائي الثابت ، والذي سنبحث عنه. إذا لم تكن RET هي الوحيدة ، كما هو الحال في x86 ، حيث تحتوي RET على وسيطة - حجم منطقة المعلمة الفرعية ، أو إذا كان RET هو أحد الآثار الجانبية لعملية أكثر تعقيدًا ، كما في ARMv7 ، حيث يمكن جلب قيمة الكمبيوتر من المكدس في نفس الوقت مع قيم السجلات الأخرى (ldmfd sp! ، {fp ، pc}) ، إذن ، على الأرجح ، لن يسفر بحث الاستدلال عن نتائج.
نحتاج أيضًا إلى وضع افتراضات معقولة على الفور حول مبدأ ترميز تعليمات المعالج قيد الدراسة. تستخدم المعالجات الحالية عدة مبادئ لتعليمات الترميز:
- مجموعة من وحدات البايت التي يتم إنشاء التعليمات منها ، ويتم ترميز التعليمات المختلفة بعدد مختلف من وحدات البايت. في هذه الفئة ، الممثل الأكثر شهرة هو عائلة x86 ، من أول 8080 معالجات إلى أحدث معالجات 64 بت. يمكن ترميز تعليمة معالج x86_64 واحدة في تسلسل من 1 إلى 16 بايت. تتضمن نفس مجموعة المعالجات ذات أطوال التعليمات المتغيرة 8051 ، والذي يتم استخدامه في المتحكمات الدقيقة.
- دفق قيم 16 بت. علاوة على ذلك ، كل تعليمة لها حجم ثابت - 16 بت.
- دفق قيم 16 بت ، بينما تكون التعليمات متغيرة في الحجم. أحد ممثلي هذه العائلة هو بنية PDP-11 ، حيث يشغل الأمر نفسه أول 16 بت ، ويمكن أن يتبعه إما قيم مباشرة أو عناوين ذاكرة للعنونة المباشرة. يتضمن ذلك تشفير THUMB في بنية ARM.
- تيار من قيم 32 بت ، ولكل تعليمة حجم ثابت يبلغ 32 بت. هذه هي غالبية معالجات RISC 32 و 64 بت: ARMv7 ، ARMv8 ، MIPS.
يساعد الاختيار بين دفق بايت متغير الطول ودفق كلمات من 16 بت على عرض صورة الذاكرة "بالعين". بغض النظر عن الطريقة التي يتم بها تشفير تعليمات المعالج ، في برنامج بطول كافٍ سيتم تكرارها حتماً. على سبيل المثال ، بناء على تعليمات x86
add
الذي يضيف قيم سجلات eax و ebx ويضع النتيجة في eax ، يتم ترميزه في وحدتي بايت:
01 d8.
بناء على تعليمات ARMv7
add r0, r0, r1
الذي يضيف قيم السجلات r0 و r1 ويضع النتيجة في r0 ، يتم ترميزه بقيمة 32 بت e0800001.
في برنامج كبير بما فيه الكفاية ، سيتم تكرار هذه التعليمات أكثر من مرة. إذا حدث تسلسل من البايتات التي تهمنا (على سبيل المثال ، 01 d8) في عنوان عشوائي غير محاذٍ ، فيمكننا افتراض أن تعليمات المعالج مشفرة بواسطة دفق من البايتات ذات الحجم المتغير. إذا تم العثور على القيمة ، على سبيل المثال ، e0800001 فقط في العناوين التي تكون مضاعفاتها 4 ، فيمكننا افتراض وجود حجم ثابت لتعليمات المعالج. بالطبع ، هناك خطأ في أننا أخذنا بايت البيانات للحصول على تعليمات ، أو حدث ذلك عن طريق الصدفة أن بعض التعليمات كانت دائمًا تتحول إلى محاذاة. ومع ذلك ، فإن تأثير هذه "الضوضاء" على برنامج بحجم كافٍ سيكون صغيراً.
عندما نظرنا إلى البرامج الثابتة التي تم تحليلها من هذه الزاوية ، أصبح من الواضح أن تعليمات المعالج المعني على الأرجح مشفرة بقيم 16 بت.
لذلك ، بناءً على افتراض أن ترميز تعليمة RET هو قيمة ثابتة 16 بت ، فلنحاول العثور عليها. نجد في صورة البرنامج أكثر قيم 16 بت شيوعًا. في حالتنا ، حدث ما يلي:
(hex) 0b01 854 5.1
سوف نبحث عن تعليمة RET ضمن هذه القيم ذات 16 بت والتي غالباً ما تصادف في الكود. سنبحث فورًا عن تعليمة CALL - مقترنة بتعليمات RET. يحتوي تعليمة CALL على معلمة واحدة على الأقل - عنوان القفز ، لذلك لا غنى عن القيم الثابتة.
نحن نفترض أنه في كثير من الحالات ، مباشرة بعد نهاية أحد البرامج الفرعية ، أي بعد بدء تعليمات RET ، يبدأ برنامج فرعي آخر ، ويتم استدعاء هذا البرنامج الفرعي بواسطة تعليمة CALL من نقطة أخرى في البرنامج. سيكون عدد كبير من القفزات على العنوان التالي مباشرة لـ RET واحدة من السمات المميزة لتعليمات CALL. بالطبع ، هذه القاعدة ليست عالمية ، لأنه في بعض الأنظمة الأساسية ، وعلى وجه الخصوص ، ARMv7 ، مباشرة بعد الانتهاء من الروتين الفرعي ، عادةً ما توجد مجموعة ثابتة. في هذه الحالة ، يمكننا اعتبار بعض النطاقات المعقولة من العناوين بعد RET فورًا كنقاط انتقال لتعليمات RET.
في حالة تعليمات CALL ، يمكن أن يكون هناك الكثير من الخيارات لترميزها إلى الروتين الفرعي. أولاً ، يمكن للمعالج استخدام ترتيب بايت مختلف في الكلمة: endian ، كما هو الحال في معظم المعالجات الحديثة ، عندما يتم كتابة عدد صحيح متعدد البايتات في الذاكرة ، بدءًا من البايت المنخفض ، و end-endian ، عندما يتم كتابة عدد صحيح متعدد البايتات في الذاكرة ، يبدأ من البايت عالية. تعمل جميع المعالجات الحديثة تقريبًا في وضع endian قليلاً ، لكن يجب ألا تتجاهل أوامر البايت الأخرى الممكنة في كلمة واحدة.
ثانياً ، يمكن أن يستخدم تعليمة CALL إما العنوان المطلق لنقطة الانتقال أو عنونة نسبة إلى العنوان الحالي. في حالة العنونة المطلقة ، يحتوي التعليمة المشفرة على العنوان الذي تريد الانتقال إليه في بعض أجزاء التعليمات المشفرة. لضمان استدعاء الروتين الفرعي من أي نقطة في مساحة العنوان ذات 16 بت إلى أي نقطة أخرى على العنوان المطلق للكلمة ذات 16 بت ، فإن التعليمات المشفرة لا تكفي ، لأنه بالإضافة إلى عنوان النقل ، تحتاج بتات كود التشغيل إلى تخزينها في مكان آخر. لذلك ، من المنطقي مراعاة كلمتين من 16 بت في صف واحد وتجربة خيارات مختلفة لتقسيم عنوان النقل بين هذه الكلمات.
بديل الترميز المطلق لعنوان روتيني هو الترميز النسبي. في التعليمات المشفرة ، نسجل الفرق بين عنوان البرنامج الفرعي والنقطة الحالية. عادةً ما يكون الترميز النسبي أفضل من مطلق ، لأنه أولاً ، البرنامج ذي التحولات النسبية مستقل موضعيًا ، أي أنه يمكن وضعه في الذاكرة من أي عنوان دون أي تغييرات في الشفرة الثنائية. ثانياً ، لترميز الإزاحة ، يمكن حجز عدد أقل من البتات من بعد مساحة العنوان ، استنادًا إلى حقيقة أنه في كثير من الحالات ، لا يكون الروتين المدعو بعيدًا عن وحدة الاستدعاء. ومع ذلك ، إذا كان إزاحة المكالمة خارج نطاق القيم القابلة للتمثيل ، فسيتعين عليك إدراج تعليمات خاصة - "القفزات".
يمكن إجراء الترميز النسبي لعنوان البرنامج الفرعي مع بعض الاختلافات: أولاً ، يمكن أخذ عنوان النقطة الحالية للبرنامج إما كعنوان للتعليم الحالي ، أو كعنوان للتعليمات التالية ، كما في معالجات x86 ، أو عنوان بعض التعليمات الأخرى بالقرب من النقطة الحالية. على سبيل المثال ، في معالجات ARM ، النقطة المرجعية هي عنوان التعليمة الحالية +8 (أي ، ليس عنوان التعليمة التي تتبع CALL ، ولكن عنوان التعليمة التالية التالية). بالإضافة إلى ذلك ، نظرًا لأنه في حالتنا ، يتم كتابة البرنامج كدفق من الكلمات ذات 16 بت ، فمن المنطقي توقع أن يتم التعبير عن الإزاحة بالكلمات. وهذا يعني أنه للحصول على عنوان الروتين الذي تم استدعاؤه ، ستحتاج إزاحة المضروبة إلى 2.
مع مراعاة كل ما سبق ، نحصل على مساحة التعداد التالية للبحث عن زوج CALL / RET في الكود الثنائي.
أولاً ، نأخذ الكلمات ذات 16 بت من قائمة أكثر القيم شيوعًا في الكود كمرشحين لتعليم RET. بعد ذلك ، نحن نبحث من خلال تعليمات CALL:
- ترتيب البايت endian الكبير والقليل endian كلمة
- الترميز المطلق والنسبي للعنوان الروتيني في التعليمات.
بالنسبة إلى الترميز المطلق ، فإننا نعتبر قيمتين 16 بت في صف واحد ، وهما: الفرز عبر خيارات متعددة لوضع حقل بت يخزن عنوانًا مطلقًا بكلمة 32 بت ، وبالنسبة للترميز النسبي ، فإننا نعتبر كل من قيم 16 بت وكلمتين 16 بت في صف واحد. . بعد ذلك ، نقوم بالفرز بين الخيارات المختلفة لوضع حقل بت يخزن الإزاحة. نتحقق مما إذا كان يتم التعبير عن الإزاحة بالبايت أو بكلمات ذات 16 بت ، أي ما إذا كان من الضروري ضرب الإزاحة في 2 ، ونقوم بالتحقق من خيارات مختلفة للنقطة المرجعية: عنوان التعليمات الحالية ، وعنوان التعليمة التالية.
لكل من الخيارات الموجودة في مساحة البحث الموضحة أعلاه ، نقوم بحساب الإحصاءات:
- من الواضح أن عدد العناوين المفترضة لبداية البرامج الفرعية غير صحيح ، أي أنها توجد في مكان لا يوجد فيه شيء ، أو حيث توجد البيانات (صفوف صريحة ، أو جداول واضحة للقيم). حتى بالنسبة للقيمة المطابقة للترميز الصحيح لتعليمات CALL ، فمن الممكن تمامًا أن يكون عدد قليل من العناوين غير الصحيحة لبداية البرنامج الفرعي ممكنًا إذا حدثت القيمة المقابلة لتعليمات CALL عن طريق الخطأ في البيانات.
- كم عدد عناوين البدء الروتينية المفترضة تكون مباشرة بعد تعليمة RET المفترضة.
- كم عدد عناوين البدء الافتراضية للروتين يتم استخدامها أكثر من مرة.
إذا كانت افتراضاتنا حول زوج من إرشادات CALL / RET صحيحة ، فينبغي أن تكون في مساحة التعداد الموصوفة. ولكن قد يكون هناك أيضا ايجابيات كاذبة. حسنًا ، نبدأ البحث.
ونجد خيار واحد فقط ممكن!
Trying 8c0d as RET After-ret-addr-set-size: 430 Matching call opcodes for 1, ff00ff00, 1: 000b003d: total: 1275, hits: 843 (66
لذلك ، فإن كلمة 8c0d ذات 16 بت مناسبة كمرشح لتعليم RET. إجمالاً ، تحتوي البرامج الثابتة على 430 موضع لعناوين البرنامج بعد هذه التعليمات مباشرة. اعتبرنا القيم 32 بت (كلمتين 16 بت في صف واحد) ، مع قيمة قناع العنوان ff 00 ff 00 ، تم العثور على تعليمة برمز 00 0b 00 3d. هناك 1275 مثل هذه التعليمات ، منها 843 (أي 66 ٪) نقل السيطرة إلى النقطة التي تلي مباشرة مرشح RET. وبالتالي ، تم تحديد إرشادات اثنين:
- RET: 8c0d (16-bit Little-Endian)
- CALL HHLL: 0bHH 3dLL (2 16-bit Little Endian)
يستخدم تعليمة CALL عنونة مطلقة ، وعند كتابة عنوان القفز ، يتم كتابة البايت العالي أولاً ، ثم كتابة البايتات المنخفضة. من الممكن أن يكون هذان في الواقع إرشادات للمعالج ، كل منها يقوم بتحميل نصف عنوان النقل ، ولكن من وجهة نظر تحليل البرنامج ، هذا ليس مهمًا. من خلال معرفة إرشادات CALL و RET ، يمكننا تحديد مجالات التعليمات البرمجية والبرنامج بدقة أكبر ، والتي ستكون مهمة لمزيد من التحليل.
أن تستمر ...
علاوة على ذلك ، سنقول كيف تمت استعادة التحولات الشرطية وغير المشروطة وبعض العمليات الحسابية والمنطقية.