بسم الله الرحمن الرحيم.
في هذا المقال سنتعرف على الدوال وأنواعها وأشكالها، وأهم الدوال التي تفيدنا في الهندسة العكسية.
الدوال تلعب دوراً محورياً في البرمجة، وخصوصاً في الهندسة العكسية — لأنها وحدة التحليل الأساسية: عندما نفتح أي برنامج في 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 |
x64 | RCX/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) — وهي المرجع نفسه الذي ستعود إليه في كل مقالات الاستغلال لاحقاً، فلا تقلق إن لم يتضح كل شيء الآن.
تمرين عملي
- اكتب دالة
addثمgcc -S -O0— افحص الـ prologue/epilogue ولاحظ[ebp+8]و[ebp+12]للمعاملين، وأن قيمة الإرجاع توضع فيeax. - جرب
-O2— ولاحظ كيف تختفي بعض الدوال الصغيرة (inlining). - استدعِ دالة Win32 (مثل
MessageBoxA) ولاحظ أن المكدس لا يُنظف بعد الـ call (stdcall) — قارنها بدالة C عادية (cdecl).