Apple XNU Kernel - Memory Corruption due to Integer Overflow in __offsetof Usage in posix_spawn on 3

ID 1337DAY-ID-29202
Type zdt
Reporter Google Security Research
Modified 2017-12-12T00:00:00


Exploit for macOS platform in category dos / poc

                                            is a pointer to a further arguments descriptor in userspace with the following structure (on 32-bit): 
   struct user32__posix_spawn_args_desc { 
     uint32_t  attr_size;  /* size of attributes block */ 
     uint32_t  attrp;    /* pointer to block */ 
     uint32_t  file_actions_size;  /* size of file actions block */ 
     uint32_t  file_actions;  /* pointer to block */ 
     uint32_t  port_actions_size;  /* size of port actions block */ 
     uint32_t  port_actions;  /* pointer to block */ 
     uint32_t  mac_extensions_size; 
     uint32_t  mac_extensions; 
     uint32_t  coal_info_size; 
     uint32_t  coal_info; 
     uint32_t  persona_info_size; 
     uint32_t  persona_info; 
 port_actions then points to another structure in userspace of this type: 
   struct _posix_spawn_port_actions { 
     int      pspa_alloc; 
     int      pspa_count; 
     _ps_port_action_t   pspa_actions[]; 
 and finally _ps_port_action_t looks like this: 
   struct _ps_port_action { 
     pspa_t      port_type; 
     exception_mask_t  mask; 
     mach_port_name_t  new_port; 
     exception_behavior_t  behavior; 
     thread_state_flavor_t  flavor; 
     int      which; 
 Note that pspa_actions is a zero-sized array. pspa_count is supposed to be the number of entries 
 in this array. 
 The following constraints are checked in posix_spawn in kern_exec.c: 
   if (px_args.port_actions_size != 0) { 
     /* Limit port_actions to one page of data */ 
     if (px_args.port_actions_size < PS_PORT_ACTIONS_SIZE(1) || 
         px_args.port_actions_size > PAGE_SIZE) { 
       error = EINVAL; 
       goto bad; 
 PS_PORT_ACTIONS_SIZE is defined like this: 
   #define  PS_PORT_ACTIONS_SIZE(x)  \ 
     __offsetof(struct _posix_spawn_port_actions, pspa_actions[(x)]) 
 if port_actions_size passes this then we reach the following code: 
   MALLOC(px_spap, _posix_spawn_port_actions_t, 
         px_args.port_actions_size, M_TEMP, M_WAITOK); 
   if (px_spap == NULL) { 
     error = ENOMEM; 
     goto bad; 
   imgp->ip_px_spa = px_spap; 
   if ((error = copyin(px_args.port_actions, px_spap, 
                       px_args.port_actions_size)) != 0) 
     goto bad; 
 This allocates a kernel heap buffer to hold the port_actions buffer and copies from userspace into it. 
 The code then attempts to check whether the pspa_count valid is correct: 
   /* Verify that the action count matches the struct size */ 
   if (PS_PORT_ACTIONS_SIZE(px_spap->pspa_count) != px_args.port_actions_size) { 
     error = EINVAL; 
     goto bad; 
 There is an integer overflow here because offsetof is just simple arithmetic. With a carefully chosen 
 value for pspa_count we can make it very large but when it's passed to the PS_PORT_ACTIONS_SIZE macro 
 the result is equal to port_actions_size. Nothing bad has happened yet but we can now get pspa_count 
 to be much larger than it should be. 
 Later on we reach the following code: 
   if (px_spap->pspa_count != 0 && is_adaptive) { 
     portwatch_count = px_spap->pspa_count; 
     MALLOC(portwatch_ports, ipc_port_t *, (sizeof(ipc_port_t) * portwatch_count), M_TEMP, M_WAITOK | M_ZERO); 
   } else { 
     portwatch_ports = NULL; 
   if ((error = exec_handle_port_actions(imgp, &portwatch_present, portwatch_ports)) != 0) 
 We can cause another integer overflow here, sizeof(ipc_port_t) is 4 (on 32-bit) so with a carefully chosen value of pspa_count 
 we can cause the integer overflow here and earlier too whilst still passing the checks. 
 exec_handle_port_actions then uses portwatch ports like this: 
   for (i = 0; i < pacts->pspa_count; i++) { 
     act = &pacts->pspa_actions[i]; 
     if (MACH_PORT_VALID(act->new_port)) { 
       kr = ipc_object_copyin(get_task_ipcspace(current_task()), 
                              act->new_port, MACH_MSG_TYPE_COPY_SEND, 
                              (ipc_object_t *) &port); 
       switch (act->port_type) { 
         if (portwatch_ports != NULL && IPC_PORT_VALID(port)) { 
          *portwatch_present = TRUE; 
         /* hold on to this till end of spawn */ 
         portwatch_ports[i] = port; 
 note that pspa_actions was allocated earlier also based on the result of an integer overflow. 
 This means we can cause an OOB write to portwatch_ports only if we can successfully read suitable valid 
 values OOB of pspa_actions. That's why this PoC first fills a kalloc.1024 buffer with suitable values before 
 freeing it and then hoping that it will get reallocated as pspa_actions (but less thatn 1024 bytes will be written) 
 such that we control what's read OOB and the ipc_object_copyin will succeed. 
 This seems to be pretty reliable. You can use this to build a nice primitive of a heap overflow with pointers 
 to ipc_port structures. 
 I don't believe there are any iOS 11 32-bit iPod/iPhone/iPad/AppleTV devices but the new Apple Watch Series 3 
 is running essentially the same kernel but has a 32-bit CPU. This PoC is provided as an Apple watch app 
 and has been tested on Apple Watch Series 3 (Watch3,2) running WatchOS 4.0.1. I also tested on an older 32-bit iOS 9 device. 
 Apple Watch Series 3 now has its own LTE modem and can be used without an iPhone making it a suitably interesting target for exploitation 
 by itself. 
 Note that all the uses of offsetof in those posix_spawn macros are quite wrong, I think you might be able to get 
 a kernel memory disclosure with one of them also on 64-bit platforms. The fix is to add correct bounds checking. 
 Please also note that this really shouldn't be attack surface reachable from an app sandbox. The MAC hook in posix_spawn 
 is very late and there's a *lot* of code which you can hit before it. 
 Proof of Concept:

# [2018-02-09]  #