远程iPhone Exploitation Part 1:iMe​​ssage与CVE-2019-8641

 

简介

这是三篇系列文章中的第一篇,将详细介绍如何在iOS 12.4上远程利用iMessage中的漏洞,无需任何用户交互,它是我在2019年12月的36C3会议上演讲的一个更详细的版本。

本系列的第一部分对该漏洞进行了深入的分析,第二部分介绍了一种远程破坏ASLR的技术,第三部分介绍了如何获得远程代码执行(RCE)。

本系列中介绍的攻击使攻击者能够在几分钟内远程控制用户的iOS设备,攻击者仅拥有用户的Apple ID(移动电话号码或电子邮件地址)。之后,攻击者可以窃取文件、密码、2FA代码、短信和电子邮件以及其他用户APP数据。还可以远程激活麦克风和摄像头。所有这些都是可能的,而无需向用户显示任何交互(打开攻击者发送的URL或者消息通知)。首先攻击利用CVE-2019-8641漏洞绕过ASLR,然后在目标设备的沙盒外执行代码。

这项研究的主要原因是:仅给出一个远程内存破坏漏洞,是否有可能在iPhone上实现远程代码执行而没有其他漏洞,并且无需任何形式的用户交互?这个系列文章表明这实际上是可能的。

这个漏洞是与娜塔莉·西尔瓦诺维奇(Natalie Silvanovich)合作的漏洞研究项目中的一部分,已于2019年7月29日报告给Apple,这个漏洞首先在iOS 12.4.1中得到了缓解,于8月26日发布,使易受攻击的代码无法通过iMessage访问,在2019年10月28日发布的iOS 13.2中完全修复。根据在exploit开发过程中获得的见解,提出了进一步的强化措施,如果实施这些措施,将来会使类似的漏洞利用变得更加困难。由于其中一些强化措施与其他messenger服务和手机操作系统相关,因此在本系列文章中会提到它们,并在最后进行总结。

对于安全研究人员来说,这里提供了一个针对iPhone XS上iOS 12.4的POC。为了防止滥用,它在整个利用过程中显示多个通知,提醒受害者正在遭受攻击。而且,它不能实现本地代码执行(即执行shellcode),因此它很难与现有的特权提升漏洞相结合。这些限制都不需要进一步的漏洞来弥补,只需要有能力的攻击者重新设计exploit。内置的限制和通知仅仅是为了阻止非研究人员滥用而设计的。需要注意的是,专业的研究员很可能已经具备了利用已公布的代码的能力,无论是自己发现漏洞还是通过补丁对比来发现漏洞,或者通过CVE-2019-8646与其他任何内存破坏漏洞相结合,进行1-day的漏洞利用。

和之前一样,我希望这项研究能够帮助其他安全研究人员和软件开发人员,介绍如何绕过缓解措施,并分享一些我的想法,以确保我所发现的漏洞能够得到缓解。

 

iMessage架构

在最终向用户显示通知并将消息写入messages数据库之前,传入的iMessages会经过多个服务和框架。下图描述了在iOS 12.4上处理iMessage而不需要与用户交互的主要服务。(红色边框表示该进程存在沙盒)

在以下截图中可以看到:

Natalie Silvanovich在之前的一篇文章中介绍了远程可访问的攻击面,其中包括众所周知的NSKeyedUnarchiver API以及iMessage数据格式。在本系列文章中,必须知道到NSKeyedUnarchiver API中的漏洞通常可以在两种不同的上下文中触发:在沙盒的imagent和非沙盒SpringBoard进程(后者管理主iOS UI,包括主屏幕),两种情况在漏洞利用方面都各有利弊。例如,虽然SpringBoard已经在沙箱外部运行,似乎是更好的目标,但它也是一个比较重要的系统进程,当崩溃时会导致设备明显的“注销重启桌面”,屏幕突然变黑,并且在锁定屏幕出现之前加载图标会出现几秒。

从iOS13开始,NSKeyedUnarchiver数据的解码不再发生在SpringBoard内部,而是发生在沙箱IMDPersistenceAgent中,这大大减少了iMessage的非沙箱攻击面。

 

iMessage 传递

为了通过iMessage传递exploit,需要能够向目标发送自定义iMessage。这需要与Apple的服务器进行交互,并处理iMessage的end2end加密。用于这项研究的一种简单的方法是通过使用FRIDA这样的工具将imagent中处理iMessage的代码HOOK到现有代码中。这样,可以通过以下方式在macOS上运行的脚本发送自定义iMessage:

  • 1.生成所需的payload(例如触发NSKeyedUnarchiver错误)并将其存储到磁盘
  • 2.调用一个小的Apple脚本,该脚本显示Messages.app将带有占位符内容(例如“ REPLACEME”)的消息发送给目标
  • 3.使用frida来Hook imagent,并将包含占位符的iMessages的内容替换为payload文件的内容

同样,也可以通过使用frida来Hook imagent中的receiver函数。

例如,当从Messages.app发送“REPLACEME”消息时,将向receiver发送以下iMessage(编码为二进制plist):

{
    gid = "008412B9-A4F7-4B96-96C3-70C4276CB2BE";
    gv = 8;
    p =     (
        "mailto:sender@foo.bar",
        "mailto:receiver@foo.bar"
    );
    pv = 0;
    r = "6401430E-CDD3-4BC7-A377-7611706B431F";
    t = "REPLACEME";
    v = 1;
    x = "<html><body>REPLACEME</body></html>";
}

然后,frida钩子会在消息被序列化、加密发送到Apple服务器之前对其进行修改:

{
    gid = "008412B9-A4F7-4B96-96C3-70C4276CB2BE";
    gv = 8;
    p =     (
        "mailto:sender@foo.bar",
        "mailto:receiver@foo.bar”
    );
    pv = 0;
    r = "6401430E-CDD3-4BC7-A377-7611706B431F";
    t = "REPLACEME";
    v = 1;
    x = "<html><body>REPLACEME</body></html>";
    ati = <content of /private/var/tmp/com.apple.messages/payload>;
}

这将导致接收设备上的imagent使用NSKeyedUnarchiver API解码ati字段中的数据。可以在这里找到允许发送自定义消息和转储传入消息的示例代码。

 

CVE-2019-8641

用于此研究的漏洞是CVE-2019-8641,对应于Project Zero issue 1917。这是NSKeyedUnarchiver组件中的另一个bug, Natalie已经对此进行了深入的讨论。虽然可能以类似的方式在本研究的NSKeyedUnarchiver组件中发现的其他内存破坏漏洞,但该漏洞似乎最容易利用。

这个BUG发生在NSSharedKeyDictionary的解档(unarchiving)过程中,NSSharedKeyDictionary是一种特殊的NSDictionary。其中,在NSSharedKeySet中预先声明了keys,以允许更快地访问元素。此外,NSSharedKeySet本身具有subSharedKeySet,基本上是可以构建一个KeySet的链表。

要理解这个漏洞,首先需要了解NSKeyedUnarchiver序列化格式。下面是一个简单的存档文件,其中包含了由plutil(1)打印的序列化NSSharedKeyDictionary(NSKeyedArchiver将对象图编码为plist),并添加了一些注释:

{
  "$archiver" => "NSKeyedArchiver"
  # The objects contained in the archive are stored in this array
  # and can be referenced during decoding using their index
  "$objects" => [
    # Index 0 always contains the nil value
    0 => "$null"
    # The serialized NSSharedKeyDictionary
    1 => {
      "$class" => <CFKeyedArchiverUID>{value = 7}
      "NS.count" => 0
      "NS.sideDic" => <CFKeyedArchiverUID>{value = 0}
      "NS.skkeyset" => <CFKeyedArchiverUID>{value = 2}
    }
    # The NSSharedKeySet associated with the dictionary    
    2 => {
      "$class" => <CFKeyedArchiverUID>{value = 6}
      "NS.algorithmType" => 1
      "NS.factor" => 3
      "NS.g" => <00>
      "NS.keys" => <CFKeyedArchiverUID>{value = 3}
      "NS.M" => 6
      "NS.numKey" => 1
      "NS.rankTable" => <00000000 0001>
      "NS.seed0" => 361949685
      "NS.seed1" => 2328087422
      "NS.select" => 0
      "NS.subskset" => <CFKeyedArchiverUID>{value = 0}
    }
    # The keys of the NSSharedKeySet 
    3 => {
      "$class" => <CFKeyedArchiverUID>{value = 5}
      "NS.objects" => [
        0 => <CFKeyedArchiverUID>{value = 4}
      ]
    }
    # The value of the first (and only) key
    4 => "the_key"
    # ObjC classes are stored in this format
    5 => {
      "$classes" => [
        0 => "NSArray"
        1 => "NSObject"
      ]
      "$classname" => "NSArray"
    }
    6 => {
      "$classes" => [
        0 => "NSSharedKeySet"
        1 => "NSObject"
      ]
      "$classname" => "NSSharedKeySet"
    }
    7 => {
      "$classes" => [
        0 => "NSSharedKeyDictionary"
        1 => "NSMutableDictionary"
        2 => "NSDictionary"
        3 => "NSObject"
      ]
      "$classname" => "NSSharedKeyDictionary"
    }
  ]
  # A reference to the root object in the archive
  "$top" => {
    "root" => <CFKeyedArchiverUID>{value = 1}
  }
  "$version" => 100000
}

这里需要注意的一点是,这种序列化格式支持通过CFKeyedArchiverUID值引用其中包含的对象。在解档(unarchiving)期间,NSKeyedUnarchiver将保留UID与对象的映射,以便可以从同一归档中的多个其他对象引用单个对象。此外,在调用对象的initWithCoder方法(在解档期间使用的构造函数)之前,会将引用添加到映射中。这样,可以对对象图进行解码,在这种情况下,可以在当前调用堆栈中进一步解档时引用该对象。有趣的是,在这种情况下,第一个对象在被引用时可能尚未完全初始化。这会产生潜在的BUG,而CVE-2019-8641正是这种情况。

下面是Objective-C (ObjC)伪代码,由NSSharedKeyDictionaryNSSharedKeySet类的initWithCoder实现。不熟悉ObjC的读者可以想到以下代码结构:

[obj doXWith:y and:z];

作为对象的方法调用,类似于

obj->doX(y, z);

在C++中

-[NSSharedKeyDictionary initWithCoder:coder] {
  self->_keyMap = [coder decodeObjectOfClass:[NSSharedKeySet class]
                         forKey:"NS.skkeyset"];
  // ... decode values etc.
}

-[NSSharedKeySet initWithCoder:coder] {
  self->_numKey = [coder decodeInt64ForKey:@"NS.numKey"];
  self->_rankTable = [coder decodeBytesForKey:@"NS.rankTable"];
  // ... copy more fields from the archive

  self->_subSharedKeySet = [coder 
                            decodeObjectOfClass:[NSSharedKeySet class]
                            forKey:@"NS.subskset"]];

  NSArray* keys = [coder decodeObjectOfClasses:[...] 
                         forKey:@"NS.keys"]];
  if (self->_numKey != [keys count]) {
    return fail(“Inconsistent archive);
  }
  self->_keys = calloc(self->_numKey, 8);
  // copy keys into _keys

  // Verify that all keys can be looked up
  for (id key in keys) {
    if ([self indexForKey:key] == -1) {
      NSMutableArray* allKeys = [NSMutableArray arrayWithArray:keys];
      [allKeys addObjectsFromArray:[self->_subSharedKeySet allKeys]];
      return [NSSharedKeySet keySetWithKeys:allKeys];
    }
  }
}

indexForKey的routine实现在下面的initWithCoder(第20行)末尾调用。

它使用键的hash来索引到_rankTable,并使用结果作为索引到_keys。它递归搜索subSharedKeySet中的key,直到找到它或不再有subSharedKeySet可用:

-[NSSharedKeySet indexForKey:] {
  NSSharedKeySet* current = self;
  uint32_t prevLength = 0;
  while (current) {
    // Compute a hash from the key and other internal values of    
    // the KeySet. Convert the hash to an index and ensure that it 
    // is within the bounds of rankTable
    uint32_t rankTableIndex = ...;
    uint32_t index = self->_rankTable[rankTableIndex];
    if (index < self->_numKey) {
      id candidate = self->_keys[index];
      if (candidate != nil) {
        if ([key isEqual:candidate]) {
          return prevLength + index;
        }
      }
    prevLength += self->_numKey;
    current = self->_subSharedKeySet;
  }
  return -1;
}

根据上面的逻辑,下面的对象图在反序列化时会导致内存损坏:

取消存档期间会发生以下情况:

  • 1.未对NSSharedKeyDictionary进行存档,然后对SharedKeySet进行解档
  • 2.SharedKeySet1的initWithCoder运行到解压它的subSharedKeySet(第6行)的位置
    • _numKey 受到攻击者的完全控制,因为尚未对其进行任何检查(只有在取消存档_keys之后才会发生此检查)
    • _rankTable 也被完全控制
    • _keys仍然是nullptr(因为ObjC对象是通过calloc分配的)
  • 3.SharedKeySet2完全未存档。它的subSharedKeySet是对SharedKeySet1的引用(仍在未归档中)。最后,它为_keys数组中的所有键调用indexForKey:(第20行)
  • 4.由于SharedKeySet2的rankTable都为零,所以只有第一个key可以自己查找(请参阅indexForKey:中的第8至15行)。然后在SharedKeySet1上查找第二个key,在这里,由于_numKey_rankTable的内容被完全控制,代码将在第10行使用受的控索引对_keys(为nullptr)进行索引,从而导致崩溃。

下图显示了崩溃时简化的调用堆栈:

因此,现在取消了大部分任意地址的引用(在本例中是0x414140,因为索引乘以8,即元素大小),并将结果用作ObjC对象指针(“id”)。

但是,对于可以使用此BUG访问的地址有两个限制:

  • 1.该地址必须被8整除(因为_keys数组存储指针大小的值)
  • 2.必须小于32G,因为索引是4字节无符号整数

幸运的是,在iOS上,内存中最有用的Things都位于0x800000000 (32G)以下,因此可以使用这个原语(primitive)进行访问。

事实证明,在适当的情况下,即使是看似无用的空指针解引用,也可以变为功能强大的exploit原语。

此时,没有相关目标进程的地址空间信息。因此,必须要先构造某种信息泄漏。这将是下一篇文章的主题。

本文翻译自Project Zero Blog

(完)