
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.
Para que esta vulnerabilidad sea útil, querremos liberar el derecho de envío de launchd a un servicio Mach que ofrece, para luego poder suplantar ese servicio ante el resto del sistema. Esto significa que necesitaremos que los puertos de tarea y hebra en el mensaje de excepción sean realmente derechos de envío al puerto de servicio Mach que queremos liberar en launchd. Luego, una vez que hayamos enviado a launchd el mensaje de excepción malicioso y liberado el puerto de servicio, intentaremos que ese mismo nombre de puerto sea reutilizado, pero esta vez para un puerto Mach del cual tenemos el derecho de recepción. De esa manera, cuando un cliente le pida a launchd que le dé un derecho de envío al puerto Mach para el servicio, launchd le dará en su lugar un derecho de envío a nuestro puerto, permitiéndonos suplantar ese servicio ante el cliente. Después de eso, hay muchas rutas diferentes para obtener privilegios del sistema.
### Desencadenando la vulnerabilidad
Para realmente desencadenar la vulnerabilidad, necesitaremos evitar la comprobación de que el mensaje fue enviado por el kernel. Esto se debe a que si enviamos el mensaje de excepción directamente a launchd, simplemente será descartado. De alguna manera, necesitamos lograr que el kernel envíe un mensaje de excepción "malicioso" que contenga un derecho de envío Mach para un servicio del sistema en lugar de los puertos reales de tarea y hebra.
Resulta que existe una trampa Mach, `task_set_special_port`, que se puede usar para establecer un derecho de envío personalizado que se utilizará en lugar del puerto de tarea real en ciertas situaciones. Una de estas situaciones es cuando el kernel genera un mensaje de excepción en nombre de una tarea: en lugar de colocar el derecho de envío de tarea real en el mensaje de excepción, el kernel utilizará el derecho de envío proporcionado por `task_set_special_port`. Más específicamente, si una tarea llama a `task_set_special_port` para establecer un valor personalizado para su puerto especial `TASK_KERNEL_PORT` y luego la tarea falla, el mensaje de excepción generado por el kernel tendrá un derecho de envío al puerto personalizado, no al puerto de tarea real, en el campo "tarea". Una API equivalente, `thread_set_special_port`, se puede usar para establecer un puerto personalizado en el campo "hebra" del mensaje de excepción generado.
Debido a este comportamiento, en realidad no es difícil hacer que el kernel genere un mensaje de excepción "malicioso" que contenga un puerto de servicio Mach en lugar del puerto de tarea y hebra. Sin embargo, aún necesitamos asegurarnos de que el mensaje de excepción que generamos se entregue a launchd.
Una vez más, asegurar que el kernel entregue el mensaje de excepción "malicioso" a launchd no es difícil si conoces la API correcta. La función `thread_set_exception_ports` establecerá cualquier derecho de envío Mach como el puerto al que se entregan los mensajes de excepción en este hilo. Por lo tanto, todo lo que necesitamos hacer es invocar `thread_set_exception_ports` con el puerto bootstrap, y entonces cualquier excepción que generemos hará que el kernel envíe un mensaje de excepción a launchd.
La última pieza del rompecabezas es obtener el tipo de excepción correcto. La vulnerabilidad solo se activará para excepciones `EXC_CRASH`. Un poco de prueba y error revela que podemos generar fácilmente excepciones `EXC_CRASH` llamando a la función estándar `abort`.
Por lo tanto, en resumen, podemos usar APIs existentes y bien documentadas para hacer que el kernel genere un mensaje de excepción `EXC_CRASH` malicioso en nuestro nombre y lo entregue a launchd, desencadenando la vulnerabilidad y liberando el puerto de servicio Mach:
1. Use `thread_set_exception_ports` para establecer launchd como el manejador de excepciones para este hilo.
2. Llame a `bootstrap_look_up` para obtener el puerto de servicio del servicio que queremos suplantar desde launchd.
3. Llame a `task_set_special_port`/`thread_set_special_port` para usar ese puerto de servicio en lugar de los puertos reales de tarea y hebra en los mensajes de excepción.
4. Llame a `abort`. El kernel enviará un mensaje de excepción `EXC_CRASH` a launchd, pero los puertos de tarea y hebra en el mensaje serán el puerto de servicio objetivo.
5. Launchd procesará el mensaje de excepción y liberará el puerto de servicio.
### Ejecutar código después del crash
Hay un problema con la estrategia anterior: llamar a `abort` matará nuestro proceso. Si queremos poder ejecutar cualquier código después de desencadenar la vulnerabilidad, necesitamos una forma de realizar el crash en otro proceso.
(Con otros tipos de excepción, un proceso podría recuperarse de la excepción. La forma en que un proceso se recuperaría es estableciendo su manejador de excepción de hilo como launchd y su manejador de excepción de tarea como sí mismo. Después de que launchd procese y no pueda manejar la excepción, el kernel enviaría la excepción al manejador de tarea, que restablecería el estado del hilo e informaría al kernel que la excepción ha sido manejada. Sin embargo, un proceso no puede capturar sus propias excepciones `EXC_CRASH`, por lo que necesitamos dos procesos).
Una estrategia es primero explotar una vulnerabilidad en otro proceso en iOS y forzar a ese proceso a configurar sus puertos de kernel y crash. Sin embargo, para una prueba de concepto, es más fácil crear una extensión de aplicación.
Las extensiones de aplicación, introducidas en iOS 8, proporcionan una forma de empaquetar alguna funcionalidad de una aplicación para que esté disponible fuera de la aplicación. El código de una extensión de aplicación se ejecuta en un proceso separado y en un entorno limitado (sandbox). Esto hace que sea muy fácil lanzar un proceso que configure sus puertos especiales, registre launchd como su manejador de excepción para `EXC_CRASH` y luego llame a `abort`.
No hay una forma soportada para que una aplicación lance programáticamente su propia extensión de aplicación y se comunique con ella. Sin embargo, Ian McDowell escribió un [excelente artículo][Multi-Process iOS App Using NSExtension] describiendo cómo usar la API privada `NSExtension` para lanzar y comunicarse con un proceso de extensión de aplicación. He usado una estrategia casi idéntica aquí. La única diferencia es que necesitamos comunicar un puerto Mach al proceso de la extensión de aplicación, lo que implica registrar un servicio ficticio con launchd al que la extensión de aplicación se conecta.
[Multi-Process iOS App Using NSExtension]: https://ianmcdowell.net/blog/nsextension/
### Evitando la reutilización de puertos en launchd
Un desafío que notarías si ejecutas el exploit como se describió es que ocasionalmente no podrías readquirir el puerto liberado. La razón es que el kernel rastrea las entradas IPC libres de un proceso en una lista libre, por lo que un nombre de puerto recién liberado será reutilizado (con un número de generación diferente) cuando se asigna un nuevo puerto en la tabla IPC. Por lo tanto, solo reasignaremos el nombre de puerto que queremos si launchd no reutiliza primero esa ranura de entrada IPC para otro puerto.
La forma de evitarlo es enterrar la ranura de entrada IPC libre en la lista libre, de modo que si launchd asigna nuevos puertos, esas otras ranuras se usarán primero. ¿Cómo hacemos esto? Podemos registrar un montón de servicios Mach ficticios en launchd con puertos de los que tenemos el derecho de recepción. Cuando llamamos a `abort`, el manejador de excepción se disparará primero, y luego el estado del proceso, incluidos los puertos Mach, se limpiará. Cuando launchd recibe la excepción `EXC_CRASH`, accidentalmente liberará el puerto de servicio objetivo, colocando la ranura de entrada IPC correspondiente a ese nombre de puerto al frente de la lista libre. Luego, cuando se destruyan el resto de los puertos Mach de nuestra extensión de aplicación, launchd recibirá notificaciones y liberará los puertos de servicio ficticios, enterrando la ranura de entrada IPC objetivo detrás de las ranuras de los puertos recién liberados. Por lo tanto, siempre que launchd asigne menos puertos que el número de servicios ficticios que registramos, la ranura objetivo aún estará en la lista libre, lo que significa que aún podemos hacer que launchd reasigne la ranura con el mismo nombre de puerto que el servicio original.
La limitación de esta estrategia es que necesitamos el entitlement `com.apple.security.application-groups` para registrar servicios con launchd. Hay otras formas de almacenar puertos Mach en launchd, pero usar grupos de aplicación es ciertamente la más fácil y es suficiente para esta prueba de concepto.
### Suplantando el servicio liberado
Una vez que hemos generado la extensión de aplicación crasher y liberado un derecho de envío Mach en launchd, necesitamos reasignar ese nombre de puerto Mach con un derecho de envío del cual tenemos el derecho de recepción. De esa manera, cualquier mensaje que launchd envíe a ese nombre de puerto será recibido por nosotros, y cada vez que launchd comparta ese nombre de puerto con un cliente, el cliente recibirá un derecho de envío a nuestro puerto. En particular, si podemos liberar el derecho de envío de launchd a un servicio Mach, entonces cualquier proceso que solicite ese servicio de launchd recibirá un derecho de envío a nuestro propio puerto en lugar del puerto de servicio real. Esto nos permite suplantar el servicio o realizar un ataque de hombre en el medio, inspeccionando todos los mensajes que el cliente envía al servicio.
Lograr que el nombre de puerto liberado sea reutilizado para que se refiera a un puerto que poseemos también es bastante simple, dado que ya hemos decidido usar el entitlement de grupos de aplicación: solo registra servicios Mach ficticios con launchd hasta que uno de ellos reutilice el nombre de puerto original. Tendremos que hacerlo en lotes, registrando un gran número de servicios ficticios juntos, verificando si alguno ha reutilizado exitosamente el nombre de puerto liberado, y luego dándolos de baja. La razón es que necesitamos estar seguros de que nuestros registros recorran toda la lista libre de puertos IPC para recuperar el nombre de puerto enterrado que queremos.
Podemos verificar si hemos logrado reutilizar exitosamente el nombre de puerto liberado buscando el servicio original con `bootstrap_look_up`: si devuelve uno de nuestros puertos de servicio registrados, hemos terminado.
Una vez que hemos logrado registrar un nuevo servicio que obtiene el mismo nombre de puerto que el original, cualquier cliente que busque el servicio original en launchd recibirá un derecho de envío a nuestro puerto, no al puerto de servicio real. Por lo tanto, estamos efectivamente suplantando el servicio original ante el resto del sistema (o al menos, ante aquellos procesos que busquen el servicio después de nuestro ataque).
Stage 1: Obteniendo el puerto host-priv
---------------------------------------------------------------------------------------------------
Una vez que tenemos la capacidad de suplantar servicios arbitrarios del sistema, el siguiente paso es obtener el puerto host-priv. Este paso es sencillo y no se ve afectado por los cambios en iOS 11.3. La idea general de este ataque es suplantar SafetyNet, hacer crash a ReportCrash y luego recuperar el puerto host-priv del puerto de tarea de ReportCrash moribundo enviado en el mensaje de excepción.
### Acerca de ReportCrash y SafetyNet
ReportCrash es responsable de generar informes de crash en iOS. Este mismo binario ofrece 4 servicios diferentes (cada uno en un proceso diferente, aunque no todos pueden estar ejecutándose en un momento dado):
1. `com.apple.ReportCrash` es responsable de generar informes de crash para procesos que fallan. Es el manejador de excepciones a nivel de host para excepciones `EXC_CRASH`, `EXC_GUARD` y `EXC_RESOURCE`.
2. `com.apple.ReportCrash.Jetsam` maneja informes de Jetsam.
3. `com.apple.ReportCrash.SimulateCrash` crea informes para crashes simulados.
4. `com.apple.ReportCrash.SafetyNet` es el manejador de excepciones registrado para el servicio `com.apple.ReportCrash`.
Los que nos interesan son `com.apple.ReportCrash` y `com.apple.ReportCrash.SafetyNet`, en adelante referidos simplemente como ReportCrash y SafetyNet. Ambos son servicios basados en MIG y ejecutan el mismo código de manera efectiva.
Cuando ReportCrash se inicia, busca el servicio SafetyNet en launchd y establece el puerto devuelto como el manejador de excepciones a nivel de tarea. La intención parece ser que si el propio ReportCrash fallara, un proceso separado generaría el informe de crash para él. Sin embargo, esta ruta de código parece estar en desuso: ReportCrash registra SafetyNet para mensajes `mach_exception_raise`, aunque tanto ReportCrash como SafetyNet solo manejan mensajes `mach_exception_raise_state_identity`. No obstante, ambos servicios aún están presentes y son accesibles desde el sandbox del contenedor de iOS.
### Primitivas de manipulación de ReportCrash
Para llevar a cabo el siguiente ataque, necesitamos ser capaces de manipular ReportCrash (o SafetyNet) para que se comporte de la manera que queremos. Específicamente, necesitamos las siguientes capacidades: iniciar ReportCrash bajo demanda, forzar la salida de ReportCrash, hacer crash a ReportCrash y asegurarnos de que ReportCrash no salga mientras lo estamos usando. Aquí describiré cómo logramos cada objetivo.
Para iniciar ReportCrash, simplemente necesitamos enviarle un mensaje Mach: launchd lo iniciará bajo demanda. Sin embargo, debido a su diseño peculiar, cualquier tipo de mensaje excepto `mach_exception_raise_state_identity` hará que ReportCrash deje de responder a nuevos mensajes y eventualmente salga. Por lo tanto, necesitamos enviar un mensaje `mach_exception_raise_state_identity` si queremos que se mantenga vivo después.
Para hacer salir a ReportCrash, podemos simplemente enviarle cualquier otro tipo de mensaje Mach.
Hay muchas formas de hacer crash a ReportCrash. La más fácil es probablemente enviar un mensaje `mach_exception_raise_state_identity` con el puerto de hebra establecido en `MACH_PORT_NULL`.
Finalmente, necesitamos asegurarnos de que ReportCrash no salga mientras lo estamos usando. Cada mensaje `mach_exception_raise_state_identity` que procesa hace que se genere otro hilo para escuchar el siguiente mensaje mientras el hilo original genera el informe de crash. ReportCrash saldrá una vez que todos los hilos pendientes que generan un informe de crash hayan terminado. Por lo tanto, si podemos bloquear uno de esos hilos mientras está en el proceso de generar un informe de crash, podemos evitar que nunca salga.
La forma más fácil que encontré para hacer eso fue enviar un mensaje `mach_exception_raise_state_identity` con un puerto personalizado en los campos de tarea y hebra. Una vez que ReportCrash intente generar un informe de crash, llamará a `task_policy_get` en el puerto "tarea", lo que hará que envíe un mensaje Mach al puerto que enviamos y espere una respuesta. Pero dado que el puerto "tarea" es solo un puerto Mach normal, podemos simplemente no responder al mensaje Mach, y ReportCrash esperará indefinidamente a que `task_policy_get` devuelva.
### Extrayendo host-priv de ReportCrash
Para la primera etapa del exploit, el plan de ataque es relativamente sencillo:
1. Iniciar el servicio SafetyNet y forzarlo a mantenerse vivo durante la duración de nuestro ataque.
2. Usar la primitiva de suplantación de servicio de launchd para suplantar SafetyNet. Esto nos da un nuevo puerto en el que podemos recibir mensajes destinados al servicio real de SafetyNet.
3. Hacer que cualquier instancia existente de ReportCrash salga. De esa manera, podemos asegurarnos de que ReportCrash busque nuestro puerto SafetyNet en el siguiente paso.
4. Iniciar ReportCrash. ReportCrash buscará SafetyNet en launchd y establecerá el puerto resultante, que es el puerto SafetyNet falso del cual poseemos el derecho de recepción, como destino para mensajes `EXC_CRASH`.
5. Desencadenar un crash en ReportCrash. Después de ver que no hay manejadores registrados para el tipo de excepción original, ReportCrash entrará en la fase de muerte del proceso. En este punto, XNU verá que ReportCrash registró el puerto SafetyNet falso para recibir excepciones `EXC_CRASH`, por lo que generará un mensaje de excepción y lo enviará a ese puerto.
6. Luego escuchamos en el puerto SafetyNet falso el mensaje `EXC_CRASH`. Será de tipo `mach_exception_raise`, lo que significa que contendrá el puerto de tarea de ReportCrash.
7. Finalmente, usamos `task_get_special_port` en el puerto de tarea de ReportCrash para obtener el puerto host de ReportCrash. Dado que ReportCrash no está en sandbox y se ejecuta como root, este es el puerto host-priv.
Al final de esta etapa de la evasión del sandbox, terminamos con un puerto host-priv utilizable. Esto solo demuestra que es un problema de seguridad grave.
Stage 2: Evadiendo el sandbox
---------------------------------------------------------------------------------------------------
Aunque tenemos el puerto host-priv, nuestro objetivo es evadir completamente el sandbox y ejecutar código como root con el entitlement `task_for_pid-allow`. El primer paso para lograrlo es simplemente evadir el sandbox.
Técnicamente hablando, no hay razón para que necesitemos obtener el puerto host-priv antes de evadir el sandbox: estos dos pasos son independientes y pueden ocurrir en cualquier orden. Sin embargo, esta etapa dejará el sistema inestable si esta o etapas posteriores fallan, por lo que vale la pena ponerla después.
El ataque de alto nivel es usar la misma vulnerabilidad de launchd nuevamente para suplantar un servicio del sistema. Sin embargo, esta vez nuestro objetivo es suplantar un servicio al que un cliente enviará su puerto de tarea en un mensaje Mach. Es fácil encontrar por experimentación en iOS 11.2.6 que si suplantamos `com.apple.CARenderServer` (en adelante CARenderServer) alojado por backboardd y luego nos comunicamos con `com.apple.DragUI.druid.source`, el daemon druid sin sandbox enviará su puerto de tarea en un mensaje Mach al puerto de servicio falso.
Este paso del exploit está roto en iOS 11.3 porque druid ya no envía su puerto de tarea en el mensaje Mach a CARenderServer. A pesar de esto, estoy seguro de que esta vulnerabilidad aún puede ser utilizada para evadir el sandbox. Una forma de abordar esto es buscar servicios sin sandbox que confíen en la entrada de otros servicios. Este tipo de "vulnerabilidades" nunca serían explotables sin la capacidad de reemplazar servicios del sistema, lo que significa que probablemente son una superficie de ataque de baja prioridad, tanto interna como externamente a Apple.
### Haciendo crash a druid
Al igual que con ReportCrash, necesitamos ser capaces de forzar a druid a reiniciarse en caso de que ya esté ejecutándose para que busque nuestro puerto CARenderServer falso en launchd. Decidí usar un error en libxpc que ya estaba programado para ser corregido para este propósito.
Mientras revisaba libxpc, encontré una lectura fuera de los límites que podría usarse para forzar a cualquier servicio XPC a hacer crash:```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 );
}
...
}
El problema es que el uso de un strlen sin verificación en datos controlados por el atacante permite que la clave para la entrada del diccionario serializado se extienda más allá del final del búfer de datos. Esto significa que el servicio XPC que deserializa el diccionario se bloqueará, ya sea cuando strlen desreferencie memoria fuera de los límites o cuando _xpc_serializer_advance intente avanzar el serializador más allá del final de los datos proporcionados.
Este error ya estaba corregido en iOS 11.3 Beta para cuando lo descubrí, así que no lo reporté a Apple. El exploit está disponible como un proyecto independiente en mi repositorio xpc-crash.
Para usar este error para bloquear druid, simplemente necesitamos enviar al servicio druid un mensaje XPC malformado de modo que la clave del diccionario no esté terminada y se extienda hasta el último byte del mensaje.
Obtener el puerto de tarea de druid en iOS 11.2.6 usando nuestra primitiva de suplantación de servicios es fácil:
Una vez que tenemos el puerto de tarea de druid, aún necesitamos descubrir cómo ejecutar código dentro del proceso druid.
El problema es que XNU protege los puertos de tarea para los binarios de plataforma de ser modificados por binarios no de plataforma. La defensa se implementa en la función task_conversion_eval, que es llamada por convert_port_to_locked_task y convert_port_to_task_with_exec_token:```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;
}
Las rutinas de conversión MIG que dependen de estas funciones, incluyendo `convert_port_to_task` y
`convert_port_to_map`, por lo tanto fallarán cuando las llamemos en la tarea de druid. Por ejemplo,
`mach_vm_write` no nos permitirá manipular la memoria de druid.
Sin embargo, al mirar el archivo MIG `osfmk/mach/task.defs` en XNU, noté algo interesante:```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);
La función task_threads, que enumera los hilos en una tarea, en realidad recibe un task_inspect_t en lugar de un task_t, lo que significa que MIG lo convierte usando convert_port_to_task_inspect en lugar de convert_port_to_task. Una rápida revisión de convert_port_to_task_inspect revela que esta función no realiza la comprobación task_conversion_eval, por lo que podemos llamarla exitosamente en binarios de plataforma. Esto es interesante porque los hilos devueltos no son derechos thread_inspect_t, sino derechos completos thread_act_t. En otras palabras, task_threads promueve un derecho de tarea no modificable a derechos de hilo modificables. Y dado que no existe un thread_conversion_eval equivalente, esto significa que podemos usar las APIs de hilos de Mach para modificar los hilos de una tarea incluso si esa tarea es un binario de plataforma.
Para aprovechar esto, escribí una biblioteca llamada threadexec que construye una capacidad completa de llamada a funciones sobre las APIs de hilos de Mach. El proyecto threadexec en sí mismo fue una tarea significativa, pero como solo es indirectamente relevante para este exploit, omitiré una explicación detallada de su funcionamiento interno.
Una vez que tenemos el puerto host-priv y la ejecución de código sin sandbox dentro de druid, la siguiente etapa del escape completo del sandbox es instalar un nuevo manejador de excepciones a nivel de host. Este proceso es sencillo dadas nuestras capacidades actuales:
EXC_BAD_ACCESS llamando a host_get_exception_ports.EXC_BAD_ACCESS.host_set_exception_ports para registrar nuestro puerto Mach como el manejador de excepciones a nivel de host para EXC_BAD_ACCESS.Después de esta etapa, cada vez que un proceso acceda a una dirección de memoria inválida (y además no tenga un manejador de excepciones registrado), se enviará un mensaje de excepción EXC_BAD_ACCESS a nuestro nuevo puerto manejador de excepciones. Esto nos dará el puerto de tarea de cualquier proceso que falle, y dado que EXC_BAD_ACCESS es una excepción recuperable, esta vez podemos usar el puerto de tarea para ejecutar código.
La siguiente etapa es desencadenar una excepción EXC_BAD_ACCESS en ReportCrash para que su puerto de tarea se envíe en un mensaje de excepción a nuestro nuevo puerto manejador de excepciones:
EXC_BAD_ACCESS. Como ReportCrash no tiene un manejador de excepciones registrado para EXC_BAD_ACCESS (recuerde que SafetyNet está registrado para EXC_CRASH), la excepción se entregará al manejador de excepciones a nivel de host.KERN_SUCCESS para indicar al kernel que la excepción ha sido manejada y que ReportCrash puede reanudarse.En este punto, tenemos ejecución de código dentro de un proceso sin sandbox, root y con task_for_pid-allow.
Las siguientes dos etapas no son estrictamente necesarias pero deberían realizarse de todas formas.
Una vez que tenemos ejecución de código dentro de ReportCrash, debemos restablecer el manejador de excepciones a nivel de host para EXC_BAD_ACCESS usando druid:
host_set_exception_ports en druid para volver a registrar el manejador de excepciones a nivel de host antiguo para EXC_BAD_ACCESS.Esto evitará que nuestro puerto manejador de excepciones reciba mensajes de excepción de otros procesos que fallen.
El último paso es restaurar el daño que hicimos a launchd cuando liberamos puertos de servicio en su espacio IPC para suplantarlos:
task_for_pid en ReportCrash para obtener el puerto de tarea de launchd.mach_port_insert_right en ReportCrash para insertar el puerto de servicio real en el espacio IPC de launchd bajo el nombre original.Después de este paso, el sistema debería volver a ser completamente funcional. Tras una explotación exitosa, no debería ser necesario forzar el reinicio del dispositivo, ya que el exploit repara todos los daños por sí mismo.
Blanket también incluye un payload de post-explotación que evita amfid y genera una shell vinculante (bind shell). Esta sección describirá cómo se logra.
Incluso después de obtener ejecución de código en ReportCrash, usar esa capacidad no es fácil: estamos limitados a realizar llamadas a funciones individuales desde dentro del proceso, lo que hace que sea tedioso realizar tareas complejas. Idealmente, nos gustaría una forma de ejecutar código de forma nativa con los privilegios de ReportCrash, ya sea inyectando código en ReportCrash o generando un nuevo proceso con los mismos (o mayores) privilegios.
Blanket elige la ruta de generación de procesos. Usamos task_for_pid y nuestro estado de binario de plataforma en ReportCrash para obtener el puerto de tarea de launchd y crear un nuevo hilo dentro de launchd que podamos controlar. Luego usamos ese hilo para llamar a posix_spawn para lanzar nuestro binario payload. El binario payload puede estar firmado con entitlements restringidos, incluyendo task_for_pid-allow, para otorgar capacidades adicionales.
Para que iOS acepte nuestro nuevo binario generado, necesitamos evadir la firma de código. Se han discutido varias estrategias a lo largo de los años, pero la estrategia actual más común es registrar un manejador de excepciones para amfid y luego realizar un parche de datos para que amfid falle al intentar llamar a MISValidateSignatureAndCopyInfo. Esto nos permite falsificar la implementación de esa función para pretender que la firma de código es válida.
Sin embargo, existe otro enfoque que creo que es más robusto y flexible: en lugar de parchear amfid por completo, simplemente podemos registrar un nuevo puerto de amfid en el kernel.
El kernel lleva un registro de qué puerto enviar mensajes a amfid usando un puerto especial de host llamado HOST_AMFID_PORT. Si tenemos ejecución de código root sin sandbox, podemos establecer este puerto a un nuevo valor. Apple ha protegido contra este ataque comprobando si la respuesta a una solicitud de validación realmente provino de amfid: el cdhash del remitente se compara con el cdhash de amfid. Sin embargo, esto en realidad no impide que el mensaje se envíe a un proceso diferente de amfid; solo evita que la respuesta provenga de un proceso que no sea amfid. Si configuramos un triángulo donde el kernel nos envía mensajes, generamos la respuesta y la pasamos a amfid, y luego amfid envía la respuesta al kernel, entonces podremos evadir la verificación del remitente.
Hay numerosas ventajas en este enfoque, de las cuales la más grande es probablemente el acceso a banderas adicionales en la rutina de servicio verify_code_directory. Aunque amfid no las usa todas, hay muchas otras banderas de salida que amfid podría establecer para controlar el comportamiento de la firma de código. Aquí hay un prototipo parcial de 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);
De particular interés para los desarrolladores de jailbreak es el parámetro `is_apple`. Este parámetro no parece ser utilizado por amfid, pero si se establece, hará que el kernel establezca la bandera de firma de código `CS_PLATFORM_BINARY`, que otorga privilegios de binario de plataforma a la aplicación. En particular, esto significa que la aplicación ahora puede usar puertos de tarea para modificar binarios de plataforma directamente.
Lagunas utilizadas en este ataque
---------------------------------------------------------------------------------------------------
Este ataque aprovecha varias lagunas que no son vulnerabilidades de seguridad en sí mismas, pero que minimizan la efectividad de varias mitigaciones de exploits. No todas estas necesitan cerrarse juntas, ya que algunas son parcialmente redundantes, pero vale la pena enumerarlas de todos modos.
En el kernel:
1. `task_threads` puede promover un `task_inspect_t` de solo inspección a un `thread_act_t` con capacidad de modificación.
2. No hay `thread_conversion_eval` que realice el rol de `task_conversion_eval` para hilos.
3. Un binario no de plataforma puede usar un derecho `task_inspect_t` para un binario de plataforma.
4. Los mensajes de excepción para procesos sin sandbox pueden ser entregados a procesos con sandbox, incluso cuando eso proporciona una forma de escapar del sandbox. No está claro si existe una solución limpia para esta laguna.
5. La ejecución de código sin sandbox, el puerto host-priv y la capacidad de bloquear un proceso `task_for_pid-allow` se pueden combinar para construir una solución alternativa de `task_for_pid`. (La solución alternativa es: llamar a `host_set_exception_ports` para establecer un nuevo manejador de excepciones a nivel de host, luego bloquear el proceso `task_for_pid-allow` para recibir su puerto de tarea y ejecutar código con la autorización.)
En extensiones de aplicación:
1. Las extensiones de aplicación que comparten un grupo de aplicaciones pueden comunicarse usando mensajes Mach, a pesar de que la documentación sugiere que la comunicación entre la aplicación anfitriona y la extensión de aplicación debería ser imposible.
Correcciones y mitigaciones recomendadas
---------------------------------------------------------------------------------------------------
Recomiendo las siguientes correcciones, aproximadamente en orden de importancia:
1. Solo desasignar puertos Mach en las rutinas de servicio de launchd cuando se devuelve `KERN_SUCCESS`. Esto corregirá la vulnerabilidad de reemplazo de puertos Mach.
2. Cerrar la laguna de `task_threads` que permite que un binario no de plataforma use el puerto de tarea de un binario de plataforma para lograr la ejecución de código.
3. Corregir problemas de bloqueo en ReportCrash.
4. El conjunto de servicios Mach accesibles desde dentro del sandbox del contenedor debe minimizarse. No veo una razón legítima para que la mayoría de las aplicaciones iOS se comuniquen con ReportCrash o SafetyNet.
5. Tantos procesos como sea posible deberían tener sandbox. No estoy seguro de si druid necesita estar sin sandbox para funcionar correctamente, pero si no es así, debería colocarse en un sandbox apropiado.
6. El código muerto debería eliminarse. SafetyNet no parece estar realizando su funcionalidad prevista. Si ya no es necesario, probablemente debería eliminarse.
7. Cerrar la solución alternativa de `task_for_pid` basada en `host_set_exception_ports`. Por ejemplo, considerar si vale la pena restringir `host_set_exception_ports` a root o restringir la usabilidad del puerto host-priv bajo algunas configuraciones. Esto viola el elegante diseño basado en capacidades de Mach, pero `host_set_exception_ports` podría ser un objetivo prometedor para abusos.
8. Considerar si vale la pena agregar `task_conversion_eval` a `task_inspect_t`.
Ejecutando blanket
---------------------------------------------------------------------------------------------------
Blanket debería funcionar en cualquier dispositivo con iOS 11.2.6.
1. Descargar el proyecto: ```
git clone https://github.com/bazad/blanket
cd blanket
headers/config.h y cambia APP_GROUP por el identificador de grupo de aplicación
que hayas especificado anteriormente.Después de eso, deberías poder compilar y ejecutar el proyecto en el dispositivo.
Si blanket se ejecuta con éxito, iniciará el binario de payload (código fuente en
blanket_payload/blanket_payload.c), que por defecto abre un shell vinculado en el puerto 4242. Puedes
conectarte a ese puerto con netcat y ejecutar comandos de shell arbitrarios.
Muchas gracias a Ian Beer y Jonathan Levin por su excelente investigación sobre la seguridad y el funcionamiento interno de iOS.
Descubrí esta vulnerabilidad en enero de 2018 y comencé a desarrollar el exploit a finales de febrero. Reporté el problema a Apple el 13 de abril. Apple asignó a la vulnerabilidad de reemplazo de puerto Mach en launchd el CVE-2018-4280, y fue parcheada en iOS 11.4.1 y macOS 10.13.6 el 9 de julio.
Blanket se publica bajo la licencia MIT.
Brandon Azad