OS X 10.10: Schwachstelle in DYLD_PRINT_TO_FILE

Stefan Esser, Co-Autor von "The iOS Hacker's Handbook" berichtet von einem Bug in OS X 10.10 Yosemite, der normalen Benutzern Root-Rechte ermöglichen kann. Das Problem ist eine neue nicht korrekt abgesicherte Umgebungsvariable namens DYLD_PRINT_TO_FILE, die von dyld, dem dynamischen Linker des Systems, benutzt wird.

Der Bug wurde mit dem Update auf 10.10.5 von Apple inzwischen behoben.

Ein dynamische Linker sorgt dafür, daß benötigte externe Programm-Bibliotheken einem Programm zur Laufzeit zur Verfügung stehen. Mac OS X verwendet seit seiner ersten Version 10.0 den dyld für diese Aufgabe.

Bereits in meinem Analyse-Artikel über den Flashback-Trojaner, den ich für heise geschrieben hatte, erwähnte ich den dyld.

Umso mehr wundert es mich, daß heise in seinem Artikel über die von Stefan dokumentierte Schwachstelle schreibt, dyld wäre mit 10.10 eingeführt worden. Auch auf apfeltalk mußte ich das bemängeln. Offenbar haben die Redakteure weder die Originalquelle gelesen noch eine Ahnung, wie OS X seit Ewigkeiten schon funktioniert.

Mit 10.10 Yosemite wurde dem dyld unter anderem zusätzlich die neue Umgebungsvariable DYLD_PRINT_TO_FILE spendiert. Diese erlaubt dem Linker, seine Fehlerausgaben in eine beliebige Datei zu schreiben. Standardmäßig schreibt er Fehler auf stderr, dem Standard-Error-Stream, raus.

Solche Tatsachen halten jedoch auch bekannte Security-Seiten wie "packet storm" nicht davon ab, Unfug zu schreiben. Packet Storm erzählt, die Verwundbarkeit würde 10.10.4 und früher betreffen. Auch dort werden die Artikel anscheinend von Katzen geschrieben, die über die Tastatur huschen. Jedenfalls haben sie ihre sagenhaften drei Zeilen nicht ohne diesen fachlichen Fehler hinbekommen. Möglicherweise haben sie es bei Metasploit abgeschrieben. Die problematische Variable wurde jedenfalls erst mit 10.10.0 eingeführt.

Normalerweise wendet der Linker Umgebungsvariablen nicht auf sicherheitskritische Programme an, damit deren Verhalten nicht manipuliert werden kann durch Unbefugte. Sicherheitskritisch sind zum Beispiel SUID-Programme, also solche, die unabhängig davon, wer sie startet, unter dem Root-User laufen, um ihre Aufgabe erledigen zu können. Ruft man ein solches Programm mit einer Umgebungsvariablen auf, wird diese ignoriert.

In dem Fall der neuen Umgebungsvariable DYLD_PRINT_TO_FILE jedoch findet die Prüfung, ob ein sicherheitsrelevantes Programm aufgerufen wurde, laut Stefan nicht statt, weil sie nicht wie die anderen Variablen in der processDyldEnvironmentVariable() Funktion behandelt wird, sondern direkt in der Hauptfunktion.

Ich bin mit Stefans Darstellung in diesem Punkt nicht ganz glücklich, weil es bei ihm so klingt, als würde processDyldEnvironmentVariable() selbst die Bereinigung vornehmen. Das Säubern passiert allerdings in pruneEnvironmentVariables(), nachdem checkLoadCommandEnvironmentVariables() processDyldEnvironmentVariable() bereits aufgerufen hatte. Zu sehen in Apples Open Source zu dyld.

Anekdote 1

Der dyld ist übrigens in C++ geschrieben. Der ganze Kernel ist C und C++. Die GUI-Libs sind C und Objective-C. Noch ein Grund, warum Apple nicht auf die C-Sprachen verzichten kann mit Swift.

Und der Fehler würde in jeder Sprache passieren, weil es ein logischer Fehler ist, wann was geprüft werden muß, und keine Eigenschaft der Programmiersprache.

Anekdote 2

Manche Entwickler, die diese Sicherheits-Funktion von dyld nicht kennen, halten das sogar für einen Bug. Zumindest sind sie genervt davon, daß das Ignorieren von Umgebungsvariablen auf der Shell zurückgemeldet wurde, wenn sicherheitsrelevante (SUID)-Binaries vor Umgebungsvariablen des dyld geschützt wurden.

Im dyld-Quellcode steht dann auch als Kommentar: "Disable warnings about DYLD_ env vars being ignored. The warnings are causing too much confusion."

In der processDyldEnvironmentVariable Funktion steht sogar dieser Kommentar für DYLD_PRINT_TO_FILE:

// handled in _main()

Es ist also kein Versehen. In 10.11 wäre das Problem laut Stefan allerdings behoben worden, indem DYLD_PRINT_TO_FILE wie alle anderen Umgebungsvariablen in processDyldEnvironmentVariable behandelt wird.

In einem Tweet bringt Stefan dieses Beispiel:

echo 'echo "$(whoami) ALL=(ALL) NOPASSWD:ALL" >&3' | DYLD_PRINT_TO_FILE=/etc/sudoers newgrp; sudo -s

Diese Zeile bedarf etwas Erklärung. Sie besteht aus zwei Befehlen, die nacheinander ausgeführt werden. Erst der links vom Semikolon, dann der rechts davon. Der linke Befehle besteht seinerseits aus vier kombinierten Kommandos: newgrp, whoami und zweimal echo.

  1. Mit newgrp wird eine Sub-Shell geöffnet. Optional kann dabei die User-Gruppe gewechselt werden. Das ist der Grund, warum newgrp ein SUID-Binary sein muß. Aber diese Option wird hier nicht benötigt. Es geht nur darum, den Umstand auszunutzen, daß newgrp unter Root startet und wir ihm etwas unterschieben wollen, das er für uns tun soll. Dazu wird newgrp mit DYLD_PRINT_TO_FILE=/etc/sudoers aufgerufen. Dadurch werden alle Debug-Ausgaben von newgrp in /etc/sudoers geschrieben. Böse. Da die Datei nun als Debug-Log verwendet wird, werden Einträge an ihrem Ende eingefügt. Weil newgrp unter Root startet, hat es die Berechtigung, die sudoers-Datei zu öffnen. Die zur Debug-Ausgabe geöffnete Datei merkt sich die Shell in einem File-Descriptor. Die Standard-File-Descriptoren sind 0 für Standardeingabe, 1 für Standardausgabe und 2 für Fehleraugaben. Der nächste freie Descriptor ist also die 3. Und der wird darum für diese neu geöffnete Datei verwendet. Die von newgrp geöffnete Sub-Shell, die nicht mehr unter Root, sondern dem aktuellen User läuft, erbt diesen Dateiverweis und kann ihn verwenden.
  2. Der Befehl whoami gibt den Namen des aktuellen Benutzers aus. Hier wird whoami jedoch mit $(whoami) benutzt. Das ist eine Kommandoersetzung (Command Substitution), die das Kommando mit seiner Ausgabe ersetzt. Das wird in geschachtelten Kommandos verwendet wie hier von echo. Man könnte anstelle von $(whoami) auch `whomai` schreiben.
  3. Der innere echo-Befehl gibt einen Text aus, der aus der Rückgabe von whoami besteht und dem festen Ende " ALL=(ALL) NOPASSWD:ALL". In meinem Fall also "macmark ALL=(ALL) NOPASSWD:ALL". Wie man sieht, wurde der Aufruf von whoami durch seine Rückgabe "macmark" ersetzt. Diese Zeile entspricht der Syntax im sudoers-File, wo festgelegt wird, wer in welcher Weise sudo verwenden darf. Mit sudo kann man einen Befehl unter einem anderen Benutzer (eventuell auch root, je nach Konfiguration) absetzen. Wenn man nun "macmark ALL=(ALL) NOPASSWD:ALL" in /etc/sudoers am Ende eintragen würde, dann bedeutet das: Der User macmark darf jedes Kommando auf jedem Host als jeder User ohne Paßwort-Eingabe absetzen.
  4. Der äußere echo-Befehl übergibt per Pipe | einen Text, den der innere echo-Befehl erzeugt, in die Subshell, die von newgrp geöffnet wurde. Die Pipe | sorgt dafür, daß die Ausgabe von echo als Eingabe für den Befehl rechts von der Pipe verwendet wird. Allerdings passiert das nicht, denn die komplette Standardausgabe (hier von echo) wird durch >&3 in die Datei geleitet, die zu File-Descriptor 3 gehört, also in /etc/sudoers. Nochmal böse, denn dieser echo Befehl läuft nicht unter root, hat nun aber Zugriff auf eine Datei, die nur root gehört. Details zur Umleitungs-Syntax: Das > besorgt die Umleitung. Links von > steht kein File-Descriptor, also wird die Standardeingabe umgeleitet. Rechts von > steht &3, also wird in die Datei, die zu File-Descriptor 3 gehört, umgeleitet. Die Sub-Shell endet dann, weil kein weiterer Befehl mehr kommt.

Das innere echo erzeugt also eine Zeile, die in die suoders-Datei geschrieben werden soll. Das äußere echo schreibt die Zeile des inneren echo in den File-Descriptor 3. Dieser File-Descriptor 3 ist die offene /etc/sudoers Datei, weil die durch DYLD_PRINT_TO_FILE für newgrp geöffnet wurde. Und newgrp konnte die öffnen, weil es unter root startet.

Danach wird mit sudo -s dem aktuellen User eine Root-Shell geöffnet. Das funktioniert, weil es jetzt so erlaubt ist laut der geänderten sudoers-Datei.

In dem Zusammenhang kann ich auf diese Artikel verweisen: Bash One-Liners Explained, Part III: All about redirections, um mehr über File-Descriptoren und Umleitungen zu lernen. Und Tweet sized Mac OS X 10.10 exploit sowie How does the DYLD privilege escalation vulnerability work on OS X?.

Das eine Problem ist, daß der Linker die DYLD_PRINT_TO_FILE an newgrp weitergibt und damit ein Root-Prozeß kompromittiert werden kann durch den Inhalt dieser Umgebungsvariablen. Und für alle anderen SUID-Binaries würde der Linker dasselbe tun. Der Linker dyld müßte die Umgebungsvariable für newgrp (und andere SUID-Binaries) droppen.

Das andere Problem ist, daß der File-Descriptor 3, den das SUID-Binary newgrp als Root öffnet, in der von newgrp erzeugten Sub-Shell, die unter dem normalen User läuft, weiter verwendet werden kann. So kann der User in eine Datei schreiben, die normalerweise nicht in seinem Zugriff steht. Hier wäre es besser, wenn der File-Desciptor, den Root geöffnet hat, nicht an Nicht-Root-Sub-Shells weitergegeben würde.

Ich würde mir wünschen, daß solche Änderungen an sensiblen Code-Stellen von kompetenten Leuten bei Apple kontrolliert werden, bevor sie auf die Menschheit losgelassen werden. Wer kommt nur auf die Idee, die eine Variable anders zu behandeln als alle anderen und dies sogar noch als Kommentar reinzuschreiben? War da ein Neuling ohne Gegenkontrolle am Werk?

Stefan bietet als Übergangslösung, bis Apple das auch in Yosemite und nicht nur in El Capitan behebt, einen Kernel-Treiber an, der das Problem lösen kann. Allerdings behebt dies nicht das eigentliche Problem und könnte durch einen gegnerischen Treiber wieder ausgehebelt werden.

Valid XHTML 1.0!

Besucherzähler


Latest Update: 11. September 2015 at 19:48h (german time)
Link: cnc.realmacmark.de/blog/osx_blog_2015-07-b.php