Local Privilege Escalation (LPE) Bugs werden leider recht häufig gefunden. Sie ermöglichen einem normalen User die Erlangung von Root-Rechten. Das ermöglicht auch einer Malware, die unter einem normalen User läuft, dann mehr Schaden anrichten zu können, wenn sie einen solchen Bug ausnutzt. In diesem Artikel schaue ich mir einen konkreten Fall an.
Was mich an dem Exploit zu diesem Fall besonders interessiert ist:
Schon 2014 wurde der Trend hin zu LPE-Bugs auf allen Plattformen beobachtet. Den Grund dafür sieht Roberto Paleari darin, daß im Kernel weniger Schutzfunktionen Verwendung finden oder dort erst später implementiert wurden als im Userland. Ich sehe den wesentlichen Grund schlicht darin, daß ein Bug im Kernel dem Angreifer einfach deutlich mehr Spielraum gibt, was die Schadfunktion als auch die Versteckmöglichkeit angeht.
LPE-Bugs sind allerdings nur dann notwendig, wenn der User nicht schon mit Systemrechten arbeitet. Darum ist bei Windows die Frage, ob der Bug im Kernel ist oder nicht, relativ egal, weil der typische Windows-User bereits System-Rechte hat. Wem das nicht klar ist und in UAC etwas anderes als eine Erziehungsmethode für Entwickler sieht, sollte UAC, das Sicherheits-Plazebo lesen. Wo man bei den Unix-Systemen also erst noch einen Local-Root-Exploit braucht, um an Systemrechte zu kommen, kann man bei Windows direkt ans Eingemachte. Insofern ist ein LPE-Bug zwar schlimm, aber trotzdem Jammern auf hohem Niveau, weil man damit in dem Punkt auf Windows-Niveau herunterfällt.
Zum Ausnutzen von Speicherfehlern muß man als Angreifer noch das ASLR-Versteckspiel gewinnen. Man muß herausfinden, wo man den gewünschten Code findet, den man anspringen möchte. Das ist jedoch noch nie eine ernsthafte Hürde auf irgendeiner Plattform gewesen.
Wenn ASLR also durch einen weiteren Bug oder ein Info-Leak überwunden wird, dann sollte das niemanden wundern. Allerdings ist es auf den Plattformen unterschiedlich einfach, ASLR im Kernel zu umgehen. Auf Apples Systemen relativ einfach und auf anderen Systemen super einfach.
Denn wie man nachlesen kann in Ich habe zwar 99 Probleme, aber ein (Windows-) Kernel Pointer ist keins. Da gibt es eine Informations-Leck Party in Ring 0, nehmen laut Alex Ionescu iOS und OS X Kernel-Info-Leaks (KASLR Bypass) sehr ernst, aber Windows nicht und Linux auch nicht. Ich habe das auch hier erwähnt.
Hier geht es um einen LPE-Bug in OS X, den Luca Todesco gefunden hat. Der Bug ist zwischen 10.9.5 und 10.10.5 einschließlich auftritt. In El Capitan 10.11 ist er nicht mehr vorhanden. Den Artikel von The Register ist wohl nicht übel, denn den findet Luca selbst ganz gut. Inzwischen hat er einen eigenen Artikel über den Bug nachgereicht.
Luca ist mit seinen 18 Jahren ziemlich gut und hat im Gegensatz zu anderslautenden Meldungen, Apple immerhin kurz vor Veröffentlichung informiert. Man sollte jedoch davon ausgehen, daß die bösen Jungs schon vorher den Bug gefunden haben.
Heise schreibt in diesem Zusammenhang, Apple würde das Rooting durch den Rootless-Modus in El Capitan erschweren. Das ist jedoch falsch, denn Rootless (SIP, System Integrity Protection) erschwert nicht das Rooting, also daß man unautorisiert Root werden kann, sondern schränkt den Root-User ein im Hinblick darauf, welche Dateien selbst dieser nicht verändern darf.
Das ist jedoch schwer wasserdicht zu bekommen, weil man oft die ausreichend berechtigten Programmteile mißbrauchen kann. Außerdem läßt sich SIP (Rootless) deaktivieren. Allerdings braucht man dazu eine Kernel Extension, die passend signiert sein muß. Das dafür nötige Zertifikat erteilt Apple nur individuell auf Anfrage.
Wenn solche Bugs gefunden werden, schaffen sie es in der Regel nur in die Schlagzeilen der Presse, wenn es um Apple geht. Dabei sind solche Probleme regelmäßig auch im Linux-Kernel und Android vorhanden. Der hier zum Beispiel zeitgleich mit dem von Luca.
Luca hat sogar zwei Lösungen für das von ihm entdeckte Problem veröffentlicht:
CR3 ist bei diesen Intel-CPUs das Control-Register, das für die Page Tables der Speicherverwaltung zuständig ist. Mit “shared CR3” wird dieses Register gemeinsam benutzt für Kernel und Userland, wodurch sie nicht mehr in getrennten Adreßräumen liegen. Dies beschleunigt den Userland/Kernel-Übergang, weil der Kernel dann keine Spezial-Routinen aufrufen muß, um User-Memory zu lesen.
Offenbar kommt nicht jede Software mit no_shared_cr3 klar. Wenn man das setzt, läuft laut einem Leserbericht VirtualBox nicht mehr. Killt man den hängenden VirtualBox-Prozeß, dann bekommt man sogar eine Kernel-Panic.
Bei 32-Bit-Software hingegen wird auf OS X der Kernel-Adreßraum nicht mit dem User-Adreßraum eines User-Programms geteilt. Bei 64-Bit-Software auf OS X passiert das jedoch standardmäßig, weil bei 64 Bits ausreichend Adreßraum verfügbar ist. Hier wird aber Sicherheit für Geschwindigkeit geopfert. Und bevor jemand schreit, das wäre woanders besser: Das Gegenteil ist der Fall, denn auf Windows und auf GNU/Linux haben nicht nur 64-, sondern sogar 32-Bit-Programme einen gemeinsamen Adreßraum mit dem jeweiligen Kernel. Dort dann mit dem Nachteil, daß 32-Bit User-Programme nicht die vollen 4GB Speicher nutzen können, selbst wenn man soviel eingebaut hat. Vergleiche dazu in Mac OS X and iOS Internals: To the Apple's Core von Jonathan Levi die Seiten 133 und 266.
Diese Kernel/User-Adreßraum-Trennung ist offenbar standardmäßig in iOS aktiviert.
By default the linker creates an unreadable segment starting at address zero named __PAGEZERO. Its existence will cause a bus error if a NULL pointer is dereferenced. The argument size is a hexadecimal number with an optional leading 0x. If size is zero, the linker will not generate a page zero segment. By default on 32-bit architectures the page zero size is 4KB. On 64-bit architectures, the default size is 4GB. The ppc64 architecture has some special cases. Since Mac OS X 10.4 did not support 4GB page zero programs, the default page zero size for ppc64 will be 4KB unless -macosx_version_min is 10.5 or later. Also, the -mdynamic-no-pic codegen model for ppc64 will only work if the code is placed in the lower 2GB of the address space, so the if the linker detects any such code, the page zero size is set to 4KB and then a new unreadable trailing segment is created after the code, filling up the lower 4GB.
The static linker creates a __PAGEZERO segment as the first segment of an executable file. This segment is located at virtual memory location 0 and has no protection rights assigned, the combination of which causes accesses to NULL, a common C programming error, to immediately crash. The __PAGEZERO segment is the size of one full VM page for the current architecture (for Intel-based and PowerPC-based Macintosh computers, this is 4096 bytes or 0x1000 in hexadecimal). Because there is no data in the __PAGEZERO segment, it occupies no space in the file (the file size in the segment command is 0).
Luca verwendet für seinen Exploit ein 32-Bit-Programm ohne __PAGEZERO Segment. Ein 64-Bit Programm hingegen müßte zwingend so ein __PAGEZERO Segment haben. Dieses Segment sorgt dafür, daß ein NULL-Pointer, also ein Zeiger auf die virtuelle Speicher-Adresse 0, nicht verwendet werden kann von dem Programm, weil der Kernel den Zugriff darauf verwehrt, da es als unlesbar markiert ist.
Der Make-Befehl für Exploit sieht so aus:
gcc *.m -o tpwn -framework IOKit -framework Foundation -m32 -Wl,-pagezero_size,0 -O3
strip tpwn
Das -pagezero_size ohne Parameter sorgt dafür, daß kein __PAGEZERO Segment erzeugt wird. Damit gibt es kein Problem, wenn man einen NULL Pointer benutzt. Das -m32 sorgt dafür, daß 32-Bit Code erzeugt wird.
Im Kernel ist Luca eine Stelle aufgefallen, die unter bestimmten Umständen vom User-Land aus, also einem Programm, das unter Nicht-Root läuft, so aufgerufen werden kann, daß eine nachgelagerte Stelle im Kernel einen NULL-Pointer verwendet. Luca sorgt in seinem Programm dafür, daß an Adresse 0 Daten liegen, die der Daten-Struktur (task_t) entsprechen, die der Kernel an der Programmstelle erwarten würde. Dafür mappt er NULL im Userland. Die Daten an virtueller Adresse 0 im Userland werden somit dann vom Kernel benutzt, um eine Speicherstelle zu verändern, deren Adresse aus dieser Datenstruktur gelesen wird (task->bsd_info, wobei void *bsd_info hier ein allgemeiner typloser Pointer ist). Da diese Daten aus dem Userland kommen, hat man hier die Privileg-Eskalation, weil Daten aus dem Userland letztendlich ein beliebiges Byte (weil der Pointer beliebig im Userland gewählt werden kann) des Kernelspeichers manipulieren können.
Die Veränderung des Bytes besteht in diesem Fall in einem bitweisen Oder mit einem festen Byte (die Konstante P_DEPENDENCY_CAPABLE), das an einer Stelle eine 1 gesetzt hat. In Summe kann man also damit eine 1 in ein Bit eines beliebiges Bytes an beliebiger Stelle im Kernelspeicher schreiben.
Diese Operation läßt sich natürlich im Exploit beliebig wiederholen mit veränderten Werten, so daß man verschiedene Stellen im Kernelspeicher verändern kann.
Da er nun Speicher manipulieren kann, ändert er einen NULL-Pointer zu einem 0x10-Pointer auf ein C++ Objekt, das er unter Kontrolle hat. Auf diesem Objekt wird ein vcall (Aufruf einer virtuellen C++ Methode) durchgeführt. Da er das Objekt kontrolliert, kann er den vtable Pointer aus Userland Speicher lesen lassen, wodurch er zwei Register-Werte setzen kann. Einer ist der Zeiger auf die gefakte vtable im RAX Register (Register AX), der andere ist ein Zeiger auf einen Fake Stack (ein sogenannter Stack Pivot) im RIP Register (Relative Instruction Pointer, Zeiger auf den nächsten auszuführenden Maschinen-Befehl).
Damit kann er den Instruction Pointer mit dem Wert aus dem AX Register setzen und eine kleine ROP Chain starten. ROP ist eine Serie von Sprüngen in bereits vorhandene ausführbare Code-Fragmente, die jeweils mit einem Return enden und so den jeweils nächsten Befehl vom Stack holen. Also Recycling von Code, der netterweise schon als ausführbar markiert ist. Die kleine ROP Folge ruft dann eine größere auf, die die UserId der Task auf 0 setzt (Root) und dann eine Shell aufmacht. Damit erscheint die Root-Shell, die man sieht, wenn man den Exploit laufen läßt.
Luca hat auch noch einen weiteren Bug gefunden, der ihm die Umgehung von Kernel ASLR ermöglicht und damit die Nutzung des ersten Bugs. ASLR und besonders KASLR ist aber per se von Anfang an zum Scheitern verurteilt, weil es nur als schlechte Notlösung gedacht war und viel zu viele Probleme hat. Wer dazu mehr wissen möchte, ist hier gut aufgehoben.
Luca weist darauf hin, daß sein Exploit keine Schwäche in ASLR benutzt, sondern nur ein Info-Leak. Das Problem mit KASLR ist jedoch, daß es völlig wertlos wird, sobald ein einziger Pointer geleakt wird, da alles im Kernel denselben Kernel-Slide verwendet, bis der Rechner neu gestartet wird. Ein grundsätzliches Problem von KASRL, was es ziemlich nutzlos macht. Dieser Exploit ist nur ein weiteres Beispiel, warum ASLR und insbesondere KASLR für Angreifer keine nennenswerte Hürde darstellt und mehr ein Stolperstein als eine Sicherheits-Funktion ist.
Die Funktion
uint64_t leak_heap_ptr(io_connect_t* co)
versucht, einen Pointer auf den Kernel-Heap zu bekommen.
Dazu wird
IOConnectCallScalarMethod(*co, 2, NULL, 0, &scalarO_64, &outputCount);
aufgerufen, was den Wert von scalarO_64 füllt.
IOServiceOpen verwendet der Mach Interface Generator (MIG), um io_open_service_extended aufzurufen.
Das Kernel-ASLR Info-Leak beruht darauf, daß IOAudioEngineUserClient eine Methode getConnectionID anbietet, die eine "connection ID" liefert, die auf der Speicher-Adresse von sich selbst basiert.
Er kompromittiert die Größe einer vm_map_copy C++ Struct, was ihm erlaubt, den benachbarten Speicherbereich eines anderen C++ Objektes zu lesen. Die ersten 8 Bytes davon wiederum sind ein Pointer auf die vtable, die im __TEXT Segment einer Kernel Extension liegt.
Er bekommt also Zugriff auf die vtable des IOAudioEngineUserClient und damit auf die Adresse einer Kernel-Erweiterung. Da das Randomisieren im Kernel aus einem festen Offset besteht, kann er nun die ursprüngliche Adresse (ohne Schlitten) vom Userland aus berechnen und mit der randomisierten Adresse vergleichen und damit den Offset (Schlitten) des KASLR bekommen. Damit kann er nun jede Kernel-Adresse berechnen, denn der Kernel-Slide, also der Wert, um den ASLR die ursprünglichen Adressen verschiebt, ist für alle Kernel-Adressen gleich bis zum nächsten Neustart des Rechners.
Apple hat das Problem mit 10.11 behoben laut Luca:
so the null page thing being fixed is what broke the exploit on 10.11?
they added task != NULL in both is_io_service_open_extended and IOHIDXControllerUserClient::initWithTask
Apple hat also den NULL-Pointer nun abgefangen und benutzt ihn nicht mehr, um versehentlich damit zu arbeiten an dieser Stelle, wodurch ein Mapping der virtuellen 0-Adresse dem Angreifer nicht mehr hilft.