CodeQL + XNU From 0 to 1

Basic

本文属于学习过程中的笔记,基本上是把现有的相关资料整合到一起,阅读已有博客/文章并复现,加入一些自己的想法后记录下来的产物。

build XNU 过程来自某大佬的博客,build xnu with codeql过程来自github


UPDATE

2021.3.7 更新,我在big sur上build xnu-7195.81.3 也是遇到了python权限的问题,使用virtualenv的方式解决了。


  • 如果是老版本,建议先搜一下有没有现成的database可以用。
  • 如果找不到,对于版本跨度比较大的情况,还是虚拟机+老版本Xcode比较稳。

Building XNU for macOS Big Sur 11.0.1 (Intel)

D4rkD0g/boringforever

1
2
curl https://jeremya.com/sw/Makefile.xnudeps > Makefile.xnudeps
make -f Makefile.xnudeps
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
--------------------------------------------------------------------------------
XNU is now ready to build!

To build the kernel for supported x86_64 machines:
cd xnu-7195.81.3
make SDKROOT=macosx TARGET_CONFIGS="RELEASE X86_64 NONE"

To build for supported arm64e machines you can, e.g.:
cd xnu-7195.81.3
make SDKROOT=macosx KDKROOT=/path/to/KDK TARGET_CONFIGS="RELEASE ARM64 T8101"

For a table of supported arm64 products, visit:
https://kernelshaman.blogspot.com/2021/02/building-xnu-for-macos-112-intel-apple.html#xnu-arm64e

See xnu's top-level README file for additional build and configuration variables
which can be passed on the command line, e.g.,
Speed up the build with: BUILD_LTO=0
Build the development kernel with: KERNEL_CONFIGS=DEVELOPMENT

Use LOGCOLORS=y to colorize the output
Use CONCISE=y to keep all the build output on a single line
--------------------------------------------------------------------------------
1
2
3
4
5
cd xnu-7195.81.3
// 正常编译xnu的命令
make SDKROOT=macosx TARGET_CONFIGS="RELEASE X86_64 NONE"
// 使用codeql编译命令
codeql database create xnu-database --language=cpp --command="make SDKROOT=macosx ARCH_CONFIGS=X86_64 KERNEL_CONFIGS=RELEASE"

剩下的就是等了:

aliyuncs.com/img/codeql_xnu/codeql-xnu%20+%20XNU%20c4461858b1454816bc8aa7d2f87a674b/Untitled.png

随后可以导入数据库到vscode中使用,也可以使用codecli

  • 遇到的问题1 : env: python : Permisson denied

aliyuncs.com/img/codeql_xnu/codeql-xnu%20+%20XNU%20c4461858b1454816bc8aa7d2f87a674b/Untitled%201.png

解决:可以尝试更换xcode版本来尝试,我也试过使用root或者使用python virtualenv来规避问题,但是还是不行。

aliyuncs.com/img/codeql_xnu/codeql-xnu%20+%20XNU%20c4461858b1454816bc8aa7d2f87a674b/Untitled%202.png

Query测试

aliyuncs.com/img/codeql_xnu/codeql-xnu%20+%20XNU%20c4461858b1454816bc8aa7d2f87a674b/Untitled%203.png

Case study

CVE-2018-4407 - ping ping ping

Setup env

Kernel crash caused by out-of-bounds write in Apple’s ICMP packet-handling code (CVE-2018-4407) - GitHub Security Lab

aliyuncs.com/img/codeql_xnu/codeql-xnu%20+%20XNU%20c4461858b1454816bc8aa7d2f87a674b/Untitled%204.png

以10.13.6为例

需要低版本的xcode来编译,直接去 https://developer.apple.com/download/more/ 下载即可。

构建 codeql xnu database :

1
2
3
4
// 老版本 xcode

make -f Makefile.xnudeps macos_version=10.13.6 xnudeps

find by codeql

导入 xnu 10.13.6 的database后,先看原作者的query,尝试理解他的思路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* @name mbuf copydata with tainted size
* @description Calling m_copydata with an untrusted size argument
* could cause a buffer overflow.
* @kind path-problem
* @problem.severity warning
* @id apple-xnu/cpp/mbuf-copydata-with-tainted-size
*/

import cpp
import semmle.code.cpp.dataflow.TaintTracking
import DataFlow::PathGraph

class Config extends TaintTracking::Configuration {
Config() { this = "tcphdr_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().matches("%copydata"))
}
}

from Config cfg, DataFlow::PathNode source, DataFlow::PathNode sink
where cfg.hasFlowPath(source, sink)
select sink, source, sink, "m_copydata with tainted size."

原作者使用了污点分析来追踪m_mtod调用到 copydata 长度参数。

说下具体的source sink的描述把:

source : 数据流起点,是一个 函数调用(functioncall),并且该函数是 m_mtod

sink : “终点”(可以这么理解吧),是 copydata 函数的第三个参数,函数名匹配使用了正则,能匹配到 copydata 系列。

结果如下:

aliyuncs.com/img/codeql_xnu/codeql-xnu%20+%20XNU%20c4461858b1454816bc8aa7d2f87a674b/Untitled%205.png

我认为难点并不是如何写污点分析的query,难点应该是 分析就结果然后构造poc

漏洞分析

从表象来看漏洞出在 m_copydata(n, 0, icmplen, (caddr_t)&icp->icmp_ip);

ip_icmp.c - apple/darwin-xnu - Sourcegraph

看起来是一个copy数据的时候没有对边界进行检查的“简单的“漏洞,但是Ian beer给作者邮件解释了root casue,这个漏洞发生的本质原因并不是这个地方。

首先看出现漏洞的函数信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
* Generate an error packet of type error
* in response to bad packet ip.
*/
void
icmp_error(
struct mbuf *n,
int type,
int code,
u_int32_t dest,
u_int32_t nextmtu)
{
...
}

这个函数是处理“有问题的IP包”的,返回一个“error packet”给发送者,相当于:发现发过来的IP包有问题之后,生成一个错误信息返回给发送者。

上面的copy函数是复制原本IP包包头的信息复制到返回包中,出现了问题。根据Ian beer的解释:

漏洞实际发生在更早的地方:

1
icp->icmp_type = type;

那么就要把这个函数从头开始分析一下了,我们重点关注:有问题的数据包在哪里进来的,在哪里被处理的,最终怎么走到copy的逻辑的。

源头: struct mbuf *n 表示有问题的数据包(incoming packet),下面贴一下 mbuf 结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
* The mbuf object
*/
struct mbuf {
struct m_hdr m_hdr;
union {
struct {
struct pkthdr MH_pkthdr; /* M_PKTHDR set */
union {
struct m_ext MH_ext; /* M_EXT set */
char MH_databuf[_MHLEN];
} MH_dat;
} MH;
char M_databuf[_MLEN]; /* !M_PKTHDR, !M_EXT */
} M_dat;
};

返回的数据包: struct mbuf *m

1
2
3
4
if (MHLEN > (sizeof(struct ip) + ICMP_MINLEN + icmplen))
m = m_gethdr(M_DONTWAIT, MT_HEADER); /* MAC-OK */
else
m = m_getcl(M_DONTWAIT, MT_DATA, M_PKTHDR);

m的分配和 icmplen 相关:

1
2
3
nlen = m_length(n);
...
icmplen = min(oiphlen + icmpelen, min(nlen, oip->ip_len));

处理: m->m_len = icmplen + ICMP_MINLEN;

这里看起来还没有问题,计算返回包m的长度;

1
2
3
4
5
6
7
8
9
10
11
MH_ALIGN(m, m->m_len); // 宏

// 展开之后:
/*
* As above, for mbufs allocated with m_gethdr/MGETHDR
* or initialized by M_COPY_PKTHDR.
*/
#define MH_ALIGN(m, len) \
do { \
(m)->m_data += (MHLEN - (len)) &~ (sizeof (long) - 1); \
} while (0)

这个宏并没有检查 MHLEN 和 len 的大小关系,这里是有整数溢出的。

在这个场景里, MHLEN 是 88, len是 m→m_len,也就是 icmplen + ICMP_MINLEN; ,如果可以控制 icmplen 大于80,这里就可以触发整数溢出, m_data 指向了其他位置。

随后使用到这个 已经整数溢出 的长度的地方,并不是copy的逻辑,而是:

1
2
icp = mtod(m, struct icmp *);
icp->icmp_type = type; // oob write here

mtod 只是返回了 mbuf 的 data 指针

宏一步一步展开之后:

1
2
3
4
5
6
7
8
9
10
11
12
#define    mtod(m, t)    ((t)m_mtod(m))

void *
m_mtod(struct mbuf *m)
{
return (MTOD(m, void *));
}

/*
* Macro version of mtod.
*/
#define MTOD(m, t) ((t)((m)->m_data))

在上面的过程之后,icp指针本来是指向 m_buf的数据部分

但是整数溢出之后,m→m_data 增加了一个很大的值(<4GB),最终在

1
2
icp->
icmp_type = type;

就发生了越解写,root case 分析完毕。

about PATCH

先说个人理解,直接对 incoming packet的 icmplen 做检查,使得这个长度必须是在合法范围内(根据包结构来计算)

aliyuncs.com/img/codeql_xnu/codeql-xnu%20+%20XNU%20c4461858b1454816bc8aa7d2f87a674b/Untitled%206.png

  • 长度必须 大于等于 sizeof(struct ip) + ICMP_MINLEN
  • 长度必须 大于等于 oiphlen+ICMP_MINLEN

后面要计算 返回包长度的时候:

aliyuncs.com/img/codeql_xnu/codeql-xnu%20+%20XNU%20c4461858b1454816bc8aa7d2f87a674b/Untitled%207.png

所以这里取到的一定是一个合法的值。

再看后续根据长度,拷贝数据的逻辑:

aliyuncs.com/img/codeql_xnu/codeql-xnu%20+%20XNU%20c4461858b1454816bc8aa7d2f87a674b/Untitled%208.png

1
2
icmplen = min(icmplen, M_TRAILINGSPACE(m) -
sizeof(struct ip) - ICMP_MINLEN);

这个长度是经过计算的,把为m data部分分配的空间大小考虑了进去,这样保证拷贝数据的长度是合法的。

至此这个洞算是修的没问题了。

Apple macOS 6LowPAN Vulnerability

CVE-2020-9967 - Apple macOS 6LowPAN Vulnerability

Setup env

有漏洞的版本 10.15.4 为目标,苹果也在Big Sur里做了修复,这些洞影响范围还是比较大的,为了方便起见使用 10.15.4 。 (≤ 10.15.4)

Building XNU for macOS Catalina 10.15.x

aliyuncs.com/img/codeql_xnu/codeql-xnu%20+%20XNU%20c4461858b1454816bc8aa7d2f87a674b/Untitled%209.png

随后测试一下数据库可用即可。

aliyuncs.com/img/codeql_xnu/codeql-xnu query

在这个版本的xnu代码(6153.101.6),bcopy在xnu中被大量使用,但是实现换成了 builtin___memmove_chk ,所以只需要把 之前污点追踪的 query的sink替换一下即可。

aliyuncs.com/img/codeql_xnu/codeql-xnu%20+%20XNU%20c4461858b1454816bc8aa7d2f87a674b/Untitled%2010.png

aliyuncs.com/img/codeql_xnu/codeql-xnu%20+%20XNU%20c4461858b1454816bc8aa7d2f87a674b/Untitled%2011.png

能覆盖到这些问题,但是需要挨个结果审计,然后构造poc才行。

About 6LowPAN

6LowPAN 在 macOS10.15引入,全称是: IPv6 over Low-Power Wireless Persona Area Networks

6LoWPAN是一种基于IPv6的低速无线个域网标准,即IPv6 over IEEE 802.15.4。让每个节点可以用IPv6地址联网。这允许节点使用开放标准直接与Internet连接。即使在最小的资源受限设备上也可以应用Internet协议,并且处理能力有限的低功率设备应该能够参与物联网。

广域无线物联网及6LoWPAN介绍

RFC 4919 - IPv6 over Low-Power Wireless Personal Area Networks (6LoWPANs): Overview, Assumptions, Problem Statement, and Goals

(瞎猜一下,感觉这个东西是对应10.15里引入的那个 “以查找未联网Mac 的功能“ )

1
2
3
4
5
frame802154.c → 802.15.4帧创建和解析逻辑

if_6lowpan.c → 6LowPAN network interface

sixlowpan.c → 6LowPAN 压缩/解压逻辑
  • IEEE 802.15.4帧格式

aliyuncs.com/img/codeql_xnu/codeql-xnu%20+%20XNU%20c4461858b1454816bc8aa7d2f87a674b/Untitled%2012.png

frame control 字段:

aliyuncs.com/img/codeql_xnu/codeql-xnu%20+%20XNU%20c4461858b1454816bc8aa7d2f87a674b/Untitled%2013.png

IPv6报文必须承载在数据帧上。解析帧的时候,首先确定header,随后解析 payload部分。

  • LoWPAN Payload

由于全IPv6报文不符合ieee802.15.4帧的要求,IPv6需要提供适配层来满足MTU的最小要求。该标准还定义了报头压缩的使用,因为预计大多数应用程序将使用IEEE 802.15.4上的IP。

LoWPAN payload (e.g., an IPv6 packet) 遵循上面的描述;IPv6 头有40字节。

在初始标准中,定义了 LoWPAN_HC1 压缩IPv6数据报。这意味着6LowPAN的payload 在接收时被压缩。

  • Data Link Layer Dispatching 数据链路层分发

aliyuncs.com/img/codeql_xnu/codeql-xnu%20+%20XNU%20c4461858b1454816bc8aa7d2f87a674b/Untitled%2014.png

首先,我们可以发送一个以太网数据包,该数据包将由demux函数处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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_typeETHERTYPE_IEEE802154 , 那么该函数会将协议族设置为PF 802154。

现在,在默认配置中,这个协议族将不会被处理,除非配置了6lowpan接口,这将导致以下代码注册一个函数 sixlowpan_input ,当处理一个802.15.4帧时将被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
* 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

调用sixlowpan_input函数来解封装802.15.4数据帧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/*
* 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);
  1. m_getcl 分配mc(mbuf cluster)来存放要处理的 802.15.4 f帧和未解压的payload
  2. 拷贝未解压的802.15.4 payload 到新分配的mc中

len 是从参数 mbuf m 中读取得到的,这是一个可控的值,在后面解析逻辑: frame802154_parse ,这个长度是直接使用的。

aliyuncs.com/img/codeql_xnu/codeql-xnu%20+%20XNU%20c4461858b1454816bc8aa7d2f87a674b/Untitled%2015.png

因为我们可以将len控制在0-0xffff之间,所以我们可以使pf->payload_len为负值(to-header len),小于预期的大小,或者大于mc中输入数据本身的大小。

随后的调用是 sixxlowpan_input(&ieee02154hdr, payload);

这个函数直接使用了 之前解析出来的pf 和 payload。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
}

之后走到 sixxlowpan_uncompress 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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); // 0

if (error != 0) {
return error;
}

if (hdroffset < 0) { // 1
/*
* 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); // 2, oob write here, `ieee02154hdr-> payload_len-3 = -2`
memcpy(payload, hdrbuf, hdrlen);
ieee02154hdr->payload_len += hdrlen - hdroffset;
}

return 0;
}

因此,如果我们将接收到的帧的len设置为0x4,则最终将计算出以下值(frame802154_parse):

c header length = 3

frame->payload_len= 1

同时,在uncompress_hdr_hc1函数中(at 0),控制流程走到 SICSLOWPAN_HC1_NH_UDP 分支:

1
2
3
*hdroffset = SICSLOWPAN_HC1_HDR_LEN;  --> *hdroffset = 3
*hdrlen = UIP_IPH_LEN; --> *hdrlen = 40
sizeof(struct ip6_hdr) = 40

再回到上层函数sixxlowpan_uncompress (at 1),hdroffset 为3,走下面的分支,能够走到 memmove调用(at 2)

1
2
3
memmove(payload + hdrlen,
payload + hdroffset,
ieee02154hdr->payload_len - hdroffset);

where : payload + 40

what : source payload buffer, 可控

length : ieee02154hdr->payload_len - hdroffset 即 payload_len - hdroffset = 1 - 3 = -2

引用

Building XNU for macOS Big Sur 11.0.1 (Intel)

D4rkD0g/boringforever

Kernel crash caused by out-of-bounds write in Apple’s ICMP packet-handling code (CVE-2018-4407) - GitHub Security Lab

CVE-2020-9967 - Apple macOS 6LowPAN Vulnerability