El último jailbreak 0day: Tachy0n
(blog.siguza.net)- tachy0n fue un exploit de escalamiento de privilegios de kernel que funcionaba en iOS 13.0~13.5; el 23 de mayo de 2020 se incluyó como 0day en unc0ver v5.0.0 y apuntó directamente al iOS más reciente de ese momento
- La clave era la condición de carrera Lightspeed en
lio_listio, que permitía liberar dos veces objetos dekalloc.16y hacer que objetos distintos apuntaran a la misma memoria - Un bug de la misma familia ya se había usado antes en el jailbreak/untether Spice para iOS 11, pero el sandbox de apps y el entorno de
racoontenían restricciones muy diferentes en privilegios, sandboxing y técnicas de spray - El exploit para unc0ver obtenía repetidamente solapamientos de
OSDatay usabaIOBufferMemoryDescriptoreIOAcceleratorFamily2para construir una fake task y un fake mach port - Desde iOS 14, Apple avanzó hacia bloquear la estrategia misma del exploit mediante separación de allocators, sequestering, PAC y hardening por tipo de objeto; el conocimiento público sobre exploits de kernel de iOS quedó muy rezagado respecto de la época de iOS 13
Por qué la publicación de tachy0n fue inusual
- tachy0n es un exploit antiguo que afectaba desde iOS 13.0 hasta 13.5, y fue publicado el 23 de mayo de 2020 al incluirse en unc0ver v5.0.0
- Para los estándares de entonces era un escalamiento local de privilegios de kernel (kernel LPE) normal, pero fue raro que se publicara como jailbreak 0day que afectaba a la versión más reciente de iOS
- Apple distribuyó un parche específico para ese bug aproximadamente una semana después de la publicación del exploit
- Este bug era 0day en iOS 13.5, pero ya se había aprovechado antes en forma de 1day
- Pwn20wnd buscó un 0day alcanzable desde el sandbox de apps, y el punto de partida fueron pruebas de regresión sobre 1days conocidos
- SockPuppet de iOS 12 fue parcheado en iOS 12.3, pero reapareció en iOS 12.4
- Ese caso dejó en evidencia la ausencia de pruebas de regresión de Apple para ese tipo de bugs, y Pwn20wnd implementó pruebas de regresión para varios 1days conocidos hasta obtener un hit
Lightspeed: condición de carrera en lio_listio
- El bug central de tachy0n es el bug Lightspeed de Synacktiv, relacionado con CVE-2020-9859 y posiblemente con CVE-2018-4344
- La vulnerabilidad está en la syscall
lio_listio, que realiza I/O de archivos asíncrona o por lotes - El kernel asigna una estructura
aio_lio_contextpara rastrear las operaciones de I/O enviadas- La estructura tiene los campos
io_waiter,io_issuedeio_completed
- La estructura tiene los campos
- El trabajo real se realiza en un thread separado y, cuando termina toda la I/O,
do_aio_completionlibera ese contexto - Si no se programó ningún trabajo, el thread actual de
lio_listiodebe liberar el contexto - El problema es que esa verificación tiene una condición de carrera
- Puede haberse enviado trabajo a otro thread, pero completarse y liberarse el contexto antes del momento de la verificación
- En ese caso,
lio_listiovuelve a revisarlio_context, que ya es un dangling pointer
Cómo un double free deriva en un exploit
- La secuencia necesaria para el exploit es la siguiente
lio_listioasignalio_context- El trabajo se completa y
do_aio_completionliberalio_context - La memoria liberada se reasigna a un objeto controlado por el atacante y se hace que parezca que
lio_context->io_issued == 0 lio_listiove eso y vuelve a liberar el objeto del atacante- La misma memoria se reasigna a otro objeto, haciendo que dos asignaciones distintas apunten a la misma memoria
- En dispositivos de 64 bits,
lio_contextcae en la zona más pequeña,kalloc.16 - Antes de iOS 14, independientemente del tipo de objeto, se compartía el mismo allocation site por tamaño
- Objetos C++, arrays de punteros y buffers de datos provistos por el usuario podían reutilizar la memoria de otros dentro del mismo bucket de tamaño
- Este double free funciona de forma distinta a un double free típico, que sin una reasignación intermedia suele terminar fácilmente en un estado fatal
lio_context->io_issuedno se vuelve 0 mientras está asignado- Tras liberarlo, el allocator sobrescribe los primeros 8 bytes con un valor canary y un puntero de freelist, o con un XOR de una dirección de objeto
- Por lo tanto, el segundo free solo ocurre si hay una reasignación intermedia y los bytes 4~7 son 0
- En la práctica, el exploit podía reintentar esta carrera varias veces, y era poco común en uso real que otro objeto del sistema, por casualidad, pusiera en 0 los bytes necesarios y provocara el double free
Uso previo en Spice
- El mismo bug se usó también en el jailbreak/untether Spice dirigido a iOS 11.x
- Spice fue trabajado por el equipo de Jake Blair junto con Sparkey y littlelailo, y en ese momento la versión más reciente era iOS 13.x
- El objetivo era crear mach port forgery tanto en el entorno de apps como en el entorno de
racoon- En exploits de kernel previos a iOS 14, si se conseguía que un valor provisto por el usuario se interpretara como un puntero a mach port, las etapas siguientes se volvían decisivamente más fáciles
- El flujo básico para crear mach port forgery con Lightspeed era el siguiente
- Provocar el primer free de
lio_context - Hacer spray de mach messages con un descriptor de OOL mach ports de tamaño 1 o 2
- Dejar la primera entrada como
MACH_PORT_NULLpara que entre enkalloc.16y parezca queio_issuedes 0 - Liberar el array de OOL mach ports con el segundo free
- Hacer spray de datos controlados en
kalloc.16para reemplazar el array de mach ports por un fake pointer
- Provocar el primer free de
Diferencias de restricciones entre el sandbox de apps y racoon
- En A7~A9(X) no había PAN, por lo que con solo
mmapymlockse podían desreferenciar direcciones de userland como si fueran punteros de kernel - Se intentó soportar también A10 y A11, pero no se completó porque desde el sandbox de apps no se encontró una filtración adecuada de direcciones de kernel ni un objetivo donde ubicar datos controlados
- Entre los 1days que se querían aprovechar estaban una filtración de información de la pila del kernel de Ian Beer y un escape del sandbox de backboardd
- El plan era filtrar un puntero de memoria compartida o ubicar datos en el segmento
__DATAdel kernel - Al no encontrarse un target adecuado, no se logró soporte A10/A11 por la ruta de apps
- El plan era filtrar un puntero de memoria compartida o ubicar datos en el segmento
- La ruta de
racoontenía condiciones distintas- Se ejecuta como root, pero tiene un sandbox más estricto que una app normal
- Al no tener acceso a IOSurface, no podía usar el spray típico de
OSUnserializeXMLvíaIOSurface::setValue - En su lugar, podía usar la llamada a
OSUnserializeXMLdentro deRootDomainUserClient::secureSleepSystemOptionspara hacer spray de algunos objetos en forma de leak
racoontenía un perfil de sandbox que permitía todas las lecturas y escrituras de sysctl, y además tenía privilegios root- Si se conocía el kernel slide, se podía usar un global de sysctl en
__DATAdel kernel como un almacén de datos en una dirección conocida - En Spice se eligió
vm.swapfileprefix
- Si se conocía el kernel slide, se podía usar un global de sysctl en
- Para obtener el kernel slide se usó CVE-2018-4413 de panicall
- Una filtración de información en
sysctl_procargsxpermitía filtrar casi una página de memoria de kernel no inicializada dekernel_map - Con eso se obtenían punteros al código del kernel y al heap, y se podía cubrir A7~A11
- En el sandbox de apps,
sysctl_procargsxestaba bloqueado, por lo que no era posible usar el mismo método
- Una filtración de información en
Estructura del exploit tachy0n para unc0ver
- Como el objetivo de unc0ver era A8~A13, no era posible ignorar A10+ ni depender de desreferencias de userland
- El exploit se diseñó con una estructura de dos capas, considerando que la etapa de corrupción de memoria podía fallar
- La capa inferior ejecuta threads freerer que llaman a
lio_listioy threads racer que unserializanOSDatamediante IOSurface - Los valores predeterminados son 4 freerer y 16 racer, y se pueden ajustar
- La capa inferior ejecuta threads freerer que llaman a
- Los datos unserializados mediante IOSurface eran un
OSDictionarycon múltiples entradasOSData- La posición correspondiente a
io_issueddebía ser 0 - Valores mágicos como
0x41414141,0x69696969y el valor de clavekse usaban para detectar solapamientos
- La posición correspondiente a
- Después de la carrera, se revisan todos los valores de
OSData- Un objeto cuyo magic value cambió se considera tomado por otro objeto del sistema y se marca como objetivo de cleanup posterior
- Si la key y el valor
kdentro del buffer difieren, se interpreta que ocurrió un solapamiento donde otro objetoOSDataapunta al mismo backing buffer
- Las funciones
maybe_reyoinkyoverlapdel código construyen esta información de solapamiento y la pasan a la capa superior - La capa superior usa los objetos
OSDatasolapados para construir un fake mach port- Libera un
OSData - Hace spray de mach messages con un descriptor OOL port
- Libera el otro
OSData - Lo reasigna con un nuevo
OSDataque contiene un puntero a un fake task port
- Libera un
Ubicar datos controlados en una dirección de kernel conocida
- El exploit podía leer con
OSDatael contenido reasignado como un array de OOL ports descriptor para filtrar el raw kernel pointer del mach port - En etapas posteriores esto se usa para filtrar las direcciones del task port y del service port
IOSurfaceRoot, pero el problema central era obtener de forma estable la dirección de kernel de un buffer controlable - El candidato encontrado en el código fuente de XNU fue
IOMemoryDescriptor- El campo
_rangeses un array deIOVirtualRange, y un únicoIOVirtualRangecabe exactamente enkalloc.16 - Sin embargo, un
IOMemoryDescriptornormal usa_singleRangeen lugar de una allocation de heap cuando hay un solo range
- El campo
IOBufferMemoryDescriptor, excepcionalmente, llama aIONew(IOAddressRange, 1)para un solo range, haciendo una allocation de heap- Un lugar cómodo para asignarlo arbitrariamente y mapearlo en el espacio de direcciones del usuario era la interfaz AGX de
IOAcceleratorFamily2- Al abrir un userclient type 0 en
IOGraphicsAccelerator2, se obtieneIOAccelContext2 - Con
::clientMemoryForType()se pueden mapear tres memory descriptors - El type 0 tiene 0x8000 bytes, por lo que se usa para identificar el victim descriptor
- Al abrir un userclient type 0 en
- El exploit usa el siguiente loop
- Abre
IOAccelContext2y obtiene dosOSDatasolapados - Libera uno de los
OSData - Conecta el
IOAccelSharedUserClient2abierto previamente medianteIOConnectAddClient() - Lee el
OSDatarestante y verifica que los primeros 8 bytes sean un kernel pointer page-aligned y que los siguientes 8 bytes sean0x8000 - Si no se cumple la condición, cierra
IOAccelContext2y repite
- Abre
Memoria pageable, fake port y zone_require
- Incluso después de mapear el memory descriptor en el proceso y conocer la dirección de kernel, quedaba el problema de que la memoria se crea como
kIOMemoryPageable - Como el fake mach port y el objeto fake task pueden ser accedidos con la preemption desactivada, el kernel debía hacer fault-in de esa página
- Esto se resolvía llamando dos veces al external method 2,
IOAccelContext2::submit_data_buffers, que invoca indirectamenteIOAccelContext2::processSidebandBuffer- Lee una estructura ubicada 0x10 bytes después del inicio de la memoria compartida
- La primera estructura tiene
tok == 0x100y se prepara para cubrir la página completa y avanzar hasta la segunda página - En la segunda página se pueden colocar luego los datos del fake object
- Las etapas posteriores llevan a construir la fake task, el fake port, el switcheroo del OOL descriptor y una primitive de lectura arbitraria
- También era necesario evadir
zone_require- En ese momento
zone_requirepermitía páginas fuera dezone_mape interpretaba los primeros0x20bytes de la página como metadata - Si se colocaba el índice de zona correcto, podía usarse como una especie de pase para la zona deseada
- Por eso se necesitaban dos páginas en total: una para task y otra para mach port
- En ese momento
- Este exploit está publicado actualmente en GitHub
Análisis y parche tras la publicación
- La publicación de un exploit 0day completo contra la versión firmada más reciente llamó la atención de la escena de jailbreak de iOS
- Brandon Azad, que entonces trabajaba en Project Zero, identificó la vulnerabilidad en menos de 4 horas tras la publicación del exploit y avisó a Apple
- Ese análisis se resume en How to unc0ver a 0-day in 4 hours or less
- 6 días después de la publicación del exploit, Synacktiv publicó un nuevo artículo donde trató la posibilidad de que el fix original de iOS 12 hubiera creado un memory leak, y que el intento de corregir ese memory leak hubiera revivido el bug original
- 9 días después de la publicación del exploit, Apple distribuyó el parche
- Más tarde se agregó a XNU una prueba de regresión para ese bug
- 54 días después de la publicación, una versión con reverse engineering llamada “tardy0n” se incluyó en el jailbreak Odyssey, también dirigido a iOS 13.0~13.5
El entorno de exploits que cambió desde iOS 14
- iOS 14 mostró un cambio en la estrategia de seguridad del kernel de Apple
- Antes de iOS 14, sin importar si la primitive inicial era un heap overflow, un over-release de objeto C++ o una type confusion, el siguiente target casi siempre era un mach port
- Uno de los cambios más importantes en iOS 14 fue en los allocators
kallocyzalloc- Se dividió el zone map en múltiples rangos “kheap”
- Se separaron los datos controlados por el usuario y los objetos de kernel en heaps distintos
- A los objetos de kernel se les aplicó sequestering, de modo que una página de dirección virtual asignada a una zona específica no se reutiliza para otra zona hasta reiniciar
- La memoria física puede liberarse, pero el rango de memoria virtual no se reutiliza para otros objetos, bloqueando en la práctica la type confusion de objetos de kernel
- Con guard pages, posiciones iniciales de allocation de zonas que cambian en cada arranque y refinamientos posteriores, la confiabilidad de ataques cross-zone bajó mucho
- Apple pasó de simplemente bloquear bugs individuales a bloquear estrategias de exploit
- Si un exploit usa una struct kmsg como corruption target, esa estructura pasa a estar firmada
- Si usa un pipe buffer como interfaz estable de lectura/escritura de kernel, los punteros relacionados quedan sujetos a PAC
- Si surge una técnica que usa un objeto no relacionado como victim, ese tipo de objeto se hardeniza
- Como resultado, en el desarrollo de exploits la estrategia de exploit empezó a valer más que el 0day inicial de corrupción de memoria
La brecha en el conocimiento público
- Se evalúa que antes de iOS 14 el conocimiento público de investigación de seguridad en iOS estaba casi al mismo nivel que el conocimiento privado
- Después de iOS 14, salvo excepciones, el intercambio de información prácticamente se detuvo
- Incluso cuando faltaban pocas semanas para la beta de iOS 19, no había exploits públicos de kernel para iOS 18 o iOS 17
- En las notas de seguridad de Apple a veces aparecen vulnerabilidades explotadas en la naturaleza, pero la información pública no logra seguir el ritmo de la investigación privada
- Que solo hayan pasado 5 años desde la publicación de tachy0n muestra lo rápido que cambió el campo de los exploits de kernel de iOS
2 comentarios
Aunque el hardware de Apple es excelente, su software está lleno de restricciones pensadas para atar al usuario.
Incluso si solo quieres que una app que tú mismo hiciste y compilaste funcione únicamente en tu propio dispositivo, necesitas una suscripción de 100 dólares.
Si eres desarrollador y usas apps open source pequeñas o medianas que compilas por tu cuenta para usarlas,
en vez de tener que hacer jailbreak con vulnerabilidades y hacer sideload en un dispositivo de Apple, es más fácil simplemente usar Android.
Opiniones en Hacker News
Es llamativo que la forma de vencer a una empresa de un billón de dólares haya sido una tarea simple y aburrida en la que Apple es especialmente débil: las pruebas de regresión.
SockPuppet, una de las grandes vulnerabilidades usadas para el jailbreak en iOS 12, fue encontrada por Ned Williamson de Project Zero y reportada a Apple; se parcheó en iOS 12.3 y luego se hizo pública en el rastreador de bugs de Project Zero.
Pero en iOS 12.4 volvió a aparecer como si nunca hubiera sido parcheada, probablemente porque Apple bifurcó XNU en una rama separada para esa versión y no aplicó el parche.
Eso era una señal fuerte de que no había pruebas de regresión para ese tipo de vulnerabilidad, y con solo automatizar algunos 1-days conocidos, Pwn habría podido acertar de inmediato.
Me pregunto cuántos operan granjas de CI que sigan ejecutando vulnerabilidades antiguas contra nuevas versiones de proyectos como Linux, FreeBSD, OpenWRT u OpenSSH.
Hace 20 años, cuando estaba en la universidad, hice QA voluntario en Mozilla, y había una colección de pruebas de regresión en constante crecimiento, centrada sobre todo en bugs de renderizado/layout y del motor de JavaScript.
Como para reproducir y verificar la corrección se creaban casos de prueba mínimos, también era fácil meterlos en el pipeline de build.
Los bugs son inevitables, pero que un bug corregido con tiempo y dinero vuelva a la vida es el peor escenario.
Las organizaciones que valoran la calidad claramente invierten en pruebas de regresión, pero muchas no respetan QA y directamente no lo hacen, o lo mandan a outsourcing al menor costo.
Que Apple no tuviera pruebas de regresión para una de las clases de bugs históricamente más visibles, el jailbreak, es realmente extraño.
Hoy Mozilla puede recibir muchas críticas, pero incluso a principios de los 2000 tenía QA y CI/CD bastante sólidos con herramientas como Tinderbox y Bugzilla.
Cuando DevOps se puso de moda y popularizó estas prácticas, pensé que eso era algo que todos ya hacían, pero estaba equivocado.
Es una situación tipo ley de Conway creada por la separación entre seguridad y desarrollo de funcionalidades.
Aunque existan procedimientos de build/release y una colección madura de pruebas de regresión, por la estructura organizacional interna es muy posible que estos temas de seguridad no entren ahí.
Fácilmente había miles, y creo que quizá era SQLite.
Es un método digno de imitar.
Si no se backportea el fix, parece probable que tampoco se backportee la prueba.
Al ver términos como separación de kheap, mitigación de task port, SSV y SPTM, sentí como cuando estás conversando bien con un amigo en otro idioma y de pronto pasan a explicar neurocirugía o física nuclear, y tu comprensión se cae por un precipicio.
Me pasó algo parecido cuando intenté interpretar una conversación sobre la remodelación de un alto horno.
Es una lástima que el jailbreak ya no sea tan activo como antes.
Casi no hice nada práctico con mi iPad con jailbreak, pero era divertido, y hoy me gustaría instalar una app de tethering, UTM y alguna solución para JIT.
SideStore también se veía prometedor, pero como mi cuenta alguna vez fue una cuenta paga de Apple Developer, me quedaron 10 app IDs que no expiran, así que no puedo instalar apps como UTM a menos que cree una cuenta nueva o vuelva a pagar.
Después de perder eso volví a Android, y para entonces Android ya había alcanzado bastante en funciones básicas.
Escuché que Apple hoy paga 1 millón de dólares por un jailbreak, y ese debe ser el piso del precio de libre mercado.
No sé si hay que pasar por un intermediario, o si existe algún correo público que no termine enterrado en soporte de primer nivel.
Si esto es cierto, Apple usó una estrategia sorprendente.
Si bloquea todas las formas de obtener root en el dispositivo, Apple puede parchear las vulnerabilidades que los desarrolladores de jailbreaks encuentran gratis.
Según el artículo, las comunidades privadas siguen teniendo exploits y Apple los parchea.
Parece que solo la comunidad pública o este desarrollador, por alguna razón, ya no funcionan así.
La frase que más me gustó de todo el texto fue: “También quiero agradecer a quien desparcheó ese bug en iOS 13.0. Eso también fue una acción muy genial”.
Dice: “No puedo imaginar dónde estaremos dentro de 5 años”, pero yo sí puedo.
iMessage seguirá permitiendo el robo de dispositivos, cuentas y datos.
El texto no decía si era tethered o untethered.
El jailbreak en el que se incluyó y distribuyó, unc0ver, creo que era “semi-untethered”.