
CVE-2017-2370 用の Ian Beer のエクスプロイト(iOS 10.2 におけるカーネルメモリの読み書き)
// ianbeer
iOS 10.2 向け CVE-2017-2370 のカーネル任意読み取り/書き込みエクスプロイト
iPod Touch 6G 14C92 でのみテスト済みです。他のデバイス/ファームウェアではそのままでは動作しません!
*** バグ *** mach_voucher_extract_attr_recipe_trap は、任意のコンテキストから呼び出すことができる Mach トラップです。これは 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) {
/* 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;
}
以下が引数構造体です(ユーザー空間から制御されます)。
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 と recipe_size はユーザー空間へのポインターです。
ポイント (a) では、ユーザー空間のポインター recipe_size から 4 バイトが sz に読み込まれます。
ポイント (b) では、sz が MACH_VOUCHER_ATTR_MAX_RAW_RECIPE_ARRAY_SIZE (5120) 未満かつ MACH_VOUCHER_TRAP_STACK_LIMIT (256) より大きい場合、sz を使ってカーネルヒープバッファーが割り当てられます。
ポイント (c) では、copyin が再度呼び出され、割り当てられたばかりのバッファーにユーザー空間のメモリーをコピーしますが、渡されるサイズは sz(割り当て済みの検証済みサイズ)ではなく args->recipe_size です。これはサイズそのものではなく、サイズへのユーザー空間ポインターです!
これにより、完全に制御可能なカーネルヒープオーバーフローが発生します。ちなみに、このコードは実際には正しく動作しません :)
*** エクスプロイト ***
私は kalloc によって割り当てられる、事前割り当て済みの mach メッセージバッファーを標的にします。先頭の 4 バイトはサイズフィールドで、バッファー内のどこでメッセージを読み書きするかを決定するために使われます。このフィールドを破壊することで、kmsg を支える kalloc の割り当て範囲外で mach メッセージが読み書きされるようにできます。
少し厄介な点として、ポートの事前割り当て済み kmsg は、カーネルによる実際の mach_msg 送信にのみ使用されます(たとえば MIG メソッドへの応答には使用されません)。そのため、kmsg に十分な制御されたコンテンツを入れるのが少し難しくなります。
カーネルがユーザー制御データを大量に含めて送信するメッセージの一種に、スレッドがクラッシュしたときに送信される例外メッセージがあります。
load_regs_and_crash.s というファイルには、ARM64 の汎用レジスターにバッファーの内容をロードする ARM64 アセンブリが含まれており、クラッシュ時に例外メッセージにそのデータバッファー(約 0x70 バイトを制御可能)が含まれるようにしています。
ポートの ikm_size フィールドを別のポートのヘッダーを指すように上書きすることで、別のポートのヘッダーを読み書きし、そのメモリー上の位置を知ることができます。その後、その 2 番目のポートを解放し、同じ場所にユーザークライアントを再割り当てして、それも読み書きできるようにします。
私は userclient の vtable ポインターを読み取り、[https://info.lookout.com/rs/051-ESQ-475/images/pegasus-exploits-technical-details.pdf] で詳述されている OSSerializer::serialize ガジェット手法を使って、2 つの制御された引数で任意の関数を呼び出します。
私は uuid_copy を呼び出します。これは memmove(arg0, arg1, 0x10) を呼び出します。arg0 または arg1 のいずれかを userclient 自体(例外メッセージを受信することで読み取れる)に向けることで、任意のカーネルメモリーを 16 バイト単位で読み書きできます。