
استغلال CVE-2023-2598 المتعلق بـ io_uring
io_uring هي واجهة استدعاء نظام لنظام Linux. لقد دعمت حتى الآن تقريبًا جميع استدعاءات النظام، وليس فقط read() و write في البداية. إنها تتيح للتطبيق بدء استدعاءات النظام التي يمكن تنفيذها بشكل غير متزامن.
في قلب كل تطبيق io_uring يوجد مخزنان حلقيان - قائمة انتظار الإرسال (SQ) وقائمة انتظار الإكمال (CQ). هذه المخازن الحلقية مشتركة بين التطبيق والنواة.
يمكننا الحصول على إدخال في قائمة انتظار الإرسال (SQE) يصف استدعاء النظام الذي تريد تنفيذه بواسطة io_uring_get_sqe. ثم يقوم التطبيق بتنفيذ استدعاء النظام io_uring_enter لإخبار النواة بوجود عمل ينتظر في قائمة انتظار الإرسال.
بعد أن تقوم النواة بتنفيذ العملية، تضع إدخال في قائمة انتظار الإكمال (CQE) في المخزن الحلقي لقائمة انتظار الإكمال والذي يمكن للتطبيق استهلاكه بعد ذلك.
تقوم الدالة io_sqe_buffer_register بتنفيذ تخطيط الصفحات الافتراضية والعناوين الفعلية.
يجب توضيح بعض المفاهيم أولاً.
يبدأ التطبيق طلبًا لمخزن مؤقت بواسطة io_uring_register. سلسلة الاستدعاءات كالتالي:
io_uring_register_buffers->io_uring_register->io_sqe_buffers_register
الكود المصدري للدالة io_sqe_buffers_register كما يلي:
int io_sqe_buffers_register(struct io_ring_ctx *ctx, void __user *arg,
unsigned int nr_args, u64 __user *tags)
{
struct page *last_hpage = NULL;
struct io_rsrc_data *data;
int i, ret;
struct iovec iov;
BUILD_BUG_ON(IORING_MAX_REG_BUFFERS >= (1u << 16));
if (ctx->user_bufs)
return -EBUSY;
if (!nr_args || nr_args > IORING_MAX_REG_BUFFERS)
return -EINVAL;
ret = io_rsrc_node_switch_start(ctx);
if (ret)
return ret;
ret = io_rsrc_data_alloc(ctx, io_rsrc_buf_put, tags, nr_args, &data);
if (ret)
return ret;
ret = io_buffers_map_alloc(ctx, nr_args);
if (ret) {
io_rsrc_data_free(data);
return ret;
}
for (i = 0; i < nr_args; i++, ctx->nr_user_bufs++) {
if (arg) {
ret = io_copy_iov(ctx, &iov, arg, i);
if (ret)
break;
ret = io_buffer_validate(&iov);
if (ret)
break;
} else {
memset(&iov, 0, sizeof(iov));
}
if (!iov.iov_base && *io_get_tag_slot(data, i)) {
ret = -EINVAL;
break;
}
ret = io_sqe_buffer_register(ctx, &iov, &ctx->user_bufs[i],
&last_hpage);
if (ret)
break;
}
WARN_ON_ONCE(ctx->buf_data);
ctx->buf_data = data;
if (ret)
__io_sqe_buffers_unregister(ctx);
else
io_rsrc_node_switch(ctx, NULL);
return ret;
}
في هذه الدالة، سننتقل إلى io_sqe_buffer_register. وسنجد خطأً منطقيًا. الكود المصدري للدالة io_sqe_buffer_register كما يلي:
static int io_sqe_buffer_register(struct io_ring_ctx *ctx, struct iovec *iov,
struct io_mapped_ubuf **pimu,
struct page **last_hpage)
{
struct io_mapped_ubuf *imu = NULL;
struct page **pages = NULL;
unsigned long off;
size_t size;
int ret, nr_pages, i;
struct folio *folio = NULL;
*pimu = ctx->dummy_ubuf;
if (!iov->iov_base)
return 0;
ret = -ENOMEM;
pages = io_pin_pages((unsigned long) iov->iov_base, iov->iov_len,
&nr_pages);
if (IS_ERR(pages)) {
ret = PTR_ERR(pages);
pages = NULL;
goto done;
}
/* If it's a huge page, try to coalesce them into a single bvec entry */
if (nr_pages > 1) {
folio = page_folio(pages[0]);
for (i = 1; i < nr_pages; i++) {
if (page_folio(pages[i]) != folio) {
folio = NULL;
break;
}
}
if (folio) {
folio_put_refs(folio, nr_pages - 1);
nr_pages = 1;
}
}
imu = kvmalloc(struct_size(imu, bvec, nr_pages), GFP_KERNEL);
if (!imu)
goto done;
ret = io_buffer_account_pin(ctx, pages, nr_pages, imu, last_hpage);
if (ret) {
unpin_user_pages(pages, nr_pages);
goto done;
}
off = (unsigned long) iov->iov_base & ~PAGE_MASK;
size = iov->iov_len;
/* store original address for later verification */
imu->ubuf = (unsigned long) iov->iov_base;
imu->ubuf_end = imu->ubuf + iov->iov_len;
imu->nr_bvecs = nr_pages;
*pimu = imu;
ret = 0;
if (folio) {
bvec_set_page(&imu->bvec[0], pages[0], size, off);
goto done;
}
for (i = 0; i < nr_pages; i++) {
size_t vec_len;
vec_len = min_t(size_t, size, PAGE_SIZE - off);
bvec_set_page(&imu->bvec[i], pages[i], vec_len, off);
off = 0;
size -= vec_len;
}
done:
if (ret)
kvfree(imu);
kvfree(pages);
return ret;
}
هنا أذكر فقط بضع نقاط مهمة.
imu يعني عنوان/صفحة افتراضية.page يعني عنوان/صفحة فعلية.folio يعني مجموعة من الصفحات المتجاورة فيزيائيًا، لمنع الموقف عندما يتم استدعاء دالة وتحتوي معاملاتها على صفحة، ولكن هذه الصفحة تنتمي إلى نطاق مستمر من الصفحات، لكننا لسنا متأكدين مما إذا كان يجب استخدام الصفحة بأكملها أو صفحة واحدة.struct iovec -> مجرد بنية تصف مخزنًا مؤقتًا، بها عنوان بداية المخزن المؤقت وطوله. لا أكثر.io_mapped_ubuf هي بنية تحمل معلومات حول مخزن مؤقت تم تسجيله في مثيل io_uring.struct io_mapped_ubuf {
u64 ubuf; // العنوان الذي يبدأ عنده المخزن المؤقت
u64 ubuf_end; // العنوان الذي ينتهي عنده
unsigned int nr_bvecs; // عدد bio_vec(s) المطلوبة لمعالجة المخزن المؤقت
unsigned long acct_pages;
struct bio_vec bvec[]; // مصفوفة من bio_vec(s)
};
العضو bio_ver هو struct يشبه iovec لكن للذاكرة الفعلية.
...
/* If it's a huge page, try to coalesce them into a single bvec entry */
if (nr_pages > 1) { // إذا كان هناك أكثر من صفحة واحدة
folio = page_folio(pages[0]); // تحويل من صفحة إلى folio
// يعيد الـ folio الذي يحتوي على هذه الصفحة
for (i = 1; i < nr_pages; i++) {
if (page_folio(pages[i]) != folio) { // folios مختلفة -> غير متجاورة فيزيائيًا
folio = NULL; // تعيين folio إلى NULL لأننا لا نستطيع الدمج في إدخال واحد
break;
}
}
if (folio) { // إذا كانت كل الصفحات في نفس الـ folio
folio_put_refs(folio, nr_pages - 1);
nr_pages = 1; // تعيين nr_pages إلى 1 لأنه يمكن تمثيلها كصفحة folio واحدة
}
}
...
الكود الذي يتحقق مما إذا كانت الصفحات من نفس الـ folio لا يتحقق فعليًا مما إذا كانت متتالية. يمكن أن تكون نفس الصفحة مخطوطة عدة مرات. أثناء التكرار، ستعيد page_folio(page) نفس الـ folio مرارًا وتكرارًا متجاوزة الفحوصات. هذا خطأ منطقي واضح. دعنا نكمل مع io_sqe_buffer_register ونرى العواقب.
...
imu = kvmalloc(struct_size(imu, bvec, nr_pages), GFP_KERNEL);
// يخصص imu بمصفوفة لعدد nr_pages من bio_vec(s)
// bio_vec - نطاق متصل من عناوين الذاكرة الفعلية
// نحتاج bio_vec لكل صفحة (فعلية)
// في حالة folio - ستكون مصفوفة bio_vec(s) بحجم 1
if (!imu)
goto done;
ret = io_buffer_account_pin(ctx, pages, nr_pages, imu, last_hpage);
if (ret) {
unpin_user_pages(pages, nr_pages);
goto done;
}
off = (unsigned long) iov->iov_base & ~PAGE_MASK;
size = iov->iov_len; // تعيين الحجم إلى ذلك الذي تم تمريره من قبل المستخدم!
/* store original address for later verification */
imu->ubuf = (unsigned long) iov->iov_base; // يتحكم به المستخدم
imu->ubuf_end = imu->ubuf + iov->iov_len; // حساب النهاية بناءً على الطول
imu->nr_bvecs = nr_pages; // سيكون 1 في حالة folio
*pimu = imu;
ret = 0;
if (folio) { // في حالة folio - نحتاج فقط إلى bio_vec واحد (فعّال!)
bvec_set_page(&imu->bvec[0], pages[0], size, off);
goto done;
}
for (i = 0; i < nr_pages; i++) {
size_t vec_len;
vec_len = min_t(size_t, size, PAGE_SIZE - off);
bvec_set_page(&imu->bvec[i], pages[i], vec_len, off);
off = 0;
size -= vec_len;
}
done:
if (ret)
kvfree(imu);
kvfree(pages);
return ret;
}
يتم تخصيص bio_vec واحد كـ nr_pages = 1. حجم المخزن المؤقت المكتوب في pimu->iov_len و pimu->bvec[0].bv_len هو ذلك الذي تم تمريره بواسطة المستخدم في iov->iov_len.
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <liburing.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <mqueue.h>
#include <sys/syscall.h>
#include <sys/resource.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <sched.h>
#include <string.h>