PEGASUS iOS Kernel Vulnerability Explained - Part 2

After our analysis of CVE-2016-4656 from last week new details have surfaced.

Intro

After our analysis of the PEGASUS iOS kernel vulnerability CVE-2016-4656 from last week that you can read in part one of this blog post there have been new discoveries about the kernel vulnerability in question:

  1. It has been discovered that OSUnserializeBinary() has already been patched once in May of this year by Apple to fix a reported UAF vulnerability. That patch was issued to close the vulnerability CVE-2016-1828 that had been reported by Brandon Azad to Apple on 11th January 2016. A detailed write up about this vulnerability is available here.

  2. It was discovered that the UAF trigger we described is in code that was not around before iOS 9 and OS X 10.11. Because PEGASUS is reportedly compatible with earlier iOS versions and Apple even patched earlier OS X versions it makes it unlikely that the UAF trigger we described is the one that the real PEGASUS threat is using.

After being notified about both these details we have decided to write a little update to our analysis incorporating this information.

The old code

Let us take a step back and look at the code of OSUnserializeBinary() before iOS 9.0 and OS X 10.11.

if (dict)
{
        if (sym)
        {
                DEBG("%s = %s\n", sym->getCStringNoCopy(), o->getMetaClass()->getClassName());
                if (o != dict) ok = dict->setObject(sym, o);
                o->release();
                sym->release();
                sym = 0;
        }
        else
        {
                sym = OSDynamicCast(OSSymbol, o);
                ok = (sym != 0);
        }
}

As you can see from the code above: before the days of iOS 9.0 and OS 10.11 dictionary keys had to be OSSymbol objects and the OSString code path had not been added, yet. This means that the UAF trigger we explained in our analysis would not work on this older version of the code. Furthermore if you looked at this code more carefully and compared it in more detail to our analysis you notice that in the older version of the code the call to setObject() had only two parameters instead of three. This is because the code above is before the security fix for CVE-2016-1828.

Let us now look at the code above in more detail and find out what codepath can trigger a UAF condition:

Trigger 1

The first way to trigger a UAF condition in this code is what is known as CVE-2016-1828:

  1. Line 398 will set key k1 of the dictionary to object o1

  2. This will increase the reference counter of o1 and k1 by one (to 2)

  3. In line 399 object o1 is released which decreases the reference counter to 1

  4. In line 400 object k1 (sym) is released which decreases the reference counter to 1

  5. At this point everything is fine because there are still references hold to both objects in the dictionary

  6. When the next object o2 is deserialized and inserted into the dictionary, while reusing the same key k1 the setObject() method in line 398 will replace o1 with o2 in the dictionary. During the replacement the reference counter of o1 is decreased by one to 0.

  7. At his point o1 is freed from memory. If the deserializer is trying to create a reference to this object again it is a UAF.

Trigger 2

The second way to trigger a UAF condition in this code is what is likely the real trigger used by PEGASUS (CVE-2016-4656):

  1. Line 398 will not call setObject() if the object o we are trying to insert is a reference to the dict itself

  2. When setObject() is not executed the reference counters of o and sym will never have been increased

  3. Line 399 will decrease the reference counter of o to some value greater or equal to 1 (because it was a reference to dict)

  4. Line 400 will decrease the reference counter of sym to most likely 0 (if the symbol was e.g. a strange random string)

  5. At this point the OSSymbol object sym is destroyed

  6. Any attempt to create a reference to the object sym after this point will be a UAF.

As you can see the code before iOS 9 and OS X 10.11 already had two separate but very related UAF trigger codepaths in it. This means together with the third codepath described in part one of this blog post there have been THREE UAF trigger codepaths in only 20 lines of code.

The fixes

Back in May when Apple released the fix for CVE-2016-1828 the only change they made to the code was to add a third parameter true to the call of the setObject() method.

if (o != dict) ok = dict->setObject(sym, o, true);

This will ensure that the code errors out in case one tries to overwrite an already set dictionary key. Therefore the UAF situation Brandon Azad reported cannot be triggered anymore. Unfortunately this was good enough for Apple and they did not perform a secure code review of OSUnserializeBinary() at this point in time. Otherwise a skilled code auditor would have realized that there were a number of direct calls to release() in the code that could also be used to free objects from objsArray early and therefore trigger UAF. This would have lead to the revelation that just adding true as third paramter to setObject was an incomplete fix.

However Apple did not perform a security review of their security patch or whoever was tasked with it did not see the other release() calls in the function. Therefore the other two UAF triggers in those 20 lines of code could still be used to exploit the kernel and fully penetrate the system. Now after PEGASUS was discovered Apple seems to have spend more energy on fixing those 20 lines of code, because they have revoked the patch they did for CVE-2016-1828 and instead restructured the function to no longer have any calls to release() in the middle of the function (except for cleaning up after an error and a temporary variable). Instead all objects from objsArray get released at the very end of the function just before returning the result. That way the reference counter can never drop to 0 during the deserialization.

You can find the source code of OSSerializeBinary.cpp with the decompiled patch applied here. Or you can have a look at the diff:

--- OSSerializeBinary.cpp       2016-05-09 22:28:11.000000000 +0200
+++ OSSerializeBinaryPatched.cpp        2016-09-05 16:19:03.000000000 +0200
@@ -237,19 +237,21 @@

 /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

-#define setAtIndex(v, idx, o)                                                  \
+#define setAtIndex(v, idx, o, max)                                             \
        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 (v##Capacity < max) {        \
+                       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;                                     \
+               } else ok = false;                                              \
        }                                                                       \
        if (ok) v##Array[idx] = o;

@@ -338,13 +340,12 @@
                    case kOSSerializeObject:
                                if (len >= objsIdx) break;
                                o = objsArray[len];
-                               o->retain();
                                isRef = true;
                                break;

                    case kOSSerializeNumber:
                                bufferPos += sizeof(long long);
-                               if (bufferPos > bufferSize) break;
+                               if (bufferPos > bufferSize || ((len != 32) && (len != 64) && (len != 16) && (len != 8))) break;
                        value = next[1];
                        value <<= 32;
                        value |= next[0];
@@ -354,7 +355,7 @@

                    case kOSSerializeSymbol:
                                bufferPos += (wordLen * sizeof(uint32_t));
-                               if (bufferPos > bufferSize)           break;
+                               if (bufferPos > bufferSize || len < 2)           break;
                                if (0 != ((const char *)next)[len-1]) break;
                        o = (OSObject *) OSSymbol::withCString((const char *) next);
                        next += wordLen;
@@ -386,8 +387,11 @@

                if (!isRef)
                {
-                       setAtIndex(objs, objsIdx, o);
-                       if (!ok) break;
+                       setAtIndex(objs, objsIdx, o, 0x1000000);
+                       if (!ok) {
+                               o->release();
+                               break;
+                       }
                        objsIdx++;
                }

@@ -395,33 +399,35 @@
                {
                        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;
+                               OSSymbol *sym2 = OSDynamicCast(OSSymbol, sym);
+                               if (!sym2 && (str = OSDynamicCast(OSString, sym)))
+                               {
+                                       sym2 = (OSSymbol *) OSSymbol::withString(str);
+                                       ok = (sym2 != 0);
+                                       if (!sym2) break;
+                               }
+
+                               if (o != dict) ok = dict->setObject(sym2, o);
+                               if (sym2 && sym2 != sym) {
+                                       sym2->release();
+                               }
                        }
                        else
                        {
-                               sym = OSDynamicCast(OSSymbol, o);
-                               if (!sym && (str = OSDynamicCast(OSString, o)))
-                               {
-                                   sym = (OSSymbol *) OSSymbol::withString(str);
-                                   o->release();
-                                   o = 0;
-                               }
-                               ok = (sym != 0);
+                               sym = o;
                        }
                }
                else if (array)
                {
                        ok = array->setObject(o);
-                   o->release();
                }
                else if (set)
                {
-                  ok = set->setObject(o);
-                  o->release();
+                       ok = set->setObject(o);
+               }
+               else if (result)
+               {
+                       ok = false;
                }
                else
                {
@@ -436,7 +442,7 @@
                        if (!end)
                        {
                                stackIdx++;
-                               setAtIndex(stack, stackIdx, parent);
+                               setAtIndex(stack, stackIdx, parent, 0x10000);
                                if (!ok) break;
                        }
                        DEBG("++stack[%d] %p\n", stackIdx, parent);
@@ -462,15 +468,19 @@
                        }
                }
        }
-       DEBG("ret %p\n", result);
-
-       if (objsCapacity)  kfree(objsArray,  objsCapacity  * sizeof(*objsArray));
-       if (stackCapacity) kfree(stackArray, stackCapacity * sizeof(*stackArray));

-       if (!ok && result)
+       if (!ok)
        {
-               result->release();
                result = 0;
        }
+       if (objsCapacity) {
+               uint32_t i;
+               for (i = (result?1:0); i < objsIndx; i++) {
+                       objsArray[i]->release();
+               }
+               kfree(objsArray,  objsCapacity  * sizeof(*objsArray));
+       }
+       if (stackCapacity) kfree(stackArray, stackCapacity * sizeof(*stackArray));
+
        return (result);
 }

Hack.Lu

We got invited by the Hack.lu conference to present our findings about CVE-2016-4656 and how to exploit the different triggers in October 2016 in Luxembourg.

Conclusion

In the last two weeks many security professionals have praised Apple for reacting lightning fast to the PEGASUS threat that has been actively exploited in the wild. This praise was given because the parties involved kept samples from independent 3rd party researchers and did not reveal any detailed information about the kernel vulnerabilities involved to the public. Without this information the public simply assumed that the PEGASUS surveillance malware was using completely new kernel vulnerabilities to takeover iOS devices and that Apple heard about these problems for the first time mid August 2016. Unfortunately after having reversed what kernel vulnerability has been used by the PEGASUS surveillance malware a completely different picture emerges:

The kernel vulnerability known as CVE-2016-4656 was only still in the code because Apple patched CVE-2016-1828 in May 2016 without doing a security review of the code in question. In only 20 lines of code THREE codepaths existed that allowed UAF. Apple fixed only one of those paths although the other release() methods were clearly right next to it in the code. Furthermore the now released patch for PEGASUS shows that with a little redesign of the code Apple was able to fix all THREE problems at the same time. We consider it a huge oversight that this did not happen after the UAF had been reported by Brandon Azad in January. If Apple had fixed CVE-2016-1828 in a different way CVE-2016-4656 would never have been abusable in the wild.

Unfortunately this is not the first time that Apple has botched security fixes. SektionEins has highlighted this troubling problem of Apple botching security patches over and over again for two years now. Not surprisingly highlighting this lack of QA of Apple's security patches has made us no friends at Apple and instead lead to the termination of our SysSecInfo security application from the iOS AppStore. You can read the full story here.

Final food for thought: Having looked into Brandon Azad's CVE-2016-1828 and PEGASUS' CVE-2016-4656 we believe that the kernel bug used in PEGASUS is harder to see (find) and harder to exploit than the bug found by Brandon Azad. This makes us believe that the exploit used in PEGASUS has not been written before the fix for CVE-2016-1828 had already been released, because otherwise the easier to exploit bug would have been found and chosen. This could mean that either CVE-2016-1828 had been used for previous incarnations of PEGASUS or that someone reversed CVE-2016-1828 and discovered that the fix was incomplete. But these are just guesses and our assumptions could be completely wrong.

Stefan Esser