syzlang语法编写案例学习 —— Looking for Remote Code Execution bugs in the Linux kernel

本文参考一下 Looking for Remote Code Execution bugs in the Linux kernel,学习如何编写syzlang语法的模板。

主要内容:如何远程控制个人Linux或Android设备?发送恶意链接、通过浏览器实现代码执行?针对通讯软件或邮件客户端?但最方便的办法是,直接发送网络包来控制内核。本文介绍了作者如何改进syzkaller来挖掘Linux内核的网络设备的漏洞,并且介绍了syzkaller的一个新的特性——pseudo-syscalls

1. Introduction

1-1 Background

fuzz内核的原理可参考 Ruffling the penguin! How to fuzz the Linux kernel

预期步骤

  • Injecting network packets:向内核注入packet,让内核去正常解析。对于普通fuzzer,直接向机器发包即可,但对于syzkaller架构则还需要调整。
  • Collecting coverage:收集代码覆盖信息。
  • Integrating into syzkaller:将以上两部分整合到syzkaller中。

2. Injecting network packets — TUN/TAP

思路:由于syzkaller的fuzz过程是在VM中进行(包括收集代码覆盖),所以如果从host端向VM内部发包进行测试的话,还涉及到同步输入的问题,很麻烦。最好是通过一个驱动,直接从用户空间取packet并注入到内核网络层,而 TUN/TAP interface 就能做到。

2-1 About TUN/TAP

TUN/TAP介绍:TUN/TAP 为用户空间的程序提供了包接收和包传输功能,可以被看作是P2P或以太网设备,可以直接从用户空间的程序接收包,而不是用物理媒介收包,可以直接向用户程序写,而不是通过物理媒介来发包。TUN/TAP 相当于一个虚拟网卡(virtual Network Interface Card),和真实NIC相比,它能直接从用户程序获取packet,然后内核会来解析这些packet(跟从硬件收包一样)。

2-2 Employing TUN/TAP

设置:设置可以参考 TUN/TAP Demystified,先打开 /dev/tun 并调用 ioctl 进行设置。

#define IFACE "syz_tun"

int tun = open("/dev/net/tun", O_RDWR | O_NONBLOCK);

struct ifreq ifr;
memset(&ifr, 0, sizeof(ifr));
// Specify the interface name:
strncpy(ifr.ifr_name, IFACE, IFNAMSIZ);
// Specify the raw TAP mode:
ifr.ifr_flags = IFF_TAP | IFF_NO_PI; // IFF_TAP 打开 TAP模式,这样设备就能在 raw Ethernet frames 层操作,而不是 higher-level protocol frames。 IFF_NO_PI 表示,不要在发往用户空间的 packet 前面追加协议头信息(如 IP version 等, packet 本身已经包含了该信息,避免重复)。

// Set the interface name and mode.
ioctl(tun, TUNSETIFF, &ifr);


// 设置该接口的 IP/MAC 地址
#define LOCAL_MAC  "aa:aa:aa:aa:aa:aa"
#define LOCAL_IPV4 "172.20.20.170"

// Assign MAC and IP addresses.
execute_command("ip link set dev %s address %s", IFACE, LOCAL_MAC);
execute_command("ip addr add %s/24 dev %s", LOCAL_IPV4, IFACE);

// 激活接口
// Activate the interface.
execute_command("ip link set %s up", IFACE);

使用:现在可以往 /dev/tun 写任意的 Ethernet frames,内核网络子系统就会来处理。注意,发送的 frame 需包含和接口相同的目的MAC和IP地址,否则接口就会 reject frame。还可以使用 read(tun, ...) 来接收 response。

// Write a packet into TUN/TAP.
write(tun, frame, length);

3. Collecting network coverage — KCOV

3-1 About KCOV

KCOV介绍:KCOV负责收集代码覆盖。KCOV包含两个部分,一是编译器中的插桩部分,在每个基本块中插入回调函数;二是内核中的运行部分,实现了这些回调函数,负责记录基本块的地址,通过 /sys/kernel/debug/kcov 目录来获取这些地址。

注意,KCOV不能一次收集所有内核任务的代码覆盖,只能收集单个用户进程的代码覆盖,这样syzkaller就能保证只收集属于单个 fuzzing input 的syscall的代码覆盖,但是在并发fuzz同一内核时就不可用了。

请添加图片描述

使用

  • 1.编译内核时配置 CONFIG_KCOV

  • 2.给当前进程设置KCOV,接下来就可以调用 syscall,内核就会将代码覆盖记录到 cover

    int fd = open("/sys/kernel/debug/kcov", ...);
    unsigned long *cover = mmap(NULL, ..., fd, 0);
    ioctl(fd, KCOV_ENABLE, ...);
    

3-2 Employing KCOV

问题:不能直接使用KCOV从包处理代码中收集代码覆盖。

中断:尽管是从用户程序发送的packet,TUN/TAP 并没有在用户程序的上下文中处理packet,而是将packet放入队列,然后内核进程在 NET_RX_SOFTIQ 软中断中处理。由于任何内核任务都有可能处理中断,KCOV 无法收集中断处理函数的代码覆盖。所以通过扩展KCOV来收集软中断的代码覆盖是很困难的。

请添加图片描述

改进TUN/TAP:对 TUN/TAP 打补丁,直接解析 packet。修改 tun_get_user() 函数,该函数原本负责处理用户空间传过来的 packet,原本该函数会调用 netif_rx_ni()->enqueue_to_backlog() 将 packet 放入队列待处理,打补丁之后,就直接调用 netif_receive_skb() 来处理 packet(只要通过TUN/TAP发送packet,就会在发送进程的上下文中处理packet,KCOV就能收集到代码覆盖了)。 注意,不要开启 CONFIG_4KSTACKS,因为栈空间不够用。

diff --git a/drivers/net/tun.c b/drivers/net/tun.c
index a3ac8636f3ba9..a569e61bc1d9e 100644
--- a/drivers/net/tun.c
+++ b/drivers/net/tun.c
@@ -1286,7 +1286,13 @@ static ssize_t tun_get_user(struct tun_struct *tun, ...
 	skb_probe_transport_header(skb, 0);

 	rxhash = skb_get_hash(skb);
+#ifndef CONFIG_4KSTACKS
+	local_bh_disable();
+	netif_receive_skb(skb);
+	local_bh_enable();
+#else
 	netif_rx_ni(skb);
+#endif

 	stats = get_cpu_ptr(tun->pcpu_stats);
 	u64_stats_update_begin(&stats->syncp);

请添加图片描述

4. Integrating into syzkaller

4-1 About syzkaller

介绍:syzkaller 最支持的系统是Linux和 **BSDs,可以参考原作者 Dmitry Vyukov 在BlueHatIL 2020 的演讲 syzkaller: adventures in continuous coverage-guided kernel fuzzing (video),本文作者发现并利用漏洞的文章 Exploiting the Linux kernel via packet sockets ,其他文档可参见 syzkaller documentation

请添加图片描述

4-2 Syscall descriptions

首先需要用syzlang声明式语言来写syscall模板,以下摘录了一段 socket 相关的 syscall 描述(详情可参考 socket_inet / socket_inet_tcp / vnet

resource sock[fd]
resource sock_in[sock]
resource sock_tcp[sock_in]

type sock_port int16be[20000:20004]

ipv4_addr [
	rand_addr	int32be[0x64010100:0x64010102]
	empty		const[0x0, int32be]
	loopback	const[0x7f000001, int32be]
] [size[4]]

sockaddr_in {
	family	const[AF_INET, int16]
	port	sock_port
	addr	ipv4_addr
} [size[16]]

socket$inet_tcp(domain const[AF_INET], type const[SOCK_STREAM],
                proto const[0]) sock_tcp

bind$inet(fd sock_in, addr ptr[in, sockaddr_in], addrlen len[addr])

listen(fd sock, backlog int32)
(1)Syscalls

以上描述了3个syscalls,socket$inet_tcp—创建TCP socket,bind$inet—将socket绑定到某个地址和端口,listen—使socket处于监听状态。

Argumentssocket$inet_tcp — 3个常量;bind$inet— 一个 IPv4 socket 文件描述符,一个指向 sockaddr_in 结构的指针,该结构的长度;listen— 一个socket 文件描述符和一个整数。

Variants$ 符号是为了区分syscall及其变体,其参数有不同的生成规则。例如,socket 可用于创建很多类型的socket。syzkaller 定义了一些常见 socket 类型的变体,例如 TCP/UDP:

socket$inet_tcp(domain const[AF_INET], type const[SOCK_STREAM],
                proto const[0]) sock_tcp
socket$inet_udp(domain const[AF_INET], type const[SOCK_DGRAM],
                proto const[0]) sock_udp

// 其他没有特定描述的 socket 变体
socket(domain flags[socket_domain], type flags[socket_type],
       proto int32) sock
socket$inet(domain const[AF_INET], type flags[socket_type],
            proto int32) sock_in

socket_domain = AF_UNIX, AF_INET, AF_INET6, AF_NETLINK, ...
socket_type = SOCK_STREAM, SOCK_DGRAM, SOCK_RAW, SOCK_RDM, ...
(2)Resources

表示不同syscall之间的相关性,例如,socket$inet返回 sock_in resource,bind$inet 接收该 resource 作为参数:

socket$inet(domain const[AF_INET], type flags[socket_type],
            proto int32) sock_in
bind$inet(fd sock_in, addr ptr[in, sockaddr_in], addrlen len[addr])

Inheritance:以上示例定义了3个socket相关的 resource 类型,存在继承关系。

resource sock[fd]
resource sock_in[sock]
resource sock_tcp[sock_in]

当生成程序时,syzkaller更有可能使用syscall定义中的resource类型,也可以使用指定type的父类型或子类型。继承的 resource 是由同一 syscall 的不同变体所返回:

socket(...) sock
socket$inet(...) sock_in
socket$inet_tcp(...) sock_tcp
(3)Types

常见的类型有,const — 常量,int32 — 4-byte整数,flags — 位flag的组合或者 enum 选项,len — 表示某个域成员的长度。

Pointer:指针类型包含其指向的对象类型的信息,例如,bind$inet 调用接收一个指向 sockaddr_in 结构的指针作为第2个参数:

bind$inet(fd sock_in, addr ptr[in, sockaddr_in], addrlen len[addr])

Data-flow:指针类型需要指定数据流方向,表示需要从指向的对象读取还是写入。bind$inet 调用中,in 表示 bind 要从 sockaddr_in 读取数据,所以在执行该调用之前 syzkaller 需要填充该结构的域。

Out pointersout 表示syscall 需要向该结构写入数据,例如,accept$inet 需要将连接对的信息存入第2个参数。

accept$inet(fd sock_in, peer ptr[out, sockaddr_in, opt],
            peerlen ptr[inout, len[peer, int32]]) sock_in
// 第2个参数中的 opt 表示该指针可选,syzkaller 可以不提供该参数。

syscall可以使用out指针来通过结构域成员来返回 resource,syzkaller 就知道可以将该resource 传递给下一个syscall了。注意,对于 accept$inet 调用,sockaddr_in 不包含resource,因而 out 标记不会有任何作用,syzkaller在执行syscall之前不会填充该域。 inout 标记表示该syscall会读写该指针。

(4)Structures and unions

Structures:以 sockaddr_in 结构为例,包含3个成员,size[16] 表示该结构会被补0到16字节。由于 sock_port 类型也会在其他地方用到,所以该类型需要单独定义,int32be中的 be 表示该整型是大端的,20000:20004 表示该整数的取值范围。

sockaddr_in {
	family	const[AF_INET, int16]
	port	sock_port
	addr	ipv4_addr
} [size[16]]

type sock_port int16be[20000:20004]

Unions:以 ipv4_addr 结构为例,当生成IPv4地址时,syzkaller 从这个 union中选择一个地址。

ipv4_addr [
# Random public addresses 100.1.1.[0-2]:
	rand_addr	int32be[0x64010100:0x64010102]
# 0.0.0.0:
	empty		const[0x0, int32be]
# LOCAL_IPV4/REMOTE_IPV4/DEV_IPV4 in executor/common_linux.h:
	local		ipv4_addr_t[const[170, int8]]
	remote		ipv4_addr_t[const[187, int8]]
	dev		ipv4_addr_t[netdev_addr_id]
	initdev		ipv4_addr_initdev
# 127.0.0.1:
	loopback	const[0x7f000001, int32be]
# 224.0.0.1:
	multicast1	const[0xe0000001, int32be]
# 224.0.0.2:
	multicast2	const[0xe0000002, int32be]
# 255.255.255.255:
	broadcast	const[0xffffffff, int32be]
# 10.1.1.[0-2] can be used for custom things within the image:
	private		int32be[0xa010100:0xa010102]
] [size[4]]
(5)Programs

当syzkaller生成syscall序列时,会根据参数类型来填充调用参数。示例如下,0x7f0000001000 表示sockaddr_in 结构位于偏移 0x1000 处,{} 中表示该结构的域成员,0x0 表示端口 20000(距离范围的下确界的偏移)。

r0 = socket$inet_tcp(0x2, 0x1, 0x0)
bind$inet(r0, &(0x7f0000001000)={0x2, 0x0, @empty=0x0}, 0x10)
listen(r0, 0x5)

4-3 Adding a syscall for packet injection

问题:如果只用 TUN/TAP ioctl 来写调用模板,可能导致只fuzz 了 TUN/TAP 代码本身,syzkaller 会对ioctl的所有参数进行变异并对ioctl随机排序,很难恰好将 /dev/tun 设置成 TAP 模式(注入packet的前提)。所以目标是使syzkaller 正确初始化 TUN/TAP 并注入packet。

(1)Pseudo-syscalls 介绍

介绍:这是syzkaller一个有用的特性,可参考 seudo-syscalls,可以组合多个syscall。

示例:以 syz_opev_dev$loop 为例(syzkaller中还定义了很多其他的 pseudo syscall,都是以 syz_ 开头),它包含两个部分,一是syzlang描述,二是C实现:

// syzlang 描述, dev表示驱动名, id表示驱动id, flags是open调用的flag
syz_open_dev$loop(dev ptr[in, string["/dev/loop#"]],
                  id intptr, flags flags[open_flags]) fd_loop
// C 实现, 用ID 替换驱动名中的 # 字符, 并用提供的 flag 来打开设备文件
// Pseudo-code.
int syz_open_dev(device, id, flags) {
	device = device.replace("#", string(id));
	return open(device, flags);
}

注意,syz_opev_dev$loop 只是 syz_open_dev 的一个变体,所有的变体都有相同的C实现,该C实现的定义位于 syz-executordefined 处。syzkaller 不会改变 pseudo syscall C 代码实现的逻辑。

漏洞示例:某个loop设备中的 found 漏洞对应的 reproducer 中的某段代码如下,参数就是按照 pseudo syscall 来生成的。

r0 = syz_open_dev$loop(&(0x7f0000000140)='/dev/loop#\x00', 0x0, 0x1)
ioctl$LOOP_SET_DIRECT_IO(r0, 0x4c05, 0x0)
(2)Pseudo-syscall for TUN/TAP

通过TUN/TAP 注入packet需要两步,一是将接口设置为 raw TAP 模式,二是通过该接口写 TUN/TAP 文件来发送packet。所以需要添加两个 Pseudo-syscall,不过第一步不需要执行很多次。

TUN/TAP setup:第一步的Pseudo-syscall定义位于 here 代码处,只需全局执行一次(在syz-executor开始时执行1次即可)。

static int tunfd = -1;

// This function is indirectly called from syz-executor's main().
static void initialize_tun(void)
{
	tunfd = open("/dev/net/tun", O_RDWR | O_NONBLOCK);

	// Call the TUNSETIFF ioctl and do other TUN/TAP setup here.
}

Pseudo-syscall:第二步的Pseudo-syscall叫做 syz_emit_ethernet,参数是 packet 和 length,其C代码实现位于 heresyz_emit_ethernet 将 packet 写入打开的 /dev/tun。 注意,我们不能在syzlang语法中定义 write$tun 来接受 tunfd 参数,因为 tunfd 不存在于syzlang描述中,而是在 syz-executor 的C代码实现中。

static long syz_emit_ethernet(volatile long a0, volatile long a1)
{
	uint32 length = a0;
	char *data = (char *)a1;

	return write(tunfd, data, length);
}

在对应的syzlang描述中,我们最开始定义 syz_emit_ethernet 接受一段随机数据作为packet。array[int8] 表示一段随机数据,显然随机生成的 packet 不能穿透更深的代码。

syz_emit_ethernet(len len[packet], packet ptr[in, array[int8]])

4-4 Inspecting coverage #1

之前修改了TUN/TAP来注入packet,现在可以开始fuzz了,通过KCOV来收集代码覆盖,不需要额外修改syzkaller。

First run:只允许 syz_emit_ethernet 来进行fuzz,可参考 code coverage report 获取代码覆盖。可以看到fuzzer卡在了 ip_rcv_core(),负责处理接收IP packet 的函数。

请添加图片描述

粗黑表示已覆盖的基本块,红色表示未覆盖的基本块。由于未经过 __IP_INC_STATS(),说明已通过 if (!skb) 检查;由于未经过 if (iph->ihl < 5 || iph->version != 4),说明 pskb_may_pull() 校验失败,进入 goto inhdr_error 分支。 这说明生成的 packet 过短,不包含 iphdr 结构。

Second run:经过syzkaller的长时间运行,能够通过if (iph->ihl < 5 || iph->version != 4)检查(syzkaller的 comparison operands collection 机制有助于通过这个检查),但是还是会卡住。因此,随机生成 packet 并不可行。

注意,还有一种方法可以分析为什么syzkaller 卡住了,你先获取一个能够 reaches a particular basic block 但是卡住了的程序,然后手动执行—manually,观察内核执行到什么位置,可以往内核代码添加 pr_err() 语句,或者使用 perf-tools 工具或者其他调试工具。

4-5 escribing packet structure——描述 packet 结构

为了能fuzz更深处,需要研究一下packet结构,具体的模板描述可参见 vnet - syzlang descriptions 。作者研读了 RFCs(包含很多互联网协议,如 IPv4, IPv6, TCP, UDP 等)。

请添加图片描述

Ethernet:修改 syz_emit_ethernet 的第2个参数指向 eth_packet 结构(描述Ethernet frame)。Ethernet frame 包含目的和源MAC地址、可选的 VLAN 标记、EtherType(表示payload中的协议类型)、payload。eth_packet 包含 Ethernet frame 的一些域成员和 eth2_packet payload。

syz_emit_ethernet(len len[packet], packet ptr[in, eth_packet])
    
eth_packet {
	dst_mac	mac_addr
	src_mac	mac_addr
	vtag	optional[vlan_tag]
	payload	eth2_packet
} [packed]		// [packed] 表示结构中的成员不需要pad对齐

MAC:如果目标MAC地址是随机生成的,则 TUN/TAP 接口会丢弃这些包,所以作者将 mac_addr 定义成一个union 结构,其中包含选项 LOCAL_MAC 地址(TUN/TAP 的接口地址)。 mac_addr_t[LAST] 表示模板,当用作 mac_addr_t[const[0xaa, int8]] 时,syzlang 编译器会创建 mac_addr_t 结构,并将 LAST 替换成 const[0xaa, int8] (也即将最后一个字节替换成指定字节)。

type mac_addr_t[LAST] {
	a0	array[const[0xaa, int8], 5]
	a1	LAST
} [packed]

mac_addr [
	empty		array[const[0x0, int8], 6]
# These match LOCAL_MAC/REMOTE_MAC in executor/common_linux.h:
	local		mac_addr_t[const[0xaa, int8]] 		// 注意: local / remote 这两个mac地址已经在 executor/common_linux.h 中定义过了, 并且在 initialize_tun() 函数中进行的初始化(将本机mac地址修改为 local, 创建邻近路由地址为 remote)
	remote		mac_addr_t[const[0xbb, int8]]
	dev		mac_addr_t[netdev_addr_id]
	broadcast	array[const[0xff, int8], 6]
	multicast	array[const[0xbb, int8], 6]
	link_local	mac_addr_link_local
	random		array[int8, 6]
]

More EthernetEthernet frame 第2部分的 eth2_packet 结构包含 EtherType 和高层协议(例如,ARP / IPv4 等)。 [varlen] 表示实际生成可执行程序时所选选项的union结构的实际大小,没有这个标记的话,union的大小为最大选项的大小。

eth2_packet [
	generic	eth2_packet_generic
	arp	eth2_packet_t[ETH_P_ARP, arp_packet]
	ipv4	eth2_packet_t[ETH_P_IP, ipv4_packet]
	ipv6	eth2_packet_t[ETH_P_IPV6, ipv6_packet]
	llc	eth2_packet_t[ETH_P_802_2, llc_packet]
	llc_tr	eth2_packet_t[ETH_P_TR_802_2, llc_packet]
	x25	eth2_packet_t[ETH_P_X25, x25_packet]
	mpls_uc	eth2_packet_t[ETH_P_MPLS_UC, mpls_packet]
	mpls_mc	eth2_packet_t[ETH_P_MPLS_MC, mpls_packet]
	can	eth2_packet_t[ETH_P_CAN, can_frame]
	canfd	eth2_packet_t[ETH_P_CANFD, canfd_frame]
] [varlen]

type eth2_packet_t[TYPE, PAYLOAD] {
	etype	const[TYPE, int16be]
	payload	PAYLOAD
} [packed]

IPv4:IPv4 packet 包含 TCP / UDP 或其他payload。

ipv4_packet [
	generic	ipv4_packet_t[flags[ipv4_types, int8], array[int8]]
	tcp	ipv4_packet_t[const[IPPROTO_TCP, int8], tcp_packet]
	udp	ipv4_packet_t[const[IPPROTO_UDP, int8], udp_packet]
	icmp	ipv4_packet_t[const[IPPROTO_ICMP, int8], icmp_packet]
	dccp	ipv4_packet_t[const[IPPROTO_DCCP, int8], dccp_packet]
	igmp	ipv4_packet_t[const[IPPROTO_IGMP, int8], igmp_packet]
	gre	ipv4_packet_t[const[IPPROTO_GRE, int8], gre_packet]
] [varlen]

// ipv4_packet_t 表示一个结构模板,包含 IPv4 头和高层 payload
type ipv4_packet_t[PROTO, PAYLOAD] {
	header	ipv4_header[PROTO]
	payload	PAYLOAD
} [packed]

type ipv4_header[PROTO] {
	ihl		bytesize4[parent, int8:4]
	version		const[4, int8:4]
	ecn		int8:2
	dscp		int8:6
	total_len	len[ipv4_packet_t, int16be]
	id		int16be[100:104]
	frag_off	int16be[0:0]
	ttl		int8
	protocol	PROTO
# Use a dummy value for the checksum for now:
	csum		int16
	src_ip		ipv4_addr
	dst_ip		ipv4_addr
	options		ipv4_options
} [packed]

TCP:最终,一个 TCP packet 包含一个 header 和一个用户层协议数据(用随机字节组成的数组来表示)。

tcp_packet {
	header	tcp_header
	payload	tcp_payload
} [packed]

tcp_header {
	src_port	sock_port
	dst_port	sock_port
# Use dummy values for the sequence numbers for now:
	seq_num		int32
	ack_num		int32
	ns		int8:1
	reserved	const[0, int8:3]
	data_off	bytesize4[parent, int8:4]
	flags		flags[tcp_flags, int8]
	window_size	int16be
# Use a dummy value for the checksum for now:
	csum		int16
	urg_ptr		int16be
	options		tcp_options
} [packed]

tcp_payload {
	payload	array[int8]
} [packed]

4-6 Inspecting coverage #2

作者在添加syzlang描述的过程中,不断查看代码覆盖,发现还是会在 ip_rcv_core() 中卡住:

请添加图片描述

可以看到,覆盖了 __IP_ADD_STATS(),但没有覆盖 ntohs(),所以在这两个语句之间退出,要么是 goto inhdr_error 要么是 goto csum_error,通过观察程序结尾可以发现覆盖了 csum_error:,所以问题出在校验和失败。因为作者在定义 IPv4 packet 时,将校验值定义为一个随机的 int16 值:

type ipv4_header[PROTO] {
# ...
	csum		int16
# ...
} [packed]

4-7 Dealing with checksums

思路:移除校验代码或者加一个配置选项,但是内核主线肯定不接受(我可以自己编译内核时移除啊);只能计算并固化校验值了。

校验分类:有两种类型的校验。

  • IPv4 checksum:这一类属于 Internet Checksum,用于 IPv4 头。 补码和?

    • The checksum field is the 16-bit one’s complement of the one’s complement sum of all 16-bit words in the header. For purposes of computing the checksum, the value of the checksum field is zero.
  • TCP checksum:这一类属于 pseudo-header checksum,用于高层协议,例如TCP,其计算过程更复杂。其计算涉及到 pseudo-headerTCP segment length,其中,pseudo-header 是从 IPv4 头中取值填充而成的,所以这种校验值计算包括了 pseudo-header / TCP header / TCP payload

请添加图片描述

Integrating checksums:经过很多 series of pull requests,作者为syzlang添加了两种 checksum 类型,使syzkaller的 program generator 计算并嵌入 checksum(新引入了 csum 类型)。

type ipv4_header[PROTO] {
# ...
	csum		csum[parent, inet, int16be]
# ...
} [packed]

tcp_header {
# ...
	csum		csum[tcp_packet, pseudo, IPPROTO_TCP, int16be]
# ...
} [packed]

这需要改变syzkaller 填充参数值的方式 — here,syzkaller默认是一个一个填充域成员,但是无法计算checksum(因为checksum必须在其他域成员填充完成后才能计算出来),所以修改了syzkaller 以填充checksum — last。syzkaller 在填充checksum之前需分析所生成的程序—analyze,例如在计算TCP checksum之前需 builds the pseudo-header

注意,其他协议(除了TCP/IPv4)也需要计算checksum,详情见vnet.txt。syzkaller中,syz-fuzzersyz-execprog 会把生成的程序序列化 serialize 为指令,这些指令告诉 syz-executor 可以使用哪些值来填充结构filling in structures,何时需要计算校验和calculate checksums,以及调用哪个syscall syscalls to callsyz-prog2c 采用了类似的机制 similar mechanism。 除了checksum,作者还添加了一些定义,例如 big-endian integers int16be, bitfields int8:1, per-process integers proc[20000, 4, int16be],有利于描述网络包。这些新的syzlang特性需要修改Go和C。

4-8 Getting first crashes

First bug:第一个bug其实是 slab-out-of-bounds in sctp_sf_ootb,对应的修复 sctp: validate chunk len before actually using it,但是没有程序可以复现。(PS:CVE-2016-9555 被评为10分,但是其实危害并不大)

Another bug:这个漏洞可以参考 syzbot report,需要 IPv6 支持:

syz_emit_ethernet(0xfdef, &(0x7f00000001c0)={
  @local={[0xaa, 0xaa, 0xaa, 0xaa, 0xaa], 0xaa},
  @dev={[0xaa, 0xaa, 0xaa, 0xaa, 0xaa]}, [], {
    @ipv6={
      0x86dd, {0x0, 0x6, "50a09c", 0xfdb9, 0x0, 0x0,
      @remote={0xfe, 0x80, [], 0xbb}, @local={0xfe, 0x80, [], 0xaa},
      {[], @udp={0x0, 0x0, 0x8}}}
    }
  }
}, &(0x7f0000000040))

这个poc会导致内核在 XFRM policy 代码中陷入死循环,报错如下所示,可见程序起源于 drivers/net/tun.c,最后执行到 net/ipv6/xfrm6_policy.c 触发漏洞,漏洞修复于 xfrm6: avoid potential infinite loop in _decode_session6(),不算远程代码执行,但可以远程拒绝服务攻击。

watchdog: BUG: soft lockup - CPU#1 stuck for 134s! [syz-executor738:4553]
Call Trace:
 _decode_session6+0xc1d/0x14f0 net/ipv6/xfrm6_policy.c:150
 __xfrm_decode_session+0x71/0x140 net/xfrm/xfrm_policy.c:2368
 xfrm_decode_session_reverse include/net/xfrm.h:1213 [inline]
 icmpv6_route_lookup+0x395/0x6e0 net/ipv6/icmp.c:372
 icmp6_send+0x1982/0x2da0 net/ipv6/icmp.c:551
 icmpv6_send+0x17a/0x300 net/ipv6/ip6_icmp.c:43
 ip6_input_finish+0x14e1/0x1a30 net/ipv6/ip6_input.c:305
 NF_HOOK include/linux/netfilter.h:288 [inline]
 ip6_input+0xe1/0x5e0 net/ipv6/ip6_input.c:327
 dst_input include/net/dst.h:450 [inline]
 ip6_rcv_finish+0x29c/0xa10 net/ipv6/ip6_input.c:71
 NF_HOOK include/linux/netfilter.h:288 [inline]
 ipv6_rcv+0xeb8/0x2040 net/ipv6/ip6_input.c:208
 __netif_receive_skb_core+0x2468/0x3650 net/core/dev.c:4646
 __netif_receive_skb+0x2c/0x1e0 net/core/dev.c:4711
 netif_receive_skb_internal+0x126/0x7b0 net/core/dev.c:4785
 napi_frags_finish net/core/dev.c:5226 [inline]
 napi_gro_frags+0x631/0xc40 net/core/dev.c:5299
 tun_get_user+0x3168/0x4290 drivers/net/tun.c:1951
 tun_chr_write_iter+0xb9/0x154 drivers/net/tun.c:1996
 call_write_iter include/linux/fs.h:1784 [inline]
 do_iter_readv_writev+0x859/0xa50 fs/read_write.c:680
 do_iter_write+0x185/0x5f0 fs/read_write.c:959
 vfs_writev+0x1c7/0x330 fs/read_write.c:1004
 do_writev+0x112/0x2f0 fs/read_write.c:1039
 __do_sys_writev fs/read_write.c:1112 [inline]
 __se_sys_writev fs/read_write.c:1109 [inline]
 __x64_sys_writev+0x75/0xb0 fs/read_write.c:1109
 do_syscall_64+0x1b1/0x800 arch/x86/entry/common.c:287
 entry_SYSCALL_64_after_hwframe+0x49/0xbe

4-9 Inspecting coverage #3

再次查看代码覆盖,发现卡在了 TCP 模式下。当TCP packet 过来的时候,内核需将它路由到某个在指定端口监听的应用程序,如果找不到该端口,则丢弃该包。

请添加图片描述

4-10 Opening a TCP socket

解决 4-9 的办法就是,在执行 syz_emit_ethernet 之前,需要 open 和 bind socket。

Enabling syscalls:syzkaller已经实现了一些socket相关的 syscall 描述,只需要在配置文件中设置即可,允许socket$inet_tcp / bind$inet / listen,即可开始fuzz。

请添加图片描述

Success:之后 syzkaller 就能成功串联起 open socket 和通过 syz_emit_ethernetsend packet。最后发现以下程序成功穿透了 __inet_lookup_skb()

# Create a socket and bind it to a port.
r0 = socket$inet_tcp(0x2, 0x1, 0x0)
bind$inet(r0, &(0x7f0000001000)={0x2, 0x0, @empty=0x0,
          [0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0]}, 0x10)
listen(r0, 0x5)

# Send a packet to the socket.
syz_emit_ethernet(0x36, &(0x7f0000002000)={
  @local={[0xaa, 0xaa, 0xaa, 0xaa, 0xaa], 0x0}, @random="4c6112cc15d8", [], {
    {0x800, @ipv4={
      {0x5, 0x4, 0x0, 0x0, 0x28, 0x0, 0x0, 0x0, 0x6, 0x0,
        @remote={0xac, 0x14, 0x0, 0xbb}, @local={0xac, 0x14, 0x0, 0xaa}, {[]}
      },
      @tcp={
        {0x1, 0x0, 0x41424344, 0x41424344, 0x0, 0x0, 0x5, 0x2, 0x0, 0x0, 0x0,
        {[]}}, {""}
      }
    }}
}})

Ports:问题是生成以上程序花费了很长时间,因为最开始,作者使用了随机的 int16be 值来表示端口号,导致syzkaller 很难给 bind$inetsyz_emit_ethernet 生成相同的端口。所以作者限制了端口号的取值:

type sock_port int16be[20000:20004]

4-11 Establishing a TCP connection

现在成功使syzkaller向 open socket 发包,怎么使 fuzzer 完全从外部连接到 TCP socket 呢?

TCP handshake:TCP 握手过程可参考 works。首先fuzzer需要发送 SYN 请求,收到 SYN/ACK,最后发送 ACK。

  • (1)第1个SYN请求需包含序列号 A,可使用任意数值;
  • (2)内核需通过 SYN/ACK 返回该序列号加1,SYN/ACK 回应还需包含一个内核产生的序列号 B;
  • (3)最后发送ACK请求,需包含 A+1 / B+1

syz_emit_ethernet 已经实现了发送 packet ,也即第1步的 SYN 部分,还需要实现接收 SYN/ACK packet,提取序列号,并用在 ACK packet 中。

请添加图片描述

New pseudo-syscall:实现新的 pseudo-syscall 来负责提取序列号,作者实现了 syz_extract_tcp_res ,可以从 TUN/TAP 接收包、提取seq/ack号,并加上某个值。

struct tcp_resources {
	uint32 seq;
	uint32 ack;
};

static long syz_extract_tcp_res(volatile long a0, volatile long a1,
				volatile long a2)
{
	char data[1000];
	size_t length = read_tun(&data[0], sizeof(data));

	if (length < sizeof(struct ethhdr))
		return -1;
	struct ethhdr *ethhdr = &data[0];

	if (ethhdr->h_proto != htons(ETH_P_IP))
		return -1;
	if (length < sizeof(struct ethhdr) + sizeof(struct iphdr))
		return -1;
	struct iphdr *iphdr = &data[sizeof(struct ethhdr)];

	if (iphdr->protocol != IPPROTO_TCP)
		return -1;
	if (length < sizeof(struct ethhdr) + iphdr->ihl * 4 +
		     sizeof(struct tcphdr))
		return -1;
	struct tcphdr *tcphdr = &data[sizeof(struct ethhdr) + iphdr->ihl * 4];

	struct tcp_resources *res = (struct tcp_resources *)a0;
	res->seq = htonl((ntohl(tcphdr->seq) + (uint32)a1));
	res->ack = htonl((ntohl(tcphdr->ack_seq) + (uint32)a2));

	return 0;
}

说明,本来只需要 seq 号加1即可,但作者加了 a1/a2,使fuzzer能够探索一些异常状态。

以上C代码对应的syzlang描述如下所示,为了简便, seq/ack 号都复用了相同的 tcp_seq_num resource。注意,syz_extract_tcp_res 通过指向struct的 out 指针返回 resource,而不是通过返回值。 syz_extract_tcp_res$synack 是一个 syz_extract_tcp_res 变体,目的是构造正确的TCP连接,将 seq 加1,但是 ack 不变。

resource tcp_seq_num[int32]: 0x41424344

tcp_resources {
	seq	tcp_seq_num
	ack	tcp_seq_num
}

# These pseudo-syscalls read a packet from /dev/net/tun and extract TCP
# sequence and acknowledgment numbers from it. They also adds the inc
# arguments to the returned values. This way sequence numbers get incremented.
syz_extract_tcp_res(res ptr[out, tcp_resources], seq_inc int32, ack_inc int32)
syz_extract_tcp_res$synack(res ptr[out, tcp_resources],
                           seq_inc const[1], ack_inc const[0])

TCP header

// 开始将 TCP header 中的 seq/ack 都设置成 int32 类型的值
tcp_header {
# ...
	seq_num		int32
	ack_num		int32
# ...
} [packed]
// 将 seq/ack 更新为新加入的 resource, 之后 syzkaller 在调用 syz_emit_ethernet 时就会采用 syz_extract_tcp_res 生成的 resource
tcp_header {
# ...
	seq_num		tcp_seq_num
	ack_num		tcp_seq_num
# ...
} [packed]

Connection established:引入 syz_extract_tcp_res,使得syzkaller 能够生成程序来设置socket 并在外部 connect 进去(以下示例可能过时了,需要更新一下)。

# Create a socket and put it into the listening state.
r0 = socket$inet_tcp(0x2, 0x1, 0x0)
bind$inet(r0, &(0x7f0000001000)={0x2, 0x0, @empty=0x0,
          [0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0]}, 0x10)
listen(r0, 0x5)

# Send a SYN request to the socket externally.
syz_emit_ethernet(0x36, &(0x7f0000002000)={
  @local={[0xaa, 0xaa, 0xaa, 0xaa, 0xaa], 0x0}, @random="4c6112cc15d8", [], {
    {0x800, @ipv4={
      {0x5, 0x4, 0x0, 0x0, 0x28, 0x0, 0x0, 0x0, 0x6, 0x0,
        @remote={0xac, 0x14, 0x0, 0xbb}, @local={0xac, 0x14, 0x0, 0xaa}, {[]}
      },
      @tcp={
        {0x1, 0x0, 0x41424344, 0x41424344, 0x0, 0x0, 0x5, 0x2, 0x0, 0x0, 0x0,
        {[]}}, {""}
      }
    }}
}})

# Receive a SYN/ACK response externally and increment SYN number by 1.
# Ignore 0x41424344, those are defaults if extraction fails.
syz_extract_tcp_res$synack(
        &(0x7f0000003000)={<r1=>0x41424344, <r2=>0x41424344}, 0x1, 0x0)

# Send an ACK to the socket externally.
# Reuse the received sequence numbers, but swap the order.
syz_emit_ethernet(0x38, &(0x7f0000004000)={
  @local={[0xaa, 0xaa, 0xaa, 0xaa, 0xaa], 0x0},
  @remote={[0xbb, 0xbb, 0xbb, 0xbb, 0xbb], 0x0}, [], {
    {0x800, @ipv4={
      {0x5, 0x4, 0x0, 0x0, 0x2a, 0x0, 0x0, 0x0, 0x6, 0x0,
        @remote={0xac, 0x14, 0x0, 0xbb}, @local={0xac, 0x14, 0x0, 0xaa}, {[]}
      },
      @tcp={
        {0x1, 0x0, r2, r1, 0x0, 0x0, 0x5, 0x10, 0x0, 0x0, 0x0,
        {[]}}, {"0c10"}
      }
    }}
}})

# Now, the TCP hansdhake is done. Accept the connection on the socket side.
r3 = accept$inet(r0, &(0x7f0000005000)={
        0x0, 0x0, @multicast1=0x0, [0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0]},
        &(0x7f0000006000)=0x10)

以上程序执行后,就能建立连接,可用 netstat 命令查看:

Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0 0.0.0.0:20000           0.0.0.0:*               LISTEN
tcp        2      0 172.20.0.170:20000      172.20.0.187:20001      ESTABLISHED

分析,syzkaller 本来不知道在调用 syz_emit_ethernet 发送 SYN 之后,要调用 syz_extract_tcp_res,但是我们定义的resource 很好的将这两个调用联系在了一起。 注意,udp2 创建连接的过程更简单。

4-12 Avoiding ARP traffic

问题:在创建TCP连接时,syz_extract_tcp_res$synack 没有收到 TCP SYN/ACK 包,而是收到了一个 ARP packet (地址解析协议,当内核收到一个新IP地址发来的包时,需要先解析远程主机的物理地址,才能继续回应)。

Assigned IP:为了避免不必要的 ARP traffic,作者指定了一个IP地址 REMOTE_IPV4,并更新了 TUN/TAP setup 代码,将该地址加入到邻居——add this address to neighbors。以下代码采用了 IP utility 来简化,syzkaller 实际上采用的是 netlink sockets

#define IFACE       "syz_tun"
#define LOCAL_MAC   "aa:aa:aa:aa:aa:aa"
#define REMOTE_MAC  "aa:aa:aa:aa:aa:bb"
#define LOCAL_IPV4  "172.20.20.170"
#define REMOTE_IPV4 "172.20.20.187"

// Assign MAC and IP addresses.
execute_command("ip link set dev %s address %s", IFACE, LOCAL_MAC);
execute_command("ip addr add %s/24 dev %s", LOCAL_IPV4, IFACE);

// Add a neighbour to avoid unnecessary ARP traffic.
execute_command("ip neigh add %s lladdr %s dev %s nud permanent",
		REMOTE_IPV4, REMOTE_MAC, IFACE);

// Activate the interface.
execute_command("ip link set %s up", IFACE);

作者还将 REMOTE_IPV4 加入到了 ipv4_addr union 结构中 — 参见 here

type ipv4_addr_t[LAST] {
	a0	const[0xac, int8]	# 172
	a1	const[0x14, int8]	# 20
	a2	const[0x14, int8]	# 20
	a3	LAST
} [packed]

ipv4_addr [
# These match LOCAL_IPV4 and REMOTE_IPV4 in executor/common_linux.h:
	local		ipv4_addr_t[const[170, int8]]
	remote		ipv4_addr_t[const[187, int8]]
# random, empty, loopback, ...
] [size[4]]

这样一来,当内核收到从 REMOTE_IPV4 发来的 TCP 请求时,内核已经知道了host的物理地址,跳过ARP请求

4-13 Adding IPv6 support

Extensions:作者扩展了 TUN/TAP setup 代码 —— setting up IPv6 addresses,并将 ipv6_addripv6_packet 的定义添加到了网络包描述中。作者还扩展了 checksum 计算代码来支持 IPv6-based pseudo-headers,为了避免 ARP traffic,作者 还将 REMOTE_IPV6 添加到了 ipv6_addr

为了避免过多的 traffic 流量,作者还关闭了 IPv6 Duplicate Address Detection and Router Solicitation,作者没能关闭 IPv6 MTD

TCP:以上操作可以使syzkaller在IPv6上建立连接——参见 here。以下示例代码可能也需要更新。

r0 = socket$inet6_tcp(0xa, 0x1, 0x0)
bind$inet6(r0, &(0x7f0000000000)={...}, 0x1c)
listen(r0, 0x5)
syz_emit_ethernet(0x4a, &(0x7f0000001000)={...})
syz_extract_tcp_res$synack(
        &(0x7f0000002000)={<r1=>0x41424344, <r2=>0x41424344}, 0x1, 0x0)
syz_emit_ethernet(0x4a, &(0x7f0000003000)={..., r2, r1, ...})
r3 = accept$inet6(r0, &(0x7f0000004000)={...}, &(0x7f0000005000)=0x1c)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp6       0      0 :::20001                :::*                    LISTEN
tcp6       0      0 fe80::aa:20001          fe80::bb:20000          ESTABLISHED

IPv6 support 是有限的,参见 6. Things to improve

4-14 Other notable things

syzbot integration:本文功能整合进syzkaller之后,syzbot 一直在挖掘 remote network bus。

Double-sided fuzzing:syzkaller可以从两端来fuzz network,一是从用户空间执行syscall,二是从外部发送网络包。这样就能发现一些有趣的漏洞,当在用户空间建立一个奇怪的socket,然后从外部往该socket发包触发漏洞,参见 5-2 Showcases

Isolation:syzkaller可以在单个VM中发起多个fuzz进程,所以进程之间需要隔离。方法是在每个fuzz进程中使用不同的命名空间——参见 separate network namespace,这样代码覆盖也是隔离的,因为KCOV能搜集到每个进程的代码覆盖。

Packet descriptions:本文展示的包描述只占 vnet.txt 的一部分,还有很大的提升空间,参见 6. Things to improve

Fragmentation:作者曾尝试将分片 packet 发给 TUN/TAP —— commit,但这个功能被禁止了(disabled),因为未知原因 UDP包被 rejected (issue),这就是为什么 syz_emit_ethernetdefinition 包含第三个参数。

syz_emit_ethernet(len len[packet], packet ptr[in, eth_packet],
                  frags ptr[in, vnet_fragmentation, opt])

KMSANAlexander PotapenkoMSAN 中添加了 checks 来挖掘信息泄露漏洞(当内核在网络上发送未初始化数据时),但是至今没能挖到 info-leak。

Socket descriptions:作者还优化了现有的 socket 相关的 syscall 描述,并找到了很多漏洞,包括 CVE-2016-9793 / CVE-2017-6074 / CVE-2017-1000112 ,最出名的是 CVE-2017-7308 —— Exploiting the Linux kernel via packet sockets

5. Found bugs

5-1 Bugs in TUN/TAP

TUN/TAP 本身的漏洞如下,需要修复才能正常 fuzz network。

BugFix
KASAN: wild-memory-access Read in skb_copy_ubufstun: make tun_build_skb() thread safe
WARNING in tun_get_usernet-backports: tun: relax check on eth_get_headlen() return value
WARNING in xdp_rxq_info_unregtun: avoid calling xdp_rxq_info_unreg() twice
WARNING: lock held when returning to user space in tun_get_usertun: add a missing rcu_read_unlock() in error path
KASAN: use-after-free Read in eth_type_transtun: correct header offsets in napi frags mode

5-2 Remote bugs

Manual:作者手动运行 syzkaller 并发现了一些漏洞。

BugFix
net/sctp: slab-out-of-bounds in sctp_sf_ootb [CVE-2016-9555]sctp: validate chunk len before actually using it
net/tcp: null-ptr-deref in __inet_lookup_listener/inet_exact_dif_matchnet: tcp: check skb is non-NULL for exact match on lookups
net/dccp: null-ptr-deref in dccp_v4_rcv/selinux_socket_sock_rcv_skbdccp: do not release listeners too soon
net/dccp: null-ptr-deref in dccp_parse_options[dccp: fool proof ccid_hc_rt]x_parse_options()
net/icmp: null-ptr-deref in icmp6_sendnet: handle no dst on skb in icmp6_send
ip6_gre: invalid reads in ip6gre_err() [CVE-2017-5897]ip6_gre: fix ip6gre_err() invalid reads
ipv4: null-ptr-deref in ipv4_pktinfo_prepare [CVE-2017-5970]ipv4: keep skb->dst around in presence of IP options

syzbot:当作者将功能整合到 syzkaller 之后,syzbot 自动报告了一些漏洞。

BugFix
inconsistent lock state in sk_clone_lock [repro 2017/08/14 13:35]tcp: fix possible deadlock in TCP stack vs BPF filter
kernel BUG at ./include/linux/skbuff.h [repro 2017/12/22 23:01]esp: Fix GRO when the headers not fully in the linear part of the skb.
possible deadlock in sch_direct_xmit [repro 2018/01/13 18:16]net: use listified RX for handling GRO_NORMAL skbs
general protection fault in arpt_do_table [repro 2018/02/21 23:19]netfilter: add back stackpointer size checks
KASAN: use-after-free Read in ip6_route_me_harder [repro 2018/02/27 16:52]netfilter: use skb_to_full_sk in ip6_route_me_harder
KMSAN: uninit-value in inet_getpeer [repro 2018/04/12 19:01]inetpeer: fix uninit-value in inet_getpeer
BUG: soft lockup in _decode_session6 [repro 2018/05/12 02:19]xfrm6: avoid potential infinite loop in _decode_session6()
WARNING: refcount bug in igmp_start_timer [repro 2018/12/22 21:42]net: use listified RX for handling GRO_NORMAL skbs
KASAN: stack-out-of-bounds Read in gue_err_proto_handler [repro 2019/01/07 18:02]fou6: Prevent unbounded recursion in GUE error handler
KASAN: slab-out-of-bounds Read in tick_sched_handle [repro 2019/01/14 00:40]fou: Prevent unbounded recursion in GUE error handler also with UDP-Lite
general protection fault in xfrmi_decode_session [repro 2019/04/16 19:31]xfrm: Reset secpath in xfrm failure
general protection fault in skb_queue_tail [repro 2019/04/23 13:33]rxrpc: fix race condition in rxrpc_input_packet()
KASAN: slab-out-of-bounds Read in skb_gro_receive [repro 2019/05/01 22:03] [CVE-2019-11683]udp: fix GRO packet of death
BUG: assuming atomic context at net/core/flow_dissector.c [repro 2019/05/13 03:56]flow_dissector: disable preemption around BPF calls
KASAN: use-after-free Read in napi_gro_frags [repro 2019/05/31 10:12]net-gro: fix use-after-free read in napi_gro_frags()
kernel BUG at include/linux/skbuff.h [repro 2019/08/19 23:22]net: ipv6: fix listify ip6_rcv_finish in case of forwarding
KMSAN: uninit-value in inet_ehash_insert [repro 2019/09/30 06:45]net-backports: ipv6: drop incoming packets having a v4mapped source address
general protection fault in ip6_sublist_rcv [repro 2019/10/24 22:21]inet: do not call sublist_rcv on empty list

More bugs:其实还有很多漏洞,本文只展示了一部分,你可以爬取 syzbot dashboard,只要在栈回溯中包含 tun_get_user() 或者 reproducer 中包含 syz_emit_ethernet 则为本文的改进所发现的漏洞。

CVEs:很多漏洞没有CVE,作者为手动找到的漏洞申请了CVE(只要不是 null-pointer-dereference),syzbot 找到的漏洞没有申请CVE,除了这个严重的漏洞——GRO packet of death

New bugs:可以看到,最近找到的漏洞 (recent bug) 定格在了2019年末,只有提升现有的 packet 描述才有可能发现新的漏洞。

(1)Showcases

来看看本文的改进所带来的一些漏洞发现。

GUE unbounded recursion:这个漏洞只需发送单个包就能导致拒绝服务,类似于之前提到的 XFRM 漏洞,参见 KASAN: slab-out-of-bounds Read in tick_sched_handlereproducer 如下所示,只展示大致结构,查看详细参数可以点进链接。 这是位于 Generic UDP Encapsulation (GUE) 代码中的内核栈溢出,参见多个patch(patch1 / patch2 / patch3,对IPv4/IPv6分别修复)。

syz_emit_ethernet(0x6a, &(0x7f00000000c0)={..., @icmp={...}, ...})

GRO packet of death:这个漏洞在上面已经提到过了,参见 KASAN: slab-out-of-bounds Read in skb_gro_receivereproducer 如下所示。 open 并 bind UDP socket,通过 UDP_GRO 选项来开启 Generic Receive Offload (GRO),从外部发包后触发漏洞,patch参见 udp: fix GRO packet of death。 该漏洞不确定能不能导致远程crash。

r0 = socket$inet(0x2, 0x2, 0x0)
bind(r0, &(0x7f0000000080)={...}, 0x7c)
setsockopt$inet_udp_int(r0, 0x11, 0x68, ...)
syz_emit_ethernet(0x2a, &(0x7f00000000c0)={..., @udp={...}, ...})

TCP vs BPF dead-lock:参见 inconsistent lock state in sk_clone_lockreproducer 如下所示,设置 TCP socket,创建外部连接,同时安装 BPF filter,漏洞是由 TCP ACK handler 和 BPF filter 导致的死锁。patch 参见tcp: fix possible deadlock in TCP stack vs BPF filter

r0 = socket$inet_tcp(0x2, 0x1, 0x0)
bind$inet(r0, &(0x7f0000001000)={...}, 0x10)
listen(r0, 0x8)
syz_emit_ethernet(0x3a, &(0x7f0000002000)={..., @tcp={...}, ...})
syz_extract_tcp_res(&(0x7f0000017000)={<r1=>0x42424242, <r2=>0x42424242}, ...)
setsockopt$SO_ATTACH_FILTER(r0, 0x1, 0x1a, &(0x7f0000017000-0x10)={...}, 0x10)
syz_emit_ethernet(0x36, &(0x7f0000004000)={..., @tcp={..., r2, r1, ...}, ...})

IPv6 routed hard:参见 KASAN: use-after-free Read in ip6_route_me_harderreproducer 如下所示。这个漏洞体现了 IPv6 support,先添加 IPv6 netfilter rule,设置 IPv6 TCP socket,然后发送 packet。添加 netfilter rule 是不需要额外的socket的,但是syzkaller并不知道。 patch 参见 netfilter: use skb_to_full_sk in ip6_route_me_harder —— Ipv6patch-IPv4

r0 = socket$inet6(0xa, 0x2, 0x0)
setsockopt$IP6T_SO_SET_REPLACE(r0, ...)
r1 = socket$inet6(0xa, 0x1, 0x0)
bind$inet6(r1, &(0x7f0000000640)={...}, 0x1c)
listen(r1, 0x2)
syz_emit_ethernet(0x4a, &(0x7f0000000100)={..., @ipv6={...}, ...})
(2)Impact

没有一个漏洞是很危险的,少数能导致远程拒绝服务,没有可以远程代码执行的。有一些内存破坏漏洞,如果没有信息泄露漏洞,则很难利用。KMSAN没有发现任何漏洞。

5-3 About that RCE

Bug:发现一个危险的内核栈溢出漏洞,但是只影响这个特定版本的内核,因为它有定制的网络协议扩展(导致漏洞)。这个栈溢出可以覆盖size和content,通常 Stack Protector 机制会阻止这类线性栈溢出的利用,除非能够提前泄露canary。

Suprise:作者关闭 KASAN 后尝试复现该漏洞,发现 Stack Protector 失效了,该内核版本还禁用了KASLR,所以能构造ROP提权,问题是你很难获取一个非公开的 kernel binary。这个漏洞和 CVE-2022-0435 很相似(exploit)。

6. Things to improve

More protocols:需要完善模板来支持更多协议,例如 SCTF,可以参见 vnet.txt 中的 TODO 部分。

IPv6 support:IPv6 的支持很有限,例如对 IPv6 Extension Headers 的支持很有限,头部很特殊(参见unusual way),只有扩展现有的 syzlang 语法才能描述—— next_header 域成员必须指定下一个header的类型,syz_extract_tcp_res 不能处理这些header (参见 doesn’t handle)。 另一个有效的改变就是,关闭IPv6 spam 以更好的隔离 syzkaller 程序,但这需要修改 syzkaller 和内核本身。

Better resources:优化 resource 可以使syzkaller更好的组装 syscall,生成有效的程序。例如,如果要支持SCTP协议,你需要扩展 syzlang 来允许将 SCTP cookies 作为 resource,并添加类似于 syz_extract_tcp_res 这样的 pseudo-syscallsyz_sctp_extract_res。 可以参见 4-11 Establishing a TCP connection 节。

Check KMSAN:KMSAN 在其他子系统找到很多 info-leak,但是在网络子系统中没找到,可能是没有正确检查 network buffer。

Reading code:如果你想深挖本文的代码,可以从最初的 pull request 开始学习,内容更详细具体,然后你需要阅读syzkaller 源码,主要代码位于 initialize_netdevices()syz_emit_ethernet() (参见 implementationsvnet.txt)。

Exercise:作者在构造TCP连接展示了 the programs 来作为测试,这些基于老版本的syzkaller,现在不管用了。现在需要新的程序进行测试,可以采用fuzz生成或者手动写。 syzkaller 把这类程序称为 runtests,可以用于检测模板描述是否有效。 在fuzz指定网络协议时,需要限制 syzkaller 只fuzz相关模块:添加导向性的 pseudo-syscall 变体,在packet描述中注释不需要的 payload,关闭常量变异。可以参考下 syzkaller 相关的 tips

7. Summary

请添加图片描述

Injecting network packets:利用 TUN/TAP 来将 packet 注入到内核。

Collecting coverage:采用KCOV来收集代码覆盖,并修改 TUN/TAP 代码来使KCOV能够收集到网络包解析代码中的代码覆盖。

Integrating into syzkaller:将以上两点整合到 syzkaller 中。作者研究了 syzkaller 的工作原理,如何写 syscall 描述,如何加入 pseudo-syscalls,然后如何一步步改进,使syzkaller能够探索到更深的代码。

Found bugs:作者最后列出了一些发现过的漏洞,还在一个特定版本的内核中发现RCE。

8. Afterword

Motivationexternal_fuzzing_network.txt 已经公开很久了,但是一直没有进行记录。写这篇文章也是为了激励大家来进行相关研究,例如挖掘 remote bugs

参考

Looking for Remote Code Execution bugs in the Linux kernel

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值