Linux-Kernel: Lokaler Root-Exploit über /proc/PID/mem

Die Datei “/proc/PID/mem” dient unter Linux dem Zugriff eines Prozesses auf seinen Speicherbereich. Sie lässt sich wie eine reguläre Datei lesen und schreiben. Eine Sicherheitslücke in diesem Mechanismus hat zur Folge, dass ein lokaler Angreifer Root-Rechte auf dem System erlangen kann. Ein Exploit hierfür ist seit mehr als einer Woche im Umlauf.

Das Schreiben auf “/proc/PID/mem” wurde vor Kernel-Version 2.6.39 durch die Präprozessor-Anweisung “#ifndef mem_write” konsequent verboten. Nur bei gesetztem “mem_write” war die Anwendung der Funktion “mem_write()” erlaubt, die für das Schreiben in die virtuelle Datei verantwortlich ist. Zur Begründung dieser uneleganten Lösung findet sich im Kernel-Code der Kommentar “Es handelt sich um ein Sicherheitsrisiko”, der die Brisanz solcher Schreibzugriffe widerspiegelt.

Seit Version 2.6.39 haben die Kernelentwickler verschiedene Schutzmechanismen in die Funktion “mem_write()” eingebaut. Daher dachten sie, es wäre nun sicher, “mem_write()” per Default zu erlauben, und entfernten die Präprozessor-Anweisung aus dem Kernel. Wie sich allerdings herausstellte, ist dieser Schutz löchrig und lässt sich für einen lokalen Root-Exploit ausnutzen. Ohne entsprechenden Fix sind alle Kernel-Versionen seit 2.6.39 von dieser Sicherheitslücke betroffen.

Der Exploit-Code ist zwar recht kurz, aber trickreich. Zentrale Idee ist es, eine Set-UID-Root-Anwendung dazu zu bringen, Shell-Code in den eigenen Speicher zu schreiben und dann auszuführen. Natürlich könnte ein Angreifer dies auch mit einer normalen Anwendung machen, aber das Set-UID-Bit auf einer Root gehörenden Datei verschafft ihm Superuser-Rechte.

Es gibt mehrere mögliche Binaries, die sich dazu verwenden lassen, wobei die Originalversion des Exploits das Programm “su” verwendet. Wie jeder Linux-Anwender weiß, gibt “su” beim Aufruf mit einem nicht-existieren Benutzernamen eine Fehlermeldung auf “stderr” aus. Der Exploit macht sich dies zunutze, indem er beliebigen Shellcode als Benutzernamen angibt und diesen dann statt von “stderr” nach “/proc/PID/mem” umleitet. Damit schreibt der Set-UID-Prozess den Shellcode in seinen Speicher und führt ihn aus. Was recht einfach klingt, erfordert aufgrund diverser Schutzmechanismen im Kernel jedoch einen recht trickreichen Exploit.

Insgesamt sind zwei Schutzmechanismen in “mem_write()” implementiert:

static ssize_t mem_write(struct file * file, const char __user *buf, size_t count, loff_t *ppos)
{
... struct task_struct *task = get_proc_task(file->f_path.dentry->d_inode);
... mm = check_mem_permission(task); copied = PTR_ERR(mm); if (IS_ERR(mm)) goto out_free;
... if (file->private_data != (void *)((long)current->self_exec_id)) goto out_mm;
...

Der erste Check wird durch die Funktion “check_mem_permission()” bereitgestellt. Diese verweist wiederum auf “__check_mem_permission()”:

static struct mm_struct *__check_mem_permission(struct task_struct *task)
{ struct mm_struct *mm; mm = get_task_mm(task); if (!mm) return ERR_PTR(-EINVAL); /* * A task can always look at itself, in case it chooses * to use system calls instead of load instructions. */ if (task == current) return mm;
...

Hier wird geprüft, ob der schreibende Prozess auch tatsächlich der Eigentümer dieser “mem”-Datei ist. Das ist der Hauptschutz, der verhindert, dass fremde Prozesse den Speicher anderer Prozesse manipulieren.

Dieser lässt sich aber umgehen: Wenn ein Prozess “exec()” aufruft, um beispielsweise “su” auszuführen, so ist ein Schreibzugriff auf den Speicher von “su” durch Stderr-Umleitung möglich. Zusätzlich dazu enthält der Kernel jedoch einen zweiten Schutz, der auf dem Wert “self_exec_id” beruht. Dieser Wert zählt einfach, wie oft ein Prozess “exec()” aufruft:

void setup_new_exec(struct linux_binprm * bprm)
{
/* massive amounts of code trimmed for the purpose of this blog post */ /* An exec changes our domain. We are no longer part of the thread group */ current->self_exec_id++; flush_signal_handlers(current, 0); flush_old_files(current->files);
}

Durch die Kontrolle von “self_exec_id” ist obige “exec()”-basierte Attacke unterbunden. Der Open-Source-Entwickler Jason Donenfeld hat allerdings demonstriert, dass man die Kindprozesse auch via “fork()” erzeugen und den Zähler “self_exec_id” so steuern kann, dass die zweite Kontrolle auch umgangen wird.

Die Schwachstelle wurde durch einen Fix von Linus Torvalds am 17. Januar in einem Kernel-Patch geschlossen.

Eine ausführliche Beschreibung der Sicherheitslücke und ein Exploit namens Mempodipper finden sich in Donenfelds Blog. Der veröffentliche Exploit enthält sowohl 32- als auch 64-Bit-Shellcode. Seit der ersten Veröffentlichung gab es mehrere Updates: Unter Gentoo ist es beispielsweise für normale User nicht möglich Set-UID-Dateien zu lesen. Damit ist dann aber nicht möglich, den Einsprungspunkt für den Shellcode via “objdump” zu finden. Das lässt sich jedoch einfach mit Ptrace umgehen. Fedora schützt die “su”-Datei noch besser, denn sie ist dort mit PIE (Position Independent Executables) kompiliert. Das hat zur Folge, dass der Adressraum der Dateien durch Address Space Layout Randomization zufällig platziert wird, und es damit nicht möglich ist, den Shellcode korrekt zu platzieren. Dies läßt sich jedoch umgehen, indem man für die Attacke “gpasswd” verwendet.

Address Space Layout Randomization (ASLR)

Insbesondere Shellcode-basierte Exploits benötigen Informationen über den Adressraum des attackierten Programms. Beispielsweise ist es bei Buffer-Overflow-Attacken notwendig, die Adresse des Puffers zu kennen, um den Shellcode entsprechend zu platzieren. ASLR ist eine Technik, die Programmen einen zufälligen Adressraums zuweist. Dadurch können Speicheradressen nicht mehr vorhergesagt werden. ASLR wurde erstmals in OpenBSD eingesetzt und ist seit Windows Vista auch Bestandteil der Microsoft-Betriebssysteme. Linux enthält ASLR seit der Kernelversion 2.6.12.

Nach oben