
Exploit de Ian Beer pour CVE-2017-2370 (lecture/écriture mémoire noyau sur iOS 10.2)
// ianbeer
exploit de lecture/écriture arbitraire du noyau pour CVE-2017-2370 pour iOS 10.2
Testé uniquement sur iPod Touch 6G 14C92 - les autres appareils/firmwares ne fonctionneront pas immédiatement !
*** le bug *** mach_voucher_extract_attr_recipe_trap est un trap mach qui peut être appelé depuis n'importe quel contexte. C'est du code tout nouveau, ajouté dans iOS 10.
kern_return_t mach_voucher_extract_attr_recipe_trap(struct mach_voucher_extract_attr_recipe_args *args) { ipc_voucher_t voucher = IV_NULL; kern_return_t kr = KERN_SUCCESS; mach_msg_type_number_t sz = 0;
if (copyin(args->recipe_size, (void *)&sz, sizeof(sz))) <---------- (a)
return KERN_MEMORY_ERROR;
if (sz > MACH_VOUCHER_ATTR_MAX_RAW_RECIPE_ARRAY_SIZE)
return MIG_ARRAY_TOO_LARGE;
voucher = convert_port_name_to_voucher(args->voucher_name);
if (voucher == IV_NULL)
return MACH_SEND_INVALID_DEST;
mach_msg_type_number_t __assert_only max_sz = sz;
if (sz < MACH_VOUCHER_TRAP_STACK_LIMIT) {
/* keep small recipes on the stack for speed */
uint8_t krecipe[sz];
if (copyin(args->recipe, (void *)krecipe, sz)) {
kr = KERN_MEMORY_ERROR;
goto done;
}
kr = mach_voucher_extract_attr_recipe(voucher, args->key,
(mach_voucher_attr_raw_recipe_t)krecipe, &sz);
assert(sz <= max_sz);
if (kr == KERN_SUCCESS && sz > 0)
kr = copyout(krecipe, (void *)args->recipe, sz);
} else {
uint8_t *krecipe = kalloc((vm_size_t)sz); <---------- (b)
if (!krecipe) {
kr = KERN_RESOURCE_SHORTAGE;
goto done;
}
if (copyin(args->recipe, (void *)krecipe, args->recipe_size)) { <----------- (c)
kfree(krecipe, (vm_size_t)sz);
kr = KERN_MEMORY_ERROR;
goto done;
}
kr = mach_voucher_extract_attr_recipe(voucher, args->key,
(mach_voucher_attr_raw_recipe_t)krecipe, &sz);
assert(sz <= max_sz);
if (kr == KERN_SUCCESS && sz > 0)
kr = copyout(krecipe, (void *)args->recipe, sz);
kfree(krecipe, (vm_size_t)sz);
}
kr = copyout(&sz, args->recipe_size, sizeof(sz));
done:
ipc_voucher_release(voucher);
return kr;
}
Voici la structure des arguments (contrôlée depuis l'espace utilisateur)
struct mach_voucher_extract_attr_recipe_args { PAD_ARG_(mach_port_name_t, voucher_name); PAD_ARG_(mach_voucher_attr_key_t, key); PAD_ARG_(mach_voucher_attr_raw_recipe_t, recipe); PAD_ARG_(user_addr_t, recipe_size); };
recipe et recipe_size sont des pointeurs espace utilisateur.
Au point (a), quatre octets sont lus depuis le pointeur espace utilisateur recipe_size dans sz.
Au point (b), si sz était inférieur à MACH_VOUCHER_ATTR_MAX_RAW_RECIPE_ARRAY_SIZE (5120) et supérieur à MACH_VOUCHER_TRAP_STACK_LIMIT (256), sz est utilisé pour allouer un tampon sur le tas du noyau.
Au point (c), copyin est appelé à nouveau pour copier la mémoire espace utilisateur dans ce tampon qui vient d'être alloué, mais au lieu de passer sz (la taille validée qui a été allouée), args->recipe_size est passé comme taille. C'est le pointeur espace utilisateur vers la taille, pas la taille !
Cela conduit à un débordement de tas du noyau complètement contrôlé. Notez que le code ne peut en fait pas fonctionner correctement :)
*** l'exploit ***
Je cible les tampons de messages mach pré-alloués qui sont alloués via kalloc. Les 4 premiers octets sont un champ de taille qui est utilisé pour déterminer où lire et écrire un message dans le tampon. En corrompant ce champ, nous pouvons provoquer la lecture et l'écriture de messages mach en dehors des limites de l'allocation kalloc soutenant le kmsg.
Il y a une légère complication : le kmsg pré-alloué d'un port ne sera utilisé que pour les envois réels de mach_msg par le noyau (pas pour les réponses aux méthodes MIG par exemple). Cela rend un peu plus difficile d'obtenir suffisamment de contenu contrôlé dedans.
Un type de message mach que le noyau envoie avec beaucoup de données contrôlées par l'utilisateur est un message d'exception, envoyé lorsqu'un thread plante.
Le fichier load_regs_and_crash.s contient de l'assembleur ARM64 qui charge les registres généraux ARM64 avec le contenu d'un tampon de sorte que lorsqu'il plante, le message d'exception contient ce tampon de données (environ 0x70 octets sont contrôlés).
En écrasant le champ ikm_size du port pour pointer vers l'en-tête d'un autre port, nous pouvons lire et écrire l'en-tête d'un autre port et apprendre où il se trouve en mémoire. Nous pouvons ensuite libérer ce second port et réallouer un user client à sa place que nous pouvons également lire et écrire.
Je lis le pointeur vtable des userclients puis j'utilise la technique du gadget OSSerializer::serialize comme détaillé dans [https://info.lookout.com/rs/051-ESQ-475/images/pegasus-exploits-technical-details.pdf] pour appeler une fonction arbitraire avec deux arguments contrôlés.
J'appelle uuid_copy qui appelle memmove(arg0, arg1, 0x10). En pointant soit arg0 soit arg1 dans le userclient lui-même (que nous pouvons lire en recevant le message d'exception), nous pouvons lire et écrire de la mémoire arbitraire du noyau par blocs de 16 octets.