
استغلال إثبات المفهوم وتحليل تفصيلي لثغرة CVE-2022-0847 (Dirty Pipe) لتصعيد الامتيازات في نواة لينكس، بما في ذلك بيئة Docker للاختبار والتصحيح.
[toc]
نُشرت هذه المقالة في الأصل في حساب الأمن العام لشركة Huawei، وهذه هي نسخة المدونة (الأكثر اكتمالاً)
رابط النشر الأصلي: https://mp.weixin.qq.com/s/6VhWBOzJ7uu80nzFxe5jpg
رقم الثغرة: CVE-2022-0847 (الاسم المستعار: Dirty Pipe)
المنتج المتأثر: linux kernel - splice syscall
الإصدارات المتأثرة: تم إدخاله في linux 5.8 بواسطة التصحيح f6dd975583bd ~ تم إصلاحه في 5.16.11 و 5.15.25 و 5.10.102
خطورة الثغرة: يمكن كتابة محتوى لا يتجاوز صفحة واحدة (كافٍ) لأي ملف قابل للقراءة، مما يسمح برفع الصلاحيات محليًا.
Docker لتحليل الثغرة: chenaotian/cve-2022-0847 (إذا لم يكن متاحًا بعد، فأنا لم أقم برفعه بعد)
يوفر:
بدء التشغيل:
cd ~/cve-2022-0847
gcc exp.c -o exp --static && cp exp ./rootfs && cd rootfs
find . | cpio -o --format=newc > ../rootfs.img
cd ../
./boot.sh
التصحيح:
gdb ./vmlinux
target remote :10086
directory /root/linux-5.13
b do_splice
b copy_page_to_iter_pipe
b pipe_write
ignore 3 15
...
p *(struct pipe_inode_info *) pipe
p (struct pipe_buffer)pipe->bufs[0]
المبدأ المختصر للثغرة هو أن استدعاء دالة
spliceيمكنه إرسال ملف إلىpipeبطريقة "النسخ الصفري" (zero-copy). على مستوى الكود، النسخ الصفري يعني استخدام ذاكرة التخزين المؤقت للملف (page cache) مباشرة كصفحةbufفي الـpipe. لكن تم إدخال ثغرة عدم تهيئة متغير هنا، مما يؤدي إلى إمكانية التعامل مع صفحة ذاكرة التخزين المؤقت للملف لاحقًا في قناة الـpipeعلى أنها صفحة ذاكرة تخزين مؤقت عادية للـpipe، وبالتالي يمكن تعديلها عن طريق "الكتابة المتتابعة" (续写). ومع ذلك، في هذه الحالة، لا تعتبر النواة صفحة ذاكرة التخزين المؤقت هذه "صفحة متسخة" (dirty page)، وبالتالي لا يتم تحديثها إلى القرص لفترة قصيرة (حتى إعادة التشغيل التالية أو ما شابه). خلال هذه الفترة، ستستخدم جميع السيناريوهات التي تصل إلى هذا الملف صفحة ذاكرة التخزين المؤقت المعدلة، مما يحقق عملية "كتابة عشوائية في أي ملف قابل للقراءة لفترة قصيرة". يمكن استخدام هذا لرفع الصلاحيات محليًا.
وفقًا للتصحيح، تقع نقطة حدوث الثغرة في دالة copy_page_to_iter_pipe، حيث تمت إضافة تهيئة لـ buf->flags. لذا فهذه ثغرة عدم تهيئة متغير.

تظهر نقطة استدعاء copy_page_to_iter_pipe في استدعاء النظام splice. تقوم دالة splice (استدعاء النظام) بنقل محتويات الملف إلى الأنبوب (pipe) بطريقة "النسخ الصفري". مقارنةً بنقل محتويات الملف مباشرةً إلى الأنبوب، فإن الأداء أفضل. سيتم شرح ذلك بالتفصيل أدناه.
أولاً، الاسم المستعار للثغرة هو Dirty Pipe، لذا دعنا نفهم الأنبوب (pipe). pipe هي قناة اتصال توفرها النواة، يتم إنشاؤها بواسطة دالة pipe/pipe2، وتعيد اثنين من واصفات الملفات: أحدهما لإرسال البيانات والآخر لاستقبالها، مثل طرفي الأنبوب. لن ندخل في التفاصيل الدقيقة للاستخدام.

باختصار، في النواة، يبلغ الطول الإجمالي لذاكرة التخزين المؤقت للـ pipe عادةً 65536 بايت، وتُدار على شكل صفحات، بإجمالي 16 صفحة (كل صفحة 4096 بايت). الصفحات ليست متجاورة، بل تُدار عبر مصفوفة، لتشكل قائمة دائرية. يتم الحفاظ على مؤشرين للقائمة: واحد للكتابة (pipe->head) وآخر للقراءة (pipe->tail). سنحلل هنا بشكل أساسي دالة pipe_write:
linux-5.13\fs\pipe.c : 400 : pipe_write
static ssize_t
pipe_write(struct kiocb *iocb, struct iov_iter *from)
{
struct file *filp = iocb->ki_filp;
struct pipe_inode_info *pipe = filp->private_data;
unsigned int head;
ssize_t ret = 0;
size_t total_len = iov_iter_count(from);
ssize_t chars;
bool was_empty = false;
bool wake_next_writer = false;
··· ···
··· ···
head = pipe->head;
was_empty = pipe_empty(head, pipe->tail);
chars = total_len & (PAGE_SIZE-1);
if (chars && !was_empty) {
//[1] إذا لم تكن ذاكرة التخزين المؤقت للـ pipe فارغة، فحاول الكتابة "بشكل متتابع" من الصفحة الحالية الأخيرة
unsigned int mask = pipe->ring_size - 1;
struct pipe_buffer *buf = &pipe->bufs[(head - 1) & mask];
int offset = buf->offset + buf->len;
if ((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) &&
offset + chars <= PAGE_SIZE) {
/*[2] المفتاح: إذا كانت علامة PIPE_BUF_FLAG_CAN_MERGE موجودة، فهذا يعني أن الصفحة تسمح بالكتابة المتتابعة.
* إذا كان طول الكتابة لا يتجاوز الصفحة، فاكتب بشكل متتابع؛ وإلا فابدأ صفحة جديدة. */
ret = pipe_buf_confirm(pipe, buf);
···
ret = copy_page_from_iter(buf->page, offset, chars, from);
···
}
buf->len += ret;
···
}
}
for (;;) {//[3] إذا تعذرت الكتابة المتتابعة على الصفحة السابقة، فابدأ صفحة جديدة
··· ···
head = pipe->head;
if (!pipe_full(head, pipe->tail, pipe->max_usage)) {
unsigned int mask = pipe->ring_size - 1;
struct pipe_buffer *buf = &pipe->bufs[head & mask];
struct page *page = pipe->tmp_page;
int copied;
if (!page) {//[4] طلب صفحة جديدة
page = alloc_page(GFP_HIGHUSER | __GFP_ACCOUNT);
if (unlikely(!page)) {
ret = ret ? : -ENOMEM;
break;
}
pipe->tmp_page = page;
}
spin_lock_irq(&pipe->rd_wait.lock);
head = pipe->head;
··· ···
pipe->head = head + 1;
spin_unlock_irq(&pipe->rd_wait.lock);
/* Insert it into the buffer array */
buf = &pipe->bufs[head & mask];
buf->page = page;//[5] وضع الصفحة الجديدة في مصفوفة الصفحات
buf->ops = &anon_pipe_buf_ops;
buf->offset = 0;
buf->len = 0;
if (is_packetized(filp))
buf->flags = PIPE_BUF_FLAG_PACKET;
else
buf->flags = PIPE_BUF_FLAG_CAN_MERGE;
//[6] تعيين العلم، افتراضيًا PIPE_BUF_FLAG_CAN_MERGE
pipe->tmp_page = NULL;
copied = copy_page_from_iter(page, 0, PAGE_SIZE, from);
//[7] عملية النسخ
··· ···
ret += copied;
buf->offset = 0;
buf->len = copied;
··· ···
}
··· ···
}
··· ···
return ret;
}
pipe الحالية فارغة (تكون فارغة عندما head==tail)، فهذا يعني وجود بيانات غير مقروءة في القناة. يتم الحصول على المؤشر head، أي الصفحة الأحدث المستخدمة للكتابة، والتحقق من len و offset لتلك الصفحة (للعثور على نهاية البيانات). ثم محاولة الكتابة بشكل متتابع على الصفحة الحالية.PIPE_BUF_FLAG_CAN_MERGE، إذا لم تكن موجودة، فلا يُسمح بالكتابة المتتابعة على الصفحة الحالية. أو إذا تجاوز طول البيانات المضافة إلى البيانات السابقة طول الصفحة الواحدة (أي عملية كتابة تتجاوز حدود الصفحة)، فلا يمكن الكتابة بشكل متتابع.alloc_page تطلب صفحة جديدة.buf->flag يتم تهيئتها افتراضيًا إلى PIPE_BUF_FLAG_CAN_MERGE، لأن الحالة الافتراضية تسمح بالكتابة المتتابعة على الصفحة.مفتاح استغلال الثغرة هو علامة PIPE_BUF_FLAG_CAN_MERGE غير المهيأة في splice، والتي تحدد ما إذا كان يمكننا الكتابة بشكل متتابع على صفحة pipe "غير المكتملة".
كما ذكر أعلاه، يدير pipe 16 صفحة كذاكرة تخزين مؤقت. طريقة النسخ الصفري في splice هي استبدال صفحة ذاكرة التخزين المؤقت للـ pipe بصفحة ذاكرة التخزين المؤقت للملف مباشرةً (تغيير مؤشر صفحة ذاكرة التخزين المؤقت للـ pipe للإشارة إلى صفحة ذاكرة التخزين المؤقت للملف).

مكدس استدعاءات استدعاء النظام splice إلى دالة الثغرة copy_page_to_iter_pipe عميق جدًا، ولن نحلله بالتفصيل. مكدس الاستدعاءات كالتالي: