
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.