
sur Mac 10.12.2
Cette élévation de privilèges exploite une vulnérabilité présente dans mach_voucher_extract_attr_recipe_trap, et le cœur de la méthode d'exploitation consiste à utiliser les messages MACH_MSG_OOL_PORTS_DESCRIPTOR.
En ce qui concerne mach_msg ool, en bref, lorsqu'un message contenant un ool descriptor est envoyé, le noyau copie les données spécifiées de l'espace utilisateur vers l'espace noyau et les conserve jusqu'à ce que la tâche destinataire ait traité le message. De même, lorsqu'un processus cible reçoit un message contenant un ool descriptor, le noyau copie les données de l'espace noyau vers l'espace utilisateur (pas nécessairement une véritable copie). Cette technique peut donc être utilisée pour écrire des données dans le tas du noyau ou pour lire des données depuis le noyau.
L'exploitation de cette vulnérabilité étant bien plus complexe que celle de Trident, je vais l'analyser pas à pas, du point d'origine de la vulnérabilité jusqu'à son exploitation. Le code est disponible sur mon github.
Parmi les nouvelles fonctionnalités ajoutées dans iOS 10 et macOS 10.12 se trouve une fonction appelée mach_voucher_extract_attr_recipe_trap, un Mach trap pouvant être appelé depuis la sandbox. Voici le code source de cette fonction :
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;
//将recipe_size的地址拷贝到sz中,此时sz存放的就是kalloc_size的值了
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;
}
args->recipe_size est écrit dans sz.sz est comprise entre MACH_VOUCHER_ATTR_MAX_RAW_RECIPE_ARRAY_SIZE (5120) et MACH_VOUCHER_TRAP_STACK_LIMIT (256), un tampon du tas du noyau est alloué selon la valeur de sz.sz utilisé pour l'allocation du tas du noyau, mais un pointeur d'espace utilisateur. Cela provoque donc un débordement de tas, et c'est exactement ce point que nous exploitons. De plus, la fonction copyin a la particularité de s'arrêter de copier lorsqu'elle rencontre une page non mappée (unmap), particularité qui sera exploitée dans notre poc :freelist, nous ne connaissons plus la position des blocs de mémoire réalloués.Il faut d'abord comprendre comment mach msg traite MACH_MSG_OOL_PORTS_DESCRIPTOR. Lorsque le noyau reçoit un message complexe et détecte un ports descriptor, il le confie à la fonction ipc_kmsg_copyin_ool_ports_descriptor (appelée par ipc_kmsg_copyin) qui lit tous les objets port. Cette fonction appelle kalloc pour allouer la mémoire nécessaire (sur 64 bits, la mémoire allouée est le double de l'entrée, la longueur de name étant de 4 octets). Elle convertit ensuite chaque port valide, depuis son name, en adresse réelle de l'objet ipc_port et la stocke. Pour un name dont la valeur est MACH_PORT_NULL或者MACH_PORT_DEAD, elle reste inchangée.
/* calculate length of data in bytes, rounding up */
if (os_mul_overflow(count, sizeof(mach_port_t), &ports_length)) {
*mr = MACH_SEND_TOO_LARGE;
return NULL;
}
if (os_mul_overflow(count, sizeof(mach_port_name_t), &names_length)) {
*mr = MACH_SEND_TOO_LARGE;
return NULL;
}
if(ports_length == 0){
return user_desc;
}
data = kalloc(ports_length); // 分配空间
...
objects = (ipc_object_t *) data;
dsc->address = data;
for ( i = 0; i < count; i++) {
mach_port_name_t name = names[i];
ipc_object_t object;
if (!MACH_PORT_VALID(name)) {
objects[i] = (ipc_object_t)CAST_MACH_NAME_TO_PORT(name);// IPC_PORT_DEAD continue;
}
...
}
Ainsi, lors de l'attaque, nous envoyons un grand nombre de MACH_PORT_DEAD pour remplir la zone mémoire avec 0xFFFFFFFFFFFFFFFF (MACH_PORT_DEAD), puis nous déclenchons la vulnérabilité pour remplacer l'un des IPC_PORT_DEAD par une zone mémoire préparée par l'attaquant. Si la zone pointée est une structure ipc port légitime, alors après la réception du message OOL PORTS, nous pouvons obtenir dans l'espace utilisateur le port name correspondant à cet ipc_port, et passer à l'étape suivante.
ipc_objectTout d'abord, nous avons déjà obtenu ce fake port. Pour réaliser la fuite d'informations, il est nécessaire de savoir selon quels paramètres le noyau le traite différemment. Regardons d'abord la structure de ipc_port :
struct ipc_port {
//ipc_object的指针就在前八个字节,是我们溢出攻击的对象
struct ipc_object ip_object; // port对象的类型 struct ipc_mqueue,ip_messages;
struct ipc_mqueue ip_messages; //消息队列
union {
struct ipc_space *receiver;
struct ipc_port *destination;
ipc_port_timestamp_t timestamp;
}data;
union {
ipc_importance_task_t imp_task;
ipc_kobject_t kobject; // port对应的内核对象
uintptr_t alias;
}kdata;
...
} __attribute__((__packed__));
Parmi ces champs se trouve l'objet noyau correspondant au port. Le type d'objet noyau auquel cet ipc_port correspond est quant à lui déterminé par les attributs de ipc_object. Nous construisons donc en réalité un ipc_object.
fakeport->io_bits = IO_BITS_ACTIVE | IKOT_CLOCK; //设置为IKOT_CLOCK对象,并处于激活状态
fakeport->io_lock_data[12] = 0x11; //设置port锁处于活动状态,防止死锁
Le noyau considérera alors cet ipc_port comme un port utilisé pour communiquer avec un objet IKOT_CLOCK. L'objectif suivant est de fuiter l'adresse de base du noyau :