P01NT
0x6 06/10

فك التجميع المتوسط (Intermediate Disassembly)

MUL وIMUL والمصفوفات والحلقات وتعليمات السلاسل وبادئات REP والدوال الجوهرية وإعادة بناء البنيات وجداول القفز، ومعمل guesser.

· 20 دقائق قراءة · 4160 كلمة
المحتويات

بسم الله الرحمن الرحيم

في هذا المقال سنتحدث عن فك التجميع المتوسط.


1. المؤشرات والذاكرة (Pointers and Memory)

في اللغات عالية المستوى مثل C، المؤشر (Pointer) هو متغير يخزّن عنوان ذاكرة. في الشيفرة على الجانب الأيسر من الشكل 7.1، يحصل البرنامج على عنوان المتغيرين الصحيحين num1 و num2 عبر معامل “عنوان” (&) ويسندهما تباعًا إلى متغير المؤشر ptr.

الشكل 7.1: يسار: شيفرة C تستخدم متغير مؤشر. يمين: تجميع البرنامج. أسفل: حالة الذاكرة أثناء التنفيذ

الشكل 7.1: يسار: شيفرة C تستخدم متغير مؤشر. يمين: تجميع البرنامج. أسفل: حالة الذاكرة أثناء التنفيذ

على مستوى التجميع، يمكن أن يشير مصطلح “مؤشر” إلى المتغير نفسه، لكنه يُستخدم غالبًا للإشارة إلى عنوان ذاكرة كقطعة من البيانات:

  • المؤشر كمتغير: تستخدم التجميع على يمين الشكل 7.1 التعليمة LEA لحساب عنوان المتغير المحلي num1 وتخزنه في المتغير المحلي ptr.
  • المؤشر كقيمة: يعرض عرض الذاكرة في أسفل الشكل 7.1 الحالة مباشرةً بعد تنفيذ أول تسلسل LEA-MOV. المتغير المحلي ptr الموجود عند 0x12FF48 يحمل الآن القيمة 0x12FF40.
    • يمكنك الإشارة إلى المتغير عند 0x12FF48 بوصفه مؤشرًا. ويمكنك أيضًا القول إن القيمة 0x12FF40 هي مؤشر، لأنها عنوان ذاكرة يُعامل كبيانات.

2. تعليمتا MUL و IMUL

تُجري تعليمتا MUL و IMUL عملية الضرب الحسابي. وتُستخدمان كثيرًا لحساب إزاحات الذاكرة للوصول إلى المصفوفات والبنيات.

الحجمالمصدر 1المصدر 2الوجهة
بايتALr/m8AX
كلمةAXr/m16DX:AX
كلمة مزدوجةEAXr/m32EDX:EAX

الجدول 7.1: ملخص معاملي المصدر والوجهة لتعليمة MUL

mov  eax, 10
mov  ecx, 7
mul  ecx      ; تُضبط edx إلى 0، وتُضبط eax إلى 70

القائمة 7.1: ضرب عددين صحيحين غير موقّعين باستخدام MUL

تُجري تعليمة MUL ضربًا غير موقّع. تأخذ معاملًا صريحًا واحدًا (سجل أو ذاكرة) وتضربه في معامل ضمني. المعامل الضمني هو AL أو AX أو EAX حسب حجم المعامل الصريح.

ولأن ناتج ضرب عددين صحيحين قد يتطلب حتى ضعف عرض المعاملين، تُخزَّن النتيجة عبر سجلين: جزء عالٍ و جزء منخفض، كما يلخصه الجدول 7.1:

  • 8-بت: اضرب AL في المعامل، وخزّن الناتج في AX.
  • 16-بت: اضرب AX في المعامل، وخزّن الناتج في DX:AX.
  • 32-بت: اضرب EAX في المعامل، وخزّن الناتج في EDX:EAX.

تُجري القائمة 7.1 ضربًا بحجم 32-بت للعددين 7 و 10. الناتج 70 يُخزَّن في EDX:EAX، حيث تُضبط EAX إلى 70 وتُضبط EDX إلى 0.

mov   eax, 10
imul  edx, eax, 7  ; تُضبط edx إلى 70

القائمة 7.2: ضرب عددين صحيحين موقّعين باستخدام IMUL

تُجري تعليمة IMUL ضربًا موقّعًا. ولها ثلاث صيغ:

  • معامل واحد (IMUL <source>): على غرار MUL، تضرب المعامل في EAX/AX/AL وتخزن الناتج عبر سجلين (مثل EDX:EAX).
  • معاملان (IMUL <dest>, <source>): تضرب المعاملين وتخزن الناتج في <dest>.
  • ثلاثة معاملات (IMUL <dest>, <source1>, <source2>): تضرب <source1> في <source2> وتخزن الناتج في <dest>.

لاحظ أنه على عكس صيغة المعامل الواحد، فإن صيغتي المعاملين وثلاثة معاملات تقتطعان النتيجة لتناسب معامل الوجهة. لكن هذا نادرًا ما يكون مصدر قلق عند حساب إزاحات المؤشرات.


3. المصفوفات (Arrays)

المصفوفة (Array) هي مجموعة مرتبة من العناصر تشترك في نوع بيانات واحد. وسواء كان نوع البيانات بدائيًا (مثل عدد صحيح أو مؤشر) أو مركبًا (مثل بنية)، فإن كل عنصر في المصفوفة له حجم موحّد. وهذا يتيح وصولًا سريعًا إلى أي عنصر بضرب الفهرس المطلوب في عرض نوع البيانات.

يمكن أن توجد المصفوفة في أي منطقة ذاكرة: محليًا على المكدس، أو ديناميكيًا في الكومة (Heap)، أو عامًا في مقطع بيانات البرنامج. لتحديد المصفوفات، ابحث عن:

  • تعليمات تصل إلى الذاكرة عبر سجل قاعدة بالإضافة إلى إزاحة محسوبة باستخدام فهرس.
  • حلقات تصل إلى الذاكرة بشكل تسلسلي.

الشكل 7.2: الوصول إلى عناصر في مصفوفة محلية

الشكل 7.2: الوصول إلى عناصر في مصفوفة محلية

يعرض الشكل 7.2 مثالًا على الوصول إلى مصفوفة في التجميع:

  • تبدأ الشيفرة على اليسار بتهيئة مصفوفة محلية من نوع int اسمها nums بثلاثة عناصر.
  • تُهيّئ التجميع على اليمين المصفوفة nums بالقيم 1 و 2 و 3. لاحظ أن تعليمات MOV تستخدم إزاحات ثابتة (مثل [ebp+nums+4]).

تصل الكتل الثلاث التالية من التعليمات (مميزة بالألوان) إلى المصفوفة لتمرير وسائط إلى printf:

  • الكتلة الأولى (الفهرس 2): تحمّل 4 (حجم int) في EAX وتضربها في 2 (الفهرس) باستخدام إزاحة يسارية (SHL) بمقدار 1. النتيجة إزاحة مصفوفة مقدارها 8.
  • الكتلة الثانية (الفهرس 1): حساب مشابه يعطي إزاحة مقدارها 4.
  • الكتلة الأخيرة (الفهرس 0): تستخدم مزيجًا من MOV و IMUL لتوليد إزاحة مقدارها 0 للوصول إلى العنصر الأول (الشيفرة غير المحسَّنة قد تُجري هذا النوع من الحسابات الزائدة).

الشكل 7.3: الوصول إلى عناصر في مصفوفة عامة

الشكل 7.3: الوصول إلى عناصر في مصفوفة عامة

تُعلن الشيفرة في الشكل 7.3 عن nums كمصفوفة عامة بدلًا من مصفوفة محلية. في التجميع، تقع المصفوفة الآن في موقع ثابت في مقطع .data. ولا تزال التجميع تصل إلى المصفوفة بحساب إزاحة قائمة على الفهرس وإضافتها إلى عنوان القاعدة للمصفوفة.


4. الحلقات (Loops)

تُستخدم المصفوفات غالبًا بالاقتران مع الحلقات. تتكون حلقة for القياسية بنمط C، مثل الموجودة في الشكل 7.4، من المكونات التالية:

  • التهيئة (int i = 0): تُنفَّذ مرة واحدة قبل بدء الحلقة؛ وتُهيّئ عادةً عدّاد الحلقة إلى صفر.
  • الشرط (i < len): تعبير منطقي يُقيَّم قبل كل دورة من الحلقة. إذا كان صحيحًا، يُنفَّذ جسم الحلقة؛ وإذا كان خاطئًا، تنتهي الحلقة.
  • الجسم (arr[i]++): جسم الحلقة الذي يُنفَّذ في كل دورة.
  • التحديث (i++): يُنفَّذ في نهاية كل دورة، عادةً بزيادة عدّاد الحلقة.

الشكل 7.4: حلقة for في لغة C ومكوناتها في التجميع

الشكل 7.4: حلقة for في لغة C ومكوناتها في التجميع

يُعرض تجميع الحلقة كمخطط من الكتل الأساسية:

  • التهيئة: تُهيّئ عدّاد الحلقة i إلى صفر في الأعلى.
  • الشرط: يُنفَّذ كتعليمة CMP تقارن i مع len يليها JNB (اقفز إذا لم يكن أقل).
    • غالبًا ما تعكس المترجمات منطق المصدر: بدلًا من فحص ما إذا كان يجب أن تستمر الحلقة (i < len)، تفحص ما إذا كان يجب أن تتوقف (i >= len).
    • تأخذ JNB القفزة إذا لم يكن i أقل من len (أي i >= len). إذا أُخذت القفزة (السهم الأخضر)، يخرج التنفيذ من الحلقة. وإلا، إذا لم تُؤخذ القفزة (السهم الأحمر)، يدخل التنفيذ إلى جسم الحلقة.
  • الجسم: يفهرس في المصفوفة باستخدام i ويزيد ذلك العنصر.
  • التحديث: يزيد i ويقفز عائدًا لأعلى لإعادة تقييم الشرط. السهم المتجه لأعلى من كتلة التحديث إلى كتلة الشرط هو المؤشر البصري الرئيسي على وجود حلقة في مخطط التفكيك.

تمرين اختراق: poindexter

ابدأ بالملف poindexter.i64. يحتوي البرنامج على شيفرة تقوم بما يلي:

  • طباعة رقم وسلسلة نصية على الشاشة أربع مرات في حلقة.

أكمل البرنامج بترقيع الشيفرة عند العنوان 401020h من أجل:

  • تمرير سلسلة عنوان من titles_array وسنة من years_array كوسائط إلى استدعاء printf عند العنوان 401043h.
  • طباعة سنة وسلسلة عنوان مختلفتين في كل دورة من الحلقة.
  • كل فيلم في titles_array صدر في السنة المطابقة لفهرسه في years_array (أي الفيلم الأول صدر عام 1984، والرابع عام 1994). من الناحية المثالية، يجب أن يطبع البرنامج السنوات والعناوين بدقة.

نصيحة: ابدأ كتابة شفرتك عند 401020h — لن تحتاج إلى كل المساحة التي تشغلها تعليمات NOP. لا تحاول ملء المساحة أو التخلص من جميع تعليمات NOP.

الحل (اضغط للعرض)
org 401020h
mov  eax, [esp+0Ch]      ; عدّاد الحلقة الحالي
mov  edx, [eax*4+402xxxh] ; عنوان السنة من years_array
push edx
mov  ecx, titles_array   ; عنوان مصفوفة العناوين
mov  edx, [ecx+eax*4]    ; أو حساب المؤشر المناسب للسلسلة
push edx
call dword ptr [_printf]
add  esp, 8

(الحل الدقيق يعتمد على عناوين المصفوفات في ملفك — راجع الحل الرسمي في ملحق المستند الأصلي.)

5. تعليمات السلاسل النصية: MOVS و STOS و SCAS

توفر مجموعة تعليمات x86 تعليمات سلاسل نصية تنفّذ بكفاءة عمليات شائعة مثل النسخ والتخزين والمسح الضوئي للسلاسل (تسلسلات من البايتات أو الكلمات أو الكلمات المزدوجة) في الذاكرة. تستخدم هذه التعليمات ضمنيًا السجلات التالية:

  • ESI (فهرس المصدر): يشير إلى عنوان المصدر (تستخدمه MOVS).
  • EDI (فهرس الوجهة): يشير إلى عنوان الوجهة (تستخدمه MOVS و STOS و SCAS).
  • EAX: يحمل قيمة البيانات (تستخدمه STOS و SCAS).

الشكل 7.5: تعليمات السلاسل النصية الشائعة في x86

الشكل 7.5: تعليمات السلاسل النصية الشائعة في x86

يحدد لاحق تعليمة السلسلة حجم العملية (b للبايت، w للكلمة، d للكلمة المزدوجة). علاوة على ذلك، تزيد هذه التعليمات تلقائيًا (أو تنقص، إذا كان علم الاتجاه DF مضبوطًا) قيمةَ ESI و/أو EDI بمقدار حجم العملية.

الشكل 7.6: نسخ أربعة بايتات باستخدام MOVSD

الشكل 7.6: نسخ أربعة بايتات باستخدام MOVSD

تنسخ MOVS البيانات من العنوان في ESI إلى العنوان في EDI.

  • يحمّل المثال في الشكل 7.6 عنوان المصدر 0x401020 في ESI وعنوان الوجهة 401030h في EDI.
  • ثم ينفّذ MOVSD. هذا يقرأ أربعة بايتات (ba 5e ba 11) من 0x401020 ويكتبها إلى 0x401030.
  • تزيد تعليمة MOVSD كلًّا من ESI و EDI بمقدار 4، محدِّثةً إيهما إلى 0x401024 و 0x401034 على التوالي.

الشكل 7.7: تصفير بايتين باستخدام STOSW

الشكل 7.7: تصفير بايتين باستخدام STOSW

تخزّن STOS القيمة في EAX/AX/AL في العنوان عند EDI.

  • يحمّل المثال في الشكل 7.7 القيمة 0x401020 في EDI ويصفّر EAX باستخدام XOR.
  • ثم ينفّذ STOSW. هذا يكتب القيمة في AX إلى العنوان 0x401020، مما يصفّر بايتين فعليًا.
  • تزيد تعليمة STOSW قيمة EDI بمقدار 2، محدِّثةً إياها إلى 0x401022.

6. بادئات تكرار التعليمات: REP و REPE/REPZ و REPNE/REPNZ

تُعدّل بادئات التكرار تعليماتِ السلاسل لتُنفَّذ في حلقة. هناك ثلاثة أنواع من بادئات التكرار:

  • REP: يكرر التعليمة طالما ECX > 0.
  • REPE/REPZ (كرر طالما متساوٍ/صفر): يكرر طالما ZF مضبوط و ECX > 0.
  • REPNE/REPNZ (كرر طالما غير متساوٍ/غير صفر): يكرر طالما ZF مصفَّر و ECX > 0.

تستخدم بادئات التكرار ECX ضمنيًا كعدّاد وتنقصه بعد كل دورة.

وبينما هناك تركيبات محتملة عديدة من بادئات التكرار وتعليمات السلاسل، فإن الأزواج الثلاثة التالية هي الأكثر شيوعًا:

  • rep movs: ينسخ البيانات من ESI إلى EDI. يجب تهيئة ECX بعدد العناصر المراد نسخها.
  • rep stos: مع ضبط EAX على صفر، يُستخدم غالبًا لتهيئة مخزن مؤقت (Buffer).
  • repne scasb: مع ضبط AL على صفر، يُستخدم غالبًا لحساب طول سلسلة نصية بإيجاد إزاحة محرف الإنهاء NULL.

الشكل 7.8: نسخ ثمانية بايتات باستخدام REP MOVSD

الشكل 7.8: نسخ ثمانية بايتات باستخدام REP MOVSD

يوضح المثال في الشكل 7.8 نسخ ثمانية بايتات باستخدام rep movs:

  • يُضبط ECX إلى 2 (كلمتان مزدوجتان = 8 بايتات). عنوان المصدر هو 0x401020 وعنوان الوجهة هو 0x401030.
  • تنسخ التعليمة 4 بايتات من 0x401020 إلى 0x401030h، وتنقص ECX، وتكرر.
  • بعد التعليمة، يشير ESI و EDI إلى نهاية الكتل المنسوخة عند 0x401028 و 0x401038 على التوالي.

الشكل 7.9: تصفير ثمانية بايتات باستخدام REP STOSB

الشكل 7.9: تصفير ثمانية بايتات باستخدام REP STOSB

يوضح المثال في الشكل 7.9 تصفير مخزن مؤقت بحجم ثمانية بايتات باستخدام rep stosb:

  • يشير EDI إلى عنوان المخزن المؤقت 0x401020، ويُصفَّر EAX، ويُضبط ECX إلى 8 (حجم المخزن المؤقت).
  • تتكرر التعليمة ثماني مرات. في كل دورة، تكتب 0 في EDI، وتزيد EDI بمقدار 1، وتنقص ECX.

7. الدوال الجوهرية (Intrinsic Functions)

في Visual C/C++، الدوال الجوهرية هي دوال خاصة توفرها المترجم وتقابل مباشرةً تعليمات آلة محددة. عادةً، تُصرَّف وظائف المكتبة القياسية (مثل memcpy) كاستدعاء لمكتبة خارجية. ومع ذلك، عند تمكين التحسين، يدمج المترجم (Inline) الدوال التي لها مقابلات جوهرية، مستبدلًا استدعاءات الدوال بتسلسلات تعليمات جوهرية.

دالة Cالتجميع الجوهريالملخص
memcpy()rep movs*نسخ البيانات من موقع إلى آخر
strcpy()repne movsbمثل memcpy لكن يتوقف عند بايت NULL
memset()rep* stos*ضبط جميع البايتات في منطقة ذاكرة إلى قيمة ما (غالبًا 0)
strlen()repne scasbتحديد طول سلسلة نصية بالمسح الضوئي بحثًا عن بايت NULL

الجدول 7.2: وظائف C الشائعة وقت التشغيل ومقابلاتها الجوهرية

يسرد الجدول 7.2 وظائف المكتبة القياسية الشائعة ومقابلاتها الجوهرية المقابلة. ولأن هذه الوظائف تُستخدم بكثرة، من الضروري التعود على تسلسلات التعليمات التي تؤدي الوظيفة نفسها.

تمرين اختراق: stringcheese

ابدأ بالملف stringcheese.i64. يحتوي البرنامج على شيفرة تقوم بما يلي:

  • مطالبة المستخدم بإدخال سلسلة نصية وقراءتها إلى مخزن مؤقت على المكدس.
  • تمرير مؤشر إلى سلسلة المستخدم إلى الدالة sub_401039.
  • توفر الدالة sub_401039 مقدمة وخاتمة للدالة وتحمّل مؤشر سلسلة المستخدم في سجل ESI.
  • تنتهي sub_401039 باستدعاء printf() لعرض محتويات المتغير العام string_length (العنوان 402034h) ومحتويات المخزن المؤقت العام copied_string_buffer (العنوان 402038h).

أكمل البرنامج بترقيع الشيفرة عند العنوان 401046h لضمان طباعة تعليمة printf() عند 40108Ah لسلسلة المستخدم وطولها:

  • اكتب strcpy() (عبر repne movsb) لنقل بيانات السلسلة إلى المخزن المؤقت العام copied_string_buffer (العنوان 402038h).
  • اكتب strlen() (عبر repne scasb) لحساب طول سلسلة المستخدم وتخزينه في المتغير العام string_length (العنوان 402034h).
الحل (اضغط للعرض)
; ESI يحمل مؤشر سلسلة المستخدم
mov  edi, 402038h        ; copied_string_buffer
mov  ecx, 0FFFFFFFFh     ; عدّاد كبير
xor  al, al
cld
repne movsb              ; strcpy: انسخ حتى NULL
not  ecx
dec  ecx                 ; ecx = طول السلسلة
mov  [402034h], ecx      ; string_length = الطول

8. البنيات (Structs)

البنية (Struct) — اختصار لـ Structure — هي نوع بيانات معرَّف من المستخدم يجمع البيانات ذات الصلة تحت اسم واحد. وتُسمى العناصر الفردية داخل البنية أعضاءً (Members) أو حقولًا (Fields).

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

يستكشف بقية هذا القسم البنيات وإعادة بنائها في IDA باستخدام العينة التالية:

  • اسم الملف: StructExample.exe
  • MD5: 7d27410b6f912652c1aa787e5b38ecdf
CAssembly
struct msg {
  char xor;
  unsigned int len;
  char buf[128];
};


int main(int argc, char** argv) {
  struct msg m;
  memset(&m, 0, sizeof(struct msg));
  scanf_s("%128s", m.buf);
  m.xor = 0x10;
  m.len = strlen(m.buf);
  sendMsg(&m);
}
main:
push    88h             ; Size
push    0               ; Val
lea     eax, [ebp+var_88]
push    eax             ; void *
call    memset
add     esp, 0Ch
lea     ecx, [ebp+var_80]
push    ecx
push    offset _Format  ; "%128s"
call    scanf
add     esp, 8
mov     [ebp+var_88], 10h
lea     edx, [ebp+var_80]
push    edx
call    strlen
add     esp, 4
mov     [ebp+var_84], eax
lea     eax, [ebp+var_88]
push    eax
call    sendMsg

القائمة 7.3: برنامج بسيط في C يستخدم بنية اسمها msg

افتح StructExample.exe وانتقل إلى main (‏0x401080). يكون فقدان معلومات البنية واضحًا في القائمة 7.3:

  • تُعرِّف الشيفرة على اليسار بنية msg بثلاثة أعضاء:
    • xor: مفتاح XOR أحادي البايت.
    • len: طول الرسالة.
    • buf: مخزن الرسالة المؤقت.
  • تستخدم الدالة main البنية msg:
    • تُعلن عن متغير محلي m من نوع struct msg وتُهيئه بـ memset.
    • تقرأ سلسلة نصية بطول يصل إلى 128 في m.buf باستخدام scanf_s.
    • تضبط m.xor إلى 0x10 و m.len إلى طول السلسلة المقروءة من scanf_s.
    • تمرر إلى sendMsg مؤشرًا إلى m.
  • لا يحتوي التجميع على اليمين على أي إشارات صريحة إلى بنية. للوهلة الأولى، البنية المخصصة على المكدس لا يمكن تمييزها عن ثلاثة متغيرات محلية مستقلة.

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

  • حجم التخصيص: تُهيئ memset المتغير var_88 بحجم 0x88. وبينما قد يكون هذا مخزنًا مؤقتًا واحدًا كبيرًا، فإنه يؤكد أن هذا النطاق من الذاكرة ينتمي معًا.
  • أنماط الوصول: تصل main إلى var_80 و var_84. هذه الإزاحات تقع ضمن نطاق الذاكرة بدءًا من var_88. التعليمات التي تصل إلى “متغيرات” عند إزاحات ثابتة داخل مساحة متغير آخر هي مؤشرات قوية على استخدام بنية.
CAssembly
struct msg {
  char xor;
  unsigned int len;
  char buf[128];
};


int sendMsg(struct msg* m) {
  xorString(m->xor, m->buf, m->len);
  ...
}
sendMsg:
mov     eax, [ebp+arg_0]
mov     ecx, [eax+4]
push    ecx
mov     edx, [ebp+arg_0]
add     edx, 8
push    edx
mov     eax, [ebp+arg_0]
movzx   ecx, byte ptr [eax]
push    ecx
call    xorString

القائمة 7.4: الدالة sendMsg تتلقى مؤشراً إلى بنية msg وتصل إلى أعضائها

تمرر الدالة main إلى sendMsg (‏0x401120) مؤشرًا إلى var_88. تجمع sendMsg، كما هو موضح في القائمة 7.4، مؤشر الوسيط (arg_0) مع الإزاحات التالية للوصول إلى أعضاء msg:

  • الإزاحة 4: تصل mov ecx, [eax+4] إلى الحقل len.
  • الإزاحة 8: تحسب add edx, 8 عنوان الحقل buf. لاحظ أن هذا العنوان يقابل عنوان var_80 في main. ويُمرر هذا العنوان إلى scanf مع سلسلة التنسيق %128s، مما يتيح لنا استنتاج أن حجم المخزن المؤقت 128 بايت.
  • الإزاحة 0: تصل movzx ecx, byte ptr [eax] إلى الحقل xor.

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

  • لها حقل بحجم 1 عند الإزاحة 0، وحقل بحجم 4 عند الإزاحة 4، وحقل بحجم 80h عند الإزاحة 8.
  • الحجم الكلي للبنية هو 88h (مستنتجًا من استدعاء memset).

لإعادة بناء تعريف البنية في IDA، افتح التبويب الفرعي Local Types‏ (View ➔ Open subviews ➔ Local types أو الاختصار Shift+F1).

الشكل 7.10: إنشاء نوع جديد في التبويب الفرعي Local Types

الشكل 7.10: إنشاء نوع جديد في التبويب الفرعي Local Types

أنشئ بنية جديدة بالنقر بزر الفأرة الأيمن واختيار Add type….

الشكل 7.11: نافذة حوار إضافة نوع جديد

الشكل 7.11: نافذة حوار إضافة نوع جديد

أدخل اسم البنية (msg). وإذا لم يكن لديك في البداية اسم وصفي مناسب، يمكنك استخدام خصائص أخرى مثل حجم البنية (مثل struct_0x88) وتحديث الاسم لاحقًا.

إذا كنت تعرف حجم البنية الدقيق وقت الإنشاء (كما نعرفه هنا)، فعّل خيار Fixed layout وأدخل Structure size. وإلا، ألغِ تفعيل Fixed layout لتعديل حجم البنية بشكل تكراري وإضافة عناصر جديدة كلما جمعت معلومات عنها.

الشكل 7.12: قائمة النقر الأيمن على نوع تعرض خيارات إضافة أعضاء بيانات جديدة

الشكل 7.12: قائمة النقر الأيمن على نوع تعرض خيارات إضافة أعضاء بيانات جديدة

لإضافة عضو بيانات جديد، انقر بزر الفأرة الأيمن على الإزاحة المطلوبة واختر Data (الاختصار D). للتنقل بين أحجام البيانات (بايت، كلمة، كلمة مزدوجة)، انقر بزر الفأرة الأيمن على عضو واختر Data (‏D) مرة أخرى. املأ بنية msg بتعريف:

  • حقل أحادي البايت عند الإزاحة 0.
  • حقل رباعي البايتات عند الإزاحة 4.
  • حقل أحادي البايت عند الإزاحة 8.

الشكل 7.13: نافذة حوار حقل المصفوفة في البنية

الشكل 7.13: نافذة حوار حقل المصفوفة في البنية

بعد ذلك، عرّف الحقل عند الإزاحة 8 كمصفوفة بالنقر بزر الفأرة الأيمن عليه واختيار Array…. تملأ نافذة حوار المصفوفة تلقائيًا حجم المصفوفة بناءً على الفجوة بين الحقل المحدد والحقل التالي (أو نهاية البنية). يجب أن تشبه البنية الآن ما يلي:

00000000 struct __fixed msg // sizeof=0x88
00000000 {
00000000     char field_0;
00000001     // padding byte
00000002     // padding byte
00000003     // padding byte
00000004     int field_4;
00000008     char field_8[128];
00000088 };

يمكننا الآن تطبيق بنية msg لتحويل الإزاحات الرقمية إلى أسماء حقول باستخدام إحدى الطريقتين:

  • انقر بزر الفأرة الأيمن على إزاحة واختر من القائمة المنسدلة Structure.
  • حدد إزاحة واضغط T لإظهار نافذة حوار البنية.

الشكل 7.14: تطبيق تعريف بنية على إزاحة

الشكل 7.14: تطبيق تعريف بنية على إزاحة

يؤدي وسم جميع الإزاحات الرقمية في sendMsg إلى إنتاج التجميع الموضح في القائمة 7.5.

CAssembly
struct msg {
  char xor;
  unsigned int len;
  char buf[128];
};


int sendMsg(struct msg* m) {
  xorString(m->xor, m->buf, m->len);
  ...
}
mov     eax, [ebp+msg]
mov     ecx, [eax+msg.field_4]
push    ecx
mov     edx, [ebp+msg]
add     edx, msg.field_8
push    edx
mov     eax, [ebp+msg]
movzx   ecx, byte ptr [eax+msg.field_0]
push    ecx
call    xorString

القائمة 7.5: الدالة sendMsg بعد تطبيق msg على جميع الإزاحات

يمكننا تطبيق تعريف البنية نفسه في main بتعريف بنية msg على المكدس بدءًا من var_88. للقيام بذلك، انتقل إلى main وانقر نقرًا مزدوجًا على متغير المكدس للانتقال إلى عرض مكدس الدالة. ثم أعلن عن المتغير المحلي var_88 كمتغير بنية:

  • ضع المؤشر على var_88.
  • انتقل إلى Edit ➔ Struct var….
  • اختر بنية msg.

الشكل 7.15: تعريف بنية msg على المكدس

الشكل 7.15: تعريف بنية msg على المكدس

الطريقة البديلة:

  • انقر بزر الفأرة الأيمن على var_88 واختر Set field type… (الاختصار Y).
  • أدخل اسم البنية msg (يمكن حذف اسم المتغير).

الشكل 7.16: تحديث نوع متغير على المكدس إلى msg

الشكل 7.16: تحديث نوع متغير على المكدس إلى msg

تتحول عمليات الوصول إلى المتغير المحلي إلى عمليات وصول لأعضاء البنية، كما هو موضح في القائمة 7.6.

CAssembly
struct msg {
  char xor;
  unsigned int len;
  char buf[128];
};


int main(int argc, char** argv) {
  struct msg m;
  memset(&m, 0, sizeof(struct msg));
  scanf_s("%128s", m.buf);
  m.xor = 0x10;
  m.len = strlen(m.buf);
  sendMsg(&m);
}
push    88h             ; Size
push    0               ; Val
lea     eax, [ebp+msg]
push    eax             ; void *
call    memset
add     esp, 0Ch
lea     ecx, [ebp+msg.field_8]
push    ecx
push    offset _Format  ; "%128s"
call    scanf
add     esp, 8
mov     [ebp+msg.field_0], 10h
lea     edx, [ebp+msg.field_8]
push    edx
call    strlen
add     esp, 4
mov     [ebp+msg.field_4], eax
lea     eax, [ebp+msg]
push    eax
call    sendMsg

القائمة 7.6: الدالة main بعد تعريف بنية msg على المكدس

بمجرد أن يتحسن فهمنا لغرض كل حقل، يمكننا إعادة تسميته. ويمكن ذلك مباشرةً من قائمة التفكيك (أينما أُشير إلى الحقول) أو من التبويب الفرعي Local Types.

CAssembly
struct msg {
  char xor;
  unsigned int len;
  char buf[128];
};


int sendMsg(struct msg* m) {
  xorString(m->xor, m->buf, m->len);
  ...
}
mov     eax, [ebp+msg]
mov     ecx, [eax+msg.len]
push    ecx
mov     edx, [ebp+msg]
add     edx, msg.buf
push    edx
mov     eax, [ebp+msg]
movzx   ecx, byte ptr [eax+msg.xor]
push    ecx
call    xorString

القائمة 7.7: الدالة sendMsg بعد تحديث جميع أسماء الحقول في msg

تنتشر أسماء الحقول المحدثة إلى جميع مراجع البنية الموجودة.

CAssembly
struct msg {
  char xor;
  unsigned int len;
  char buf[128];
};


int main(int argc, char** argv) {
  struct msg m;
  memset(&m, 0, sizeof(struct msg));
  scanf_s("%128s", m.buf);
  m.xor = 0x10;
  m.len = strlen(m.buf);
  sendMsg(&m);
}
push    88h             ; Size
push    0               ; Val
lea     eax, [ebp+msg]
push    eax             ; void *
call    memset
add     esp, 0Ch
lea     ecx, [ebp+msg.buf]
push    ecx
push    offset _Format  ; "%128s"
call    scanf
add     esp, 8
mov     [ebp+msg.xor], 10h
lea     edx, [ebp+msg.buf]
push    edx
call    strlen
add     esp, 4
mov     [ebp+msg.len], eax
lea     eax, [ebp+msg]
push    eax
call    sendMsg

القائمة 7.8: الدالة main بعد تحديث جميع أسماء الحقول في msg

لاحظ أن تعريف msg في الشيفرة المصدرية يوضح أن الحقل الأول من نوع char، وهو نوع بيانات أحادي البايت. قد يقودك هذا إلى الاعتقاد بأن الحقل التالي len يجب أن يقع عند الإزاحة 1. لكن التجميع يوضح أن len يقع عند الإزاحة 4 بدلًا من ذلك. ويعود ذلك إلى قواعد حشو البنيات (Structure Padding). افتراضيًا، يفضل مترجم C محاذاة أنواع الأعضاء عند مضاعفات أحجامها لتحسين الوصول إلى الذاكرة. فعلى سبيل المثال، int، وهو نوع بيانات رباعي البايتات، سيوضع عند عناوين تمثل مضاعفات 4. يجب أن تكون مدركًا لقواعد الحشو الافتراضية عند إعادة بناء البنيات، ولكن يمكن أيضًا تحديد الحشو يدويًا باستخدام توجيهات المترجم (مثل #pragma pack).

تمرين اختراق: structs_make_bucks

ابدأ بالملف structs_make_bucks.i64. يحتوي البرنامج على شيفرة تقوم بما يلي:

  • مطالبة المستخدم بإدخال عدد البرغر والبطاطس المقلية والميلك شيك المطلوب طلبها.
  • تخزين العناصر المدخلة في بنية Order على المكدس.
  • تمرير مؤشر إلى بنية Order إلى الدالة sub_4010B2.
  • تنتهي الدالة sub_4010B2 باستدعاء الدالة sub_401076، التي تأخذ كوسائط عدد البرغر والبطاطس والميلك شيك المطلوبة.
  • تحسب الدالة sub_401076 السعر الإجمالي (برغر: 7$؛ بطاطس: 4$؛ ميلك شيك: 3$) وتطبعه.
struct Order {
+0 int burgers;
+4 int fries;
+8 int shakes;
}

أكمل البرنامج بترقيع الدالة sub_4010B2 من أجل:

  • استخدام مؤشر بنية Order الممرر كوسيط إلى sub_4010B2 لاستدعاء الدالة sub_401076 بعدد البرغر والبطاطس والميلك شيك الصحيح.

عرّف بنية Order في IDA وطَبِّقها على بنية المكدس في الدالة sub_401000.

الحل (اضغط للعرض)
sub_4010B2:
mov  eax, [ebp+arg_0]
mov  ecx, [eax]        ; burgers
push ecx
mov  edx, [eax+4]      ; fries
push edx
mov  ecx, [eax+8]      ; shakes
push ecx
call sub_401076
add  esp, 0Ch

(ثم عرّف بنية Order في Local Types وطَبِّقها على المكدس في sub_401000.)

9. عبارات Switch وجداول القفز (Switch Statements and Jump Tables)

عبارة switch هي بنية تحكم في التدفق تختار كتلة شيفرة واحدة للتنفيذ من بين حالات متعددة بناءً على قيمة تعبير ما. وفي لغة C، تكون الصيغة كما يلي:

switch(expression) {
  case value1: statement_1; break;
  case value2: statement_2; break;
  ...
  default: default_statement; break;
}

عبارة switch:

  • تُقيّم التعبير داخل عبارة switch.
  • تطابق قيمة التعبير مع إحدى القيم المحددة في تسميات الحالات (case labels).
  • تنفّذ كتلة الشيفرة المقابلة للحالة المطابقة؛ وإلا، إذا لم يوجد تطابق، تنفّذ الحالة الافتراضية (default).

قد تختار المترجمات استراتيجيات مختلفة لتنفيذ عبارة switch حسب الحالات:

  • مع عدد صغير من الحالات، تُولّد المترجمات سلسلة من أزواج مقارنة-قفزة مشروطة، مطابقةً لسلسلة من عبارات if-else.
  • مع حالات متسلسلة كثيرة، تستخدم المترجمات غالبًا جدول قفز (Jump Table). يستبدل هذا التحسين المقارنات بعملية فهرسة في جدول من العناوين لكل كتلة حالة.
switch (fruit) {
  case 'a':
    printf("apple"); break;
  case 'b':
    printf("banana"); break;
  case 'c':
    printf("cantalope"); break;
  default:
    printf("no fruit"); break;
}

القائمة 7.9: عبارة switch بثلاث حالات وحالة افتراضية

الشكل 7.17: التنفيذ الافتراضي لعبارة switch هو عادةً سلسلة من المقارنات

الشكل 7.17: التنفيذ الافتراضي لعبارة switch هو عادةً سلسلة من المقارنات

عبارة switch في القائمة 7.9 لها ثلاث حالات وحالة افتراضية. كما هو موضح في الشكل 7.17، يُصرَّف هذا إلى سلسلة من المقارنات يقيّمها البرنامج واحدة تلو الأخرى.

switch (fruit) {
  case 'a':
    printf("apple"); break;
  case 'b':
    printf("banana"); break;
  case 'c':
    printf("cantalope"); break;
  case 'd':
    printf("dates"); break;
  default:
    printf("no fruit"); break;
}

القائمة 7.10: عبارة switch بأربع حالات وحالة افتراضية. تستخدم المترجمات الاستدلال لتحديد الطريقة الأكثر كفاءة لتنفيذ عبارة switch ذات الحالات الكثيرة.

الشكل 7.18: عبارة switch منفَّذة كجدول قفز

الشكل 7.18: عبارة switch منفَّذة كجدول قفز

تضيف القائمة 7.10 حالة إلى عبارة switch في القائمة 7.9. هذا يدفع المترجم لاستخدام جدول قفز لتنفيذ عبارة switch، كما هو موضح في الشكل 7.18. يقوم البرنامج بما يلي:

  • يحسب فهرس الحالة بطرح 'a' (أصغر قيمة حالة) من fruit.
  • يحدد ما إذا كان الفهرس المحسوب يتجاوز أكبر فهرس حالة (3). إذا حدث ذلك، يقفز إلى الحالة الافتراضية.
  • يفهرس في جدول القفز للعناوين الموجود عند 0x40113a. تكشف IDA جدول القفز وتضع عليه تسمية بالبادئة jpt_. المدخل الأول في جدول القفز هو عنوان كتلة الشيفرة للحالة a، والمدخل الثاني هو عنوان الحالة b، وهكذا.

10. معمل — guesser.exe (Lab - guesser.exe)

معلومات العينة

  • اسم الملف: guesser.exe
  • MD5: d60f46728627e44ab8742e17e7cd4562

أهداف المعمل

هذا معمل متوسط المستوى للتدرب على تقنيات التنقيح وتحليل بنى التجميع التالية:

  • المصفوفات
  • الحلقات
  • تعليمات السلاسل النصية وبادئات تكرار التعليمات

ابدأ بالتفاعل مع guesser.exe من سطر الأوامر. لهذا المعمل هدفان:

  1. تحليل guesser.exe لمعرفة مفتاح التسجيل الصحيح الذي يتيح لك لعب لعبة تخمين الأرقام المخفية خلف نافذة التسجيل.
  2. استخدام تقنيات التنقيح لإيجاد طرق للعب لعبة تخمين الأرقام دون مفتاح التسجيل الصحيح.

قبل بدء التحليل، أعد تسمية الدوال التالية:

  • sub_4014E8 إلى _printf
  • sub_4013CD إلى _srand

الأسئلة

  1. ما القيمة التي يجب أن ترجعها استدعاء الدالة sub_401000 عند 0x4010F9 حتى يقبل البرنامج مفتاح التسجيل؟
  2. ما وسيط الدالة الممرر إلى sub_401000 عند 0x4010F9؟
  3. حلّل الدالة sub_401000. ما العملية التي تطبقها على وسيط الدالة؟
  4. استعد مفتاح التسجيل.
  5. صِف طريقة باستخدام تعديل السجلات لتجاوز نافذة التسجيل دون مفتاح التسجيل.
  6. صِف طريقة باستخدام ترقيع القفزات لتجاوز نافذة التسجيل دون مفتاح التسجيل.

ملاحظة: الحل التفصيلي لهذا المعمل في درس حل معمل guesser.


خلاصة المقال

المفهومالنقاط الرئيسية
المؤشراتمتغير يخزن عنوان ذاكرة؛ LEA لحساب العنوان، MOV لقراءة القيمة
MULضرب غير موقّع؛ معامل ضمني AL/AX/EAX؛ الناتج في EDX:EAX
IMULضرب موقّع؛ ثلاث صيغ (معامل/معاملان/ثلاثة معاملات)
المصفوفاتسجل قاعدة + فهرس × حجم العنصر؛ ابحث عن حلقات وصول تسلسلي
الحلقاتتهيئة ← شرط (CMP + قفزة معكوسة) ← جسم ← تحديث؛ السهم الصاعد = حلقة
MOVS/STOS/SCASنسخ/تخزين/مسح ضوئي؛ تستخدم ESI وEDI وEAX ضمنيًا
بادئات التكرارREP (ECX)، REPE/REPZ (ECX+ZF)، REPNE/REPNZ (ECX+¬ZF)
الدوال الجوهريةmemcpy=rep movs، memset=rep stos، strlen=repne scasb
البنياتمعلوماتها تُفقد عند التصريف؛ أعد بناءها من أنماط الوصول (قاعدة+إزاحات ثابتة)
حشو البنياتالأعضاء تُحاذى عند مضاعفات أحجامها؛ char ثم int يعني فجوة 3 بايتات
switchحالات قليلة = سلسلة مقارنات؛ حالات كثيرة = جدول قفز (بادئة jpt_ في IDA)

المصدر الأصلي: Malware Analysis Crash Course — FLARE Learning Hub