Jump to content
  • Entries

    16114
  • Comments

    7952
  • Views

    86386536

Contributors to this blog

  • HireHackking 16114

About this blog

Hacking techniques include penetration testing, network security, reverse cracking, malware analysis, vulnerability exploitation, encryption cracking, social engineering, etc., used to identify and fix security flaws in systems.

Source: https://bugs.chromium.org/p/project-zero/issues/detail?id=882

mach_ports_register is a kernel task port MIG method.

It's defined in MIG like this:

  routine mach_ports_register(
      target_task : task_t;
      init_port_set : mach_port_array_t =
            ^array[] of mach_port_t);

Looking at the generated code for this we notice something kinda weird; here's the mach message structure
which actually gets sent:

  typedef struct {
    mach_msg_header_t Head;
    // start of the kernel processed data
    mach_msg_body_t msgh_body;
    mach_msg_ool_ports_descriptor_t init_port_set;
    // end of the kernel processed data
    NDR_record_t NDR;
    mach_msg_type_number_t init_port_setCnt;
  } Request __attribute__((unused));

The message contains an OOL ports descriptor, which is expected, but also contains a separate init_port_setCnt value
even though the ool_ports_descriptor_t already has the correct length of the descriptor.

When the kernel process this ool ports descriptor in ipc_kmsg_copyin_ool_ports_descriptor it will kalloc a buffer large enough
for all the ports and then copyin and convert them all. It does this using the init_port_set.count value, not init_port_setCnt.

The generated MIG code however calls mach_ports_register like this:

  OutP->RetCode = mach_ports_register(target_task, (mach_port_array_t)(In0P->init_port_set.address), In0P->init_port_setCnt);

without verifying that In0P->init_port_setCnt is equal to init_port_set.count.

This means that when we reach mach_ports_register lots of stuff goes wrong:

  kern_return_t
  mach_ports_register(
    task_t      task,
    mach_port_array_t memory,                       <-- points to kalloc'ed buffer
    mach_msg_type_number_t  portsCnt)               <-- completely controlled, not related to size of kalloc'ed buffer
  {
    ipc_port_t ports[TASK_PORT_REGISTER_MAX];
    unsigned int i;

    if ((task == TASK_NULL) ||
        (portsCnt > TASK_PORT_REGISTER_MAX) ||
        (portsCnt && memory == NULL))
      return KERN_INVALID_ARGUMENT;                 <-- portsCnt must be >=1 && <= 3

    for (i = 0; i < portsCnt; i++)
      ports[i] = memory[i];                         <-- if we only sent one OOL port but set portsCnt >1 this will read a mach_port_t (a pointer) out of bounds
    for (; i < TASK_PORT_REGISTER_MAX; i++)
      ports[i] = IP_NULL;

    itk_lock(task);
    if (task->itk_self == IP_NULL) {
      itk_unlock(task);
      return KERN_INVALID_ARGUMENT;
    }

    for (i = 0; i < TASK_PORT_REGISTER_MAX; i++) {
      ipc_port_t old;

      old = task->itk_registered[i];
      task->itk_registered[i] = ports[i];
      ports[i] = old;
    }

    itk_unlock(task);

    for (i = 0; i < TASK_PORT_REGISTER_MAX; i++)
      if (IP_VALID(ports[i]))
        ipc_port_release_send(ports[i]);           <-- this can decrement the ref on a pointer which was read out of bounds if we call this function multiple times

    if (portsCnt != 0)
      kfree(memory,
            (vm_size_t) (portsCnt * sizeof(mach_port_t)));   <-- this can call kfree with the wrong size

    return KERN_SUCCESS;
  }

For this PoC I've patched the MIG generated code to always only send one OOL mach port but still set init_port_setCnt to a controlled value - you should see a kernel
panic decrementing an invalid reference or something like that.

This bug however could be exploited quite nicely to cause a mach_port_t UaF which could have all kinds of fun consequences (getting another task's task port for example!)

tested on OS X 10.11.6 (15G31) on MacBookPro10,1


Proof of Concept:
https://gitlab.com/exploit-database/exploitdb-bin-sploits/-/raw/main/bin-sploits/40654.zip