P01NT
0x08 الذاكرة 09/19

الدوال

الدوال بأنواعها ودوال الذاكرة والإدخال المهمة للمحلل، وبنية Prologue وEpilogue واتفاقيات الاستدعاء في الـ assembly.

· 5 دقائق قراءة · 981 كلمة
المحتويات

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

في هذا المقال سنتعرف على الدوال وأنواعها وأشكالها، وأهم الدوال التي تفيدنا في الهندسة العكسية.

الدوال تلعب دوراً محورياً في البرمجة، وخصوصاً في الهندسة العكسية — لأنها وحدة التحليل الأساسية: عندما نفتح أي برنامج في IDA أو Ghidra، فإن أول ما نراه هو قائمة الدوال، ومن تحليلها نفهم كيف يعمل البرنامج.

ما هي الدوال؟

وحدات أساسية لإعادة استخدام الكود وتنظيمه — “كتل” من التعليمات مصممة لتنفيذ مهام معينة. كل دالة يمكن أن تقبل معاملات (Arguments) وتُرجع قيمة (Return Value).

return_type function_name(parameter_list) {
    // body: الكود الذي ينفذ المهمة
}

مثال بسيط

int add(int a, int b) {
    return a + b;
}

int main() {
    int result = add(5, 3);
    printf("The sum is: %d\n", result);
    return 0;
}

ملاحظة هامة: الدالة لا تُنفذ بنفسها — يجب استدعاؤها من دالة أخرى. main هي نقطة البداية التي يبدأ منها تنفيذ البرنامج، ومنها تنتشر الاستدعاءات لبقية الدوال.

أشكال الدوال

1. بدون معاملات وبدون إرجاع

void hello() {
    printf("Hello, World!\n");
}

int main() {
    hello();
    return 0;
}

2. مع معاملات وبدون إرجاع

void displayNumber(int number) {
    printf("The number is: %d\n", number);
}

int main() {
    displayNumber(10);
    return 0;
}

قاعدة سريعة: الدالة التي لا تُرجع قيمة نُعرّفها بـ void. لكن انتبه — void تعني “لا قيمة”، بينما الدالة قد تعدّل بيانات خارجية عبر المؤشرات (سنشرحها في المقال القادم).

3. بدون معاملات مع إرجاع

int getNumber() {
    return 42;
}

int main() {
    int number = getNumber();
    printf("The number is: %d\n", number);
    return 0;
}

4. مع معاملات ومع إرجاع

#include <stdio.h>

int add(int a, int b) {
    int sum = a + b;
    return sum;
}

int main() {
    int num1 = 10, num2 = 20;
    int result = add(num1, num2);
    printf("The sum of %d and %d is %d\n", num1, num2, result);
    return 0;
}

أنواع الدوال

1. الدوال المكتبية (Library Functions)

توفرها مكتبات لغة C مثل printf و scanf:

#include <stdio.h>

int main() {
    printf("Hello, World!\n");
    return 0;
}

2. الدوال المخصصة (User-Defined Functions)

يكتبها المبرمج لتنفيذ عمليات محددة:

int add(int a, int b) {
    return a + b;
}

3. الدوال الداخلية (Inline Functions)

تُعرّف بـ inline لتحسين الأداء عندما تُستدعى بشكل متكرر:

inline int square(int x) {
    return x * x;
}

int main() {
    int result = square(5);
    printf("The square of 5 is %d\n", result);
    return 0;
}

نقطة مهمة لمجال RE: inline ليست ضماناً — هي طلب من المترجم قد يتجاهله. ومع التحسين (-O2) قد يُدمج المترجم الدالة داخلياً حتى دون inline. النتيجة في الـ disassembly: الدوال المدمجة تختفي — لا ترى لها call ولا إطار مكدس، بل كودها منسوخ داخل الدالة المستدعية. هذا يفسر لماذا قد لا تطابق قائمة الدوال في IDA ما كتبته في المصدر.

أهم الدوال في الهندسة العكسية

1. دوال الذاكرة

تتحكم في تخصيص وإدارة الذاكرة (malloc, free, realloc, calloc) — تُستخدم بكثرة وتكشف عن نقاط ضعف:

الدالةالوظيفة
mallocتخصيص ذاكرة في الـ heap
callocتخصيص + تصفير
reallocإعادة تخصيص بحجم جديد
freeتحرير الذاكرة

سنتوسع فيها عند شرح إدارة الذاكرة (Memory Management) في مقالات لاحقة.

2. دوال الإدخال والإخراج

تساعد على فهم كيفية قراءة البيانات وكتابتها (printf, scanf, fread, fwrite, gets…) — تحليلها يكشف نقاط ضعف الإدخال.

ملاحظة: gets تعتبر من أخطر الدوال إطلاقاً لأنها تقرأ بلا أي تحديد لحجم الإدخال — أزلت من المعيار الحديث.

3. دوال السلاسل النصية

أهداف شائعة لثغرات Buffer Overflow (strcpy, strcat, sprintf…) — شرحناها بالتفصيل في مقال المصفوفات والسلاسل.

ما هي Buffer Overflow؟

هي الثغرة التي تحدث عندما تتجاوز كمية البيانات المدخلة سعة المخزن المؤقت (Buffer)، فتذهب البيانات الزائدة إلى الذاكرة المجاورة وتُفسد ما فيها.

تشبيه: عندك قنينة عصير تسع لتراً واحداً فقط (هي الـ Buffer). إذا وضعت فيها أكثر من لتر، سينكب العصير على الكنزة (الذاكرة المجاورة)، فيُفسدها. العصير هو البيانات، والكنزة هي ما يلي الـ buffer في الذاكرة.

كيف تظهر الدوال في الـ Assembly؟ (الأهم)

هذا القسم هو قلب المقال — لأن الدوال هي الجسر المباشر من لغة C إلى الـ disassembly.

أ. بنية الدالة: Prologue → Body → Epilogue

كل دالة تتحول إلى ثلاث كتل في الـ assembly:

; PROLOGUE — بناء إطار المكدس
push ebp              ; حفظ الإطار السابق
mov  ebp, esp         ; ebp = قمة المكدس (مرجع ثابت)
sub  esp, 16          ; حجز مساحة للمتغيرات المحلية

; BODY — الكود الفعلي
mov  eax, [ebp+8]     ; قراءة المعامل الأول
add  eax, [ebp+12]    ; إضافة المعامل الثاني
mov  [ebp-4], eax     ; تخزين في متغير محلي

; EPILOGUE — تفكيك الإطار والعودة
mov  esp, ebp         ; استعادة المكدس
pop  ebp              ; استعادة الإطار السابق
ret                   ; العودة للمتصل

هذا الشكل ستراه في كل دالة عند تحليل أي binary — عوّد عينك عليه.

ب. Calling Conventions — الفرق بين من يفكك ومن يفهم

عند استدعاء add(5, 3)، كيف تُمرَّر القيم ومن يرتب المكدس؟ هذا تحدده الاتفاقيات:

الاتفاقيةتمرير المعاملاتتنظيف المكدسمستخدمة في
cdeclعلى الـ stackالمتصل (caller)C على ويندوز x86
stdcallعلى الـ stackالدالة (callee)Win32 API
fastcallأول معاملين في ECX/EDX ثم stackالدالةويندوز x86
x64RCX/RDX/R8/R9 ثم stackالدالةويندوز/Linux 64

لماذا يهمك؟ عند التحليل سترى نمطين:

  • بعد استدعاء cdecl يظهر add esp, X — المتصل ينظف المعاملات.
  • بعد استدعاء stdcall لا يظهر ذلك — الدالة تنظفه بنفسها.

فهم الـ calling conventions هو ما يجعل الـ disassembly readable بدل كونه سلسلة أوامر عشوائية.

ج. خريطة الـ Stack Frame

عند استدعاء دالة، تتوزع عناصر الإطار على المكدس وفق خريطة ثابتة: المتغيرات المحلية عند إزاحات سالبة عن EBP، وعنوان الرجوع عند [ebp+4]، والمعاملات عند [ebp+8] وما بعده.

سنرسم هذه الخريطة كاملة بتتبع ESP تعليمة بتعليمة في مقال الذاكرة الظاهرية (Stack vs Heap) — وهي المرجع نفسه الذي ستعود إليه في كل مقالات الاستغلال لاحقاً، فلا تقلق إن لم يتضح كل شيء الآن.

تمرين عملي

  1. اكتب دالة add ثم gcc -S -O0 — افحص الـ prologue/epilogue ولاحظ [ebp+8] و [ebp+12] للمعاملين، وأن قيمة الإرجاع توضع في eax.
  2. جرب -O2 — ولاحظ كيف تختفي بعض الدوال الصغيرة (inlining).
  3. استدعِ دالة Win32 (مثل MessageBoxA) ولاحظ أن المكدس لا يُنظف بعد الـ call (stdcall) — قارنها بدالة C عادية (cdecl).