
Exploit de Ian Beer para CVE-2017-2370 (lectura/escritura de memoria del kernel en iOS 10.2)
// ianbeer
Exploit arbitrario de lectura/escritura del kernel para CVE-2017-2370 en iOS 10.2
Solo probado en iPod Touch 6G 14C92 - ¡otros dispositivos/firmwares no funcionarán tal cual!
*** el bug *** mach_voucher_extract_attr_recipe_trap es una trampa mach que puede ser llamada desde cualquier contexto. Es código completamente nuevo, añadido en 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) {
/* mantener recetas pequeñas en la pila por velocidad */
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;
}
Aquí está la estructura de argumentos (controlada desde espacio de usuario)
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 y recipe_size son punteros de espacio de usuario.
En el punto (a) se leen cuatro bytes del puntero de espacio de usuario recipe_size en sz.
En el punto (b) si sz era menor que MACH_VOUCHER_ATTR_MAX_RAW_RECIPE_ARRAY_SIZE (5120) y mayor que MACH_VOUCHER_TRAP_STACK_LIMIT (256) sz se utiliza para asignar un búfer del montón del kernel.
En el punto (c) se llama a copyin nuevamente para copiar memoria de espacio de usuario en ese búfer que acaba de ser asignado, pero en lugar de pasar sz (el tamaño validado que fue asignado) se pasa args->recipe_size como el tamaño. ¡Esto es el puntero de espacio de usuario al tamaño, no el tamaño!
Esto conduce a un desbordamiento del montón del kernel completamente controlado. Nótese que el código realmente no puede funcionar correctamente :)
*** el exploit ***
Apunto a búferes de mensajes mach preasignados que se asignan mediante kalloc. Los primeros 4 bytes son un campo de tamaño que se usa para determinar dónde en el búfer leer y escribir un mensaje. Al corromper este campo podemos hacer que los mensajes mach se lean y escriban fuera de los límites de la asignación kalloc que respalda el kmsg.
Hay una ligera complicación en que el kmsg preasignado de un puerto solo se usará para envíos reales de mach_msg por parte del kernel (no para respuestas a métodos MIG, por ejemplo.) Esto hace que sea un poco más complicado obtener suficiente contenido controlado en ellos.
Un tipo de mensaje mach que el kernel envía con muchos datos controlados por el usuario es un mensaje de excepción, enviado cuando un hilo se bloquea.
El archivo load_regs_and_crash.s contiene código ARM64 que carga los registros de propósito general ARM64 con el contenido de un búfer de tal manera que cuando se bloquea el mensaje de excepción contiene ese búfer de datos (aproximadamente 0x70 bytes están controlados).
Al sobrescribir el campo ikm_size del puerto para que apunte al encabezado de otro puerto, podemos leer y escribir el encabezado de otro puerto y aprender dónde está en memoria. Luego podemos liberar ese segundo puerto y reasignar un user client en su lugar que también podemos leer y escribir.
Leo el puntero vtable del user client y luego uso la técnica del gadget OSSerializer::serialize como se detalla en [https://info.lookout.com/rs/051-ESQ-475/images/pegasus-exploits-technical-details.pdf] para llamar a una función arbitraria con dos argumentos controlados.
Llamo a uuid_copy que llama a memmove(arg0, arg1, 0x10). Al apuntar arg0 o arg1 al propio user client (que podemos leer recibiendo el mensaje de excepción) podemos leer y escribir memoria arbitraria del kernel en fragmentos de 16 bytes.