
CVE-2018-4280: Schwachstelle durch Mach-Port-Ersetzung in launchd auf iOS 11.2.6, die zu Sandbox-Escape, Privilegieneskalation und Umgehung der Codesignatur führt.
Blanket ist ein Sandbox-Escape, der auf iOS 11.2.6 abzielt, obwohl die Hauptschwachstelle erst in
iOS 11.4.1 gepatcht wurde. Er nutzt eine Schwachstelle bei der Ersetzung von Mach-Ports in launchd
(CVE-2018-4280) sowie mehrere kleinere Schwachstellen in anderen Diensten aus, um Code im
ReportCrash-Prozess auszuführen, der ohne Sandbox läuft, als root arbeitet und über die
Berechtigung task_for_pid-allow verfügt. Dies verschafft Blanket die vollständige Kontrolle über
jeden Prozess, der auf dem Telefon läuft, einschließlich sicherheitskritischer Prozesse wie amfid.
Der Exploit besteht aus mehreren Stufen. Dieses README erklärt die Hauptschwachstelle und die Stufen des Sandbox-Escape Schritt für Schritt.
Bei der Recherche zur Absturzberichterstattung auf iOS entdeckte ich eine Schwachstelle in launchd, die die Ersetzung eines Mach-Ports betrifft. Indem ein Prozess auf eine bestimmte Weise abstürzt, kann er den Kernel dazu bringen, eine Mach-Nachricht an launchd zu senden, die dazu führt, dass launchd ein Senderecht auf einen Mach-Port in seinem IPC-Namespace übermäßig freigibt. Dadurch kann ein Angreifer gegenüber dem Rest des Systems jeden launchd-Dienst impersonieren bzw. sich als ihn ausgeben, den er nachschlagen kann, was zahlreiche Wege zur Privilegieneskalation eröffnet.
Diese Schwachstelle ist auch unter macOS vorhanden, aber das Auslösen der Schwachstelle auf iOS ist schwieriger, da launchd Prüfungen enthält, die sicherstellen, dass die Mach-Exception-Nachricht vom Kernel stammt.
Launchd multiplexiert mehrere verschiedene Mach-Nachrichten-Handler über seinen Hauptport,
einschließlich eines MIG-Handlers für Exception-Nachrichten. Wenn ein Prozess eine
mach_exception_raise- oder mach_exception_raise_state_identity-Nachricht an seinen eigenen
Bootstrap-Port sendet, empfängt launchd diese Nachricht und verarbeitet sie als Exception auf
Host-Ebene.
Leider ist die Verarbeitung dieser Nachrichten durch launchd fehlerhaft. Wenn der Exception-Typ
EXC_CRASH ist, gibt launchd die in der Nachricht übermittelten Thread- und Task-Ports frei und
gibt dann KERN_FAILURE aus der Service-Routine zurück, wodurch das MIG-System die Thread- und
Task-Ports erneut freigibt. (Die Annahme ist, dass eine Service-Routine, die Erfolg zurückgibt, den
Besitz aller Ressourcen in der Mach-Nachricht übernommen hat, während eine Service-Routine, die
einen Fehler zurückgibt, den Besitz keiner der Ressourcen übernommen hat.)
Hier ist der Code aus launchds Service-Routine für mach_exception_raise-Nachrichten, dekompiliert
mit IDA/Hex-Rays und zur besseren Lesbarkeit leicht bearbeitet:```C
kern_return_t __fastcall
catch_mach_exception_raise( // (a) The service routine is
mach_port_t exception_port, // called with values directly
mach_port_t thread, // from the Mach message
mach_port_t task, // sent by the client. The
exception_type_t exception, // thread and task ports could
mach_exception_data_t code, // be arbitrary send rights.
mach_msg_type_number_t codeCnt)
{
__int64 __stack_guard; // ST28_8@1
kern_return_t kr; // w0@1 MAPDST
kern_return_t result; // w0@4
__int64 codes_left; // x25@6
mach_exception_data_type_t code_value; // t1@7
int pid; // [xsp+34h] [xbp-44Ch]@1
char codes_str[1024]; // [xsp+38h] [xbp-448h]@7
__stack_guard = *__stack_chk_guard_ptr;
pid = -1;
kr = pid_for_task(task, &pid);
if ( kr )
{
_os_assumes_log(kr);
_os_avoid_tail_call();
}
if ( current_audit_token.val[5] ) // (b) If the message was sent by
{ // a process with a nonzero PID
result = KERN_FAILURE; // (any non-kernel process),
} // the message is rejected.
else
{
if ( codeCnt )
{
codes_left = codeCnt;
do
{
code_value = *code;
++code;
__snprintf_chk(codes_str, 0x400uLL, 0, 0x400uLL, "0x%llx", code_value);
--codes_left;
}
while ( codes_left );
}
launchd_log_2(
0LL,
3LL,
"Host-level exception raised: pid = %d, thread = 0x%x, "
"exception type = 0x%x, codes = { %s }",
pid,
thread,
exception,
codes_str);
kr = deallocate_port(thread); // (c) The "thread" port sent in
if ( kr ) // the message is deallocated.
{
_os_assumes_log(kr);
_os_avoid_tail_call();
}
kr = deallocate_port(task); // (d) The "task" port sent in the
if ( kr ) // message is deallocated.
{
_os_assumes_log(kr);
_os_avoid_tail_call();
}
if ( exception == EXC_CRASH ) // (e) If the exception type is
result = KERN_FAILURE; // EXC_CRASH, then KERN_FAILURE
else // is returned. MIG will
result = 0; // deallocate the ports again.
}
*__stack_chk_guard_ptr;
return result;
}
Das tut der Code:
1. Diese Funktion ist die Mach-Dienstroutine für `mach_exception_raise`-Ausnahmemeldungen: Sie wird direkt vom Mach-System aufgerufen, wenn launchd eine `mach_exception_raise`-Mach-Ausnahmemeldung verarbeitet. Die Argumente der Dienstroutine werden aus der Mach-Meldung geparst und werden daher vom Absender der Meldung kontrolliert.
2. Unter (b) prüft launchd, ob die Mach-Ausnahmemeldung vom Kernel gesendet wurde. Das Audit-Token des Absenders enthält im Feld 5 die PID des sendenden Prozesses, die nur beim Kernel Null ist. Wenn die Meldung nicht vom Kernel gesendet wurde, wird sie abgelehnt.
3. Die Thread- und Task-Ports aus der Meldung werden unter (c) und (d) explizit freigegeben.
4. Unter (e) prüft launchd, ob der Ausnahmetyp `EXC_CRASH` ist, und gibt in diesem Fall `KERN_FAILURE` zurück. Die Absicht ist, sicherzustellen, dass `EXC_CRASH`-Meldungen nicht bearbeitet werden, vermutlich damit ReportCrash als Corpse-Handler aufgerufen wird. Wenn jedoch an dieser Stelle `KERN_FAILURE` zurückgegeben wird, werden die Task- und Thread-Ports später bei der Bereinigung der Ausnahmemeldung erneut freigegeben. Das bedeutet, dass diese beiden Ports doppelt freigegeben werden.
Damit diese Sicherheitslücke nützlich ist, wollen wir launchds Sende-Recht auf einen Mach-Dienst freigeben, den launchd anbietet, damit wir diesen Dienst gegenüber dem Rest des Systems imitieren können. Das bedeutet, wir müssen dafür sorgen, dass die Task- und Thread-Ports in der Ausnahmemeldung tatsächlich Sende-Rechte auf den Mach-Dienst-Port sind, den wir in launchd freigeben möchten. Sobald wir launchd die bösartige Ausnahmemeldung gesendet und den Dienst-Port freigegeben haben, werden wir versuchen, denselben Portnamen wiederverwenden zu lassen, diesmal jedoch für einen Mach-Port, für den wir das Empfangs-Recht besitzen. Auf diese Weise wird launchd, wenn ein Client launchd um ein Sende-Recht auf den Mach-Port des Dienstes bittet, dem Client stattdessen ein Sende-Recht auf unseren Port geben, sodass wir diesen Dienst gegenüber dem Client imitieren können. Danach gibt es viele verschiedene Wege, um Systemprivilegien zu erlangen.
### Auslösen der Sicherheitslücke
Um die Sicherheitslücke tatsächlich auszulösen, müssen wir die Prüfung umgehen, dass die Meldung vom Kernel gesendet wurde. Denn wenn wir die Ausnahmemeldung direkt an launchd senden, wird sie einfach verworfen. Wir müssen also irgendwie erreichen, dass der Kernel eine „bösartige" Ausnahmemeldung sendet, die anstelle der echten Thread- und Task-Ports ein Mach-Sende-Recht für einen Systemdienst enthält.
Wie sich herausstellt, gibt es einen Mach-Trap, `task_set_special_port`, mit dem in bestimmten Situationen ein benutzerdefiniertes Sende-Recht anstelle des echten Task-Ports verwendet werden kann. Eine dieser Situationen liegt vor, wenn der Kernel im Namen einer Task eine Ausnahmemeldung erzeugt: Anstatt das echte Task-Sende-Recht in die Ausnahmemeldung zu setzen, verwendet der Kernel das von `task_set_special_port` bereitgestellte Sende-Recht. Genauer gesagt: Wenn eine Task `task_set_special_port` aufruft, um einen benutzerdefinierten Wert für ihren speziellen Port `TASK_KERNEL_PORT` festzulegen, und die Task dann abstürzt, enthält die vom Kernel erzeugte Ausnahmemeldung im Feld „task" ein Sende-Recht auf den benutzerdefinierten Port, nicht auf den echten Task-Port. Eine entsprechende API, `thread_set_special_port`, kann verwendet werden, um im Feld „thread" der erzeugten Ausnahmemeldung einen benutzerdefinierten Port zu setzen.
Aufgrund dieses Verhaltens ist es überhaupt nicht schwierig, den Kernel dazu zu bringen, eine „bösartige" Ausnahmemeldung zu erzeugen, die anstelle des Task- und Thread-Ports einen Mach-Dienst-Port enthält. Wir müssen jedoch weiterhin sicherstellen, dass die von uns erzeugte Ausnahmemeldung an launchd zugestellt wird.
Auch hier ist es nicht schwierig, sicherzustellen, dass der Kernel die „bösartige" Ausnahmemeldung an launchd zustellt, wenn man die richtige API kennt. Mit der Funktion `thread_set_exception_ports` kann ein beliebiges Mach-Sende-Recht als Port festgelegt werden, an den Ausnahmemeldungen für diesen Thread zugestellt werden. Wir müssen also nur `thread_set_exception_ports` mit dem Bootstrap-Port aufrufen, und jede von uns erzeugte Ausnahme veranlasst den Kernel, eine Ausnahmemeldung an launchd zu senden.
Das letzte Puzzleteil ist der richtige Ausnahmetyp. Die Sicherheitslücke wird nur für `EXC_CRASH`-Ausnahmen ausgelöst. Ein wenig Versuch und Irrtum zeigt, dass wir `EXC_CRASH`-Ausnahmen leicht erzeugen können, indem wir die Standardfunktion `abort` aufrufen.
Zusammengefasst können wir also vorhandene und gut dokumentierte APIs verwenden, um den Kernel zu veranlassen, in unserem Namen eine bösartige `EXC_CRASH`-Ausnahmemeldung zu erzeugen und an launchd zuzustellen, wodurch die Sicherheitslücke ausgelöst und der Mach-Dienst-Port freigegeben wird:
1. Verwende `thread_set_exception_ports`, um launchd als Ausnahme-Handler für diesen Thread festzulegen.
2. Rufe `bootstrap_look_up` auf, um den Dienst-Port für den Dienst zu erhalten, den wir von launchd aus imitieren möchten.
3. Rufe `task_set_special_port`/`thread_set_special_port` auf, um in Ausnahmemeldungen diesen Dienst-Port anstelle der echten Task- und Thread-Ports zu verwenden.
4. Rufe `abort` auf. Der Kernel sendet eine `EXC_CRASH`-Ausnahmemeldung an launchd, aber die Task- und Thread-Ports in der Meldung sind der Ziel-Dienst-Port.
5. Launchd verarbeitet die Ausnahmemeldung und gibt den Dienst-Port frei.
### Ausführen von Code nach dem Absturz
Es gibt ein Problem mit der obigen Strategie: Der Aufruf von `abort` beendet unseren Prozess. Wenn wir nach dem Auslösen der Sicherheitslücke überhaupt Code ausführen wollen, brauchen wir eine Möglichkeit, den Absturz in einem anderen Prozess herbeizuführen.
(Bei anderen Ausnahmetypen könnte ein Prozess die Ausnahme tatsächlich abfangen und sich davon erholen. Die Erholung würde so ablaufen, dass der Prozess seinen Thread-Ausnahme-Handler auf launchd und seinen Task-Ausnahme-Handler auf sich selbst setzt. Nachdem launchd die Ausnahme verarbeitet und nicht behandeln konnte, würde der Kernel die Ausnahme an den Task-Handler senden, der den Thread-Zustand zurücksetzt und dem Kernel mitteilt, dass die Ausnahme behandelt wurde. Ein Prozess kann jedoch seine eigenen `EXC_CRASH`-Ausnahmen nicht abfangen, daher brauchen wir zwei Prozesse.)
Eine Strategie besteht darin, zuerst eine Sicherheitslücke in einem anderen Prozess auf iOS auszunutzen und diesen Prozess zu zwingen, seine Kernel-Ports zu setzen und abzustürzen. Für einen Proof-of-Concept ist es jedoch einfacher, eine App-Erweiterung zu erstellen.
App-Erweiterungen, die mit iOS 8 eingeführt wurden, bieten eine Möglichkeit, einen Teil der Funktionalität einer App zu kapseln, sodass er außerhalb der App verfügbar ist. Der Code einer App-Erweiterung läuft in einem separaten, sandboxierten Prozess. Das macht es sehr einfach, einen Prozess zu starten, der seine speziellen Ports setzt, launchd als Ausnahme-Handler für `EXC_CRASH` registriert und dann `abort` aufruft.
Es gibt keine unterstützte Möglichkeit für eine App, ihre eigene App-Erweiterung programmatisch zu starten und mit ihr zu kommunizieren. Ian McDowell hat jedoch einen [großartigen Artikel][Multi-Process iOS App Using NSExtension] geschrieben, der beschreibt, wie man mit der privaten `NSExtension`-API einen App-Erweiterungsprozess startet und mit ihm kommuniziert. Ich habe hier eine nahezu identische Strategie verwendet. Der einzige Unterschied besteht darin, dass wir einen Mach-Port an den App-Erweiterungsprozess übermitteln müssen, was das Registrieren eines Dummy-Dienstes bei launchd beinhaltet, mit dem sich die App-Erweiterung verbindet.
[Multi-Process iOS App Using NSExtension]: https://ianmcdowell.net/blog/nsextension/
### Verhindern der Port-Wiederverwendung in launchd
Eine Herausforderung, die einem auffällt, wenn man den Exploit wie beschrieben ausführt, ist, dass man den freigegebenen Port gelegentlich nicht zurückerlangen kann. Der Grund dafür ist, dass der Kernel die freien IPC-Einträge eines Prozesses in einer Freiliste (Freelist) verwaltet, und so wird ein gerade freigegebener Portname (mit einer anderen Generationsnummer) wiederverwendet, wenn ein neuer Port in der IPC-Tabelle allokiert wird. Wir werden den gewünschten Portnamen also nur dann wiederbelegen können, wenn launchd diesen IPC-Eintrag nicht zuerst für einen anderen Port wiederverwendet.
Der Weg, dies zu umgehen, besteht darin, den freien IPC-Eintrag tief in der Freiliste zu vergraben, damit bei der Allokation neuer Ports durch launchd zuerst diese anderen Slots verwendet werden. Wie machen wir das? Wir können eine Reihe von Dummy-Mach-Diensten bei launchd registrieren, mit Ports, für die wir das Empfangs-Recht besitzen. Wenn wir `abort` aufrufen, feuert zuerst der Ausnahme-Handler, und anschließend wird der Prozesszustand einschließlich der Mach-Ports bereinigt. Wenn launchd die `EXC_CRASH`-Ausnahme empfängt, gibt es versehentlich den Ziel-Dienst-Port frei und setzt den IPC-Eintrag, der diesem Portnamen entspricht, an den Kopf der Freiliste. Wenn dann die übrigen Mach-Ports unserer App-Erweiterung zerstört werden, empfängt launchd Benachrichtigungen und gibt die Dummy-Dienst-Ports frei, wodurch der Ziel-IPC-Eintrag hinter den Slots der gerade freigegebenen Ports vergraben wird. Solange launchd also weniger Ports allokiert, als wir Dummy-Dienste registriert haben, befindet sich der Ziel-Slot weiterhin in der Freiliste, was bedeutet, dass wir launchd weiterhin dazu bringen können, den Slot mit demselben Portnamen wie den ursprünglichen Dienst wiederzubelegen.
Die Einschränkung dieser Strategie ist, dass wir die Berechtigung `com.apple.security.application-groups` benötigen, um Dienste bei launchd zu registrieren. Es gibt andere Möglichkeiten, Mach-Ports in launchd zu verstecken, aber die Verwendung von Application Groups ist sicherlich der einfachste Weg und genügt für diesen Proof-of-Concept.
### Imitieren des freigegebenen Dienstes
Sobald wir die Absturz-App-Erweiterung gestartet und ein Mach-Sende-Recht in launchd freigegeben haben, müssen wir diesen Mach-Portnamen mit einem Sende-Recht wiederbelegen, für das wir das Empfangs-Recht besitzen. Auf diese Weise werden alle Meldungen, die launchd an diesen Portnamen sendet, von uns empfangen, und jedes Mal, wenn launchd diesen Portnamen mit einem Client teilt, erhält der Client ein Sende-Recht auf unseren Port. Wenn wir insbesondere launchds Sende-Recht auf einen Mach-Dienst freigeben können, erhält jeder Prozess, der diesen Dienst von launchd anfordert, ein Sende-Recht auf unseren eigenen Port anstelle des echten Dienst-Ports. Das erlaubt uns, den Dienst zu imitieren oder einen Man-in-the-Middle-Angriff durchzuführen, bei dem wir alle Meldungen einsehen können, die der Client an den Dienst sendet.
Den freigegebenen Portnamen so wiederverwenden zu lassen, dass er auf einen Port verweist, der uns gehört, ist ebenfalls recht einfach, da wir uns bereits entschieden haben, die Application-Groups-Berechtigung zu nutzen: Registriere einfach Dummy-Mach-Dienste bei launchd, bis einer von ihnen den ursprünglichen Portnamen wiederverwendet. Das müssen wir in Chargen tun, indem wir eine große Anzahl von Dummy-Diensten gemeinsam registrieren, prüfen, ob einer den freigegebenen Portnamen erfolgreich wiederverwendet hat, und sie dann wieder deregistrieren. Der Grund ist, dass wir sicher sein müssen, dass unsere Registrierungen bis ganz nach hinten in die IPC-Port-Freiliste reichen, um den vergrabenen Portnamen zu finden, den wir wollen.
Wir können prüfen, ob wir den freigegebenen Portnamen erfolgreich wiederverwendet haben, indem wir den ursprünglichen Dienst mit `bootstrap_look_up` nachschlagen: Wenn er einen unserer registrierten Dienst-Ports zurückgibt, sind wir fertig.
Sobald wir einen neuen Dienst registriert haben, der denselben Portnamen wie der ursprüngliche erhält, erhalten alle Clients, die den ursprünglichen Dienst in launchd nachschlagen, ein Sende-Recht auf unseren Port, nicht auf den echten Dienst-Port. Damit imitieren wir den ursprünglichen Dienst gegenüber dem Rest des Systems (oder zumindest gegenüber den Prozessen, die den Dienst nach unserem Angriff nachschlagen).
Stufe 1: Erlangen des host-priv-Ports
---------------------------------------------------------------------------------------------------
Sobald wir in der Lage sind, beliebige Systemdienste zu imitieren, besteht der nächste Schritt darin, den host-priv-Port zu erlangen. Dieser Schritt ist unkompliziert und wird von den Änderungen in iOS 11.3 nicht beeinflusst. Die grundlegende Idee dieses Angriffs ist es, SafetyNet zu imitieren, ReportCrash zum Absturz zu bringen und dann den host-priv-Port aus dem Task-Port des sterbenden ReportCrash zu extrahieren, der in der Ausnahmemeldung gesendet wird.
### Über ReportCrash und SafetyNet
ReportCrash ist für die Erstellung von Absturzberichten auf iOS verantwortlich. Diese eine Binärdatei bietet tatsächlich 4 verschiedene Dienste an (jeder in einem eigenen Prozess, wobei nicht alle zu einem bestimmten Zeitpunkt laufen müssen):
1. `com.apple.ReportCrash` ist für die Erstellung von Absturzberichten für abstürzende Prozesse verantwortlich. Es ist der Ausnahme-Handler auf Host-Ebene für `EXC_CRASH`-, `EXC_GUARD`- und `EXC_RESOURCE`-Ausnahmen.
2. `com.apple.ReportCrash.Jetsam` kümmert sich um Jetsam-Berichte.
3. `com.apple.ReportCrash.SimulateCrash` erstellt Berichte für simulierte Abstürze.
4. `com.apple.ReportCrash.SafetyNet` ist der registrierte Ausnahme-Handler für den Dienst `com.apple.ReportCrash`.
Die für uns interessanten Dienste sind `com.apple.ReportCrash` und `com.apple.ReportCrash.SafetyNet`, die im Folgenden einfach als ReportCrash und SafetyNet bezeichnet werden. Beide sind MIG-basierte Dienste und führen praktisch denselben Code aus.
Beim Start sucht ReportCrash den SafetyNet-Dienst in launchd und setzt den zurückgegebenen Port als Ausnahme-Handler auf Task-Ebene. Die Absicht scheint zu sein, dass ein separater Prozess den Absturzbericht für ReportCrash erstellt, falls ReportCrash selbst abstürzt. Dieser Codepfad sieht jedoch funktionslos aus: ReportCrash registriert SafetyNet für `mach_exception_raise`-Meldungen, obwohl sowohl ReportCrash als auch SafetyNet nur `mach_exception_raise_state_identity`-Meldungen verarbeiten. Dennoch sind beide Dienste weiterhin vorhanden und aus der iOS-Container-Sandbox erreichbar.
### Manipulations-Primitiven für ReportCrash
Um den folgenden Angriff durchzuführen, müssen wir ReportCrash (oder SafetyNet) so manipulieren können, dass es sich so verhält, wie wir es wollen. Konkret benötigen wir die folgenden Fähigkeiten: ReportCrash bei Bedarf starten, ReportCrash zum Beenden zwingen, ReportCrash zum Absturz bringen und sicherstellen, dass ReportCrash nicht beendet wird, während wir es verwenden. Hier beschreibe ich, wie wir jedes Ziel erreichen.
Um ReportCrash zu starten, müssen wir ihm lediglich eine Mach-Meldung senden: launchd startet es bei Bedarf. Aufgrund seines eigentümlichen Designs führt jedoch jeder Meldungstyp außer `mach_exception_raise_state_identity` dazu, dass ReportCrash nicht mehr auf neue Meldungen reagiert und schließlich beendet wird. Wenn wir also möchten, dass es danach am Leben bleibt, müssen wir eine `mach_exception_raise_state_identity`-Meldung senden.
Um ReportCrash zu beenden, können wir ihm einfach einen beliebigen anderen Typ von Mach-Meldung senden.
Es gibt viele Möglichkeiten, ReportCrash zum Absturz zu bringen. Am einfachsten ist es wahrscheinlich, eine `mach_exception_raise_state_identity`-Meldung zu senden, bei der der Thread-Port auf `MACH_PORT_NULL` gesetzt ist.
Schließlich müssen wir sicherstellen, dass ReportCrash nicht beendet wird, während wir es verwenden. Jede `mach_exception_raise_state_identity`-Meldung, die es verarbeitet, veranlasst es, einen weiteren Thread zu starten, der auf die nächste Meldung wartet, während der ursprüngliche Thread den Absturzbericht erstellt. ReportCrash wird beendet, sobald alle noch laufenden Threads, die einen Absturzbericht erstellen, fertig sind. Wenn wir also einen dieser Threads blockieren können, während er gerade einen Absturzbericht erstellt, können wir verhindern, dass er jemals beendet wird.
Der einfachste Weg, den ich dafür gefunden habe, war, eine `mach_exception_raise_state_identity`-Meldung mit einem benutzerdefinierten Port in den Task- und Thread-Feldern zu senden. Sobald ReportCrash versucht, einen Absturzbericht zu erstellen, ruft es `task_policy_get` auf dem „task"-Port auf, wodurch es eine Mach-Meldung an den von uns gesendeten Port schickt und auf eine Antwort wartet. Da der „task"-Port jedoch nur ein gewöhnlicher Mach-Port ist, können wir die Mach-Meldung einfach nicht beantworten, und ReportCrash wartet unbegrenzt darauf, dass `task_policy_get` zurückkehrt.
### Extrahieren von host-priv aus ReportCrash
Für die erste Stufe des Exploits ist der Angriffsplan relativ einfach:
1. Starte den SafetyNet-Dienst und zwinge ihn, für die Dauer unseres Angriffs am Leben zu bleiben.
2. Nutze die Primitive zur Imitation von launchd-Diensten, um SafetyNet zu imitieren. Das verschafft uns einen neuen Port, auf dem wir Meldungen empfangen können, die für den echten SafetyNet-Dienst bestimmt sind.
3. Bringe eine vorhandene Instanz von ReportCrash zum Beenden. Auf diese Weise können wir sicherstellen, dass ReportCrash im nächsten Schritt unseren SafetyNet-Port nachschlägt.
4. Starte ReportCrash. ReportCrash wird SafetyNet in launchd nachschlagen und den resultierenden Port, also den unechten SafetyNet-Port, für den wir das Empfangs-Recht besitzen, als Ziel für `EXC_CRASH`-Meldungen festlegen.
5. Löse einen Absturz in ReportCrash aus. Nachdem ReportCrash feststellt, dass für den ursprünglichen Ausnahmetyp keine Handler registriert sind, tritt es in die Phase des Prozessendes ein. An diesem Punkt sieht XNU, dass ReportCrash den unechten SafetyNet-Port zum Empfang von `EXC_CRASH`-Ausnahmen registriert hat, und erzeugt daher eine Ausnahmemeldung und sendet sie an diesen Port.
6. Dann lauschen wir auf dem unechten SafetyNet-Port auf die `EXC_CRASH`-Meldung. Sie wird vom Typ `mach_exception_raise` sein, was bedeutet, dass sie den Task-Port von ReportCrash enthält.
7. Schließlich verwenden wir `task_get_special_port` auf dem ReportCrash-Task-Port, um den Host-Port von ReportCrash zu erhalten. Da ReportCrash nicht sandboxiert ist und als root läuft, ist dies der host-priv-Port.
Am Ende dieser Stufe des Sandbox-Escapes verfügen wir über einen nutzbaren host-priv-Port. Das allein zeigt bereits, dass es sich um ein schwerwiegendes Sicherheitsproblem handelt.
Stufe 2: Die Sandbox verlassen
---------------------------------------------------------------------------------------------------
Auch wenn wir den host-priv-Port haben, ist es unser Ziel, die Sandbox vollständig zu verlassen und Code als root mit der Berechtigung `task_for_pid-allow` auszuführen. Der erste Schritt, um das zu erreichen, besteht darin, die Sandbox einfach zu verlassen.
Technisch gesehen gibt es keinen Grund, den host-priv-Port vor dem Verlassen der Sandbox zu erlangen: Diese beiden Schritte sind unabhängig und können in beliebiger Reihenfolge erfolgen. Diese Stufe wird das System jedoch instabil hinterlassen, wenn sie oder nachfolgende Stufen fehlschlagen, daher ist es sinnvoll, sie weiter nach hinten zu legen.
Der Angriff auf hoher Ebene besteht darin, dieselbe launchd-Sicherheitslücke erneut zu nutzen, um einen Systemdienst zu imitieren. Diesmal ist es jedoch unser Ziel, einen Dienst zu imitieren, an den ein Client seinen Task-Port in einer Mach-Meldung sendet. Durch Experimentieren auf iOS 11.2.6 lässt sich leicht herausfinden, dass der nicht sandboxierte druid-Daemon seinen Task-Port in einer Mach-Meldung an den unechten Dienst-Port sendet, wenn wir `com.apple.CARenderServer` (im Folgenden CARenderServer) imitieren, der von backboardd gehostet wird, und dann mit `com.apple.DragUI.druid.source` kommunizieren.
Dieser Schritt des Exploits ist auf iOS 11.3 fehlerhaft, weil druid seinen Task-Port nicht mehr in der Mach-Meldung an CARenderServer sendet. Trotzdem bin ich zuversichtlich, dass diese Sicherheitslücke weiterhin verwendet werden kann, um die Sandbox zu verlassen. Ein möglicher Ansatz besteht darin, nach nicht sandboxierten Diensten zu suchen, die Eingaben von anderen Diensten vertrauen. Diese Art von „Sicherheitslücken" wäre ohne die Fähigkeit, Systemdienste zu ersetzen, nie ausnutzbar, was bedeutet, dass sie wahrscheinlich eine Angriffsfläche mit niedriger Priorität sind, sowohl intern als auch extern für Apple.
### druid zum Absturz bringen
Wie bei ReportCrash müssen wir druid bei Bedarf neu starten können, falls es bereits läuft, damit es unseren unechten CARenderServer-Port in launchd nachschlägt. Ich habe mich entschieden, einen Fehler in libxpc zu verwenden, dessen Behebung bereits geplant war.
Beim Durchsehen von libxpc fand ich einen Out-of-Bounds-Read, der verwendet werden konnte, um einen beliebigen XPC-Dienst zum Absturz zu bringen:```C
void _xpc_dictionary_apply_wire_f
(
OS_xpc_dictionary *xdict,
OS_xpc_serializer *xserializer,
const void *context,
bool (*applier_fn)(const char *, OS_xpc_serializer *, const void *)
)
{
...
uint64_t count = (unsigned int)*serialized_dict_count;
if ( count )
{
uint64_t depth = xserializer->depth;
uint64_t index = 0;
do
{
const char *key = _xpc_serializer_read(xserializer, 0, 0, 0);
size_t keylen = strlen(key);
_xpc_serializer_advance(xserializer, keylen + 1);
if ( !applier_fn(key, xserializer, context) )
break;
xserializer->depth = depth;
++index;
}
while ( index < count );
}
...
}
Das Problem besteht darin, dass die Verwendung eines ungeprüften strlen auf Angreifer-kontrollierte Daten es ermöglicht, dass der Schlüssel für den serialisierten Wörterbucheintrag über das Ende des Datenpuffers hinausreicht. Das bedeutet, dass der XPC-Dienst, der das Wörterbuch deserialisiert, abstürzt, entweder wenn strlen auf Speicher außerhalb der Grenzen zugreift oder wenn _xpc_serializer_advance versucht, den Serializer über das Ende der bereitgestellten Daten hinaus zu verschieben.
Dieser Fehler war bereits in iOS 11.3 Beta behoben, als ich ihn entdeckte, daher habe ich ihn nicht an Apple gemeldet. Der Exploit ist als eigenständiges Projekt in meinem xpc-crash-Repository verfügbar.
Um diesen Fehler zu nutzen, um druid zum Absturz zu bringen, müssen wir lediglich eine fehlerhafte XPC-Nachricht an den druid-Dienst senden, sodass der Schlüssel des Wörterbuchs nicht terminiert ist und sich bis zum letzten Byte der Nachricht erstreckt.
Das Erlangen des Task-Ports von druid auf iOS 11.2.6 mithilfe unserer Dienst-Impersonierungs-Primitive ist einfach:
Sobald wir druids Task-Port haben, müssen wir immer noch herausfinden, wie wir Code innerhalb des druid-Prozesses ausführen können.
Das Problem ist, dass XNU die Task-Ports von Plattform-Binärdateien davor schützt, von Nicht-Plattform-Binärdateien modifiziert zu werden. Die Verteidigung ist in der Funktion task_conversion_eval implementiert, die von convert_port_to_locked_task und convert_port_to_task_with_exec_token aufgerufen wird:```C
kern_return_t
task_conversion_eval(task_t caller, task_t victim)
{
/*
* Tasks are allowed to resolve their own task ports, and the kernel is
* allowed to resolve anyone's task port.
*/
if (caller == kernel_task) {
return KERN_SUCCESS;
}
if (caller == victim) {
return KERN_SUCCESS;
}
/*
* Only the kernel can can resolve the kernel's task port. We've established
* by this point that the caller is not kernel_task.
*/
if (victim == kernel_task) {
return KERN_INVALID_SECURITY;
}
#if CONFIG_EMBEDDED /* * On embedded platforms, only a platform binary can resolve the task port * of another platform binary. / if ((victim->t_flags & TF_PLATFORM) && !(caller->t_flags & TF_PLATFORM)) { #if SECURE_KERNEL return KERN_INVALID_SECURITY; #else if (cs_relax_platform_task_ports) { return KERN_SUCCESS; } else { return KERN_INVALID_SECURITY; } #endif / SECURE_KERNEL / } #endif / CONFIG_EMBEDDED */
return KERN_SUCCESS;
}
MIG conversion routines that rely on these functions, including `convert_port_to_task` and
`convert_port_to_map`, will thus fail when we call them on druid's task. For example,
`mach_vm_write` won't allow us to manipulate druid's memory.
However, while looking at the MIG file `osfmk/mach/task.defs` in XNU, I noticed something
interesting:```C
/*
* Returns the set of threads belonging to the target task.
*/
routine task_threads(
target_task : task_inspect_t;
out act_list : thread_act_array_t);
Die Funktion task_threads, die die Threads einer Task aufzählt, nimmt tatsächlich eine task_inspect_t entgegen statt einer task_t, was bedeutet, dass MIG sie mit convert_port_to_task_inspect statt mit convert_port_to_task umwandelt. Ein kurzer Blick auf convert_port_to_task_inspect zeigt, dass diese Funktion die Prüfung task_conversion_eval nicht durchführt, was bedeutet, dass wir sie erfolgreich auf Platform-Binaries aufrufen können. Das ist interessant, denn die zurückgegebenen Threads sind keine thread_inspect_t-Rechte, sondern vollwertige thread_act_t-Rechte. Anders ausgedrückt: task_threads stuft ein nicht modifizierbares Task-Recht zu modifizierbaren Thread-Rechten herauf. Und da es kein entsprechendes thread_conversion_eval gibt, bedeutet dies, dass wir die Mach-Thread-APIs verwenden können, um die Threads in einer Task zu modifizieren, selbst wenn es sich bei dieser Task um ein Platform-Binary handelt.
Um dies auszunutzen, habe ich eine Bibliothek namens threadexec geschrieben, die auf der Mach-Thread-API eine vollwertige Funktionsaufruf-Fähigkeit aufbaut. Das threadexec-Projekt war für sich genommen eine beträchtliche Aufgabe, aber da es für diesen Exploit nur indirekt relevant ist, verzichte ich auf eine detaillierte Erklärung seiner internen Funktionsweise.
Sobald wir den Host-Priv-Port und unsandboxed Codeausführung in druid haben, ist die nächste Stufe des vollständigen Sandbox-Escape, einen neuen Exception-Handler auf Host-Ebene zu installieren. Dieser Prozess ist mit unseren derzeitigen Fähigkeiten unkompliziert:
EXC_BAD_ACCESS, indem wir host_get_exception_ports aufrufen.EXC_BAD_ACCESS sein wird.host_set_exception_ports aufzurufen, um unseren Mach-Port als Exception-Handler auf Host-Ebene für EXC_BAD_ACCESS zu registrieren.Nach dieser Stufe wird jedes Mal, wenn ein Prozess auf eine ungültige Speicheradresse zugreift (und auch keinen registrierten Exception-Handler besitzt), eine EXC_BAD_ACCESS-Exception-Nachricht an unseren neuen Exception-Handler-Port gesendet. Dies gibt uns den Task-Port jedes abstürzenden Prozesses, und da EXC_BAD_ACCESS eine behebbare Exception ist, können wir den Task-Port diesmal nutzen, um Code auszuführen.
Die nächste Stufe besteht darin, in ReportCrash eine EXC_BAD_ACCESS-Exception auszulösen, sodass dessen Task-Port in einer Exception-Nachricht an unseren neuen Exception-Handler-Port gesendet wird:
EXC_BAD_ACCESS-Exception erzeugt. Da ReportCrash keinen Exception-Handler für EXC_BAD_ACCESS registriert hat (SafetyNet ist schließlich für EXC_CRASH registriert), wird die Exception an den Exception-Handler auf Host-Ebene zugestellt.KERN_SUCCESS zurück, um dem Kernel mitzuteilen, dass die Exception behandelt wurde und ReportCrash fortgesetzt werden kann.An diesem Punkt haben wir Codeausführung innerhalb eines unsandboxed, als root laufenden Prozesses mit der Berechtigung task_for_pid-allow.
Die nächsten beiden Stufen sind nicht unbedingt erforderlich, sollten aber ohnehin durchgeführt werden.
Sobald wir Codeausführung in ReportCrash haben, sollten wir den Exception-Handler auf Host-Ebene für EXC_BAD_ACCESS mithilfe von druid zurücksetzen:
host_set_exception_ports in druid auf, um den alten Exception-Handler auf Host-Ebene für EXC_BAD_ACCESS erneut zu registrieren.Dadurch wird verhindert, dass unser Exception-Handler-Port weiterhin Exception-Nachrichten für andere abstürzende Prozesse erhält.
Der letzte Schritt besteht darin, den Schaden an launchd zu beheben, den wir verursacht haben, als wir Service-Ports in seinem IPC-Namespace freigegeben haben, um diese Dienste zu imitieren:
task_for_pid in ReportCrash auf, um den Task-Port von launchd zu erhalten.mach_port_insert_right in ReportCrash auf, um den echten Service-Port unter dem ursprünglichen Namen in launchds IPC-Raum einzufügen.Nach Abschluss dieses Schritts sollte das System wieder voll funktionsfähig sein. Nach einer erfolgreichen Ausnutzung sollte kein erzwungener Neustart des Geräts erforderlich sein, da der Exploit alle Schäden selbst repariert.
Blanket enthält außerdem eine Post-Exploitation-Payload, die amfid umgeht und eine Bind-Shell startet. Dieser Abschnitt beschreibt, wie das erreicht wird.
Selbst nachdem wir Codeausführung in ReportCrash erlangt haben, ist die Nutzung dieser Fähigkeit nicht einfach: Wir sind darauf beschränkt, einzelne Funktionsaufrufe innerhalb des Prozesses auszuführen, was die Durchführung komplexer Aufgaben mühsam macht. Idealerweise möchten wir eine Möglichkeit, Code nativ mit ReportCrashs Berechtigungen auszuführen, entweder durch Injizieren von Code in ReportCrash oder durch Starten eines neuen Prozesses mit denselben (oder höheren) Berechtigungen.
Blanket wählt den Weg des Prozess-Spawnings. Wir verwenden task_for_pid und unseren Status als Platform-Binary in ReportCrash, um launchds Task-Port zu erhalten und einen neuen Thread innerhalb von launchd zu erstellen, den wir kontrollieren können. Diesen Thread verwenden wir dann, um posix_spawn aufzurufen und unsere Payload-Binary zu starten. Die Payload-Binary kann mit eingeschränkten Berechtigungen signiert werden, einschließlich task_for_pid-allow, um zusätzliche Fähigkeiten zu gewähren.
Damit iOS unsere neu erzeugte Binary akzeptiert, müssen wir die Codesignierung umgehen. Im Laufe der Jahre wurden verschiedene Strategien diskutiert, aber die derzeit gebräuchlichste Strategie besteht darin, einen Exception-Handler für amfid zu registrieren und dann einen Daten-Patch durchzuführen, sodass amfid beim Versuch, MISValidateSignatureAndCopyInfo aufzurufen, abstürzt. Dadurch können wir die Implementierung dieser Funktion vortäuschen und so tun, als ob die Codesignatur gültig wäre.
Es gibt jedoch einen anderen Ansatz, den ich für robuster und flexibler halte: Anstatt amfid zu patchen, können wir einfach einen neuen amfid-Port im Kernel registrieren.
Der Kernel verfolgt mithilfe eines speziellen Host-Ports namens HOST_AMFID_PORT, an welchen Port Nachrichten an amfid gesendet werden sollen. Wenn wir unsandboxed Root-Codeausführung haben, können wir diesen Port auf einen neuen Wert setzen. Apple hat sich gegen diesen Angriff abgesichert, indem geprüft wird, ob die Antwort auf eine Validierungsanfrage tatsächlich von amfid stammt: Der cdhash des Absenders wird mit dem cdhash von amfid verglichen. Dies verhindert jedoch nicht, dass die Nachricht an einen anderen Prozess als amfid gesendet wird; es verhindert nur, dass die Antwort von einem Nicht-amfid-Prozess stammt. Wenn wir ein Dreieck aufbauen, bei dem der Kernel Nachrichten an uns sendet, wir die Antwort erzeugen und an amfid übergeben und amfid die Antwort dann an den Kernel sendet, können wir die Absenderprüfung umgehen.
Dieser Ansatz bietet zahlreiche Vorteile, von denen der größte wahrscheinlich der Zugriff auf zusätzliche Flags in der Service-Routine verify_code_directory ist. Obwohl amfid nicht alle Flags verwendet, gibt es viele weitere Ausgabe-Flags, die amfid setzen könnte, um das Verhalten der Codesignierung zu steuern. Hier ist ein partieller Prototyp von verify_code_directory:```C
kern_return_t
verify_code_directory(
mach_port_t amfid_port,
amfid_path_t path,
uint64_t file_offset,
int32_t a4,
int32_t a5,
int32_t a6,
int32_t * entitlements_valid,
int32_t * signature_valid,
int32_t * unrestrict,
int32_t * signer_type,
int32_t * is_apple,
int32_t * is_developer_code,
amfid_a13_t a13,
amfid_cdhash_t cdhash,
audit_token_t audit);
Für Jailbreak-Entwickler ist der Parameter `is_apple` von besonderem Interesse. Dieser Parameter
scheint von amfid nicht verwendet zu werden; ist er jedoch gesetzt, veranlasst er den Kernel,
das Codesigning-Flag `CS_PLATFORM_BINARY` zu setzen, das der Anwendung Plattform-Binärprivilegien
gewährt. Insbesondere bedeutet dies, dass die Anwendung nun Task-Ports verwenden kann, um
Plattform-Binärdateien direkt zu modifizieren.
---------------------------------------------------------------------------------------------------
In diesem Angriff ausgenutzte Schlupflöcher
---------------------------------------------------------------------------------------------------
Dieser Angriff nutzt mehrere Schlupflöcher aus, die selbst keine Sicherheitslücken darstellen,
aber die Effektivität verschiedener Exploit-Mitigationen minimieren. Nicht alle davon müssen
zusammen geschlossen werden, da einige teilweise redundant sind, aber es ist trotzdem sinnvoll, sie alle aufzulisten.
Im Kernel:
1. `task_threads` kann ein reines Inspektions-`task_inspect_t` in ein änderungsfähiges `thread_act_t` umwandeln.
2. Es gibt kein `thread_conversion_eval`, das für Threads die Rolle von `task_conversion_eval` übernimmt.
3. Ein Nicht-Plattform-Binärprogramm darf ein `task_inspect_t`-Recht für ein Plattform-Binärprogramm verwenden.
4. Ausnahmemeldungen für nicht-sandboxed Prozesse können an sandboxed Prozesse zugestellt werden, obwohl
dies einen Weg bietet, aus der Sandbox auszubrechen. Es ist nicht klar, ob es eine saubere Lösung
für dieses Schlupfloch gibt.
5. Nicht-sandboxed Codeausführung, der Host-Priv-Port und die Möglichkeit, einen Prozess mit `task_for_pid-allow`
zum Absturz zu bringen, können kombiniert werden, um einen `task_for_pid`-Workaround zu erstellen. (Der Workaround besteht darin,
`host_set_exception_ports` aufzurufen, um einen neuen Exception-Handler auf Host-Ebene zu setzen, und dann den
`task_for_pid-allow`-Prozess zum Absturz zu bringen, um seinen Task-Port zu erhalten und Code mit der Berechtigung auszuführen.)
In App-Erweiterungen:
1. App-Erweiterungen, die sich eine App-Gruppe teilen, können über Mach-Nachrichten kommunizieren, obwohl die
Dokumentation nahelegt, dass eine Kommunikation zwischen der Host-App und der App-Erweiterung
unmöglich sein sollte.
Empfohlene Korrekturen und Minderungsmaßnahmen
---------------------------------------------------------------------------------------------------
Ich empfehle die folgenden Korrekturen, grob in der Reihenfolge ihrer Wichtigkeit:
1. Gib Mach-Ports in den launchd-Service-Routinen nur bei Rückgabe von `KERN_SUCCESS` frei. Dies
behebt die Verwundbarkeit durch Mach-Port-Austausch.
2. Schließe das `task_threads`-Schlupfloch, das einem Nicht-Plattform-Binärprogramm erlaubt, den Task-Port eines
Plattform-Binärprogramms zu verwenden, um Codeausführung zu erreichen.
3. Behebe Absturzprobleme in ReportCrash.
4. Die Menge der Mach-Dienste, die aus der Container-Sandbox erreichbar sind, sollte minimiert werden. Ich sehe
keinen legitimen Grund für die meisten iOS-Apps, mit ReportCrash oder SafetyNet zu kommunizieren.
5. So viele Prozesse wie möglich sollten sandboxed werden. Ich bin mir nicht sicher, ob druid
nicht-sandboxed sein muss, um ordnungsgemäß zu funktionieren; falls nicht, sollte es in eine geeignete Sandbox gelegt werden.
6. Toter Code sollte eliminiert werden. SafetyNet scheint seine vorgesehene Funktionalität nicht zu erfüllen.
Wenn es nicht mehr benötigt wird, sollte es wahrscheinlich entfernt werden.
7. Schließe den auf `host_set_exception_ports` basierenden `task_for_pid`-Workaround. Erwäge zum Beispiel,
ob es sich lohnt, `host_set_exception_ports` auf root zu beschränken oder die Verwendbarkeit
des Host-Priv-Ports unter bestimmten Konfigurationen einzuschränken. Dies verletzt das elegante,
auf Capabilities basierende Design von Mach, aber `host_set_exception_ports` könnte ein vielversprechendes Ziel für Missbrauch sein.
8. Ziehe in Betracht, ob es sich lohnt, `task_conversion_eval` zu `task_inspect_t` hinzuzufügen.
Blanket ausführen
---------------------------------------------------------------------------------------------------
Blanket sollte auf jedem Gerät mit iOS 11.2.6 funktionieren.
1. Lade das Projekt herunter: ```
git clone https://github.com/bazad/blanket
cd blanket
headers/config.h und ändern Sie APP_GROUP auf die App-Group-Kennung,
die Sie zuvor angegeben haben.Danach sollten Sie das Projekt auf dem Gerät erstellen und ausführen können.
Wenn blanket erfolgreich ist, führt es die Payload-Binärdatei aus (Quellcode in
blanket_payload/blanket_payload.c), die standardmäßig eine Bind-Shell auf Port 4242 startet. Sie können
sich mit Netcat mit diesem Port verbinden und beliebige Shell-Befehle ausführen.
Vielen Dank an Ian Beer und Jonathan Levin für ihre hervorragende Forschung zu iOS-Sicherheit und Interna.
Ich entdeckte diese Sicherheitslücke im Januar 2018 und begann Ende Februar mit der Entwicklung des Exploits. Ich meldete das Problem am 13. April an Apple. Apple wies die Mach-Port-Ersetzungs-Sicherheitslücke in launchd als CVE-2018-4280 zu, und sie wurde am 9. Juli in iOS 11.4.1 und macOS 10.13.6 behoben.
Blanket wird unter der MIT-Lizenz veröffentlicht.
Brandon Azad