导语:necp_client_action系统调用是网络扩展控制策略(NECP)内核子系统的一部分。本文是对fuzzing macOS necp_client_action系统调用时发现的堆溢出漏洞的分析。
本文是对fuzzing macOS necp_client_action系统调用时发现的堆溢出漏洞的分析。necp_client_action系统调用是网络扩展控制策略(NECP)内核子系统的一部分。此漏洞首先在XNU内核版本4570.1.46中找到,并在10.13.4内核更新(版本4570.51.1)中进行了修补。执行该漏洞会导致堆溢出,该溢出可能会变成信息泄漏并最终在内核中执行任意代码。
可以在我们的NotQuite0DayFriday存储库中找到这篇文章和代码:https://github.com/grimm-co/NotQuite0DayFriday/tree/master/2018.04.06-macos。
受影响的版本:XNU内核版本4570.1.46及更高版本,直到4570.51.1。
发现环境:运行定制版本的XNU 4570.1.46的VMware Fusion中的macOS High Sierra
$ clang poc.c -o poc $ ./poc $ clang leak.c -o leak $ ./leak $ clang uaf.c -o uaf $ ./uaf
在开发necp_client_action的系统调用fuzzer期间,我们研究了NECP子系统,也因此追踪到了一条因未检查的用户提供的长度导致memcpy的代码路径。
通过查看bsd / net / necp_client.c中的necp_client_action函数,我们可以看到它仅仅是几种不同操作的包装。当将necp_client_action的操作参数设置为NECP_CLIENT_UPDATE_CACHE时,会发生易受攻击的情况,然后necp_client_action调用函数necp_client_update_cache。necp_client_update_cache函数被用于管理连接的TCP相关信息。因此,如果未使用为其分配有效连接的NECP客户端UUID调用该函数,则该函数将会出错。在检查UUID后,此函数将用户提供的necp_cache_buffer结构体复制到堆栈中。这个结构如下所示:
typedef struct necp_cache_buffer { u_int8_t necp_cache_buf_type; // NECP_CLIENT_CACHE_TYPE_* u_int8_t necp_cache_buf_ver; // NECP_CLIENT_CACHE_TYPE_*_VER u_int32_t necp_cache_buf_size; mach_vm_address_t necp_cache_buf_addr; } necp_cache_buffer;
如果buf_type和buf_ver分别为NECP_CLIENT_CACHE_TYPE_TFO和NECP_CLIENT_CACHE_TYPE_TFO_VER_1,则necp_client_update_cache将尝试从用户读取necp_tcp_tfo_cache结构并使用它调用tcp_heuristics_tfo_update。下面显示的这个结构是值得注意的,因为它包含一个用户控制的缓冲区,长度最多为0x10个字符,长度为缓冲区。
typedef struct necp_tcp_tfo_cache { u_int8_t necp_tcp_tfo_cookie[NECP_TFO_COOKIE_LEN_MAX]; u_int8_t necp_tcp_tfo_cookie_len; u_int8_t necp_tcp_tfo_heuristics_success:1; u_int8_t necp_tcp_tfo_heuristics_loss:1; u_int8_t necp_tcp_tfo_heuristics_middlebox:1; u_int8_t necp_tcp_tfo_heuristics_success_req:1; u_int8_t necp_tcp_tfo_heuristics_loss_req:1; u_int8_t necp_tcp_tfo_heuristics_rst_data:1; u_int8_t necp_tcp_tfo_heuristics_rst_req:1; } necp_tcp_tfo_cache;
tcp_heuristics_tfo_update(在bsd / netinet / tcp_cache.c中)通过以下几行代码处理necp_tcp_tfo_cache结构:
if (necp_buffer->necp_tcp_tfo_cookie_len != 0) { tcp_cache_set_cookie_common(&tcks, necp_buffer->necp_tcp_tfo_cookie, necp_buffer->necp_tcp_tfo_cookie_len); }
如果cookie长度不为零,它将调用tcp_cache_set_cookie_common,如下所示。该函数查找与当前连接关联的tcp_cache结构并将该cookie复制到该结构中。
static void tcp_cache_set_cookie_common(struct tcp_cache_key_src *tcks, u_char *cookie, u_int8_t len) { struct tcp_cache_head *head; struct tcp_cache *tpcache; /* Call lookup/create function */ tpcache = tcp_getcache_with_lock(tcks, 1, &head); if (tpcache == NULL) return; tpcache->tc_tfo_cookie_len = len; memcpy(tpcache->tc_tfo_cookie, cookie, len); tcp_cache_unlock(head); }
但是,tc_tfo_cookie长度至多为0x10字节。因此,如果攻击者指定的参数大于0x10字节,则memcpy会溢出堆上的tcp_cache结构并将数据写入该堆。由于源cookie位于堆栈上,因此将会溢出堆栈。tcp_getcache_with_lock函数会尝试为与连接关联的本地和远程主机查找现有的tcp_cache结构。如果找不到,则会创建一个新的。但是,将会创建的tcp_cache结构数量有限制。
漏洞修复
那么这个bug是如何修复的?虽然XNU内核的源代码没有被更新以反映最新的可用XNU内核二进制文件,但我们可以通过反汇编以了解修复。在地址0xFFFFFF800060DE59的4570.51.1内核中,添加了以下指令:
cmp ebx,0x10 mov edx,0x10 cmovb edx,ebx ...
将edx字节从necp_tcp_tfo_cookie(rsi)复制到tpcache(rdi),此程序集将necp_tcp_tfo_cookie_len参数与0x10进行比较,并且只有在它小于0x10时,该值才会用于以下memcpy中。因此,任何写入超过0x10字节的尝试都会导致写入0x10字节。
堆分析
这个bug允许在内核堆中发生堆溢出。更具体地说,溢出的tcp_cache结构位于kalloc.80区域中。因此,任何剥削尝试自然会将tcp_cache结构溢出到kalloc.80区域中的另一个分配中。我们试着通过寻找kalloc.80分配来开始我们的分析,虽然kalloc.80分配的数量非常有限,但我们还是找到了三个有用的分配。
第一次分配由necp_set_socket_attributes函数(在bsd / net / necp_client.c中)完成。该功能允许用户标记具有域和帐户属性的套接字。这些字符串可以是任意大小,并且可以在用户需要时创建,读取和释放。这些分配非常适合阅读任何泄漏的内容。此外,它们也可以用来修饰内核堆。但是,每个套接字只能创建两个NECP属性字符串。因此,NECP属性字符串的总数受开放文件描述符的限制。
找到的第二个分配是IO矢量元数据结构(struct uio,在bsd / sys / uio_internal.h中定义)。该结构用于在内核中执行分散/收集内存操作。当通过uio_create创建uio结构体(在bsd / kern / kern_subr.c中)时,IO向量信息数组将立即存储在uio结构体之后。因此,通过调整存储在uio结构中的IO向量的数量,我们可以控制大小。通过创建一个具有单个IO矢量的uio结构,uio结构将在kalloc.80区域中声明。这些结构可以通过使用recvmsg_x和sendmsg_x系统调用来声明和释放。这个结构对堆修饰比NECP分配字符串更有用,因为每个线程可以创建一个结构。
第三个分配用于POSIX共享内存子系统(在bsd / kern / posix_shm.c中)。该子系统允许进程设置在两个进程中映射的共享内存区域。这些共享内存区域使用pshminfo结构进行跟踪,如下所示。由于结构中早期的pshm_usecount引用计数器,此结构在开发过程中将很有用。
#define PSHMNAMLEN 31 /* maximum name segment length we bother with */ struct pshminfo { unsigned int pshm_flags; unsigned int pshm_usecount; off_t pshm_length; mode_t pshm_mode; uid_t pshm_uid; gid_t pshm_gid; char pshm_name[PSHMNAMLEN + 1]; /* segment name */ struct pshmobj *pshm_memobjects; struct label* pshm_label; };
产生信息泄漏
现在我们有了目标分配设置,可以开始研究利用了。我们的第一个目标是获取信息泄漏并推导出内核的slide。这是通过用NECP属性字符串填充kalloc.80区域,释放每个其他NECP属性字符串,然后导致溢出来实现的。一旦发生溢出,我们将读出NECP属性字符串,查找其内容已更改的字符串。新内容将包含内核堆栈中的字节。这个信息泄漏在包含的leak.c文件中演示。
利用开发
不幸的是,我们没有在修复漏洞之前完成漏洞利用。所以这里我们介绍的是一个包含uaf.c文件的POC。这个POC是不确定的,可能需要几次运行才能正确执行。由于XNU内核区域分配器的设计,不可能溢出到内核堆元数据中。因此,我们必须尝试覆盖另一个内核堆块的内容。而且,由于堆溢出不能控制写入的内容,我们必须找到一种方法来创建更强大的利用原语。
如前所述,我们将集中精力开发pshminfo结构。该结构在字节4中包含一个引用计数器。当SHM区域被删除时,该引用计数器递减。如果计数器达到零,则释放pshminfo结构。通过溢出到pshminfo的引用计数器并破坏它,我们将能够在发生免费漏洞之后将这个不受控制的堆溢出变为用途。
在免费使用之前,我们必须处理一些实现细节。首先,为了释放结构,pshm_flags字段必须设置PSHM_DEFINED(0x2)或PSHM_ALLOCATED标志(0x4),并且不设置PSHM_INDELETE(0x80)和PSHM_ALLOCATING(0x100)标志。由于pshm_flags字段在引用计数器之前,我们也需要将其溢出。
我们需要处理的下一个问题是获取紧接在tcp_cache结构之后的pshminfo结构。这很容易实现,因为kalloc.80区域相对静态,并且不被内核中的许多事物使用。但是,我们需要确定在溢出的tcp_cache结构之后紧接着哪个pshminfo结构。由于pshm_flags字段将被破坏,它很可能无效。此外,如果pshm_flags字段设置不正确,shm_unlink在调用时将失败。因此,我们可以通过在每个SHM区域上调用shm_unlink并查找失败来检测溢出的SHM区域。
一旦我们确定了溢出的SHM区域,我们接下来需要更正该区域的pshminfo的pshm_flags字段。不幸的是,我们不能控制堆溢出的内容,所以我们不能直接设置这个值。但是,在我们的测试中,溢出到pshm_flags字段中的值是未初始化的堆栈数据。因此,每个necp_client_action系统调用的值可能不同。所以,我们的概念验证不断触发堆溢出,直到标志被设置为可接受的值。
现在我们已经成功设置了一个有效的pshm_flags值并损坏了引用计数,我们可以在空闲后使用该引用。在最近发生的腐败之前,我们的概念证明会再次打开溢出的SHM区域252。因此,我们很可能能够比损坏的参考计数器更多的关闭SHM区域。我们的概念验证关闭了每个SHM区域,并立即创建一个NECP属性字符串。一旦pshminfo的引用计数器命中0,它将被释放,并且下一个属性字符串将被放置在相同的存储位置。由于内核仍然保持对SHM区域的其他引用,所以我们将在pshminfo结构上的空闲状态之后使用。为了说明免费后的使用情况,我们的概念验证打印出有关通过proc_info系统调用获取的SHM区域的信息。由于SHM区域的pshminfo结构和我们的NECP属性字符串位于同一个内存区域,因此pshminfo结构将被我们的NECP属性字符串(0x41)的内容覆盖。
我们开发计划中的下一步是设置NECP属性字符串,以便对剩下涉及到SHM地区的可以免费使用。在释放的内存中,将分配一个uio结构,我们可以通过NECP属性字符串控制和读取占用该内存区域的字符串。通过读取和修改uio结构的能力,攻击应该能够实现对内核内存的任意读写。