شرح شامل: تشريح Top Chunk بايت بايت، كل سطر في كود الاستغلال مشروح سطراً بسطر، لماذا أوقفت glibc الحديثة هذه التقنية تحديداً، الأنظمة المتأثرة، ومختبر تفاعلي.
قبل أي استغلال يجب أن نفهم Top Chunk من الداخل. ليس مجرد "بقية الـ heap" — إنه هيكل بيانات دقيق يتحكم في كل تخصيص مستقبلي.
| الإزاحة | الحجم | الحقل | قيمة نموذجية | الوصف |
|---|---|---|---|---|
| top − 0x10 | 8 bytes | prev_size | 0x0 | يُستخدم فقط إذا كان الـ chunk السابق free. عادة 0. |
| top − 0x08 | 8 bytes | size ← هدفنا | 0x20fd1 | حجم top chunk الكلي + flags في البتات الثلاثة الأدنى. |
| top + 0x00 | variable | user space | 0x00… | الذاكرة الخام غير المستخدمة. malloc يقطع منها chunks جديدة. |
الـ nibble الأخير من حقل الـ size ليس جزءاً من الحجم الفعلي — البتات الثلاثة الأدنى محجوزة كـ flags:
| البت | الاسم | القيمة | المعنى |
|---|---|---|---|
| bit 0 | PREV_INUSE (P) | 0x1 | الـ chunk السابق (في ذاكرة أدنى) مُستخدم حالياً بالبرنامج. |
| bit 1 | IS_MMAPPED (M) | 0x2 | هذا الـ chunk تم تخصيصه بـ mmap() وليس بـ brk(). |
| bit 2 | NON_MAIN_ARENA (A) | 0x4 | الـ chunk ينتمي لـ thread arena ثانوية وليس الـ main arena. |
| bits 3–63 | الحجم الفعلي | size & ~0x7 | الحجم الحقيقي للـ chunk. دائماً محاذى لـ 16 بايت. |
القيمة 0x20fd1 تعني: حجم فعلي 0x20fd0 بايت (نطرح bit 0) + الـ chunk السابق مُستخدم (PREV_INUSE=1). هذا ≈ 135,000 بايت وهو ما يراه pwndbg في vis_heap_chunks.
هذا هو المنطق الحقيقي داخل _int_malloc في glibc عند التخصيص من top chunk:
السطر size = chunksize(victim) يقرأ مباشرة من الذاكرة بدون أي تحقق. إذا كتبنا 0xfffffffffffffff1 في ذلك الحقل، يعتقد malloc أن top chunk يغطي كل الـ virtual address space ويتصرف وفق ذلك.
في بداية 2019، أضاف مطورو glibc سطراً واحداً فقط — لكنه قضى على House of Force بالكامل. دعونا نشرح المشكلة والحل بدقة.
glibc 2.28 — بدون حماية:
glibc 2.29+ — مع الحماية:
av->system_mem متغير داخل كل arena يتتبع الحجم الفعلي للذاكرة المطلوبة من الـ OS عبر brk() أو mmap(). إذا كان الـ heap الافتراضي ~135 KB (0x21000)، فأي قيمة أكبر منه في حقل top chunk ستُكشف فوراً:
0xfffffffffffffff1 > 0x21000 → true → crash!
House of Force تعمل بالكامل. لا يوجد أي فحص على حجم top chunk. أبسط وأقوى نسخة من التقنية — الهدف المثالي هو أي برنامج يستخدم هذه الإصدارات.
الـ tcache (thread-local cache) أضاف تعقيداً لتقنيات أخرى مثل Fastbin Dup، لكن House of Force لا يتأثر به إطلاقاً — ما زلنا نستطيع تخريب top chunk بنفس الطريقة.
كودنا يستهدف glibc 2.23 تحديداً — من هذا النطاق.
House of Force ميت رسمياً من هذا الإصدار. سطر واحد أضيف في commit بعنوان "malloc: Add integrity check for top chunk" أنهى التقنية تماماً.
حتى لو استطعت تجاوز فحص system_mem بطريقة ما، فإن __malloc_hook و__free_hook و__realloc_hook حُذفت كلياً من glibc 2.34. الهدف المفضل للتقنية لم يعد موجوداً في الأنظمة الحديثة.
على الأنظمة الحديثة: نعم. لكنه مهم جداً لـ: (1) فهم heap internals — أساس كل تقنيات الـ heap exploitation، (2) أنظمة Embedded/IoT تستخدم glibc قديم، (3) CTFs التي تحدد إصدار glibc يدوياً، (4) فهم لماذا أُضيف فحص system_mem يساعد على فهم حدود الحمايات الأخرى.
ELF("libc.so.6") يقرأ الـ offsets الثابتة لكل دالة داخل المكتبة. بعد أن نكتشف عنوان runtime لأي دالة (عبر الـ leak)، يستطيع pwntools حساب عنوان base المكتبة وبالتالي عنوان أي دالة أخرى ديناميكياً — حتى مع ASLR.
ASLR تعشوئ عنوان base المكتبة عند كل تشغيل، لكن المسافة (offset) بين أي دالتين داخل نفس المكتبة ثابتة دائماً. بمجرد معرفة عنوان puts() runtime، نطرح منه الـ offset الثابت لـ puts → نحصل على base → نضيف offset أي دالة أخرى. هذا ما يفعله السطر 12 بالضبط.
البينري يُخصّص chunk واحداً لنفسه عند التشغيل بحجم 0x20 (32 بايت: 8 header + 24 data). لهذا top chunk يبدأ بعده مباشرة عند heap_base + 0x20. إذا كان البينري خصّص أكثر، تحتاج تتبّع ذلك في GDB بـ vis.
glibc يتطلب أن يكون حجم الـ chunk محاذياً (aligned to 16 bytes)، أي آخر nibble = 0. كذلك البت 0 (PREV_INUSE) يجب أن يكون 1. إذاً: 0xfffffffffffffff0 | 0x1 = 0xfffffffffffffff1. القيمة ...ff ستُسبب مشكلة لأن آخر nibble = f وليس 0 | flags.
sendlineafter(prompt, data) ينتظر الـ prompt ثم يرسل data + "\n". أما sendafter فيرسل بدون \n — مهم جداً لحقل data لأن بعض البرامج تقرأ حتى \n وتتوقف، مما يقطع الـ payload ويمنع وصول bytes الـ overflow.
عندما يطلب malloc chunk بحجم evil_size، يستهلك evil_size + header (0x10) بايت من top chunk. الـ top chunk الجديد يقع عند top_chunk + evil_size + 0x10. نريد top chunk الجديد أن يكون قبل malloc_hook بمقدار header كامل (0x10) بحيث تبدأ user data الـ chunk التالي عند malloc_hook بالضبط. الحل: نطرح 0x20 = 0x10 header الـ chunk الحالي + 0x10 الـ header القادم.
Python يتعامل مع الأعداد كـ arbitrary-precision integers — لا حد للحجم. إذا كان malloc_hook < top_chunk، سيكون الناتج رقماً سالباً في Python مثل -5. لكن البينري يتوقع unsigned 64-bit integer. الـ &= 0xffffffffffffffff يقنع Python بتمثيل الرقم كـ two's complement 64-bit: -5 → 0xfffffffffffffffb.
glibc تفحص __malloc_hook في بداية كل استدعاء لـ malloc(). إذا لم يكن null، تستدعيه مع نفس المعاملات. نحن وضعنا system() فيه، وأرسلنا عنوان /bin/sh كـ "حجم". النتيجة: malloc(binsh_addr) → system(binsh_addr) → system("/bin/sh") → shell!
دوال مثل system() تنفّذ داخلياً execve("/bin/sh", ["sh", "-c", cmd], env). لهذا السلسلة /bin/sh\x00 مُضمّنة في binary المكتبة. next(libc.search(b"/bin/sh")) يبحث في bytes المكتبة ويعيد عنوانها — نفس العنوان المستخدم في libc نفسها.
House of Force تعمل على الأنظمة التي تستخدم glibc ≤ 2.28 وتسمح بـ heap overflow يصل لـ top chunk. إليك التفصيل:
عدّل كود الاستغلال أو حمّل ملف binary لمحاكاة خطوات الـ exploit ورؤية النتائج خطوة بخطوة.
هذا المحاكي تعليمي — يشرح الخطوات ويحاكي المخرجات بعناوين ديناميكية واقعية. لتشغيل الـ exploit فعلياً تحتاج Linux مع glibc ≤ 2.28 والبينري المستهدف.