ثغرة Heap Overflow في امتدادات PHP: عندما تتحكم الـ Metadata في الـ Allocator
ترفع صورة. السيرفر يستخرج العنوان والفنان منها. المعرض يبدو مرتباً. ما لا تراه هو امتداد أصلي صغير يخصص buffer بحجم ثابت 56 بايت ثم يستدعي strcpy وكأن التحقق من الطول أمر اختياري. في تلك اللحظة تتحول الصورة من مجرد ديكور إلى أداة تعيد كتابة ذاكرة العملية.
هذا المقال يشرح الفكرة من الأساس، ويعرض سيناريو كاملاً مصطنعاً، والأنماط الضعيفة، والطرق العملية للإصلاح.
ما هي ثغرة Heap Overflow في امتدادات PHP؟
امتدادات PHP عبارة عن كود C أصلي يُحمّل داخل عملية PHP. تتحدث هذه الامتدادات مع Zend Memory Manager وليس مع malloc النظام. Zend يقسم الذاكرة إلى bins بأحجام ثابتة. عندما يطلب الامتداد 56 بايت يحصل على block من الـ bin المناسب. هذا الـ block يعيش على free list هي في جوهرها قائمة مرتبطة بمؤشرات forward فقط.
إذا قام الامتداد بعد ذلك بـ strcpy غير محدود داخل هذا الـ block، فإن الـ overflow ينتقل مباشرة إلى الـ chunk التالي أو إلى بيانات الـ free list نفسها. وبما أن مدخل الـ free list مجرد مؤشر، فإن الكتابة فوقه تتيح لك تحديد مكان التخصيص التالي. الطريق من "metadata صورة جميلة" إلى "أنا أتحكم في الـ allocator" قصير جداً.
المكون الثاني الشائع هو تسريب معلومات. بدون معرفة العناوين الأساسية تبقى الكتابة عمياء. Local File Inclusion (أو أي وسيلة تقرأ /proc/self/maps) يوفر العناوين الناقصة. بمجرد أن يتمكن المهاجم من توجيه الـ free list المفسد نحو مدخل GOT، واستبدال مؤشر دالة بـ system، يتحول الـ free التالي إلى أمر.
لماذا يهمك هذا الموضوع؟
- الامتدادات المخصصة نادراً ما تخضع لنفس مستوى المراجعة الذي يلقاه PHP الأساسي.
- مكتبات معالجة الصور وmetadata سطح هجوم متكرر لأن المستخدمين يتوقعون رفع الملفات.
- قوائم الـ free الصغيرة في Zend لا تجري تقريباً أي فحوصات سلامة، لذا يكفي overflow واحد في كثير من الحالات للحصول على تخصيص عشوائي.
-
عندما تستطيع عمل free لسلسلة تتحكم فيها عبر
_efreeمُختطف، تحصل على RCE دون الحاجة إلى gadgets إضافية. - يظهر النمط نفسه في أي runtime يعتمد على allocators حسب الحجم ويثق في الكود الأصلي.
مثال عملي: معرض صور يثق في الـ Metadata
تخيل معرض صور بسيط. يرفع المستخدمون ملفات PNG. امتداد مخصص يستخرج ثلاثة حقول نصية: Title و Artist و Copyright. لكل حقل يخصص الامتداد buffer بحجم ثابت وينسخ النص باستخدام strcpy. يعرض المعرض أيضاً endpoint للعرض يقبل باراميتر مسار ويعيد محتوى الملف كـ base64 data URI. يقوم الـ endpoint بفحص وجود سطحي فقط.
الاكتشاف
يقبل الـ endpoint مسارات مطلقة. قراءة /proc/self/maps تكشف عناوين تحميل الامتداد وlibc وbinary الـ PHP الرئيسي. هذا الطلب الواحد يكفي لإسقاط ASLR لبقية الهجوم.
رفع PNG يحتوي حقل Title أطول من 55 بايت ينتج مخرجات مشوهة أو تعطلاً، مما يؤكد وجود الـ overflow. وجود عدة text chunks بنفس المفتاح يؤدي إلى تخصيصات متكررة من نفس الـ bin ويمنح تحكماً في شكل الـ free list.
مسار الاستغلال
- تسريب الـ maps وتسجيل base الامتداد وbase الـ libc.
- صنع PNG تخصص text chunks فيه ثلاثة blocks بحجم 56 بايت متتالية.
-
عمل overflow على الـ block الأول بحيث يشير مؤشر الـ free list للـ block الثاني إلى عنوان مختار (مثل مدخل GOT لـ
_efreeداخل الامتداد). - في التخصيص التالي يسلم الـ allocator العنوان الذي اختاره المهاجم.
-
كتابة عنوان
systemفي ذلك الموقع. -
إجبار free لسلسلة تحتوي الأمر المطلوب. يتحول
_efreeالمختطف إلىsystem(command).
يمكن أن يكون الأمر العملي مجرد إلحاق قائمة مجلد بملف يستطيع الـ LFI قراءته لاحقاً، أو فتح reverse shell. بمجرد السيطرة على العملية يصبح الباقي استغلالاً عادياً بعد الاختراق.
المتغيرات المجاورة
- يعمل نفس الـ overflow ضد أي allocator يعتمد على الحجم ويخزن بيانات الـ free list داخل الـ chunk.
-
إذا استخدم الامتداد
strncpyبطول مأخوذ من النص الذي يتحكم فيه المهاجم، تنتقل الثغرة ببساطة إلى integer overflow أو غياب الـ null terminator. - عندما يتم تحصين الـ endpoint لكن صفحة خطأ أو ملف سجل ما زال يكشف مسارات مطلقة، يمكن الحصول على تسريب المعلومات رغم ذلك.
أمثلة كود ضعيفة
جانب الـ C (الامتداد)
// ضعيف
char *buf = emalloc(56);
strcpy(buf, text_chunk->text); // نسخ غير محدود
meta->title = buf;
// مُصلح
size_t len = strlen(text_chunk->text);
if (len >= 56) {
php_error_docref(NULL, E_WARNING, "metadata too long");
return;
}
char *buf = emalloc(len + 1);
memcpy(buf, text_chunk->text, len + 1);
meta->title = buf;
جانب الـ PHP (عارض الملفات)
// ضعيف
$path = urldecode($_GET['file']);
if (file_exists($path)) {
echo base64_encode(file_get_contents($path));
}
// مُصلح
$path = realpath($_GET['file'] ?? '');
$base = realpath(__DIR__ . '/uploads');
if ($path === false || strpos($path, $base) !== 0) {
http_response_code(403);
exit;
}
echo base64_encode(file_get_contents($path));
الدفاع / كيفية الإصلاح
-
لا تستخدم أبداً دوال نسخ السلاسل غير المحدودة في الامتدادات. فضل
memcpyمع فحص طول صريح، أو واجهات Zend string التي تحمل الطول معها. - تعامل مع كل طول text chunk على أنه تحت سيطرة المهاجم. ضع حداً صارماً وارفض الحقول الكبيرة مبكراً.
- اربط الامتدادات مع canaries للـ stack والـ heap، وفكر في استخدام AddressSanitizer أثناء التطوير.
-
قيد أي endpoint يقدم ملفات على مجلد معروف باستخدام
realpathوفحص البادئة. -
عطّل أو ضع في sandbox القدرة على قراءة
/procمن عملية الويب إذا لم تكن ضرورية. - اجعل أقسام الـ GOT والأقسام القابلة للكتابة في الامتداد أصغر ما يمكن، واستخدم full RELRO عند الربط.
- راجع كل امتداد مخصص بنفس نموذج التهديد الذي تطبقه على التطبيق الرئيسي.
أفكار ختامية
الصورة لا ينبغي أن تكون قادرة على إعادة كتابة العملية التي تعرضها. عندما يخلط امتداد أصلي بين تخصيصات بحجم ثابت ودوال سلاسل C كلاسيكية، يتحول الـ allocator إلى سطح قابل للكتابة آخر. اربط ذلك مع تسريب معلومات فيصبح باقي الهجوم شبه آلي.
أكثر parser للـ metadata أماناً هو الذي لا يثق أبداً في طول البيانات التي يُعطى إياها.