Le dernier jailbreak 0day : Tachy0n
(blog.siguza.net)- tachy0n est un exploit d’élévation de privilèges kernel qui fonctionnait sur iOS 13.0 à 13.5, inclus en tant que 0day dans unc0ver v5.0.0 le 23 mai 2020, ciblant directement la dernière version d’iOS de l’époque
- Son cœur repose sur la condition de concurrence Lightspeed de
lio_listio, qui permettait de libérer deux fois un objetkalloc.16afin que deux objets différents pointent vers la même zone mémoire - Des bugs de la même famille avaient déjà été utilisés dans le jailbreak/untether Spice pour iOS 11, mais le sandbox applicatif et l’environnement
racoonimposaient des contraintes très différentes en matière de privilèges, de sandbox et de techniques de spray - L’exploit pour unc0ver obtenait de manière répétée des recouvrements d’
OSData, puis utilisaitIOBufferMemoryDescriptoretIOAcceleratorFamily2pour construire une fake task et un faux port mach - Depuis iOS 14, Apple s’est orienté vers une stratégie qui bloque la stratégie d’exploitation elle-même via la séparation des allocateurs, le sequestering, PAC et le durcissement par objet, et les connaissances publiques sur les exploits kernel iOS sont aujourd’hui nettement en retard par rapport à l’époque d’iOS 13
Pourquoi la publication de tachy0n était inhabituelle
- tachy0n est un ancien exploit affectant iOS 13.0 à 13.5, rendu public le 23 mai 2020 dans unc0ver v5.0.0
- Selon les critères de l’époque, il s’agissait d’un kernel LPE local standard, mais il était rare qu’un jailbreak 0day visant la dernière version d’iOS soit publié ainsi
- Apple a diffusé un correctif dédié à ce bug environ une semaine après la publication de l’exploit
- Ce bug était un 0day sur iOS 13.5, mais avait déjà été exploité auparavant sous forme de 1day
- Pwn20wnd a trouvé un 0day atteignable depuis le sandbox applicatif, en partant de tests de régression sur des 1day connus
- SockPuppet sur iOS 12 avait été corrigé dans iOS 12.3, puis réapparaissait dans iOS 12.4
- Ce cas révélait l’absence de tests de régression d’Apple sur ce type de bug, et Pwn20wnd a implémenté des tests de régression sur plusieurs 1day connus, obtenant un hit
Lightspeed : condition de concurrence dans lio_listio
- Le bug central de tachy0n est le bug Lightspeed de Synacktiv, lié à CVE-2020-9859 et possiblement à CVE-2018-4344
- La vulnérabilité se situe dans le syscall
lio_listio, qui effectue des E/S fichiers asynchrones ou par lots - Le kernel alloue une structure
aio_lio_contextpour suivre les opérations d’E/S soumises- La structure contient les champs
io_waiter,io_issuedetio_completed
- La structure contient les champs
- Les opérations réelles sont exécutées sur un thread séparé, et quand toutes les E/S sont terminées,
do_aio_completionlibère ce contexte - Si aucune opération n’est planifiée, c’est le thread courant de
lio_listioqui doit libérer le contexte - Le problème est qu’il y a une condition de concurrence dans cette vérification
- Une opération peut avoir été soumise à un autre thread, puis déjà terminée et le contexte libéré avant le moment du test
- Dans ce cas,
lio_listioréexamine unlio_contextdevenu un dangling pointer
Comment le double free devient exploitable
- La séquence nécessaire à l’exploit est la suivante
lio_listioallouelio_context- L’opération se termine et
do_aio_completionlibèrelio_context - L’attaquant réalloue cette mémoire avec un objet sous son contrôle et lui donne l’apparence de
lio_context->io_issued == 0 lio_listiovoit cet état et libère à nouveau l’objet de l’attaquant- Cette même mémoire est réallouée pour un autre objet, de sorte que deux allocations différentes pointent vers la même zone mémoire
- Sur les appareils 64 bits,
lio_contextva dans la plus petite zone,kalloc.16 - Avant iOS 14, quel que soit le type d’objet, les allocations partageaient le même site d’allocation selon leur taille
- Objets C++, tableaux de pointeurs et buffers de données fournis par l’utilisateur pouvaient réutiliser mutuellement leur mémoire dans le même bucket de taille
- Ce double free se comporte différemment d’un double free classique qui mène facilement à un état fatal sans réallocation intermédiaire
lio_context->io_issuedn’est pas à 0 tant que l’allocation est active- Après libération, l’allocateur écrase les 8 premiers octets avec une valeur canary et un pointeur de freelist ou une valeur XOR avec l’adresse de l’objet
- Le second free ne se produit donc que s’il y a une réallocation intermédiaire et que les octets 4 à 7 valent 0
- En pratique, l’exploit pouvait retenter cette course de nombreuses fois, et il était rare dans le monde réel qu’un autre objet du système mette accidentellement les bons octets à 0 pour déclencher le double free
Utilisation antérieure dans Spice
- Le même bug avait aussi été utilisé dans le jailbreak/untether Spice visant iOS 11.x
- Spice était développé au sein de l’équipe de Jake Blair avec Sparkey et littlelailo, alors que la version la plus récente à l’époque était iOS 13.x
- L’objectif était de produire une forgerie de port mach à la fois dans l’environnement applicatif et dans l’environnement
racoon- Dans les exploits kernel iOS d’avant iOS 14, il devenait ensuite beaucoup plus facile d’avancer dès lors qu’on pouvait faire interpréter des valeurs fournies par l’utilisateur comme un pointeur de port mach
- Le flux de base pour obtenir une forgerie de port mach via Lightspeed était le suivant
- Déclencher le premier free de
lio_context - Effectuer un spray de messages mach avec un descripteur OOL mach ports de taille 1 ou 2
- Mettre la première entrée à
MACH_PORT_NULLpour que l’objet aille danskalloc.16et donne l’apparence d’unio_issuedà 0 - Libérer le tableau OOL mach ports avec le second free
- Spray de données contrôlées dans
kalloc.16pour remplacer le tableau mach ports par un fake pointer
- Déclencher le premier free de
Différences de contraintes entre le sandbox applicatif et racoon
- Sur A7 à A9(X), l’absence de PAN permettait de déréférencer des adresses userland comme des pointeurs kernel avec seulement
mmapetmlock - Une prise en charge d’A10 et A11 avait aussi été envisagée, mais elle n’a pas abouti dans le sandbox applicatif faute de fuite d’adresse kernel appropriée et de cible adaptée pour placer les données contrôlées
- Les 1day envisagés incluaient la fuite d’informations de pile kernel de Ian Beer et une évasion du sandbox backboardd
- Le plan consistait à faire fuiter un pointeur de mémoire partagée ou à placer des données dans le segment kernel
__DATA - Faute d’avoir trouvé une cible adaptée, la voie applicative n’a pas pris en charge A10/A11
- Le plan consistait à faire fuiter un pointeur de mémoire partagée ou à placer des données dans le segment kernel
- La voie
racoonreposait sur des conditions différentes- Le processus tournait en root, mais avec un sandbox plus strict qu’une application classique
- Sans accès à IOSurface, il était impossible d’utiliser le spray classique
OSUnserializeXMLviaIOSurface::setValue - À la place, il était possible de spray certains objets sous forme de fuite en exploitant l’appel à
OSUnserializeXMLdansRootDomainUserClient::secureSleepSystemOptions
racoondisposait d’un profil sandbox autorisant toutes les lectures et écritures sysctl, ainsi que des privilèges root- Si l’on connaissait le slide kernel, il devenait possible d’utiliser une globale sysctl dans le
__DATAdu kernel comme zone de stockage à adresse connue - Spice utilisait pour cela
vm.swapfileprefix
- Si l’on connaissait le slide kernel, il devenait possible d’utiliser une globale sysctl dans le
- Pour obtenir le slide kernel, Spice utilisait CVE-2018-4413 de panicall
- La fuite d’informations de
sysctl_procargsxpermettait de faire fuiter presque une page entière de mémoire kernel non initialisée dekernel_map - Cela fournissait des pointeurs vers le code kernel et le heap, permettant de gérer A7 à A11
- Dans le sandbox applicatif,
sysctl_procargsxétait bloqué, rendant cette méthode inutilisable
- La fuite d’informations de
Structure de l’exploit tachy0n pour unc0ver
- La cible d’unc0ver allait de A8 à A13, il n’était donc pas possible d’ignorer A10+ ni de s’appuyer sur un déréférencement userland
- L’exploit a été conçu avec une architecture à deux couches pour tenir compte d’une étape de corruption mémoire susceptible d’échouer
- La couche basse exécute des threads freerer appelant
lio_listioet des threads racer qui désérialisentOSDatavia IOSurface - La configuration par défaut utilise 4 freerer et 16 racer, avec possibilité d’ajustement
- La couche basse exécute des threads freerer appelant
- Les données désérialisées via IOSurface étaient un
OSDictionarycontenant plusieurs entréesOSData- La position correspondant à
io_issueddevait être à 0 - Des valeurs magiques comme
0x41414141,0x69696969et la clékservaient à détecter les recouvrements
- La position correspondant à
- Après la course, toutes les valeurs
OSDatasont vérifiées- Si une valeur magique a changé, on considère qu’un autre objet système s’est emparé de l’objet, qui est alors marqué pour nettoyage ultérieur
- Si la clé et la valeur
kdans le buffer diffèrent, on conclut qu’un autre objetOSDatapointe vers le même backing buffer et qu’un recouvrement s’est produit
- Les fonctions
maybe_reyoinketoverlapdu code construisent ces informations de recouvrement et les transmettent à la couche supérieure - La couche supérieure utilise les objets
OSDatarecouvrés pour construire un faux port mach- Un
OSDataest libéré - Un spray de messages mach avec un descripteur de ports OOL est effectué
- L’autre
OSDataest libéré - Une nouvelle réallocation est faite avec un
OSDatacontenant un pointeur vers un faux task port
- Un
Placer des données contrôlées à une adresse kernel connue
- L’exploit pouvait lire la réallocation du tableau de descripteurs OOL ports sous forme d’
OSData, ce qui permettait de faire fuiter le pointeur kernel brut du port mach - Cette primitive sert ensuite à faire fuiter l’adresse du task port et du port de service
IOSurfaceRoot, mais la question centrale était d’obtenir de façon fiable l’adresse kernel d’un buffer contrôlable - Un candidat trouvé dans les sources XNU était
IOMemoryDescriptor- Le champ
_rangesest un tableau deIOVirtualRange, et un seulIOVirtualRangetient exactement danskalloc.16 - Mais un
IOMemoryDescriptorordinaire utilise_singleRangeau lieu d’une allocation heap lorsqu’il n’y a qu’une seule plage
- Le champ
IOBufferMemoryDescriptorconstitue une exception, car il appelleIONew(IOAddressRange, 1)même pour une seule plage et réalise ainsi une allocation heap- Un emplacement pratique pour allouer cela à volonté et le mapper dans l’espace d’adressage utilisateur était l’interface AGX de
IOAcceleratorFamily2- En ouvrant un userclient de type 0 sur
IOGraphicsAccelerator2, on obtientIOAccelContext2 ::clientMemoryForType()permet de mapper trois memory descriptors- Le type 0 fait 0x8000 octets, ce qui sert à identifier le descripteur victime
- En ouvrant un userclient de type 0 sur
- L’exploit utilisait la boucle suivante
- Ouvrir
IOAccelContext2et récupérer deuxOSDatarecouvrés - Libérer un
OSData - Connecter un
IOAccelSharedUserClient2déjà ouvert avecIOConnectAddClient() - Lire l’
OSDatarestant et vérifier que les 8 premiers octets sont un pointeur kernel aligné sur une page et que les 8 suivants valent0x8000 - Si la condition n’est pas remplie, fermer
IOAccelContext2et recommencer
- Ouvrir
Mémoire pageable, faux port et zone_require
- Même après avoir mappé le memory descriptor dans le processus et obtenu son adresse kernel, il restait un problème : la mémoire était créée comme
kIOMemoryPageable - Le faux port mach et le faux objet task pouvant être accédés avec la préemption désactivée, le kernel devait donc fault-in la page concernée côté kernel
- Cela était géré en appelant deux fois la méthode externe 2
IOAccelContext2::submit_data_buffers, qui invoque indirectementIOAccelContext2::processSidebandBuffer- Elle lit une structure située 0x10 octets après le début de la mémoire partagée
- La première structure a
tok == 0x100et est conçue pour couvrir toute la page afin de continuer sur la seconde page - La seconde page peut ensuite contenir les données du faux objet
- Les étapes suivantes enchaînent avec le faux task, le faux port, le switcheroo du descripteur OOL et la construction d’une primitive de lecture arbitraire
- Il fallait également contourner
zone_require- À l’époque,
zone_requireacceptait des pages situées hors dezone_mapet interprétait les premiers0x20octets de la page comme des métadonnées - En y mettant le bon index de zone, on pouvait s’en servir comme d’un laissez-passer pour la zone voulue
- C’est pourquoi deux pages étaient nécessaires : une pour task et une pour le port mach
- À l’époque,
- Cet exploit est aujourd’hui publié sur GitHub
Analyse après publication et correctif
- La publication d’un exploit 0day complet pour la dernière version signée a attiré l’attention de la scène jailbreak iOS
- Brandon Azad, alors chez Project Zero, a compris la vulnérabilité et l’a signalée à Apple dans les 4 heures suivant la publication de l’exploit
- Son analyse est résumée dans How to unc0ver a 0-day in 4 hours or less
- Six jours après la publication de l’exploit, Synacktiv a publié un nouvel article expliquant que le correctif initial sur iOS 12 avait créé une fuite mémoire, et qu’une tentative de corriger cette fuite avait pu réintroduire le bug d’origine
- Apple a publié un correctif 9 jours après la publication de l’exploit
- XNU a ensuite reçu un test de régression pour ce bug
- Cinquante-quatre jours après la publication, une version rétro-ingéniérée, “tardy0n”, a été intégrée au jailbreak Odyssey, visant elle aussi iOS 13.0 à 13.5
Un environnement d’exploitation transformé depuis iOS 14
- iOS 14 illustre un changement de stratégie d’Apple en matière de sécurité kernel
- Avant iOS 14, quelle que soit la primitive initiale — heap overflow, over-release d’objet C++, type confusion, etc. — la cible suivante était en général un port mach
- L’un des changements majeurs d’iOS 14 concernait l’allocateur,
kallocetzalloc- La zone map a été divisée en plusieurs plages “kheap”
- Les données contrôlées par l’utilisateur et les objets kernel ont été séparés dans des heaps distincts
- Un sequestering a été appliqué aux objets kernel afin qu’une page d’adresse virtuelle allouée à une zone donnée ne soit jamais réutilisée par une autre zone avant redémarrage
- La mémoire physique peut être libérée, mais la plage de mémoire virtuelle n’est pas réutilisée pour d’autres objets, ce qui bloque pratiquement la type confusion sur objets kernel
- Avec l’ajout de guard pages, de positions de départ d’allocation de zone différentes à chaque démarrage et d’autres raffinements ultérieurs, la fiabilité des attaques cross-zone a fortement diminué
- Apple est passé d’une logique de blocage de bugs individuels à une logique de blocage des stratégies d’exploitation
- Si un exploit utilise une structure kmsg comme cible de corruption, cette structure est signée
- Si un pipe buffer sert d’interface stable de lecture/écriture kernel, les pointeurs concernés sont protégés par PAC
- Si une méthode exploitant des objets non liés comme victimes apparaît, le type d’objet correspondant est durci
- Il en résulte une situation où, dans le développement d’exploit, la stratégie d’exploitation a parfois plus de valeur qu’un 0day de corruption mémoire initial
Rupture dans les connaissances publiques
- Avant iOS 14, les connaissances publiques en recherche de sécurité iOS étaient considérées comme presque au niveau des connaissances privées
- Depuis iOS 14, hormis quelques exceptions, le partage d’informations s’est pratiquement arrêté
- À quelques semaines d’une beta iOS 19, il est noté qu’aucun exploit kernel public n’existe pour iOS 18 ou iOS 17
- Les notes de sécurité d’Apple mentionnent parfois des vulnérabilités exploitées dans la nature, mais les informations publiques ne suivent plus la recherche privée
- Le fait que tachy0n n’ait été publié qu’il y a 5 ans montre à quel point le domaine des exploits kernel iOS a évolué rapidement
2 commentaires
Le matériel d’Apple est excellent, mais son logiciel est truffé de mécanismes conçus pour tenir les utilisateurs en laisse.
Même si vous voulez simplement faire tourner sur votre propre appareil une app que vous avez vous-même créée et compilée, il vous faut un abonnement à 100 dollars.
Si vous êtes développeur, que vous utilisez de petites ou moyennes apps open source et que vous les compilez vous-même pour les utiliser,
il est plus simple de prendre un Android que de devoir exploiter des vulnérabilités et jailbreaker un appareil Apple pour faire du sideloading.
Commentaires sur Hacker News
Ce qui est frappant, c’est que la façon dont une entreprise valant 1 000 milliards de dollars a été battue relevait d’une tâche simple et ennuyeuse, un point sur lequel Apple est particulièrement faible : les tests de régression.
SockPuppet, l’une des grandes vulnérabilités utilisées pour le jailbreak d’iOS 12, a été découverte par Ned Williamson de Project Zero et signalée à Apple ; elle a été corrigée dans iOS 12.3, puis publiée dans le bug tracker de Project Zero.
Mais dans iOS 12.4, elle est réapparue comme si elle n’avait jamais été corrigée, probablement parce qu’Apple avait forké XNU dans une branche séparée pour cette version sans y appliquer le patch.
C’était un signal fort qu’il n’existait pas de tests de régression pour ce type de vulnérabilité, et il aurait suffi d’automatiser quelques 1-day connus pour que Pwn fasse mouche immédiatement.
Je me demande combien d’organisations exploitent une ferme CI qui rejoue en continu d’anciennes vulnérabilités sur les nouvelles versions de projets comme Linux, FreeBSD, OpenWRT ou OpenSSH.
Il y a 20 ans, quand j’étais à l’université, j’ai fait du QA bénévole chez Mozilla : il existait une suite de tests de régression en croissance constante, centrée sur les bugs du moteur JavaScript ou du rendu/layout.
Comme on créait des cas de test minimaux pour reproduire les bugs et vérifier les correctifs, il était aussi facile de les intégrer au pipeline de build.
Les bugs sont inévitables, mais voir ressusciter un bug qu’on a dépensé du temps et de l’argent à corriger, c’est le pire scénario.
Les organisations qui se soucient de la qualité investissent clairement dans les tests de régression, mais beaucoup ne respectent pas le QA et soit n’en font pas du tout, soit l’externalisent au moins-disant.
Qu’Apple n’ait pas eu de tests de régression pour les jailbreaks, historiquement l’une des catégories de bugs les plus scrutées, est vraiment étrange.
On peut adresser diverses critiques à Mozilla aujourd’hui, mais même au début des années 2000, avec des outils comme Tinderbox et Bugzilla, l’organisation disposait d’un QA et d’une CI/CD assez solides.
Quand DevOps est devenu à la mode et a popularisé ces pratiques, je pensais que tout le monde les appliquait déjà, mais c’était une illusion de ma part.
C’est une situation à la Conway, produite par la séparation entre sécurité et développement fonctionnel.
Même avec une procédure de build/release et une suite mature de tests de régression, il est fort possible que, du fait de la structure interne de l’organisation, ces problèmes de sécurité n’y entrent pas.
Il y en avait facilement des milliers, et je crois que c’était probablement SQLite.
C’est une approche à prendre en exemple.
Si les correctifs ne sont pas backportés, il paraît probable que les tests ne le soient pas non plus.
En lisant des termes comme kheap separation, atténuation du task port, SSV ou SPTM, j’ai eu l’impression de parler couramment avec un ami dans une langue étrangère, puis de voir soudain la conversation basculer vers une explication de neurochirurgie ou de physique nucléaire, avec ma compréhension qui tombe d’une falaise.
C’était un peu pareil la fois où j’ai essayé d’interpréter une discussion sur la rénovation d’un haut-fourneau.
C’est dommage que le jailbreak ne soit plus aussi actif qu’avant.
Je n’ai presque rien fait de pratique avec mon iPad jailbreaké, mais c’était amusant ; aujourd’hui, j’aimerais y installer une app de tethering, UTM et une solution pour le JIT.
SideStore avait aussi l’air prometteur, mais comme mon compte avait autrefois été un compte Apple Developer payant, il me reste 10 app IDs qui n’expirent pas, et je ne peux donc pas installer des apps comme UTM sans créer un nouveau compte ou repayer.
Après l’avoir perdu, je suis revenu à Android, qui à ce moment-là avait déjà largement rattrapé son retard sur les fonctionnalités de base.
J’ai entendu dire qu’Apple payait aujourd’hui 1 million de dollars pour un jailbreak, ce qui doit être le plancher du prix de marché libre.
Je ne sais pas s’il faut passer par un intermédiaire, ou s’il existe une adresse e-mail publique qui ne finira pas enterrée au support de premier niveau.
Si c’est vrai, Apple emploie une stratégie étonnante.
En verrouillant toutes les voies permettant d’obtenir root sur l’appareil, Apple peut corriger les vulnérabilités que les développeurs de jailbreak trouvent gratuitement.
D’après l’article, les communautés privées disposent toujours d’exploits, et Apple les corrige.
C’est seulement la communauté publique, ou ce développeur, qui ne semble plus fonctionner ainsi pour une raison quelconque.
La meilleure phrase de tout le texte, pour moi, était : « Je tiens aussi à remercier la personne qui a unpatché ce bug dans iOS 13.0. C’était vraiment très cool de sa part. »
« Je n’arrive pas à imaginer où nous en serons dans 5 ans » : moi, je peux l’imaginer.
iMessage permettra toujours de compromettre les appareils, les comptes et les données.
Le texte ne disait pas s’il s’agissait d’un jailbreak tethered ou untethered.
Le jailbreak dans lequel elle a été incluse et distribuée, unc0ver, était, je crois, « semi-untethered ».