Linuxer sind stolz auf das offene Entwicklungsmodell ihres Lieblingsbetriebsystems. Der Code ist für jedermann einsehbar und Hunderte Augen begutachten auf der Kernel-Mailingliste die Patches für jede Release. Dennoch schlummern auch im Linux-Kern einige unentdeckte Sicherheitslücken, die findige Zeitgenossen gelegentlich aufspüren. Gerade erregt ein Angriff über die Datei »/proc/Prozess-ID/mem«
Aufsehen.
Diese virtuelle Datei dient einem jeden Linux-Prozess zum Zugriff auf seinen Speicherbereich. Sie lässt sich wie eine reguläre Datei lesen und schreiben. Eine seit Mai 2011 bestehende Sicherheitslücke in diesem Mechanismus hatte zur Folge, dass ein lokaler Angreifer Rootrechte auf einem Linux-System erlangen kann. Ein geeigneter Exploit ist seit Mitte Januar im Umlauf. Vor der Kernelversion 2.6.39 verhinderte die Präprozessor-Anweisung »#ifndef mem_write«
das Schreiben auf »/proc/Prozess-ID/mem«
. Zur Begründung dieser wenig eleganten Lösung findet sich im Kernelquelltext der warnende Kommentar "Hier gibt es ein Sicherheitsrisiko", der die Brisanz solcher Schreibzugriffe widerspiegelt.
Inzwischen hatten die Kernelentwickler verschiedene Schutzmechanismen in die Funktion »mem_write()«
eingebaut. Daher dachten sie, es wäre nun sicher, das Ausführen der Funktion standardmäßig zu erlauben, und entfernten die Präprozessor-Anweisung in 2.6.39. Wie sich nun herausstellte, ist dieser Schutz löchrig und lässt sich für einen lokalen Root-Exploit ausnutzen.
Der Exploit-Code des Open-Source-Entwicklers Jason Donenfeld ist zwar recht kurz, aber trickreich [1]. Er bringt eine Set-UID-Root-Anwendung dazu, Shellcode in den eigenen Speicher zu schreiben und dann auszuführen. Natürlich könnte ein Angreifer dies auch mit jeder Anwendung machen, aber das Set-UID-Bit auf einer Root gehörenden Datei verschafft ihm Superuser-Rechte.
Wahl des Vehikels
Es gibt mehrere Binaries, die sich dazu verwenden ließen, wobei die Originalversion des Exploit das Programm »su«
einsetzt. Wie jeder Linux-Anwender weiß, gibt »su«
beim Aufruf mit einem nicht existierenden Benutzernamen eine Fehlermeldung auf der Standardfehlerausgabe aus. Der Exploit macht sich dies zunutze, indem er beliebigen Shellcode als Benutzernamen angibt und diesen dann von Stderr in »/proc/Prozess-ID/mem«
umleitet. Damit schreibt der Set-UID-Prozess den Shellcode in seinen Speicher und führt ihn aus. Was recht einfach klingt, erfordert aufgrund der Schutzvorkehrungen des Kernels jedoch einen trickreichen Exploit.
01 static ssize_t mem_write(struct file * file, const char __user *buf, size_t count, loff_t *ppos)
02 {
03 [...]
04 struct task_struct *task = get_proc_task(file->f_path.dentry->d_inode);
05 [...]
06 mm = check_mem_permission(task);
07 copied = PTR_ERR(mm);
08 if (IS_ERR(mm))
09 goto out_free;
10 [...]
11 if (file->private_data != (void *)((long)current->self_exec_id))
12 goto out_mm;
13 [...]
Insgesamt sind zwei Schutzmechanismen in »mem_write()«
implementiert. Die erste Sicherheitsüberprüfung stellt die Funktion »check_mem_permission()«
bereit (Listing 1). Sie verweist auf »__check_mem_permission()«
(Listing 2), die prüft, ob der schreibende Prozess auch tatsächlich der Eigentümer dieser »mem«
-Datei ist. Hierin besteht der hauptsächliche Schutz, der verhindert, dass Prozesse den Speicher anderer Prozesse manipulieren.
01 static struct mm_struct *__check_mem_permission(struct task_struct *task)
02 {
03 struct mm_struct *mm;
04
05 mm = get_task_mm(task);
06 if (!mm)
07 return ERR_PTR(-EINVAL);
08
09 /*
10 * A task can always look at itself, in case it chooses
11 * to use system calls instead of load instructions.
12 */
13 if (task == current)
14 return mm;
15 [...]
Die Sicherung 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. Daher enthält der Kernel zusätzlich einen zweiten Schutz, der auf dem Wert »self_exec_id«
beruht (Listing 3). Dieser zählt einfach, wie oft ein Prozess »exec()«
aufruft, was manche Attacke abfängt.
01 void setup_new_exec(struct linux_binprm * bprm)
02 {
03 [...]
04 /* An exec changes our domain. We are no longer part of the thread
05 group */
06
07 current->self_exec_id++;
08
09 flush_signal_handlers(current, 0);
10 flush_old_files(current->files);
11 }
Jason Donenfeld hat allerdings demonstriert, dass er die Kindprozesse auch via »fork()«
erzeugen und den Zähler »self_exec_id«
so steuern kann, dass die zweite Kontrolle umgangen wird. Die Schwachstelle hat Linus Torvalds am 17. Januar mit einem Kernelpatch geschlossen, das beispielsweise in Kernel 3.2.2 und 3.3 enthalten ist.
Ruck, zuck – Root
Eine ausführliche Beschreibung der Sicherheitslücke und des Exploit namens Mempodipper finden sich in Donenfelds Blog [1]. Wie Abbildung 1 zeigt, läuft das Programm bei ungepatchten Kernelversionen wie am Schnürchen – der Angreifer erhält einen Rootprompt.
Abbildung 1: Angriff mit Live-Kommentar: Ungepatchte Kernel, hier Version 3.0.0-12 in Ubuntu Oneiric, trickst der Mempodipper-Exploit von Jason Donenfeld ohne Weiteres aus.
Seit der ersten Veröffentlichung des Exploit haben sich interessante Befunde ergeben: Unter Gentoo beispielsweise dürfen normale User Set-UID-Dateien nicht lesen. Damit kann ein Angreifer auch den Einsprungspunkt für den Shellcode nicht via »objdump«
finden. Diese Einschränkung lässt sich aber umgehen, indem er zu Ptrace greift.
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. Address Space Layout Randomization (ASLR, [2]) ist eine Technik, die Programmen einen zufälligen Adressraum zuweist. Dadurch lassen sich Speicheradressen nicht mehr vorhersagen.
Das Verfahren kam zuerst in Open BSD zum Einsatz und ist seit Windows Vista auch Bestandteil der Microsoft-Betriebssysteme. Linux enthält ASLR seit Kernelversion 2.6.12.