
Ian Beer의 CVE-2017-2370 익스플로잇 (iOS 10.2 커널 메모리 r/w)
// 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) 지점에서 userspace 포인터 recipe_size에서 sz로 4바이트가 읽힙니다.
(b) 지점에서 sz가 MACH_VOUCHER_ATTR_MAX_RAW_RECIPE_ARRAY_SIZE(5120)보다 작고 MACH_VOUCHER_TRAP_STACK_LIMIT(256)보다 크면 sz를 사용하여 커널 힙 버퍼를 할당합니다.
(c) 지점에서 copyin이 다시 호출되어 방금 할당된 버퍼로 사용자 공간 메모리를 복사하는데, 할당된 검증 크기인 sz를 전달하는 대신 args->recipe_size가 크기로 전달됩니다. 이것은 크기를 가리키는 userspace 포인터이지 크기 자체가 아닙니다!
이로 인해 완전히 제어 가능한 커널 힙 오버플로가 발생합니다. 참고로 이 코드는 실제로는 제대로 동작할 수 없습니다 :)
*** 익스플로잇 ***
저는 kalloc으로 할당되는 사전 할당된 mach 메시지 버퍼를 대상으로 삼습니다. 처음 4바이트는 메시지를 버퍼의 어디에서 읽고 쓸지 결정하는 데 사용되는 크기 필드입니다. 이 필드를 변조하면 kmsg를 뒷받침하는 kalloc 할당 경계를 벗어나 mach 메시지를 읽고 쓸 수 있습니다.
한 가지 약간의 복잡한 점은 포트의 사전 할당된 kmsg가 커널의 실제 mach_msg 전송에만 사용된다는 것입니다(예를 들어 MIG 메서드에 대한 응답에는 사용되지 않음). 따라서 그 안에 충분한 제어된 콘텐츠를 넣기가 조금 더 까다로워집니다.
커널이 사용자 제어 데이터를 많이 포함하여 보내는 mach 메시지 유형 중 하나는 스레드가 크래시할 때 전송되는 예외 메시지입니다.
load_regs_and_crash.s 파일에는 ARM64 범용 레지스터에 버퍼의 내용을 로드하는 ARM64 어셈블리가 포함되어 있어, 크래시가 발생하면 예외 메시지에 해당 데이터 버퍼가 포함됩니다(약 0x70바이트가 제어됨).
포트의 ikm_size 필드를 다른 포트의 헤더를 가리키도록 덮어쓰면 다른 포트의 헤더를 읽고 쓸 수 있으며 메모리에서 그 위치를 알아낼 수 있습니다. 그런 다음 두 번째 포트를 해제하고 그 자리에 사용자 클라이언트를 재할당하여 역시 읽고 쓸 수 있습니다.
저는 userclients의 vtable 포인터를 읽은 다음, [https://info.lookout.com/rs/051-ESQ-475/images/pegasus-exploits-technical-details.pdf]에 자세히 설명된 OSSerializer::serialize 가젯 기법을 사용하여 두 개의 제어된 인자로 임의 함수를 호출합니다.
저는 uuid_copy를 호출하는데, 이 함수는 memmove(arg0, arg1, 0x10)를 호출합니다. arg0 또는 arg1을 userclient 자체를 가리키도록 지정하면(예외 메시지를 수신하여 읽을 수 있음) 16바이트 단위로 임의의 커널 메모리를 읽고 쓸 수 있습니다.