
no Mac 10.12.2
Este escalonamento de privilégios explora uma vulnerabilidade em mach_voucher_extract_attr_recipe_trap, e o núcleo do método de exploração é através de mensagens MACH_MSG_OOL_PORTS_DESCRIPTOR.
Sobre mach_msg ool, em termos simples, quando um msg contendo um ool descriptor é enviado, o kernel copia os dados especificados do espaço do usuário para o espaço do kernel, e o kernel mantém esses dados até que a tarefa de destino processe a mensagem. Da mesma forma, quando o processo de destino recebe uma mensagem contendo um ool descriptor, o kernel copia os dados do espaço do kernel para o espaço do usuário (não necessariamente uma cópia real). Portanto, essa técnica pode ser usada para escrever dados no heap do kernel ou ler dados do kernel.
Como a exploração desta vulnerabilidade é muito mais complexa que a do Tridente, vou analisá-la passo a passo, desde o ponto de origem até a exploração completa. O código pode ser consultado no meu github.
Em iOS 10 e macOS 10.12, uma nova função chamada mach_voucher_extract_attr_recipe_trap foi adicionada. É um Mach trap que pode ser chamado dentro do sandbox. Abaixo está o código-fonte desta função:
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 é copiado para sz.sz estiver entre MACH_VOUCHER_ATTR_MAX_RAW_RECIPE_ARRAY_SIZE (5120) e MACH_VOUCHER_TRAP_STACK_LIMIT (256), um buffer do heap do kernel será alocado de acordo com o valor de sz.sz usado para alocar o heap do kernel, e sim um ponteiro de espaço do usuário, causando um estouro de heap. É este ponto que exploramos. Além disso, a função copyin tem uma característica: se encontrar uma página não mapeada, ela interrompe a cópia. Esta característica será explorada em nosso PoC:Primeiro, é necessário entender o tratamento de MACH_MSG_OOL_PORTS_DESCRIPTOR em mach msg. Quando o kernel recebe uma mensagem complexa e descobre que é ports descriptor, ele chama a função ipc_kmsg_copyin_ool_ports_descriptor (chamada por ipc_kmsg_copyin) para ler todos os objetos port. Esta função chama kalloc para alocar a memória necessária (em 64 bits, a memória alocada é o dobro da entrada, os nomes têm 4 bytes), e então converte os port válidos de name para o endereço real do objeto ipc_port, salvando-os. Para entradas com name igual a MACH_PORT_NULL ou MACH_PORT_DEAD, eles permanecem inalterados.
/* 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;
}
...
}
Portanto, durante o ataque, enviaremos um grande número de MACH_PORT_DEAD para preencher a área de memória com 0xFFFFFFFFFFFFFFFF (MACH_PORT_DEAD), e então acionaremos a vulnerabilidade para modificar um desses IPC_PORT_DEAD para uma área de memória preparada pelo atacante. Se a área apontada for uma estrutura ipc port válida, após receber a mensagem OOL PORTS, será possível obter no espaço do usuário o port name correspondente a esse ipc_port, para prosseguir com o próximo passo do ataque.
ipc_objectPrimeiro, já obtivemos este fake port. Para realizar a fuga de informação, precisamos saber como o kernel o trata com base em seus parâmetros. Primeiro, vejamos a estrutura 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__));
Há um objeto do kernel correspondente ao port, e o tipo de objeto do kernel ao qual este ipc_port corresponde é determinado pelas propriedades de ipc_object. Portanto, estamos na verdade construindo em torno de ipc_object.
fakeport->io_bits = IO_BITS_ACTIVE | IKOT_CLOCK; //设置为IKOT_CLOCK对象,并处于激活状态
fakeport->io_lock_data[12] = 0x11; //设置port锁处于活动状态,防止死锁
O kernel tratará este ipc_port como um port para comunicação com o objeto IKOT_CLOCK. O próximo objetivo é vazar o endereço base do kernel: