



السمعة:
بسم الله
بعد ما حكينا عن الـHeap والـStack وفهمنا هيكلة البناء الداخلي لكل منهما وآلية عملهما اليوم سنتطرق لشرح موضوع تخصيص الذاكرة الديناميكية بطريقة موسعة وسنتكلم عن امور ادارة الذواكر الحرة ونحكي عن اشهر الثغرات المتكررة في هاد الموضوع.
Dynamic memory allocation
يعد فهم تخصيص الذاكرة الديناميكية شيء مهم جداً لتحليل البرمجيات واستغلال الثغرات وفهم آلية عمل البرنامج من الداخل...
بداية يتم تخصيص الذاكرة الديناميكية يدوياً عبر دوال اساسية ما هي هذه الدوال؟
1.malloc() and free():
تخصيص وتحرير الذاكرة الديناميكية.
تخصص كتلة ذاكرة في الهيب بحجم محدد (بالبايت) ولكن دون تهيئة.
تحرر الذاكرة المخصصة سابقاً
البنية الداخلية للكتلة إلي حكينا عنها بالمقال السابق (heap chunk) هي بتكون في منظم الذاكرة (متل glibc):
خلينا نفصلن:
size_t size:
بيحتفظ بحجم الكتلة الحالية مع الهيدر
البتات التلاتة إلي على اليمين (LSb) تستخدم للـ Flags
وهي صورة موضحة لمفهوم الـLSb:
الـFlags حكينا عنن بالمقال السابق
• PREV_INUSE.
• IS_MAPPED.
• NON_MAIN_ARENA.
-Prev_size:
يستخدم فقط اذا كانت الكتلة السابقة حرة (يعني PREV_INUSE = 0 بالكتلة الحالية)
ويتيح الدمج (Coalescing) عند التحرير لتقليل التجزئة (fragmentation).
عملية التخصيص (malloc):
لآلية النظرية:
1.تطلب من نظام إدارة الذاكرة كتلة بحجم (size) بايت
2.بيروح بيبحث بالـBins (قوائم الكتل الحرة رح نحكي عليها).
3.إذا ما انوجدت كتلة مناسبة بالـBins: منطلب ذاكرة من النظام عبر sbrk() او mmap().
4.إذا كانت الكتلة أكبر من المساحة المطلوب تخصيصها بتتقسم هي الكتلة وبيتم إرجاع جزء للمستخدم.
الهيكل الداخلي: يضيف النظام معلومات إدارية (metadata) قبل العنوان المرجع هي متل عملية الsaved ebp بالستاك.
مثال:
وهيك بتكون الذاكرة:
عملية التحرير (free):
1.بيتحقق من صحة المؤشر (هل بيشير إلى بداية كتلة؟)
2.إذا كانت الكتلة المجاورة حرة بيتم دمجها مع الكتلة الحالية
3.بعدين بيرجع الكتلة إلى إدارة الكتلة المحررة (Bins).
لازم نركز على دالة free لان بتخرب الدنيا..
فقرة أوعك:
1.اوعك تحرر مؤشر مانك مخصصو بأحد دوال التخصيص
2.أوعك تحرر نفس الكتلة مرتين
3.أوعك تستخدم الكتلة بعد تحريرها
4.لازم بعد التحرير تعين المؤشر لـ Null فوراً
2.calloc():
هي الدالة وظيفتها تخصيص كتل ذاكرة لمصفوفة من العناصر وتهيئتها بقيمة صفر.
تختلف عن malloc() بأنها تهيء الذاكرة بأصفار مما يزيد أمان البرنامج (يعني بتتجنب قيم الـ 'garbage' العشوائية) ولهذا السبب تعتبر أبطأ من malloc().
الآلية النظرية:
1.أول الشي بتخصص كتلة ذاكرة بتكفي لعدد (num) من العناصر بحجم (size) معين لكل عنصر.
2.بعدين بتهيء جميع البايتات بالأصفار (وهاد الفرق الجوهري بينها وبين malloc())
وبيكون الحساب الكلي : num * size
طيب كيف بتتم عملية التهيئة؟
النظام يستخدم
memset: هي دالة تستخدم لملىء كتلة من الذاكرة بقيمة معينة.
ptr: بيدل على مؤشر كتلة الذاكرة إلي بدنا نملئها.
الـ0: هي عبارة عن value للقيمة المراد وضعها للبايتات
num*size: عدد البايتات الي بدنا نملأها.
شلون بتشتغل؟
بتقوم بنسخ الصفر إلي بالـvalue إلى أول عدد (num) بدءاً من العنوان إلي بيشير إليه ptr.
ملاحظة هامة: بسبب التهيئة
calloc (4, 5) لا تساوي malloc(20)
يعني calloc رح يكون فيها 20 قيمة كلهن بيساووا الصفر أما malloc بيكونوا بقيم عشوائية (من بقايا البيانات السابقة 'garbage').
مثال:
3.realloc():
وظيفتها تغيرر حجم الكتلة المخصصة والها سيناريوهين:
1.التوسيع في نفس المكان إذا كان هناك مساحة كافية بعد الكتلة (لأن الكتلة التالية حرة) وكان حجمها كافي فإن realloc تقوم بتوسيع الكتلة في المكان. (بهي الحالة بيكون المؤشر الجديد هو نفسو القديم).
2.النقل: إذا ما كان في مساحة كافية
بتخصص كتلة جديدة بحجم مناسب وتنقل البيانات من الكتلة القديمة إلى الجديدة باستخدام memcpy().
بعدين بتحرر الكتلة القديمة وبتعيد المؤشر إلى الكتلة الجديدة.
مثال:
4.alloca():
هي الدالة ما رح نحكي عنها بسبب تخصيصها على الستاك وخطورة استخدامها لانها بتسبب ثغرات overflow
بس لازم نعرف نقطة مهمة فيها انو الذاكرة تحرر تلقائياً عند الخروج من الدالة بدون دالة free() يعني متل الستاك.
شو هي إدارة الكتل المحررة (Bins):
وقت بيتم تحرير كتلة ذاكرة بواسطة دالة free() ما بيتم إرجاعها للنظام مباشرة بل يتم الاحتفاظ بها بقوائم منظمة اسمها Bins لإعادة استخدامها لاحقاً. هذا النظام يحسن الأداء ويقلل التجزئة (fragmentation).
أنواع الـBins الرئيسية:
1.Fast Bins:
هاد النوع بيعمل على إدارة الكتل الصغيرة متكررة التحرير.
هي عبارة عن 10 قوائم سريعة فردية (single linked-lists) بأحجام ثابتة (16, 24, 32, 40, 48, 56, 64, 72, 80, 88 بايت) بنظام x64.
بتشتغل بنمط LIFO (آخر كتلة تحررت هي أول كتلة بينعاد استخدامها).
طبعاً هون ما بيتم دمج الكتل المجاورة الحرّة (لتسريع عمليات التخصيص)
شلون نا بتتم عملية الدمج ؟
هون بيكون NON_MAIN_ARENA = 1 لتجنب الدمج.
وتدمج لاحقاً عند امتلاء الFast Bins أو عند طلب كتلة كبيرة باستخدام الدالة malloc_consolidate()
2.small Bins:
تقوم بإدارة الكتل متوسطة الحجم (32 -> 1024 بايت ) بنظام x64.
هي عبارة عن 62 قائمة صغيرة (كل قائمة لنطاق حجمي معين)
بتشتغل بنمط FIFO (First-In-First-Out) (يعني أول كتلة تحررت هي أول كتلة تعاد).
وهون بيتم دمج الكتل المجاورة الحرة لتكوين كتل أكبر.
3.Large Bins:
تقوم بإدارة الكتل الكبير (>1024) وهي عبارة عن 63 قائمة كبيرة (كل قائمة لنطاق حجمي معين).
الكتل يتم ترتيبها حسب الحجم داخل كل قائمة. تستخدم خوارزمية Best-fit لتقليل التجزئة. والبحث فيها أبطأ بسبب الحاجة لمسح القائمة.
4.Unstored Bins:
عبارة عن مؤقت لتخزين الكتل المحررة حديثاً قبل فرزها. وهي قائمة واحدة فقط.
تعمل كـ منطقة عازلة تقوم بتصنيف الكتل داخل (small/large Bins).
دورة حياة الكتل المحررة:
عند استدعاء دالة free(*ptr)
.يتم التحقق من حجم الكتلة
* إذا <= 88 توضع في الـFast Bins
* واذا اكبر توضع في الـ Unstored Bins وبعدها يتم فرزها حسب الحجم (small/large Bins).
2.عند التخصيص بـ malloc()
* أول شي بتبحث بالـ Fast Bins اذا كان الحجم مناسب بتخصص كتلة
* إذا لا بتفوت على الـ Unstored Bins (إذا كانت القائمة مناسبة بتخصصها)
* اذا لا بتبحث بـ small/large Bins
مثال عملي مع تحرير الذاكرة:
التحليل باستخدام GDB مع pwndbg:
بإصدار glibc 2.26 تم إدخال آلية تسمى tcache بهدف تحسن أداء التخصيصات الصغيرة في البيئات متعددة الخيوط (multi-threaded) الفكرة الرئيسية هي توفير مخبأ محلي (cache) لكل خيط (thread-local cache).
شو المشكلة إلي قامت بحلها هذه الآلية؟
في البيئات متعددة الخيوط (multi-threaded) كان الوصول للذاكرة المشتركة (main arena) يسبب contention (نزاع أو تنافس) على القفل (mutex) مما يبطىء الأداء
(طبعاً بالـ multi-threaded بكون في شي اسمو synchronized (تزامن) وبكون في قفل بيعمل block مشان يمشي هاد التزامن وما يأثر thread عالتاني)
فهون خصصو مخبأ داخلي cache لكل thread وصار كل خيط بيدير كتلة محررة بشكل مستقل دون القفل (وصار يقلل زمن التخصيص بنسبة 70 إلي 80%).
الـtcache: هي عبارة عن 64 قائمة لكل ثريد بغض النظر عن معمارية النظام.
أكبر حجم كتلة 1032 بـx64.
وكل قائمة بتحتوي على 7 كتل.
عند التخصيص:
• إذا كان الحجم مناسب <= 1032 بيبحث بقائمة tcache المناسبة للحجم إذا وجدت كتلة بحجم مناسب: يقوم بإزالتها من القائمة (LIFO) وإرجاعها.
• إذا لم توجد بيكون التخصيص من الـBins التقليدية.
عند التحرير:
• إذا كانت القائمة غير ممتلئة (<7 كتل) بيضيف الكتلة إلى الـtcache
• وإذا كانت ممتلئة: بيرسل الكتلة إلى الـ Fast Bins أو الـUnstored Bins.
هي جداول توضيحية تختصر تقريباً إلي حكينا عليه فوق.
الاداء:
الخصائص:
شو هي أشهر الثغرات المتكررة التي تحدث من ورا التخصيص:
Heap overflow:
نوع من ثغرات تجاوز السعة (buffer overflow) يحدث في منطقة الذاكرة الديناميكية (heap) حيث يتم تخصيص الذاكرة أثناء وقت التشغيل Runtime باستخدام الدوال.
تحدث الثغرة عند كتابة بيانات تفوق الحجم المخصص للمنطقة في الذاكرة مما يؤدي إلى:
1.تلف البيانات المجاورة في الـHeap وتعديلها.
مثال:
2.تعديل الـmetadata.
مثال:
3.تعديل مؤشر الدالة (function Pointer) وإضافة shellcode.
مثال:
Use After Free (UAF):
خطأ في إدارة الذاكرة يحدث عندما يستمر البرنامج في استخدام مؤشر إلى ذاكرة بعد تحريرها (باستخدام free()) ويؤدي إلى:
1.تلف البيانات
مثال:
2.تنفيذ malicious-code
مثال:
Double free:
خطأ في إدارة الذاكرة يحدث عندما يُحرر المؤشر مرتين متتاليتين دون إعادة التخصيص بينهم مما يؤدي إلى:
1.تلف الـHeap
مثال:
2.تنفيذ malicious-code
مثال:
تخيل 45% من الثغرات بين (2020 - 2023) كانت مرتبطة بمشاكل ادارة الذاكرة
كيف ممكن ناخد احتياطاتنا مشان ما تصير معنا هي الثغرات:
1. اعادة تعيين المؤشرات بعد التحرير:
2. في ادوات للتحقق الديناميكي:
- valgrind:
3. اليات لحماية نظام التشغيل:
- ASLR: عشوائية عناوين الذاكرة.
- DEP: منع تنفيذ كود من مناطق غير تنفيذية
ان شاء الله كون شرحت اغلب الامور المتعلقة بالتخصيص الديناميكي. بعض الامور بالثغرات لسا ما شرحناها متل الـstruct, typedef باذن الله رح يكون الها شرح وافي وكافي.
ان اصبت من الله وان اخطأت فمن نفسي والشيطان والسلام عليكم ورحمة الله.
بعد ما حكينا عن الـHeap والـStack وفهمنا هيكلة البناء الداخلي لكل منهما وآلية عملهما اليوم سنتطرق لشرح موضوع تخصيص الذاكرة الديناميكية بطريقة موسعة وسنتكلم عن امور ادارة الذواكر الحرة ونحكي عن اشهر الثغرات المتكررة في هاد الموضوع.
Dynamic memory allocation
يعد فهم تخصيص الذاكرة الديناميكية شيء مهم جداً لتحليل البرمجيات واستغلال الثغرات وفهم آلية عمل البرنامج من الداخل...
بداية يتم تخصيص الذاكرة الديناميكية يدوياً عبر دوال اساسية ما هي هذه الدوال؟
1.malloc() and free():
تخصيص وتحرير الذاكرة الديناميكية.
C:
malloc(size_t size);
C:
free(void *ptr);
البنية الداخلية للكتلة إلي حكينا عنها بالمقال السابق (heap chunk) هي بتكون في منظم الذاكرة (متل glibc):
C:
strucut malloc_chunk{
size_t PREV_SIZE; // حجم الكتلة السابقة (إذا كانت حرّة)
size_t size; //حجم الكتلة الحالية + Flags
// (fd/bk Pointers) حقول إدارة الكتل الحرة
};
خلينا نفصلن:
size_t size:
بيحتفظ بحجم الكتلة الحالية مع الهيدر
البتات التلاتة إلي على اليمين (LSb) تستخدم للـ Flags
وهي صورة موضحة لمفهوم الـLSb:
الـFlags حكينا عنن بالمقال السابق
• PREV_INUSE.
• IS_MAPPED.
• NON_MAIN_ARENA.
-Prev_size:
يستخدم فقط اذا كانت الكتلة السابقة حرة (يعني PREV_INUSE = 0 بالكتلة الحالية)
ويتيح الدمج (Coalescing) عند التحرير لتقليل التجزئة (fragmentation).
عملية التخصيص (malloc):
لآلية النظرية:
1.تطلب من نظام إدارة الذاكرة كتلة بحجم (size) بايت
2.بيروح بيبحث بالـBins (قوائم الكتل الحرة رح نحكي عليها).
3.إذا ما انوجدت كتلة مناسبة بالـBins: منطلب ذاكرة من النظام عبر sbrk() او mmap().
4.إذا كانت الكتلة أكبر من المساحة المطلوب تخصيصها بتتقسم هي الكتلة وبيتم إرجاع جزء للمستخدم.
الهيكل الداخلي: يضيف النظام معلومات إدارية (metadata) قبل العنوان المرجع هي متل عملية الsaved ebp بالستاك.
مثال:
C:
#include <stdio.h>
#include <stdlib.h>
int main() {
//تخصيص ذاكرة ل 10 اعداد صحيحة = 40 بايت
int *nums = (int*)malloc(10 * sizeof(int));
//فحص نجاح التخصيص
if(nums == NULL){
printf("failed..!")
return 1;
}
//استخدام الذاكرة المخصصة
for(int i = 0; i < 10; i++) {
nums[i] = i * 10;
}
//تحرير الذاكرة المخصصة
free(nums);
return 0;
}
وهيك بتكون الذاكرة:
كود:
0x1000: [Flags + حجم الكتلة] -> metadata (مخفي عن المستخدم)
0x1008: [nums[0]] -> المؤشر nums يشير إلى هنا.
0x100C: [nums[1]]
~ ~ ~
~ ~ ~
~ ~ ~
0x1028: [nums[9]]
عملية التحرير (free):
1.بيتحقق من صحة المؤشر (هل بيشير إلى بداية كتلة؟)
2.إذا كانت الكتلة المجاورة حرة بيتم دمجها مع الكتلة الحالية
3.بعدين بيرجع الكتلة إلى إدارة الكتلة المحررة (Bins).
لازم نركز على دالة free لان بتخرب الدنيا..
فقرة أوعك:
1.اوعك تحرر مؤشر مانك مخصصو بأحد دوال التخصيص
2.أوعك تحرر نفس الكتلة مرتين
3.أوعك تستخدم الكتلة بعد تحريرها
4.لازم بعد التحرير تعين المؤشر لـ Null فوراً
2.calloc():
هي الدالة وظيفتها تخصيص كتل ذاكرة لمصفوفة من العناصر وتهيئتها بقيمة صفر.
تختلف عن malloc() بأنها تهيء الذاكرة بأصفار مما يزيد أمان البرنامج (يعني بتتجنب قيم الـ 'garbage' العشوائية) ولهذا السبب تعتبر أبطأ من malloc().
الآلية النظرية:
كود:
void *calloc(size_t num, size_t size);
1.أول الشي بتخصص كتلة ذاكرة بتكفي لعدد (num) من العناصر بحجم (size) معين لكل عنصر.
2.بعدين بتهيء جميع البايتات بالأصفار (وهاد الفرق الجوهري بينها وبين malloc())
وبيكون الحساب الكلي : num * size
طيب كيف بتتم عملية التهيئة؟
النظام يستخدم
كود:
memset(ptr, 0, num * size)
memset: هي دالة تستخدم لملىء كتلة من الذاكرة بقيمة معينة.
ptr: بيدل على مؤشر كتلة الذاكرة إلي بدنا نملئها.
الـ0: هي عبارة عن value للقيمة المراد وضعها للبايتات
num*size: عدد البايتات الي بدنا نملأها.
شلون بتشتغل؟
بتقوم بنسخ الصفر إلي بالـvalue إلى أول عدد (num) بدءاً من العنوان إلي بيشير إليه ptr.
ملاحظة هامة: بسبب التهيئة
calloc (4, 5) لا تساوي malloc(20)
يعني calloc رح يكون فيها 20 قيمة كلهن بيساووا الصفر أما malloc بيكونوا بقيم عشوائية (من بقايا البيانات السابقة 'garbage').
مثال:
C:
#include <stdio.h>
#include <stdlib.h>
int main() {
//تخصيص مصفوفة من 5 عناصر من نوع float
float *temps = (float*)calloc(5, sizeof(float));
if (temps == NULL) {
printf("Failed..!\n");
retrun 1;
}
//التأكد من التهيئة بصفر
for(int i = 0; i < 10; i++) {
printf("temps[%d] = %f\n", i, temps[i] ); // هون رح يطبع الكل 0.0
}
//تحرير الذاكرة
free(temps);
return 0;
}
3.realloc():
وظيفتها تغيرر حجم الكتلة المخصصة والها سيناريوهين:
1.التوسيع في نفس المكان إذا كان هناك مساحة كافية بعد الكتلة (لأن الكتلة التالية حرة) وكان حجمها كافي فإن realloc تقوم بتوسيع الكتلة في المكان. (بهي الحالة بيكون المؤشر الجديد هو نفسو القديم).
2.النقل: إذا ما كان في مساحة كافية
بتخصص كتلة جديدة بحجم مناسب وتنقل البيانات من الكتلة القديمة إلى الجديدة باستخدام memcpy().
بعدين بتحرر الكتلة القديمة وبتعيد المؤشر إلى الكتلة الجديدة.
مثال:
C:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
char *buffer = (char*)malloc(10);
if(buffer == NULL) retrun 1;
strcpy(buffer, "Hello");
//توسيع الذاكرة لاستيعاب نص اطول
char *new_buffer = (char*)realloc(buffer, 20);
if(new_buffer == NULL) {
free(buffer); //الحفاظ على الكتلة الاصلية
return 1;
}
buffer = new_buffer; //التحديث بعد التأكد
strcat(buffer, "World"); // الان المساحة كافية
free(buffer);
return 0;
}
4.alloca():
هي الدالة ما رح نحكي عنها بسبب تخصيصها على الستاك وخطورة استخدامها لانها بتسبب ثغرات overflow
بس لازم نعرف نقطة مهمة فيها انو الذاكرة تحرر تلقائياً عند الخروج من الدالة بدون دالة free() يعني متل الستاك.
شو هي إدارة الكتل المحررة (Bins):
وقت بيتم تحرير كتلة ذاكرة بواسطة دالة free() ما بيتم إرجاعها للنظام مباشرة بل يتم الاحتفاظ بها بقوائم منظمة اسمها Bins لإعادة استخدامها لاحقاً. هذا النظام يحسن الأداء ويقلل التجزئة (fragmentation).
أنواع الـBins الرئيسية:
1.Fast Bins:
هاد النوع بيعمل على إدارة الكتل الصغيرة متكررة التحرير.
هي عبارة عن 10 قوائم سريعة فردية (single linked-lists) بأحجام ثابتة (16, 24, 32, 40, 48, 56, 64, 72, 80, 88 بايت) بنظام x64.
بتشتغل بنمط LIFO (آخر كتلة تحررت هي أول كتلة بينعاد استخدامها).
طبعاً هون ما بيتم دمج الكتل المجاورة الحرّة (لتسريع عمليات التخصيص)
شلون نا بتتم عملية الدمج ؟
هون بيكون NON_MAIN_ARENA = 1 لتجنب الدمج.
وتدمج لاحقاً عند امتلاء الFast Bins أو عند طلب كتلة كبيرة باستخدام الدالة malloc_consolidate()
2.small Bins:
تقوم بإدارة الكتل متوسطة الحجم (32 -> 1024 بايت ) بنظام x64.
هي عبارة عن 62 قائمة صغيرة (كل قائمة لنطاق حجمي معين)
بتشتغل بنمط FIFO (First-In-First-Out) (يعني أول كتلة تحررت هي أول كتلة تعاد).
وهون بيتم دمج الكتل المجاورة الحرة لتكوين كتل أكبر.
3.Large Bins:
تقوم بإدارة الكتل الكبير (>1024) وهي عبارة عن 63 قائمة كبيرة (كل قائمة لنطاق حجمي معين).
الكتل يتم ترتيبها حسب الحجم داخل كل قائمة. تستخدم خوارزمية Best-fit لتقليل التجزئة. والبحث فيها أبطأ بسبب الحاجة لمسح القائمة.
4.Unstored Bins:
عبارة عن مؤقت لتخزين الكتل المحررة حديثاً قبل فرزها. وهي قائمة واحدة فقط.
تعمل كـ منطقة عازلة تقوم بتصنيف الكتل داخل (small/large Bins).
دورة حياة الكتل المحررة:
عند استدعاء دالة free(*ptr)
كود:
void *ptr = malloc(20);
free(*ptr);
.يتم التحقق من حجم الكتلة
* إذا <= 88 توضع في الـFast Bins
* واذا اكبر توضع في الـ Unstored Bins وبعدها يتم فرزها حسب الحجم (small/large Bins).
2.عند التخصيص بـ malloc()
* أول شي بتبحث بالـ Fast Bins اذا كان الحجم مناسب بتخصص كتلة
* إذا لا بتفوت على الـ Unstored Bins (إذا كانت القائمة مناسبة بتخصصها)
* اذا لا بتبحث بـ small/large Bins
مثال عملي مع تحرير الذاكرة:
C:
#include <stdio.h>
#include <stdlib.h>
int main() {
void *a = malloc(32); // كتلة صغيرة (fast bin)
void *b = malloc(1024); // كتلة كبيرة (Unstored bin)
void *c = malloc(2048); // كتلة كبيرة اخرى
free(a); // تذهب الى fast bin
free(b); // تذهب الى Unstored bin
free(c); // unstored bin
malloc(40); //اذا وجد حجم مناسب بياخد من fast bin
return 0;
}
التحليل باستخدام GDB مع pwndbg:
بإصدار glibc 2.26 تم إدخال آلية تسمى tcache بهدف تحسن أداء التخصيصات الصغيرة في البيئات متعددة الخيوط (multi-threaded) الفكرة الرئيسية هي توفير مخبأ محلي (cache) لكل خيط (thread-local cache).
شو المشكلة إلي قامت بحلها هذه الآلية؟
في البيئات متعددة الخيوط (multi-threaded) كان الوصول للذاكرة المشتركة (main arena) يسبب contention (نزاع أو تنافس) على القفل (mutex) مما يبطىء الأداء
(طبعاً بالـ multi-threaded بكون في شي اسمو synchronized (تزامن) وبكون في قفل بيعمل block مشان يمشي هاد التزامن وما يأثر thread عالتاني)
فهون خصصو مخبأ داخلي cache لكل thread وصار كل خيط بيدير كتلة محررة بشكل مستقل دون القفل (وصار يقلل زمن التخصيص بنسبة 70 إلي 80%).
الـtcache: هي عبارة عن 64 قائمة لكل ثريد بغض النظر عن معمارية النظام.
أكبر حجم كتلة 1032 بـx64.
وكل قائمة بتحتوي على 7 كتل.
عند التخصيص:
• إذا كان الحجم مناسب <= 1032 بيبحث بقائمة tcache المناسبة للحجم إذا وجدت كتلة بحجم مناسب: يقوم بإزالتها من القائمة (LIFO) وإرجاعها.
• إذا لم توجد بيكون التخصيص من الـBins التقليدية.
عند التحرير:
• إذا كانت القائمة غير ممتلئة (<7 كتل) بيضيف الكتلة إلى الـtcache
• وإذا كانت ممتلئة: بيرسل الكتلة إلى الـ Fast Bins أو الـUnstored Bins.
هي جداول توضيحية تختصر تقريباً إلي حكينا عليه فوق.
الاداء:
الخصائص:
شو هي أشهر الثغرات المتكررة التي تحدث من ورا التخصيص:
Heap overflow:
نوع من ثغرات تجاوز السعة (buffer overflow) يحدث في منطقة الذاكرة الديناميكية (heap) حيث يتم تخصيص الذاكرة أثناء وقت التشغيل Runtime باستخدام الدوال.
تحدث الثغرة عند كتابة بيانات تفوق الحجم المخصص للمنطقة في الذاكرة مما يؤدي إلى:
1.تلف البيانات المجاورة في الـHeap وتعديلها.
مثال:
C:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
char *buffer1 = (char*)malloc(10); // كتلة اولى
char *buffer2 = (char*)malloc(10); // كتلة مجاورة
// تهيئة buffer2
strcpy(buffer2, "SECRET");
// تجاوز سعة buffer1
strcpy(buffer1, "HEAP-OVERFLOW-ATTACK");
//buffer2 اصبحت معدلة بسبب التجاور
printf("buffer2 = %s\n", buffer2); // يظهر "flow_attack"
free(buffer1);
free(buffer2);
return 0;
}
2.تعديل الـmetadata.
مثال:
C:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
// تخصيص كتلتين
char *chunk1 = (char*)malloc(16);
char *chunk2 = (char*)malloc(16);
// كتابة بيانات تجاوزية لتعديل metadata الخاصة بـ chunk2
memset(chunk1, 'A', 32); // (تجاوز السعة) 16 < 32
// عند تحرير chunk2 النظام يكشف تلف metadata وقد يتوقف البرنامج
free(chunk2);
printf("the program isn't stopped yet\n"); // قد لا تظهر
return 0;
}
3.تعديل مؤشر الدالة (function Pointer) وإضافة shellcode.
مثال:
C:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef void (*func_ptr)(); // مؤشر دالة
void normal_func() {
write(1, "normal\n", 12);
}
void hacked_func() {
write(1, "hackin\n", 16);
}
int main() {
func_ptr *fptr = (func_ptr*)malloc(sizeof(func_ptr));
char *buffer = (char*)malloc(16);
*fptr = normal_func; // تعيين الدالة العادية
// تجاوز سعة buffer لتعديل fptr
strcpy(buffer, "AAAAAAAAAAAAAAAA" // 16 حرف
"\x40\x10\x40"); // عنوان hacked_func
(*fptr)(); // يتم استدعاء hacked_func
return 0;
}
Use After Free (UAF):
خطأ في إدارة الذاكرة يحدث عندما يستمر البرنامج في استخدام مؤشر إلى ذاكرة بعد تحريرها (باستخدام free()) ويؤدي إلى:
1.تلف البيانات
مثال:
C:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
int *ptr1 = (int*)malloc(sizeof(int));
*ptr1 = 100
printf("the basic value: %d\n", *ptr1); // 100
free(ptr1);
// كتلة جديدة بنفس الحجم ptr1
char *ptr2 = (char*)malloc(sizeof(int));
*ptr2 = 'X'; // تهيئة القيمة
// استخدام المؤشر المحرر (UAF)
*ptr1 = 200; // كتابة على ذاكرة محررة
printf("the new ptr2: %c\n", *ptr); // ستطبع قيمة غير متوقعة
return 0;
}
2.تنفيذ malicious-code
مثال:
C:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
void (*printFunc)(); // مؤشر لدالة
} Object;
void legitFunc() {printf("legit function\n");}
void evilFunc() {printf("exploit function\n");}
int main() {
Object *obj = (Object*)malloc(sizeof(Object));
obj->printFunc = legitFunc;
obj->printFunc(); // استدعاء للدالة
free(obj); // تحرير المؤشر
// تخصيص كتلة بنفس الحجم تحت سيطرة الـ Attacker
unsigned long *fake = (unsigned long*)malloc(sizeof(Object));
*fake = (unsigned long)evilFunc;
//استخدام المؤشر المحرر (UAF)
obj->printFunc(); // يستدعي evilFunc
return 0;
}
Double free:
خطأ في إدارة الذاكرة يحدث عندما يُحرر المؤشر مرتين متتاليتين دون إعادة التخصيص بينهم مما يؤدي إلى:
1.تلف الـHeap
مثال:
C:
#include <stdlib.h>
#include <stdio.h>
int main() {
int *ptr = (int*)malloc(sizeof(int));
*ptr = 10;
free(ptr); // التحرير الاول - صحيح
// .. شوية تعليمات ..
free(ptr); // التحرير التاني - خطأ double free
return 0;
}
2.تنفيذ malicious-code
مثال:
C:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct Data {
char buffer[32];
void (*security_check)();
};
void normal_check() {printf("normal_check\n");}
void malicious_code() {printf("hackin code\n");}
int main() {
struct Data *d1 = (struct Data*)malloc(sizeof(struct Data));
d1->security_check = normal_check;
free(d1); // تحرير أولي
// المهاجم يسيطر على الكتلة المحررة
struct Data *d2 = (struct Data*)malloc(sizeof(struct Data));
struct Data *d3 = (struct Data*)malloc(sizeof(struct Data));
// تعبة d2 بيانات ضارة
memset(d2->buffer, 'A', 32);
d2->security_check = malicious_code;
free(d1); // double free - تعديل الـ metadata
// اعادة تخصيص الذاكرة التالفة
struct Data *d4 = (struct Data*)malloc(sizeof(struct Data));
d4->security_check(); // تنفيذ الكود الضار
return 0;
}
تخيل 45% من الثغرات بين (2020 - 2023) كانت مرتبطة بمشاكل ادارة الذاكرة
كيف ممكن ناخد احتياطاتنا مشان ما تصير معنا هي الثغرات:
1. اعادة تعيين المؤشرات بعد التحرير:
C:
free(ptr);
ptr = NULL; // منع استخدام المؤشر بعد التحرير
2. في ادوات للتحقق الديناميكي:
- valgrind:
كود:
valgrind --leak-check=full ./programName
3. اليات لحماية نظام التشغيل:
- ASLR: عشوائية عناوين الذاكرة.
- DEP: منع تنفيذ كود من مناطق غير تنفيذية
ان شاء الله كون شرحت اغلب الامور المتعلقة بالتخصيص الديناميكي. بعض الامور بالثغرات لسا ما شرحناها متل الـstruct, typedef باذن الله رح يكون الها شرح وافي وكافي.
ان اصبت من الله وان اخطأت فمن نفسي والشيطان والسلام عليكم ورحمة الله.
التعديل الأخير: