
CVE-2018-4280: Vulnerabilidad de reemplazo de Mach port en launchd en iOS 11.2.6 que conduce a escape del sandbox, escalada de privilegios y omisión de validación de firma de código.
Blanket es una evasión de sandbox dirigida a iOS 11.2.6, aunque la vulnerabilidad principal solo se parcheó en iOS 11.4.1. Explota una vulnerabilidad de reemplazo de puertos Mach en launchd (CVE-2018-4280), así como varias vulnerabilidades menores en otros servicios, para ejecutar código dentro del proceso ReportCrash, el cual no tiene sandbox, se ejecuta como root y posee el permiso task_for_pid-allow. Esto le otorga a blanket control sobre cada proceso que se ejecuta en el teléfono, incluidos aquellos críticos para la seguridad como amfid.
El exploit consta de varias etapas. Este README explicará la vulnerabilidad principal y las etapas de la evasión del sandbox paso a paso.
Mientras investigaba el reporte de fallos en iOS, descubrí una vulnerabilidad de reemplazo de puertos Mach en launchd. Al fallar de una manera particular, un proceso puede hacer que el kernel envíe un mensaje Mach a launchd que provoca que launchd desasigne en exceso un derecho de envío a un puerto Mach en su espacio de nombres IPC. Esto permite a un atacante suplantar cualquier servicio de launchd que pueda buscar al resto del sistema, lo que abre numerosas vías para la escalada de privilegios.
Esta vulnerabilidad también está presente en macOS, pero desencadenarla en iOS es más difícil debido a las comprobaciones en launchd que aseguran que el mensaje de excepción Mach proviene del kernel.
Launchd multiplexa múltiples manejadores de mensajes Mach en su puerto principal, incluyendo un manejador MIG para mensajes de excepción. Si un proceso envía un mensaje mach_exception_raise o mach_exception_raise_state_identity a su propio puerto bootstrap, launchd recibirá y procesará ese mensaje como una excepción a nivel de host.
Desafortunadamente, el manejo de estos mensajes por parte de launchd tiene errores. Si el tipo de excepción es EXC_CRASH, entonces launchd desasignará los puertos de hilo y tarea enviados en el mensaje y luego devolverá KERN_FAILURE desde la rutina de servicio, provocando que el sistema MIG desasigne nuevamente los puertos de hilo y tarea. (La suposición es que si una rutina de servicio devuelve éxito, entonces ha tomado posesión de todos los recursos en el mensaje Mach, mientras que si la rutina de servicio devuelve un error, entonces no ha tomado posesión de ninguno de los recursos).
Aquí está el código de la rutina de servicio de launchd para mensajes mach_exception_raise, descompilado usando IDA/Hex-Rays y ligeramente editado para legibilidad:```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;
}
Esto es lo que hace el código:
1. Esta función es la rutina de servicio Mach para mensajes de excepción `mach_exception_raise`: se invoca directamente por el sistema Mach cuando launchd procesa un mensaje de excepción Mach `mach_exception_raise`. Los argumentos de la rutina de servicio se analizan desde el mensaje Mach y, por lo tanto, están controlados por el remitente del mensaje.
2. En (b), launchd comprueba que el mensaje de excepción Mach fue enviado por el kernel. El token de auditoría del remitente contiene el PID del proceso emisor en el campo 5, que será cero solo para el kernel. Si el mensaje no fue enviado por el kernel, se rechaza.
3. Los puertos de tarea y hebra del mensaje se desasignan explícitamente en (c) y (d).
4. En (e), launchd comprueba si el tipo de excepción es `EXC_CRASH` y devuelve `KERN_FAILURE` si es así. La intención es asegurarse de no manejar mensajes `EXC_CRASH`, presumiblemente para que ReportCrash sea invocado como el manejador del cadáver. Sin embargo, devolver `KERN_FAILURE` en este punto hará que los puertos de tarea y hebra se desasignen nuevamente cuando el mensaje de excepción se limpie más tarde. Esto significa que esos dos puertos se desasignarán en exceso.