Apps iOS sur Mac ARM : Opportunités de pentesting - Partie II


Article de blog 29 septembre 2021 par Jim Aspers, spécialiste de la sécurité chez Bureau Veritas Cybersecurity.

La possibilité d'exécuter des applications iOS sur des Mac ARM est une fonctionnalité intéressante pour les chercheurs en sécurité applicative. Dans la première partie, nous avons montré qu'avec un peu de bricolage, il est possible d'exécuter n'importe quelle application iOS arbitraire sur macOS. Les applications nécessitant un accès au microphone, à l'appareil photo, au Bluetooth ou aux services de localisation de l'appareil ont toutes fonctionné comme prévu.

App coding design

Bien que nous ayons démontré la possibilité d'exécuter des applications iOS arbitraires, nous l'avons fait en utilisant une application légitime et en remplaçant son contenu par l'application que nous essayions d'exécuter ; le processus de lancement n'a pas été compris en profondeur. Afin d'obtenir un contrôle total sur l'application lancée et l'environnement dans lequel elle a été déployée, une compréhension plus solide du processus de lancement était nécessaire.

En outre, nous avons constaté que de nombreuses applications intégrant la détection de jailbreak marquaient nos Macs comme jailbreakés, ce qui nécessitait un contournement manuel. Nous avons cherché un moyen solide de contourner ce problème, sans patcher ou instrumenter l'application cible.

Lanceur d'application personnalisable : launchr

Il est souhaitable d'avoir le plus de contrôle possible sur l'application testée. Concrètement, cela inclut (mais n'est pas limité à) :

  • La possibilité de spécifier des fichiers pour les descripteurs d'entrée, de sortie et d'erreur de l'application ;
  • La possibilité de spécifier des variables d'environnement ;
  • Lancer le processus d'application dans un état suspendu pour une instrumentation précoce.


Le lancement de binaires iOS non graphiques sur macOS peut se faire de manière assez triviale en utilisant l'appel de système privé posix_spawnattr_set_platform_np() pour définir l'attribut de plateforme du processus à '2'(PLATFORM_IOS, voir <mach-o/loader.h dans les sources XNU avant le lancement (comme l'a expliqué Samuel Groß dans son article). Lancer une application graphique iOS est un jeu différent, cependant. Nous avons partiellement inversé la façon dont macOS gère les lancements d'applications graphiques en général, et nous avons appliqué ces connaissances au cas spécifique d'iOS.

Plusieurs composants du système d'exploitation se sont avérés impliqués dans le lancement d'une application iOS, notamment

  • runningboardd
  • secinitd
  • lsd
  • appinstalld/com.apple.MobileInstallationService
  • containermanagerd

Nombre de ces composants sont impliqués dans le sandboxing et d'autres tâches liées à la sécurité. Comme nous préférerions lancer directement une application en contournant tous ces composants liés à la sécurité, nous avons analysé le lancement d'une simple application système (Calculator.app) et comparé le flux observé. Cette fois, il est apparu que seul runningboardd était activement impliqué. L'attention s'est donc portée sur runningboardd.

A ce stade, le soupçon que nous devions parler à runningboardd d'une manière ou d'une autre pour lancer des applications directement a été établi. En dehors d'un ensemble limité de composants de base, Apple conserve le code source et la documentation privée de l'API pour elle-même, de sorte qu'il n'est pas possible de rechercher simplement dans le code source de runningboardd ou de lire les interfaces qu'il expose. La rétro-ingénierie complète du code sous-jacent (RunningBoard.framework, RunningBoardServices.framework) n'est pas non plus une tâche réaliste.

Adobe Stock 317757317

Comment les autres le font

La façon la plus simple d'apprendre à lancer une application est de tricher un peu et d'espionner les propres programmes d'Apple. Il y a (au moins) deux façons courantes pour un utilisateur de macOS de lancer une application iOS dans un scénario d'utilisation normale : en utilisant Finder.app en double-cliquant sur le wrapper bundle de l'application (voir l'article précédent sur ce sujet), ou en utilisant la commande 'open' avec le wrapper bundle comme argument. D'après notre première analyse, nous pensons que les deux enverront à un moment donné un message XPC à runningboardd, lui demandant de lancer l'application cible. Bien que 'open' semble être le choix le plus évident pour la rétro-ingénierie en raison de sa taille limitée et donc du bruit limité dans les résultats, Finder.app a l'avantage d'être un processus en cours d'exécution en permanence. Cela en fait une cible facile à atteindre avec des outils de traçage tels que xpcspy.

En regardant la sortie, nous trouvons les appels XPC attendus :

Secura Blog i OS pentesting apps 1

Le contenu complet est trop volumineux pour être affiché ici. Ce que nous voulons dire ici, c'est que cet appel semble contenir des informations qui sont utilisées pour le spawn au niveau du système d'exploitation de l'application cible (identifiant du bundle cible, variables d'environnement, attributs de LaunchServices tels que les drapeaux de spawn et les options d'exécution, ...). Un autre élément clé de cette sortie est le sélecteur envoyé au destinataire : executeLaunchRequest:identifier:error :. En Objective-C sur macOS/iOS, un "sélecteur" envoyé à un objet peut être interprété comme une méthode de classe ou d'instance exécutée. Comme nous savons que le sélecteur est envoyé à runningboardd, et que nous voyons la clé "rbs_selector", notre première hypothèse est que l'appel API dont résulte ce message XPC est exposé par RunningBoardServices.

Nous utilisons frida-trace en lançant à nouveau Calculator.app depuis le Finder pour découvrir à quelle classe appartient exactement ce sélecteur :

Secura blog i OS Apps Pentesting 2

Le sélecteur exact que nous attendions n'a pas été trouvé, mais nous avons trouvé quelque chose qui y ressemble. L'argument le plus intéressant du sélecteur, un objet "LaunchRequest", est également présent ici. Une inspection plus poussée de cet objet a montré qu'il s'agissait d'un objet RBSLaunchRequest construit à partir d'un objet RBSLaunchContext, qui contenait à son tour le contenu du dictionnaire indiqué dans la sortie xpcspy. Cette recherche et d'autres recherches similaires nous ont montré que nous devions envisager d'utiliser l'objet RBSLaunchRequest et les objets apparentés pour atteindre nos objectifs.


Jeu d'imitation

En creusant un peu dans les différentes implémentations de méthodes exposées par RunningBoardServices.framework, et en procédant par essais et erreurs, nous avons découvert que nous pouvions lancer une application graphique macOS à partir de notre propre code en utilisant seulement quelques appels à des API non documentées. Comme nous l'avons constaté lors de nos expériences avec le lancement de binaires iOS sur macOS, la clé pour lancer un exécutable iOS au lieu d'un exécutable macOS réside dans un paramètre de spécification de plateforme. En recherchant des symboles contenant la chaîne "platform", nous avons remarqué qu'un objet RBSProcessIdentity pouvait être initialisé avec un argument de plateforme. Le traçage de l'appel particulier a en effet montré qu'il était frappé, et que la valeur de cet argument était 1 ("PLATFORM_MACOS") lors du lancement de Calculator.app :

Secura blog i OS Apps Pentesting 3

Cela s'est avéré être la clé pour lancer des applications graphiques iOS à partir de notre propre code. Si nous fournissons PLATFORM_IOS au lieu de PLATFORM_MACOS avec ce sélecteur lors de la création de l'objet RBSProcessIdentity et que nous demandons le lancement d'un bundle d'applications iOS (pas seulement un wrapper, mais un véritable bundle d'applications iOS), macOS lance joyeusement l'application. Comme aucune vérification supplémentaire n'est appliquée si nous lançons l'application de cette manière, nous pouvons simplement signer ad-hoc l'ensemble du bundle et la signature sera acceptée. Cette méthode est idéale pour expérimenter des correctifs sur l'application cible.

Secura blog i OS Apps Pentesting 5
Secura blog i OS Apps Pentesting 4

Utiliser la force

Maintenant que nous pouvons lancer une application iOS avec les objets RBSProcessIdentity et RBSLaunchContext entièrement sous notre contrôle, nous pouvons nous mettre au travail. À ce stade, nous cherchons des solutions pour au moins les points suivants :

  • Nous voulons être en mesure d'arrêter l'exécution du processus enfant juste après l'avoir créé ;
  • Nous voulons être en mesure d'effectuer confortablement des appels de crochet à l'intérieur du processus enfant sans avoir à compter sur l'instrumentation ;
  • Nous voulons pouvoir contrôler la politique de bac à sable du processus enfant.


Démarrage suspendu

Lorsque nous évaluons la résistance des applications au craquage ou à la modification de l'exécution en général, nous rencontrons parfois des applications qui mettent en œuvre des contre-mesures anti-débogage dans une phase précoce du démarrage de l'application. Cela peut être dès les routines d'initialisation de la bibliothèque, qui peuvent être difficiles à contourner si l'on utilise l'obscurcissement ou s'il y a simplement des centaines de routines d'initialisation à énumérer. Dans de tels cas, il est important de pouvoir attacher un débogueur à un stade très précoce, avant même que le mécanisme de protection anti-débogage ne soit activé. C'est ce que nous appelons l'instrumentation précoce.

Si nous n'avions pas d'autres options, un simple while() qui attend qu'un processus soit créé et qui attache immédiatement un débogueur ferait probablement l'affaire. Cependant, comme nous avons maintenant un contrôle total sur le processus créé, nous pouvons demander au noyau de mettre le processus enfant en mode suspendu juste après sa création (c'est-à-dire qu'il s'arrête juste à _dyld_start, le point d'entrée de n'importe quel exécutable *OS) :

Secura blog i OS Apps Pentesting 6

Une fois que nous avons terminé ce que nous voulons faire avec l'application (par exemple, placer des points d'arrêt, sauter des instructions, patcher la mémoire), nous pouvons continuer son exécution en envoyant un SIGCONT.

Un effet secondaire très gênant du lancement d'une application en mode suspendu est que le Puppet Master de macOS, sous la forme de runningboardd, attend un "battement de cœur" de la part du processus créé. S'il ne reçoit pas un tel signe de vie à temps, l'application est laissée dans un état inutilisable et ne sera plus utilisable (un kill -9 sera nécessaire). Cela se produit environ 30 secondes après le lancement de l'application en mode suspendu et l'arrêt de l'exécution, avec le message d'erreur suivant :

Secura blog i OS Apps Pentesting 7

Aucune solution n'a encore été trouvée pour résoudre ce problème. En scriptant l'utilisation de lldb, 30 secondes suffisent pour faire notre magie en général, mais une solution adéquate serait la bienvenue.


Le retour de DYLD_INTERPOSE

La deuxième exigence était une méthode pratique pour modifier le comportement de l'application. Lorsqu'il s'agit de modifier le flux d'exécution des applications, il y a généralement trois possibilités :

  • Patching du code machine sur le disque ;
  • Modifier le code en mémoire à l'aide d'un outil de type débogueur ;
  • Utiliser un cadre d'instrumentation dynamique (par exemple Frida) ;
  • Utiliser les fonctions d'accrochage natives de macOS(dyld interposing).


Comme l'application de correctifs au code machine sur disque ou en mémoire nécessite l'écriture de code assembleur, ce ne sont pas les choix les plus pratiques et les plus flexibles. Un outil tel que Frida est excellent et offre un large éventail de cas d'utilisation. Cependant, pour appliquer des modifications persistantes et potentiellement complexes, une méthode moins lourde telle que dyld interposing (pour les appels de fonctions C) ou swizzling (pour les classes Obj-C) est préférable. Nous pouvons déjà parfaitement swizzler avec la solution à notre disposition, en écrivant notre propre dylib et en l'injectant avec une variable d'environnement DYLD_INSERT_LIBRARIES. En ce qui concerne les appels de fonctions C, comme le souligne Samuel Groß dans son article (référencé précédemment dans ce billet), macOS ne permet pas l'interposition pour les exécutables iOS. Cependant, comme nous pouvons maintenant lancer l'application dans un état suspendu, nous pouvons appliquer la méthode d'injection de code de Groß pour patcher dyld en mémoire afin de contourner cette restriction. Cette méthode est également mise en œuvre dans notre lanceur.

Une application pratique triviale de cette fonctionnalité est la correction de l'incompatibilité d'une application avec la plateforme matérielle iOS émulée par macOS (iPad Pro3rd gen avec iOS 14.7 au moment de la rédaction). Pour cette raison, l'application de notre client ne pouvait pas être exécutée sur macOS ; nous avons reçu une erreur indiquant que notre "iPad" était trop récent pour exécuter l'application. Cependant, avec quelques lignes de code, une bibliothèque dynamique a été compilée pour se faire passer pour un ancien modèle d'iPad pris en charge par l'application (iPad7). Par conséquent, nous avons pu exécuter et évaluer correctement l'application.

Secura blog i OS Apps Pentesting 8

Correction de l'App Sandbox

En testant une application basée sur Xamarin qui mettait en œuvre Cryoprison pour la détection de jailbreak, il a été constaté que les applications iOS lancées de cette manière pouvaient accéder à des fichiers tels que /bin/bash. Comme ces fichiers ne devraient pas être présents sur les installations iOS jailbreakées, ou du moins ne devraient pas être accessibles depuis la sandbox de l'application, les implémentations courantes de détection de jailbreak vérifient l'accès à ces fichiers système pour déterminer s'ils sont exécutés dans un environnement jailbreaké ou non jailbreaké. Le fait que l'application Xamarin testée ait détecté une plateforme jailbreakée dans ce cas a conduit à soupçonner que le profil de bac à sable appliqué par macOS aux applications iOS n'était pas tout à fait conforme au bac à sable natif d'iOS.

Pour comprendre les mécanismes de sandboxing de macOS, nous avons procédé à une rétro-ingénierie partielle du programme sandbox-exec. Avec une section de texte d'environ 1 ko, ce binaire semblait être le candidat idéal pour commencer le processus de rétro-ingénierie :

Secura blog i OS Apps Pentesting 9

Le déroulement du programme consistait essentiellement en deux étapes :

  • Configurer le profil de bac à sable à utiliser et appliquer le profil à son propre processus ;
  • exécuter() l'exécutable cible.


Comme execve() exécute une image binaire dans le processus de l'appelant, le profil de bac à sable appliqué lors de la première étape est appliqué à l'exécutable cible. Ainsi, l'exécutable cible n'a pas besoin d'être conscient de l'existence d'un bac à sable.

Une analyse plus approfondie a montré que deux appels d'API non documentés peuvent suffire pour préparer et appliquer un profil de bac à sable :

  • sandbox_compile_file(), qui prend en entrée un fichier de définition du bac à sable ;
  • sandbox_apply(), qui prend la sortie du premier appel et l'applique au processus de l'appelant.


En utilisant la méthode d'injection de bibliothèque décrite précédemment (en employant DYLD_INSERT_LIBRARIES) pour obtenir une exécution précoce du code dans le contexte du processus de l'application iOS lancée, nous avons imité ce flux. L'appel à sandbox_apply() a cependant échoué, car un profil de bac à sable était déjà en place pour le processus à ce moment-là. Toute possibilité de modifier une politique de bac à sable à partir du processus lui-même une fois qu'il a été activé pourrait entraîner des vulnérabilités en matière de sécurité, il est donc logique que cela ne soit pas autorisé.

Nous avons donc cherché une méthode pour lancer notre application cible sans bac à sable, afin de pouvoir ensuite appliquer notre propre bac à sable personnalisé. L'analyse du processus de lancement de l'application nous avait déjà appris que le composant macOS secinitd était impliqué dans l'application des profils de bac à sable. L'analyse de la bibliothèque sous-jacente libsystem_secinit.dylib a montré qu'il existe en effet un moyen de lancer des applications iOS sans qu'un bac à sable ne soit appliqué automatiquement : le droit "com.apple.private.security.no-sandbox". En effet, après avoir ajouté ce droit aux droits de notre application cible (car nous les contrôlons aussi entièrement, bien sûr !), nous pouvons maintenant appliquer notre propre profil de bac à sable personnalisé en injectant une bibliothèque contenant le code simple suivant :

Secura blog i OS Apps Pentesting 10

La spécification d'un profil de bac à sable soigneusement élaboré pour l'utilisation de nos applications cibles devrait nous permettre de contourner complètement tous les mécanismes de détection de jailbreak courants par défaut, sans avoir à compter sur les contournements de détection de jailbreak conventionnels. L'efficacité de la détection de jailbreak peut toujours être évaluée en supprimant simplement les restrictions du bac à sable et en inspectant le comportement de l'application. En conclusion, cela rendra l'assessment des applications plus efficace.

En pratique, l'utilisation de cette approche avec un profil de bac à sable qui interdit l'accès en lecture/écriture à tout emplacement en dehors du container de l'application a permis à une application de test de ne plus soulever de drapeaux de jailbreak :

Secura blog i OS Apps Pentesting 11

Conclusion

Laptop phone digital

Dans cet article, nous avons expliqué comment exécuter des applications graphiques iOS sous macOS avec un contrôle total. Il a été question de la façon dont cela peut être utilisé pour le pentesting d'applications et la recherche en sécurité. Il y a des améliorations à apporter, mais jusqu'à présent, il semble que la solution dont nous disposons soit parfaitement adaptée à la recherche et aux tests de sécurité. Nous allons tester autant d'applications que possible dans un avenir proche afin de pouvoir conclure sur l'applicabilité de cette approche pour les tests d'applications au quotidien.

Pour une implémentation WIP de l'outil de lancement discuté, voir srepsa/launchr (github.com).