Blog post 29 septiembre 2021 por Jim Aspers, Especialista en Seguridad de Bureau Veritas Cybersecurity
Aplicaciones iOS en Macs ARM: Oportunidades de Pentesting | Parte II
La posibilidad de ejecutar aplicaciones iOS en Macs ARM es una característica interesante para los investigadores de seguridad de aplicaciones. En la parte I, demostramos que, con algunos retoques, es posible ejecutar cualquier aplicación arbitraria de iOS en macOS. Las aplicaciones que requerían acceso al micrófono, la cámara, el Bluetooth o los servicios de localización del dispositivo funcionaron todas como se esperaba.
Aunque demostramos la posibilidad de ejecutar aplicaciones arbitrarias de iOS, lo hicimos utilizando una aplicación legítima y sustituyendo su contenido por la aplicación que intentábamos ejecutar; no se comprendió en profundidad el proceso de lanzamiento. Para obtener un control total sobre la aplicación lanzada y el entorno en el que se desplegaba, se requería una comprensión más sólida del proceso de lanzamiento.
Además, se descubrió que muchas aplicaciones que integraban la detección de jailbreak marcaban nuestros Mac como jailbroken, lo que requería un bypass manual. Buscamos una forma sólida de sortear esto, sin parchear ni instrumentar la aplicación de destino.
Lanzador de aplicaciones personalizable: launchr
Es deseable tener el mayor control posible sobre la aplicación bajo prueba. Concretamente, esto incluye (pero no se limita a):
- Tener la capacidad de especificar archivos para los descriptores de entrada, salida y error de la aplicación;
- Poder especificar variables de entorno;
- Desencadenar el proceso de la aplicación en un estado suspendido para una instrumentación temprana.
El lanzamiento de binarios iOS no gráficos en macOS puede hacerse de forma bastante trivial utilizando la llamada al sistema privada posix_spawnattr_set_platform_np() para establecer el atributo de plataforma del proceso en '2'(PLATFORM_IOS, véase <mach-o/loader.h en las fuentes de XNU antes del lanzamiento (como explica Samuel Groß en su artículo). Spawning una aplicación gráfica iOS es un tipo diferente de juego, sin embargo. Hicimos ingeniería inversa parcialmente sobre cómo macOS maneja los lanzamientos de aplicaciones gráficas en general, y aplicamos este conocimiento al caso específico de iOS.
Varios componentes del SO resultaron estar implicados en el lanzamiento de una aplicación iOS, entre otros
- runningboardd
- secinitd
- lsd
- appinstalld/com.apple.MobileInstallationService
- containermanagerd
Muchos de estos componentes están implicados en el sandboxing y otras tareas relevantes para la seguridad. Como preferiríamos lanzar directamente una aplicación eludiendo cualquiera de esos componentes relacionados con la seguridad, analizamos el lanzamiento de una sencilla aplicación del sistema (Calculator.app) y comparamos el flujo observado. Esta vez, parecía que sólo runningboardd participaba activamente. Esto desplazó el foco de atención hacia runningboardd.
En este punto, se había establecido la sospecha de que necesitábamos hablar con runningboardd de algún modo para lanzar aplicaciones directamente. Aparte de un conjunto limitado de componentes básicos, Apple guarda para sí el código fuente y la documentación privada de la API, por lo que no es posible simplemente buscar en el código fuente de runningboardd o leer sobre las interfaces que expone. La ingeniería inversa completa del código que hay detrás (RunningBoard.framework, RunningBoardServices.framework) tampoco es una tarea realista.
Cómo lo hacen otros
La forma más sencilla de aprender a iniciar una aplicación por nosotros mismos es hacer un poco de trampa y espiar los propios programas de Apple. Existen (al menos) dos formas habituales en las que un usuario de macOS puede iniciar una aplicación de iOS en un escenario de uso habitual: utilizando Finder.app haciendo doble clic en el paquete envoltorio de la aplicación (véase el post anterior sobre este tema), o utilizando el comando 'abrir' con el paquete envoltorio como argumento. A partir de nuestro primer análisis, sospechamos que ambos enviarán en algún momento un mensaje XPC a runningboardd, solicitándole que inicie la aplicación de destino. Aunque 'open' parece ser la opción más obvia para la ingeniería inversa debido a su tamaño limitado y, por tanto, al ruido limitado en los resultados, lo bueno de Finder.app es que es un proceso en ejecución constante. Esto lo convierte en un objetivo fácil de fijar con herramientas de rastreo como xpcspy.
Observando la salida, encontramos las llamadas XPC esperadas:
El contenido completo es demasiado grande para mostrarlo aquí. Lo que queremos decir aquí es que esta llamada parece contener información que se utiliza para el spawn a nivel de sistema operativo de la aplicación de destino (identificador del bundle de destino, variables de entorno, atributos de LaunchServices como banderas de spawn y opciones de ejecución, ...). Otro dato clave de esta salida es el selector que se envió al extremo receptor: executeLaunchRequest:identifier:error:. En Objective-C en macOS/iOS, un "selector" que se envía a un objeto puede interpretarse como un método de clase o instancia que se está ejecutando. Como sabemos que el selector se está enviando a runningboardd, y vemos la clave "rbs_selector", nuestra primera suposición es que la llamada a la API de la que resulta este mensaje XPC está expuesta por RunningBoardServices.
Utilizamos frida-trace al lanzar de nuevo Calculator.app desde Finder para averiguar a qué clase pertenece exactamente este selector:
No hemos encontrado el selector exacto que esperábamos, pero sí algo parecido. El argumento más interesante del selector, algún objeto "LaunchRequest", también está presente aquí. Una inspección más detallada de este objeto mostró que se trataba de un objeto RBSLaunchRequest que se construía a partir de un objeto RBSLaunchContext, que a su vez contenía el contenido del diccionario mostrado en la salida de xpcspy. Ésta y otras investigaciones similares nos mostraron que debíamos estudiar el uso del objeto RBSLaunchRequest y objetos relacionados para alcanzar nuestros objetivos.
Juego de imitación
Tras indagar un poco en varias implementaciones de métodos relacionados expuestos por RunningBoardServices.framework, y algunos experimentos de prueba y error, descubrimos que podíamos lanzar una aplicación gráfica de macOS desde nuestro propio código utilizando sólo unas pocas llamadas a APIs no documentadas. Como ya sabíamos por nuestros experimentos con el lanzamiento de binarios iOS planos en macOS, la clave para lanzar un ejecutable iOS en lugar de uno macOS reside en un parámetro de especificación de plataforma. Buscando símbolos que contuvieran la cadena "platform", nos dimos cuenta de que un objeto RBSProcessIdentity podía inicializarse con un argumento de plataforma. El rastreo de la llamada en particular mostró efectivamente que se estaba ejecutando, y que el valor de este argumento era 1 ("PLATAFORMA_MACOS") al lanzar Calculator.app:
Esto resultó ser la clave para lanzar aplicaciones iOS gráficas desde nuestro propio código. Si proporcionamos PLATFORM_IOS en lugar de PLATFORM_MACOS con este selector al crear el objeto RBSProcessIdentity y solicitamos el lanzamiento de un paquete de aplicaciones iOS (no sólo una envoltura, sino un paquete de aplicaciones iOS real), macOS lanza la aplicación alegremente. Como no se aplica ninguna otra comprobación si lanzamos la aplicación de esta forma, podemos firmar ad-hoc todo el paquete y la firma será aceptada. Esto es ideal cuando se experimenta con parches en la aplicación de destino.
Utilizar la fuerza
Ahora que podemos lanzar una aplicación iOS con los objetos RBSProcessIdentity y RBSLaunchContext totalmente bajo nuestro control, podemos ponernos manos a la obra. En este momento estamos buscando soluciones para al menos los siguientes puntos:
- Queremos ser capaces de detener la ejecución del proceso hijo justo después de haberlo generado;
- Queremos ser capaces de enganchar cómodamente llamadas dentro del spawned sin tener que depender de la instrumentación;
- Queremos poder controlar la política de sandbox del proceso hijo.
Inicio suspendido
Cuando evaluamos la resistencia de las aplicaciones al cracking o a la modificación en tiempo de ejecución en general, a veces nos encontramos con aplicaciones que implementan contramedidas antidepuración en una fase temprana del inicio de la aplicación. Esto puede ser tan temprano como las rutinas de inicialización de la biblioteca, que pueden ser dolorosas de eludir si se utiliza ofuscación o simplemente hay cientos de rutinas de inicialización que enumerar. En estos casos, es importante poder acoplar un depurador en una fase muy temprana, incluso antes de que se active el mecanismo de protección antidepuración. Esto es lo que llamamos instrumentación temprana.
Si nos quedáramos sin opciones, un simple while() que espere a que se genere un proceso y luego adjunte inmediatamente un depurador probablemente bastaría. Sin embargo, como ahora tenemos el control total sobre el proceso generado, podemos ordenar al núcleo que ponga el proceso hijo en modo suspendido justo después de ser generado (es decir, que se detenga justo en _dyld_start, el punto de entrada de cualquier ejecutable del *OS):
Una vez que terminemos lo que queramos hacer con la aplicación (por ejemplo, poner puntos de interrupción, saltar instrucciones, parchear memoria), podemos continuar su ejecución enviando un SIGCONT.
Un efecto secundario muy inconveniente de lanzar una aplicación en modo suspendido es que el Puppet Master de macOS en forma de runningboardd espera un "latido" del proceso generado. Si no recibe dicha señal de vida a tiempo, la aplicación queda en un estado inutilizable y ya no se podrá utilizar (será necesario un kill -9). Esto ocurre unos 30 segundos después de desovar la aplicación en modo suspendido y no continuar la ejecución, con el siguiente mensaje de error:
Aún no se ha encontrado ninguna solución para abordar esta cuestión. Scripting el uso de lldb hace que 30 segundos son suficientes para hacer nuestra magia en general, pero una solución adecuada sería bienvenida.
Traer de vuelta DYLD_INTERPOSE
El segundo requisito era un método conveniente para modificar el comportamiento de la aplicación. Cuando se trata de modificar el flujo de ejecución de las aplicaciones, generalmente existen tres posibilidades:
- Parchear código máquina en disco;
- Parchear código en memoria utilizando una herramienta similar a un depurador;
- Utilizando un marco de instrumentación dinámica (por ejemplo, Frida);
- Utilizando las funciones de enganche nativas de macOS(interposición dyld).
Como parchear código máquina en disco o en memoria requiere que escribamos código ensamblador, éstas no son las opciones más convenientes y flexibles. Una herramienta como Frida es estupenda y tiene un amplio conjunto de casos de uso. Sin embargo, para aplicar modificaciones persistentes y potencialmente complejas, es preferible un método menos engorroso como dyld interposing (para llamadas a funciones C) o swizzling (para clases Obj-C). Ya podemos swizzlear perfectamente con la solución que tenemos entre manos, escribiendo nuestra propia dylib e inyectándola con una variable de entorno DYLD_INSERT_LIBRARIES. Teniendo en cuenta las llamadas a funciones C, tal y como señala Samuel Groß en su artículo (al que se ha hecho referencia anteriormente en este post), macOS no permite la interposición de ejecutables iOS. Sin embargo, como ahora podemos lanzar la aplicación en un estado suspendido, podemos aplicar el método de inyección de código de Groß para parchear dyld en memoria con el fin de eludir esta restricción. Esto también se implementa en nuestro lanzador.
Una aplicación práctica trivial de esta función es solucionar la incompatibilidad de una aplicación con la plataforma de hardware iOS emulada por macOS (iPad Pro3rd gen con iOS 14.7 en el momento de escribir estas líneas). Debido a esto, la aplicación de nuestro cliente no podía ejecutarse en macOS; recibíamos un error que indicaba que nuestro 'iPad' era demasiado nuevo para ejecutar la aplicación. Sin embargo, con unas pocas líneas de código, se compiló una biblioteca dinámica para hacerse pasar por un modelo de iPad más antiguo que era compatible con la aplicación (iPad7). Como resultado, pudimos ejecutar y evaluar correctamente la aplicación.
Arreglando App Sandbox
Mientras probaba una aplicación basada en Xamarin que implementaba Cryoprison para la detección de jailbreak, se descubrió que las aplicaciones de iOS lanzadas de esta forma podían acceder a archivos como /bin/bash. Dado que dichos archivos no deberían estar presentes en instalaciones de iOS con jailbreak, o al menos no deberían ser accesibles desde dentro del sandbox de la aplicación, las implementaciones comunes de detección de jailbreak comprueban el acceso a dichos archivos del sistema para determinar si se están ejecutando dentro de un entorno con jailbreak o sin él. El hecho de que la aplicación de Xamarin sometida a prueba detectara en este caso una plataforma con jailbreak hizo sospechar que el perfil de sandbox aplicado por macOS a las aplicaciones de iOS no se ajustaba del todo al sandbox nativo de iOS.
Para comprender la mecánica del sandboxing de macOS, realizamos en parte ingeniería inversa del programa sandbox-exec. Con una sección de texto de aproximadamente 1kB de tamaño, este binario parecía el candidato perfecto para iniciar el proceso de ingeniería inversa:
El flujo del programa consistía básicamente en dos pasos:
- Configurar el perfil sandbox a utilizar y aplicar el perfil a su propio proceso;
- execve( ) el ejecutable de destino.
Como execve() ejecuta una imagen binaria en el proceso del llamante, el perfil sandbox aplicado en el primer paso se aplica al ejecutable de destino. De este modo, no es necesario que el ejecutable de destino sea "sandbox-aware".
Un análisis más detallado demostró que dos llamadas a la API no documentadas pueden bastar para preparar y aplicar un perfil sandbox:
- sandbox_compile_file(), que toma como entrada un archivo de definición de la caja de arena;
- sandbox_apply(), tomando la salida de la llamada anterior y aplicándola al proceso del llamante.
Utilizando el método de inyección de bibliotecas descrito anteriormente (empleando DYLD_INSERT_LIBRARIES) para lograr la ejecución temprana de código en el contexto de proceso de la aplicación iOS lanzada, imitamos este flujo. Sin embargo, la llamada a sandbox_apply() falló, ya que en ese momento ya existía un perfil de sandbox para el proceso. Cualquier posibilidad de cambiar una política de caja de arena desde dentro del propio proceso una vez que se hubiera activado podría dar lugar a vulnerabilidades de seguridad, por lo que tiene sentido que esto no esté permitido.
Por lo tanto, pasamos a buscar un método para lanzar nuestra aplicación objetivo sin sandbox, con el fin de poder aplicar después nuestro propio sandbox personalizado. El análisis del proceso de lanzamiento de la aplicación ya nos había enseñado que el componente secinitd de macOS estaba implicado en la aplicación de perfiles sandbox. El análisis de la biblioteca subyacente libsystem_secinit.dylib mostró que, efectivamente, existe una forma de lanzar incluso aplicaciones iOS sin que se aplique automáticamente un sandbox: el entitlement "com.apple.private.security.no-sandbox". De hecho, tras añadir este entitlement a los entitlements de nuestra aplicación de destino (ya que también los controlamos totalmente, ¡por supuesto!), ahora podemos aplicar nuestro propio perfil de sandbox personalizado inyectando una biblioteca que contenga el siguiente código sencillo:
La especificación de un perfil de sandbox cuidadosamente elaborado para que lo utilicen nuestras aplicaciones objetivo debería permitirnos eludir por completo cualquier mecanismo común de detección de jailbreak por defecto, sin tener que recurrir a las derivaciones convencionales de detección de jailbreak. La eficacia de la detección de jailbreak puede seguir evaluándose simplemente eliminando las restricciones del sandbox e inspeccionando el comportamiento de la aplicación. En conclusión, esto hará que las assessment de las aplicaciones sean más eficientes.
En la práctica, el uso de este enfoque con un perfil de caja de arena que prohíbe el acceso de lectura/escritura a cualquier ubicación fuera del container de la aplicación dio como resultado que una aplicación de prueba ya no levantara banderas de jailbreak:
Conclusión
En este post, se discutió cómo ejecutar aplicaciones gráficas de iOS bajo macOS con control total. Se discutió cómo se puede poner esto en uso para el pentesting de aplicaciones y la investigación de seguridad. Hay mejoras por hacer, pero hasta ahora parece que la solución que tenemos entre manos es perfectamente adecuada para la investigación y el security testing. Probaremos tantas aplicaciones como podamos en un futuro próximo para poder concluir sobre la aplicabilidad de este enfoque para las pruebas de aplicaciones cotidianas.
Para ver una implementación WIP de la herramienta de lanzamiento comentada, consulte srepsa/launchr (github.com).