
CVE-2018-4280: Mach port replacement vulnerability in launchd on iOS 11.2.6 leading to sandbox escape, privilege escalation, and codesigning bypass.
Blanket is a sandbox escape targeting iOS 11.2.6, although the main vulnerability was only patched
in iOS 11.4.1. It exploits a Mach port replacement vulnerability in launchd (CVE-2018-4280), as
well as several smaller vulnerabilities in other services, to execute code inside the ReportCrash
process, which is unsandboxed, runs as root, and has the task_for_pid-allow entitlement. This
grants blanket control over every process running on the phone, including security-critical ones
like amfid.
The exploit consists of several stages. This README will explain the main vulnerability and the stages of the sandbox escape step-by-step.
While researching crash reporting on iOS, I discovered a Mach port replacement vulnerability in launchd. By crashing in a particular way, a process can make the kernel send a Mach message to launchd that causes launchd to over-deallocate a send right to a Mach port in its IPC namespace. This allows an attacker to impersonate any launchd service it can look up to the rest of the system, which opens up numerous avenues to privilege escalation.
This vulnerability is also present on macOS, but triggering the vulnerability on iOS is more difficult due to checks in launchd that ensure that the Mach exception message comes from the kernel.
Launchd multiplexes multiple different Mach message handlers over its main port, including a MIG
handler for exception messages. If a process sends a mach_exception_raise or
mach_exception_raise_state_identity message to its own bootstrap port, launchd will receive and
process that message as a host-level exception.
Unfortunately, launchd's handling of these messages is buggy. If the exception type is EXC_CRASH,
then launchd will deallocate the thread and task ports sent in the message and then return
KERN_FAILURE from the service routine, causing the MIG system to deallocate the thread and task
ports again. (The assumption is that if a service routine returns success, then it has taken
ownership of all resources in the Mach message, while if the service routine returns an error, then
it has taken ownership of none of the resources.)
Here is the code from launchd's service routine for mach_exception_raise messages, decompiled
using IDA/Hex-Rays and lightly edited for readability:
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;
}
This is what the code does:
mach_exception_raise exception messages: it gets
invoked directly by the Mach system when launchd processes a mach_exception_raise Mach
exception message. The arguments to the service routine are parsed from the Mach message, and
hence are controlled by the message's sender.EXC_CRASH, and returns KERN_FAILURE if
so. The intent is to make sure not to handle EXC_CRASH messages, presumably so that
ReportCrash is invoked as the corpse handler. However, returning KERN_FAILURE at this point
will cause the task and thread ports to be deallocated again when the exception message is
cleaned up later. This means those two ports will be over-deallocated.In order for this vulnerability to be useful, we will want to free launchd's send right to a Mach service it vends, so that we can then impersonate that service to the rest of the system. This means that we'll need the task and thread ports in the exception message to really be send rights to the Mach service port we want to free in launchd. Then, once we've sent launchd the malicious exception message and freed the service port, we will try to get that same port name reused, but this time for a Mach port to which we hold the receive right. That way, when a client asks launchd to give them a send right to the Mach port for the service, launchd will instead give them a send right to our port, letting us impersonate that service to the client. After that, there are many different routes to gain system privileges.