Récemment, Google a levé le voile sur Coruna, un kit d’exploitation sophistiqué visant les iPhone à travers une vaste gamme de versions d’iOS. Cette campagne d’attaque s’appuie sur des chaînes d’exploits pour compromettre les appareils à distance simplement via la visite de sites web piégés.
Dans ce billet, je vous propose une vue d’ensemble, accessible mais technique, de l’envers du décor : comment dumper les implants directement depuis un iPhone compromis, décortiquer l’exploit WebKit, analyser le shellcode loader et plonger dans le kernel exploit.
L’objectif : dévoiler étape par étape les grandes lignes de cette chaîne d’attaque, sans prétendre à l’exhaustivité, mais en offrant un aperçu concret des méthodes employées par Coruna pour prendre le contrôle d’un appareil Apple.
Voici un aperçu de la chaîne côté userland, de Safari jusqu’à l’exécution du kernel exploit dont on va parler.
Une infrastructure toujours active Link to heading
L’article de Google inclut des indicateurs de compromission (IOC) permettant d’identifier si un système a été touché par l’attaque.
Dans ce cas précis, Google a publié la liste des sites web utilisés dans la campagne, à partir desquels les appareils pouvaient être compromis.
En analysant ces domaines et en les testant depuis un VPS, il apparaît que certains d’entre eux sont encore accessibles.
Requête CURL vers l’un des serveurs hébergeant le kit d’exploitation
Récupération de la chaîne d’exploits Link to heading
Pour récupérer la chaîne d’exploit, j’utilise un iPhone X jailbreaké sous iOS 16.6. Cela me permet d’obtenir un shell root ainsi qu’un accès à un debugger.
J’en ai également profité pour utiliser mitmproxy afin d’intercepter les requêtes HTTP envoyées au serveur en question. En se connectant au site web depuis Safari avec le trafic passant par le proxy, on observe que plusieurs fichiers JavaScript sont téléchargés.
Ensuite, des requêtes sont envoyées en boucle vers des serveurs de commande et de contrôle (C2) avec des noms de domaines tels que yvgy29glwf72qnl[.]xyz/details/show[.]html. Cependant, ces requêtes n’aboutissent pas car les serveurs sont tous indisponibles, le nom de domaine n’existe plus.
À ce stade, l’iPhone vient d’être infecté.
Fenêtre MITM pour visualiser les requêtes HTTP
Les requêtes envoyées vers le serveur C2 utilisent le User-Agent PowerManagement%20configd%20plugin/161.0.0, ce qui suggère que le trafic tente d’imiter un composant système lié à la gestion de l’énergie.
Détail de la requête HTTP vers le C2
D’après l’article de Google, on sait que du code est exécuté dans le démon powerd. Depuis la console, on observe que ce daemon tente d’établir des connexions vers Internet. Probablement vers le C2 qui n’est plus accessible.
Console pour analyser les logs de powerd
En attachant powerd à un debugger, il est possible de placer des breakpoints sur les fonctions effectuant les requêtes HTTP afin de tenter de récupérer l’implant responsable des communications avec le serveur C2.
Pour faire simple, on place un breakpoint sur toutes les fonctions HTTP.
Ci-dessous on peut voir qu’on a déclenché le breakpoint sur CFURLRequestSetHTTPRequestMethod.
Avec la commande bt, la stack révèle des adresses appartenant à une région mémoire inconnue. En inspectant la région, on voit bien la magic value 0xfeedfacf, indiquant la présence d’un binaire Mach-O chargé directement en mémoire.

Commandes LLDB pour récuperer l’implant
Il ne reste plus qu’à dumper la mémoire pour récupérer l’implant, même si dans cet article on ne l’analyse pas.
Je sais aussi d’après Google, que le kernel exploit qui permet ensuite d’élever les privileges et d’écrire dans la mémoire du noyau est présent dans la mémoire de powerd.
En attachant de nouveau le démon powerd au débogueur, on observe la création de nombreux threads. En les inspectant, un peu au hasard je dois l’admettre, on finit par trouver le début d’un fichier Mach‑O, identifiable par sa valeur magique feedfacf.
Recherche de l’exploit dans LLDB
On peut dumper la mémoire à l’adresse où se trouve le mach-o : 0x102b94000.
(lldb) memory read --force --binary --outfile /tmp/dump.bin -s1 -c 0x200000 0x102b94000
2097152 bytes written to '/tmp/dump.bin'
On se retrouve bien avec un Mach-O :
dump.bin: Mach-O 64-bit dynamically linked shared library arm64
Analyse webkit Link to heading
La chaîne d’exploitation commence par la page group.html, qui embarque un chargeur de modules JavaScript obfusqué et deux modules pour tout ce qui est lié à la mémoire et à l’identification de l’appareil cible.
Après avoir identifié le modèle d’appareil, la version de WebKit et la présence ou non de PAC (ARM64e) à l’aide du User-Agent, le chargeur télécharge depuis le serveur un exploit WebKit/JSC spécifique à la cible (d’après Google, il y a 5 variantes disponibles).
L’exploit chargé va permettre d’avoir des primitives de lecture et écriture dans la mémoire du processus de Webkit com.apple.Webkit.WebContent à l’aide d’une confusion de types (CVE-2024-23222).
Le code va exécuter 100 000 fois une fonction WASM pour forcer le compilateur JIT à produire du code natif et abuser d’une confusion de types.
for (let i = 0; i < (1316243308 ^ 1316340172); i++) try {
I()
} catch (i) {}
Cette boucle force le compilateur JIT à produire du code natif dans une zone mémoire RWX, technique connue sous le nom de JIT spraying.
Après avoir bypass ASLR, escape la sandbox et contourné l’authentification de pointeurs, l’exploit créé une zone mémoire RWX pour y charger un Mach-O.
Le fichier est ensuite exécuté pour communiquer avec le code JavaScript qui va télécharger une configuration et le payload principal, tous deux chiffrés. Le payload est un fichier binaire avec un header f00dbeef (little-endian) listant le reste des payloads à télécharger.
00000000: efbe 0df0 0100 0000 0000 0700 0300 0000 ................
00000010: 1800 0000 7808 0000 7856 3412 0300 0000 ....x...xV4.....
00000020: 2e2f 0000 0000 0000 0000 0000 0000 0000 ./..............
...
00000110: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000120: 1300 0000 0000 30f2 6c68 2a65 deb7 cf02 ......0.lh*e....
00000130: 0dd6 40d1 30a2 a73e 9442 ccdd c441 520c [email protected]..>.B...AR.
00000140: 9516 20a4 1426 05ad 3438 3030 3034 3836 .. ..&..48000486
00000150: 3538 3436 3366 3937 3165 3735 3266 6639 58463f971e752ff9
00000160: 3363 3137 3637 6539 6165 3766 3334 3331 3c1767e9ae7f3431
00000170: 2e6d 696e 2e6a 7300 0000 0000 0000 0000 .min.js.........
00000180: 0000 0000 0000 0000 0000 30f3 230d daa3 ..........0.#...
00000190: 80a7 899e 52be 22cc 926a 4b76 0930 3e14 ....R."..jKv.0>.
000001a0: c3ed 55d5 9049 d3b2 0ee1 2974 6234 3432 ..U..I....)tb442
000001b0: 6162 3131 3362 3832 3966 6638 6337 6266 ab113b829ff8c7bf
000001c0: 3334 6166 6134 6432 6439 3937 3838 3966 34afa4d2d997889f
000001d0: 3330 3866 2e6d 696e 2e6a 7300 0000 0000 308f.min.js.....
000001e0: 0000 0000 0000 0000 0000 0000 0000
...
Chaque payload est ensuite téléchargé depuis le même serveur. L’un des binaires, un shellcode ARM64 est d’abord chargé en mémoire, puis il sera chargé d’exécuter les autres modules Mach-O.
Shellcode Link to heading
Le rôle du shellcode est de bootstrap la chaîne de chargement de l’implant. Il reçoit une dylib (bibliothèque dynamique) , la charge en mémoire, résout les symboles, applique les relocalisations, puis appelle les constructeurs, le tout sans utiliser dyld (le linker dynamique d’Apple).
Le point d’entrée ci-dessous reçoit depuis le code JavaScript une structure qui correspond au contexte pour préparer le mach-o à charger.
void _start(context* ctx)
{
ctx->load_macho = load_macho // offset +0x30
ctx->lookup_symbol = lookup_symbols // offset +0x38
ctx->unload_macho = unload_macho // offset +0x130
}
Le loader embarque aussi quelques wrappers pour des syscalls, pour éviter d’utiliser la libC.
De plus il semble qu’il tente de corriger l’instabilité laissée dans le processus JavaScriptCore après l’exploitation. Ça permet d’éviter que le garbage collector ou le compilateur JIT ne modifie ou tente d’accéder aux zones mémoires altérées et donc de crasher le processus.
Si le processus crash, ça veut dire que l’ensemble de la chaine d’exploitation est finie et cela peut aussi générer un crashlog.
Donc d’abord il va installer un signal handler pour le signal SIGSEGV via sigaction. Ce handler va permettre de justement reappliquer les patches après modification des zones mémoires corrompues. Ensuite en fonction du signal reçu (0x1337/0x1338/1339), le handler peut réappliquer ces corrections si les pages de mémoire sont modifiées.
Le tout fonctionne sans modifier directement les pages signées de JavaScriptCore, mais en s’appuyant sur une zone mémoire anonyme de 32 Ko allouée avec mmap : mmap(0, 0x8000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON, 0, 0);.
En résumé, ce shellcode est bien plus qu’un simple loader : c’est un mini dyld, capable de charger des Mach-O en mémoire avec aussi le support de PAC et du support de classes ObjC via objc_map_images.
Mais sa particularité la plus intéressante réside dans sa capacité à stabiliser le processus WebContent après exploitation en patchant les pages de code pour éviter tout crash. On est sur une chaine d’exploitation remarquablement soignée, conçue pour durer dans un processus compromis sans laisser de traces.
Kernel exploit Link to heading
Passons à l’analyse du kernel exploit qu’on a dumpé via LLDB. Voici le détail de la chaîne côté kernel :
Par défaut j’utilise la vue “Triage Summary” de Binary Ninja pour avoir un aperçu du binaire que je reverse. On peut voir qu’on a très peu d’informations et aucun symboles. Tout comme le shellcode, sauf que là c’est un fichier Mach-O, on devrait pouvoir avoir plus d’infos.
Binary Ninja : vue de triage
En fait, vu que c’est un dump en mémoire il y a une difference d’offsets par rapport au fichier sur disque. Mais les strings liées aux symboles importés sont bel et bien présentes dans le binaire.
A l’aide du module Python LIEF on peut recuperer la liste et re-symboliser le binaire pour avoir une meilleure lisibilité dans Binary Ninja. J’ai écrit un petit script qui permet de corriger ce souci.
$ uv run symbolicate.py
resolved 508 symbols from indirect table
__LINKEDIT fileoff : 0x4c000
LC_DYLD_CHAINED_FIXUPS: dataoff : 0x4c000
LC_DYLD_EXPORTS_TRIE: dataoff : 0x4d6a0
LC_SYMTAB: symoff : 0x4daf0, stroff : 0x4f390
LC_DYSYMTAB: indirectsymoff : 0x4eba0
LC_DATA_CODE: dataoff : 0x4daf0
508 symbols
saved to dump_symbolicated.bin
Rechargeons le nouveau binaire dans Binary Ninja et voilà ! La table d’import est visible et cela rend le reverse plus simple.
Binary Ninja : vue de triage avec les imports
Analyse Link to heading
Avant de tenter quoi que ce soit, l’exploit effectue plusieurs vérifications anti‑VM ainsi que du fingerprinting de l’appareil :
- Vérification de la la présence de
/usr/libexec/corelliumdet que le numéro de série de l’appareil n’est pas “CORELLIUM”. Si c’est le cas, cela signifie que l’exploit tourne sur une VM, l’exploit va donc abandonner. - Check du
hw.modelvia sysctl pour identifier le matériel. - Vérification des modes dévelopeur et “security research”, sûrement pour detecter un Security Research Device.
- Analyse la chaîne de version du noyau XNU pour gérer des offsets ou gadgets spécifiques à la version.
Binary Ninja : check de Corellium
Ensuite l’exploit doit avant tout récupérer des primitives de lecture et d’écriture dans la mémoire du noyau.
Pour cela il va abuser du sous-système IOSurface en créant un objet IOSurface dont l’adresse noyau est leakée dans la réponse d’IOConnectCallMethod.
À partir de cette adresse, il corrompt des pointeurs internes de l’objet en question, puis déclenche des sélecteurs IOKit (0x10, 0x21, 0x33) via IOConnectCallMethod pour forcer le noyau à déréférencer les pointeurs corrompus, permettant ainsi d’avoir nos primitives de lecture et d’écriture dans le noyau.
Ensuite, pour s’attaquer à la partie de la signature de code gérée par AMFI, l’exploit doit contourner une protection appelée Page Protection Layer (PPL).
PPL est un mécanisme de sécurité du kernel qui empêche la modification directe de certaines pages mémoire critiques, notamment celles liées aux politiques de signature de code. Même avec l’exécution de code dans le kernel, ces pages restent protégées et ne peuvent normalement être modifiées que par un service interne de ce système.
Sans entrer dans les détails (car ce bypass mérite un article à lui seul), il utilise des commandes spéciales du GPU pour écrire directement dans une zone de mémoire physique où le CPU n’a pas accès. Et va ensuite remapper la mémoire pour que le CPU puisse la lire sans pour autant la modifier.
Suite à ça, l’exploit patch en mémoire AMFI afin d’activer le Developer Mode via le flag developer_mode_status, ainsi que le mode Security Research via le flag allows_security_research.
Avec ces mécanismes activés, il devient possible d’exécuter du code non signé et d’injecter des bibliothèques dynamiques dans les processus du système, ce qui peut ensuite être utilisé pour déployer l’implant final.
Binary Ninja : Activation des flags via AFMI
Post-exploitation Link to heading
Une fois les primitives kernel R/W obtenues et les protections d’AMFI contournées, l’exploit met en place plusieurs mécanismes permettant de prendre le contrôle total de l’iPhone qui seront sûrement utiles pour que l’implant prenne le relais.
La première étape c’est l’injection d’entitlements qui permet de :
- obtenir le task port de n’importe quel processus via
task_for_pid - interagir directement avec certains services IOKit
- manipuler des snapshots APFS ou des images disque
L’exploit monte également la partition système en lecture seule dans /private/var/MobileSoftwareUpdate/mnt1, pas sûr de comprendre pourquoi il fait ça, sûrement lié à l’implant.
En parallèle, il y a plusieurs interactions avec des services du SEP qui sont effectuées via le KeyStore.
Ensuite pour que l’implant puisse garder ses privilèges et ne pas relancer l’exploit il y a un “special port” spécifique utilisé. Ça permet aussi à l’implant d’avoir un canal de communication IPC via host_get_special_port avec le kernel pour lancer des commandes privilégiées.
Dans l’ensemble, la partie post-exploitation sert vraiment à préparer le terrain pour l’implant avec un maximum de privilèges.
Conclusion Link to heading
Pour conclure, on a une chaîne d’exploitation complète de WebKit jusqu’au kernel, le tout très stable et sans crash.
Pour cet article, j’ai testé la chaîne sur plusieurs appareils et différentes versions d’iOS. Elle a toujours abouti avec succès, sans provoquer de crash d’aucun processus système sur les iPhone testés.
Chaque étape, de l’exécution de code dans WebKit, à l’obtention des privilèges kernel, jusqu’à la post-exploitation et l’injection d’entitlements est soigneusement orchestrée pour préparer l’environnement d’un implant.
Un point intéressant : alors que la partie WebKit est obfusquée, l’exploit kernel reste étonnamment lisible et non-obfusqué.
Pendant l’écriture de l’article, Clément Lecigne a publié sur VirusTotal l’implant et les modules mentionnés dans la liste d’IOC du post de Google, ça mériterait aussi un article dédié, merci à eux.
Merci aussi à Trenchant pour cette chaîne d’exploits qui, grâce à plusieurs événements malencontreux, nous fournit une pépite à analyser.
Dans les sources je laisse des articles et une vidéo qui traitent du même sujet que je vous conseille vivement de regarder.
Liens et sources :