mach_port_kobject() and the kernel address obfuscation

The mach_port_kobject() API function in iOS 8.1.2 and OSX 10.10 can be used to defeat the kernel address obfuscation mitigation.

Introduction

/images/iosobfuscation.jpg

For a few years now SektionEins has been performing OS X and iOS security research on application and kernel level. We have shared our discoveries with the public during numerous talks at various security conferences. Since 2013 we have also organized a number of iOS security trainings around the globe, where we teach the students everything there is to know to get started with iOS security research at the kernel and/or application level. In 2015 we are planning to extend these trainings to the OS X platform as well.

During our trainings one important topic is a discussion of the various security mitigations that Apple started to introduce into the iOS kernel with iOS 6 and has since then improved throughout iOS 7 and iOS 8 (respective OS X Mavericks and OS X Yosemite). We critically analyse these mitigations and discuss not only how they function but also their shortcomings. One of these mitigations is the obfuscation of kernel addresses when they are returned by kernel API functiont to user land processes. This mitigation has been repeatedly under attack by previous work by Esser and Mandt.

Both these weaknesses have been part of our training program for a while now, but aside from these public problems we also discussed a lesser known (or unknown) problem with the mach_port_kobject() API function that renders the whole kernel address obfuscation mitigation completely worthless. The trick we will discuss in this blog posting has also been abused in the wild in June 2014 by the PANGU jailbreak, but Apple did not analyse it correctly or misunderstood it, so that an attempt to fix it for iOS 8 and OS X Yosemite failed. Because of Apple's failure to fix the vulnerability correctly, the trick was then re-used in the latest iOS 8.1.1 and iOS 8.1.2 jailbreaks.

Because of this we decided to explain the problem, Apple's fix and why it failed to close the vulnerability.

Upcoming Trainings

  • OS X Kernel Internals for Security Researchers Training at SyScan 2015, Singapore

  • iOS Kernel Exploitation Training in Frankfurt, Germany

  • iOS/OS X Kernel Exploitation/Internals Training at YOUR COMPANY

    • Dates: 4 or 5 Days

    • Location: At your company

    • Instructor: Stefan Esser

    • Language: Deutsch / English

    • Capacity: depends on you

    • More Info: Ask us for potential dates, locations and prices training@sektioneins.de

Kernel Address Obfuscation

Within the XNU kernel that is used in iOS and OS X a number of kernel API functions return kernel space addresses back to user-space programs. Because this provides attackers with easy access to kernel pointers and also endangers the KASLR mitigation in Mountain Lion and iOS 6 Apple started to either kill these information leaks by removing API functions, by nullifying them or by obfuscating their value before returning them back to user-space. This is implemented as a macro that uses a simple addition of a secret random value to obfuscate the kernel addresses. The obfuscation macro is called VM_KERNEL_ADDRPERM, which is defined in the XNU source code in the file /osfmk/mach/vm_param.h:

#define VM_KERNEL_ADDRPERM(_v)           \
    (((vm_offset_t)(_v) == 0) ?          \
      (vm_offset_t)(0) :                 \
      (vm_offset_t)(_v) + vm_kernel_addrperm)

The actual random value for VM_KERNEL_ADDRPERM is generated inside kernel_bootstrap_thread in the file /osfmk/kern/startup.c and looks like this in iOS <= 7.x:

/*
 * Initialize the global used for permuting kernel
 * addresses that may be exported to userland as tokens
 * using VM_KERNEL_ADDRPERM(). Force the random number
 * to be odd to avoid mapping a non-zero
 * word-aligned address to zero via addition. */
vm_kernel_addrperm = (vm_offset_t)early_random() | 1;

With the release of iOS 8 and OS X Yosemite Apple changed this code to no longer rely on the early_random() random generator and instead use the cryptographically secure PRNG. This is most likely in response to Tarjei Mandt's research into the insecurities of the early_random() random number generator, which he described in his paper Attacking the iOS 7 early_random() PRNG. The new code therefore looks like this:

/*
 * Initialize the global used for permuting kernel
 * addresses that may be exported to userland as tokens
 * using VM_KERNEL_ADDRPERM(). Force the random number
 * to be odd to avoid mapping a non-zero
 * word-aligned address to zero via addition.
 * Note: at this stage we can use the cryptographically secure PRNG
 * rather than early_random().
 */
read_random(&vm_kernel_addrperm, sizeof(vm_kernel_addrperm));
vm_kernel_addrperm |= 1;

Regardless of how good the actual random number is, from looking at the implementation of the obfuscation it should be obvious that if it is possible for us to get both the obfuscated version of an address and its de-obfuscated version, then we can easily calculate the value of VM_KERNEL_ADDRPERM with a simple substraction of both values. And because the kernel uses the same obfuscation everywhere this basically means we can de-obfuscate all the kernel pointers returned after knowing the single secret.

ADDRESS + SECRET = OBFUSCATED ADDRESS
SECRET = OBFUSCATED ADDRESS - ADDRESS

During SyScan 2013 one such problem was shown by Esser in Kernel Information Leak in pipe() Address Obfuscation. The OS X kernel allowed to get the heap address of a kernel PIPE structure via two different system calls and only one of them applied the obfuscation. Back then this problem only affected OS X, because it was already fixed in iOS.

mach_port_kobject()

Within the XNU kernel there exists a mach API function called mach_port_kobject() that returns the kobject associated with a mach port back into user-space. This function is defined in the file /osfmk/ipc/mach_debug.c. Prior to iOS 6 and OS X Mountain Lion this function was implemented like this:

kern_return_t
mach_port_kobject(
        ipc_space_t                     space,
        mach_port_name_t                name,
        natural_t                       *typep,
        mach_vm_address_t               *addrp)
{
        ...

        port = (ipc_port_t) entry->ie_object;
        assert(port != IP_NULL);

        ip_lock(port);
        is_read_unlock(space);

        if (!ip_active(port)) {
                ip_unlock(port);
                return KERN_INVALID_RIGHT;
        }

        *typep = (unsigned int) ip_kotype(port);
        *addrp = (mach_vm_address_t)port->ip_kobject;
        ip_unlock(port);
        return KERN_SUCCESS;

}

As you can see the value of ip_kobject is directly returned to user-space by this API. Now let us check where this value initially comes from. By searching for the string ip_kobject we can find that it is assigned by calling the functions ipc_kobject_set_atomically()/ipc_kobject_set(), which are both defined in the file /osfmk/kern/ipc_koject.c:

void
ipc_kobject_set(
        ipc_port_t                      port,
        ipc_kobject_t           kobject,
        ipc_kobject_type_t      type)
{
        ip_lock(port);
        ipc_kobject_set_atomically(port, kobject, type);

#if CONFIG_MACF_MACH
        mac_port_label_update_kobject (&port->ip_label, type);
#endif

        ip_unlock(port);
}

void
ipc_kobject_set_atomically(
        ipc_port_t                      port,
        ipc_kobject_t           kobject,
        ipc_kobject_type_t      type)
{
        assert(type == IKOT_NONE || ip_active(port));
#if     MACH_ASSERT
        port->ip_spares[2] = (port->ip_bits & IO_BITS_KOTYPE);
#endif  /* MACH_ASSERT */
        port->ip_bits = (port->ip_bits &~ IO_BITS_KOTYPE) | type;
        port->ip_kobject = kobject;
}

As you can see this is a simple assignment and we have to look into who calls these functions. One such call for example is within labelh_new_user which is defined in the file /osfmk/ipc/ipc_labelh.c:

kern_return_t
labelh_new_user(ipc_space_t space, struct label *inl, mach_port_name_t *namep)
{
        ...
        /* Allocate new label handle, insert port and label. */
        lh = (ipc_labelh_t)zalloc(ipc_labelh_zone);
        lh_lock_init(lh);
        lh->lh_port = port;
        lh->lh_label = *inl;
        lh->lh_type = LABELH_TYPE_USER;
        lh->lh_references = 1;          /* unused for LABELH_TYPE_USER */

        /* Must call ipc_kobject_set() with port unlocked. */
        ip_unlock(lh->lh_port);
        ipc_kobject_set(lh->lh_port, (ipc_kobject_t)lh, IKOT_LABELH);

        return (KERN_SUCCESS);
}

As you can see the value for ip_kobject is in this case a kernel heap address allocated via the zalloc() zone heap allocator. If you backtrace other such calls you will also find that direct pointers to kernel IOKit C++ objects are returned, which contain the usual vtables, who just wait to be overwritten to allow arbitrary code execution in kernel space. Because of this, starting with iOS 6 and OS X Mountain Lion Apple protects this API function by using the obfuscation macros, as you can see here:

kern_return_t
mach_port_kobject(
        ipc_space_t                     space,
        mach_port_name_t                name,
        natural_t                       *typep,
        mach_vm_address_t               *addrp)
{
        ...
        *typep = (unsigned int) ip_kotype(port);
        kaddr = (mach_vm_address_t)port->ip_kobject;
        ip_unlock(port);

        if (0 != kaddr && is_ipc_kobject(*typep))
                *addrp = VM_KERNEL_ADDRPERM(VM_KERNEL_UNSLIDE(kaddr));
        else
                *addrp = 0;

        return KERN_SUCCESS;
}

As you can see Apple is now protecting the kernel address by using the VM_KERNEL_ADDRPERM obfuscation macro. You can furthermore see that Apple also used the VM_KERNEL_UNSLIDE macro to additionally protect against potential KASLR leaks. This is however also a weakness as we will discuss in the next chapter.

A First Attack

As we have seen before Apple uses the VM_KERNEL_UNSLIDE macro inside mach_port_kobject(). This raises the question why would they do that? So far we have only seen mach_port_kobject() returning kernel heap addresses and they would never been unlid as you can see from the definition of the macro in /osfmk/mach/vm_param.h:

#define VM_KERNEL_IS_SLID(_o)                                                  \
                (((vm_offset_t)(_o) >= vm_kernel_base) &&                      \
                 ((vm_offset_t)(_o) <  vm_kernel_top))
#define VM_KERNEL_IS_KEXT(_o)                                                  \
                (((vm_offset_t)(_o) >= vm_kext_base) &&                        \
                 ((vm_offset_t)(_o) <  vm_kext_top))
#define VM_KERNEL_UNSLIDE(_v)                                                  \
                ((VM_KERNEL_IS_SLID(_v) ||                                     \
                  VM_KERNEL_IS_KEXT(_v)) ?                                     \
                        (vm_offset_t)(_v) - vm_kernel_slide :                  \
                        (vm_offset_t)(_v))

The VM_KERNEL_UNSLIDE macro will only unslide addresses if it believes them to be within the main kernel binary or within the area of kernel extensions. But can mach_port_kobject() ever return such an address? The answer is easily found by backtracing again calls to ipc_kobject_set() and selecting another one. This time the backtrace takes us back into the function ipc_host_init() which is defined in the file /osfmk/kern/ipc_host.c:

void ipc_host_init(void)
{
        ...
        ipc_kobject_set(port, (ipc_kobject_t) &realhost, IKOT_HOST_SECURITY);
        kernel_set_special_port(&realhost, HOST_SECURITY_PORT,
                                ipc_port_make_send(port));

        port = ipc_port_alloc_kernel();
        if (port == IP_NULL)
                panic("ipc_host_init");

        ipc_kobject_set(port, (ipc_kobject_t) &realhost, IKOT_HOST);
        kernel_set_special_port(&realhost, HOST_PORT,
                                ipc_port_make_send(port));

        port = ipc_port_alloc_kernel();
        if (port == IP_NULL)
                panic("ipc_host_init");

        ipc_kobject_set(port, (ipc_kobject_t) &realhost, IKOT_HOST_PRIV);
        kernel_set_special_port(&realhost, HOST_PRIV_PORT,
                                ipc_port_make_send(port));
        ...
}

As you can see from this code for HOST mach ports the ip_kobject pointer is filled with the address of realhost, which is a data structure in the kernel's __DATA segment. This means if you call the mach_port_kobject() function on a mach port of type IKOT_HOST it will first unslide the current address of realhost and then obfuscate it and give it back to us.

This fact breaks the whole security of the obfuscation, because the address of realhost in the kernel's __DATA segment is a static address that does not change for one specific kernel. This means we only need to leak the kernel binary one time to know the unslid address of this data structure. Then we can use a simple substraction to determine the secret:

vm_addr_perm = obfuscated &realhost - &realhost

Apple's "Fix"

When we take a look into the source code of OS X Yosemite we can see that Apple was aware that something was wrong here and they attempted to fix the problem. It becomes obvious when you see that they introduced a new obfuscation and unsliding macro called VM_KERNEL_UNSLIDE_OR_PERM. This macro is defined along with the others in /osfmk/mach/vm_param.h and does either obfuscate or unslide.

/*
 * ...
 *
 * VM_KERNEL_UNSLIDE_OR_ADDRPERM:
 *     Use this macro when you are exposing an address to userspace that could
 *     come from either kernel text/data *or* the heap. This is a rare case,
 *     but one that does come up and must be handled correctly.
 *
 * Nesting of these macros should be considered invalid.
 */

...

#define VM_KERNEL_UNSLIDE_OR_PERM(_v)                                          \
                ((VM_KERNEL_IS_SLID(_v) ||                                     \
                  VM_KERNEL_IS_KEXT(_v)) ?                                     \
                        (vm_offset_t)(_v) - vm_kernel_slide :                  \
                        VM_KERNEL_ADDRPERM(_v))

As you can see Apple's attempt to fix the mach_port_kobject() problem was to either obfuscate or unslide the address, depending if it is inside the kernel binary or not. And indeed this would protect you from the first attack described above. However Apple released this fix to the general public after June 2014, when a more advanced attacked had already been used in a public jailbreak.

The Real Attack

As described several times already the magic of breaking the kernel address obfuscation via mach_port_kobject() is backtracing the calls to ipc_set_kobject() and checking what values end up in the ip_kobject field. Let's do this one last time to put a final nail into the coffin. This time we end up in the file /osfmk/device/device_init.c in the function device_service_create():

void
device_service_create(void)
{
        master_device_port = ipc_port_alloc_kernel();
        if (master_device_port == IP_NULL)
                panic("can't allocate master device port");

        ipc_kobject_set(master_device_port, 1, IKOT_MASTER_DEVICE);
        ...
}

Take a second to read and understand this code. This code fills the ip_kobject field with the value 1 for all mach ports of type IKOT_MASTER_DEVICE. This means calling mach_port_kobject() on a master port will return the value of 1 + SECRET. I leave it as an exercise for the reader to deduce the secret algorithm that will give you the obfuscation secret in this case... :P

Final Words

Until Apple finally fixes this problem the kernel address obfuscation is completely worthless. Exploiters and jailbreakers use this problem to determine the address of e.g. open IOKit driver connections in memory. They then know exactly where in the kernel heap there is a C++ object with a vtable pointer that just waits to be overwritten. Recent jailbreaks have made use of this technique and combined it with other techniques that Esser has discussed in Tales from iOS 6 Exploitation at Hack In The Box 2013 to achieve controlled arbitrary code execution in kernel space.

Stefan Esser