
en Mac 10.12.2
Esta escalada de privilegios explota una vulnerabilidad en mach_voucher_extract_attr_recipe_trap, y el núcleo del método de explotación consiste en usar mensajes MACH_MSG_OOL_PORTS_DESCRIPTOR.
Respecto a mach_msg ool, en términos simples, cuando se envía un msg que contiene un ool descriptor, el kernel copia los datos especificados del espacio de usuario al espacio del kernel, y el kernel mantiene estos datos hasta que la tarea objetivo procesa el mensaje. De manera similar, cuando el proceso objetivo recibe un mensaje que contiene un ool descriptor, el kernel copia los datos del espacio del kernel al espacio de usuario (no necesariamente una copia real). Por lo tanto, se puede aprovechar esta técnica para escribir datos en el heap del kernel o leer datos desde el kernel.
Dado que la explotación de esta vulnerabilidad es mucho más compleja que la de Tridente, la iré analizando paso a paso, desde el punto de origen de la vulnerabilidad hasta su explotación. El código se puede consultar en mi github
En las nuevas funciones añadidas en iOS 10 y macOS 10.12 hay una llamada mach_voucher_extract_attr_recipe_trap, que es un Mach trap invocable dentro de la sandbox. A continuación se muestra el código fuente de esta función:
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 se escribe en sz.sz está entre MACH_VOUCHER_ATTR_MAX_RAW_RECIPE_ARRAY_SIZE (5120) y MACH_VOUCHER_TRAP_STACK_LIMIT (256), se asigna un búfer en el heap del kernel según el valor de sz.sz (que se usó para asignar el heap del kernel), sino un puntero de espacio de usuario, lo que provoca un desbordamiento de heap. Este es el punto que aprovechamos para el ataque. Además, la función copyin tiene la característica de que se detiene cuando encuentra una página no mapeada (unmap), característica que se aprovechará en nuestro PoC:freelist, no conocemos la posición de los bloques de memoria reasignados.Primero hay que entender el procesamiento de MACH_MSG_OOL_PORTS_DESCRIPTOR en mach msg. Cuando el kernel recibe un mensaje complejo y descubre que es un ports descriptor, lo pasa a la función ipc_kmsg_copyin_ool_ports_descriptor (llamada por ipc_kmsg_copyin) para leer todos los objetos port. Esta función llama a kalloc para asignar la memoria necesaria (en 64 bits el tamaño asignado es el doble de la entrada, la longitud de name es de 4 bytes), luego convierte los port válidos de name a la dirección real del objeto ipc_port y los guarda. Para las entradas name que son MACH_PORT_NULL o MACH_PORT_DEAD, se mantienen sin cambios.
/* 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;
}
...
}
Por lo tanto, durante el ataque, enviamos una gran cantidad de MACH_PORT_DEAD para llenar la región de memoria con 0xFFFFFFFFFFFFFFFF (MACH_PORT_DEAD), luego desencadenamos la vulnerabilidad para modificar uno de los IPC_PORT_DEAD a un área de memoria preparada por el atacante. Si el área apuntada es una estructura ipc port válida, después de recibir el mensaje OOL PORTS, podemos obtener en el espacio de usuario el port name correspondiente a ese ipc_port, para el siguiente paso del ataque.
ipc_objectPrimero ya hemos obtenido este fake port. A continuación, para realizar la filtración de información, debemos saber qué parámetros usa el kernel para procesarlo de manera diferente. Primero observemos la estructura 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__));
Hay un objeto del kernel correspondiente al puerto, y el tipo de objeto del kernel al que corresponde este ipc_port está determinado por los atributos de ipc_object, por lo que en realidad estamos construyendo sobre ipc_object.
fakeport->io_bits = IO_BITS_ACTIVE | IKOT_CLOCK; //设置为IKOT_CLOCK对象,并处于激活状态
fakeport->io_lock_data[12] = 0x11; //设置port锁处于活动状态,防止死锁
El kernel reconocerá este ipc_port como un port para la comunicación con el objeto IKOT_CLOCK. El siguiente objetivo es filtrar la dirección base del kernel: