文章目录
- 1. Introduction
- 2. Injecting network packets — TUN/TAP
- 3. Collecting network coverage — KCOV
- 4. Integrating into syzkaller
- 4-1 About syzkaller
- 4-2 Syscall descriptions
- 4-3 Adding a syscall for packet injection
- 4-4 Inspecting coverage #1
- 4-5 escribing packet structure——描述 packet 结构
- 4-6 Inspecting coverage #2
- 4-7 Dealing with checksums
- 4-8 Getting first crashes
- 4-9 Inspecting coverage #3
- 4-10 Opening a TCP socket
- 4-11 Establishing a TCP connection
- 4-12 Avoiding ARP traffic
- 4-13 Adding IPv6 support
- 4-14 Other notable things
- 5. Found bugs
- 6. Things to improve
- 7. Summary
- 8. Afterword
- 参考
本文参考一下 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处于监听状态。
Arguments:socket$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 pointers:out
表示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-executor
的 defined 处。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代码实现位于 here ,syz_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 Ethernet:Ethernet 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-header
和TCP 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-fuzzer
和 syz-execprog
会把生成的程序序列化 serialize 为指令,这些指令告诉 syz-executor
可以使用哪些值来填充结构filling in structures,何时需要计算校验和calculate checksums,以及调用哪个syscall syscalls to call。syz-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_ethernet
来 send 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$inet
和 syz_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_addr 和 ipv6_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_ethernet
的 definition 包含第三个参数。
syz_emit_ethernet(len len[packet], packet ptr[in, eth_packet],
frags ptr[in, vnet_fragmentation, opt])
KMSAN:Alexander Potapenko 在 MSAN 中添加了 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。
5-2 Remote bugs
Manual:作者手动运行 syzkaller 并发现了一些漏洞。
syzbot:当作者将功能整合到 syzkaller 之后,syzbot 自动报告了一些漏洞。
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_handle。reproducer 如下所示,只展示大致结构,查看详细参数可以点进链接。 这是位于 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_receive,reproducer 如下所示。 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_lock,reproducer 如下所示,设置 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_harder ,reproducer 如下所示。这个漏洞体现了 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 —— Ipv6 ,patch-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-syscall
— syz_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()
(参见 implementations 和 vnet.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
Motivation:external_fuzzing_network.txt 已经公开很久了,但是一直没有进行记录。写这篇文章也是为了激励大家来进行相关研究,例如挖掘 remote bugs。