[syzkaller源码阅读] 0. Run & Read doc

出于毕设和研究生阶段的工作需要,我不得不尽早做完syzkaller的审计工作。为了方便复盘和分享,干脆边审边写blog了,挖个大坑。

start

syzkaller是目前最流行的内核模糊测试框架,要fuzz内核的话基本上没有别的更好的选择。

注:整个审计工作将保持在这个commit上进行:c799dfdd5648677612604d10e2c13075eda21582

如果你此前没有审计过go项目,也不是很懂fuzz,那么应该先对着readme给项目跑起来,再去读一遍文档,理解开发者的设计意图,做完了这些事情,才可以有底气去开始我们的审计。

运行第一个kernel fuzz实例并不困难,照着readme一步一步来就好了,大概的步骤是编译syzkaller -> 编译内核 -> 创建用户空间镜像 -> syzkaller config -> Run syz-manager, 主要参阅docs/setup.md, docs/usage.md

你可以从syzkaller的在线实例上获取现成的kernel config: syzbot

这是一个syzkaller config示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"target": "linux/amd64",
"http": "0.0.0.0:56741",
"workdir": "/home/pikaball/work/fuzz/v6.11",
"kernel_obj": "/home/pikaball/work/fuzz/linux",
"image": "/home/pikaball/work/fuzz/v6.11/bullseye.img",
"syzkaller": "/home/pikaball/work/fuzz/syzkaller",
"sshkey": "/home/pikaball/work/fuzz/v6.11/bullseye.id_rsa",
"procs": 12,
"type": "qemu",
"vm": {
"count": 4,
"cpu": 2,
"mem": 8192,
"kernel": "/home/pikaball/work/fuzz/linux/arch/x86/boot/bzImage"
}
}

syz-manager跑起来之后,访问你在配置文件中指定的webui地址,应该可以看到如下仪表盘:

syz-manager dashboard

多运行一段时间,确认vm没有爆出一些一看就不对的错误。

  • lost connection: ssh 出问题了,建议在syzkaller启动前用你获得的内核镜像和用户空间镜像手动起一个qemu,看看能不能走ssh连进去
  • no output from test machine: 多半是内核编译得不对,请确保采用了版本完全匹配的.config

总之,fuzz顺利启动了,接下来我们去从头到尾看一遍文档。

docs

syzkaller根目录的readme给了一些指引:

1
2
3
4
5
6
7
8
- [How to install syzkaller](docs/setup.md)
- [How to use syzkaller](docs/usage.md)
- [How syzkaller works](docs/internals.md)
- [How to install syzbot](docs/setup_syzbot.md)
- [How to contribute to syzkaller](docs/contributing.md)
- [How to report Linux kernel bugs](docs/linux/reporting_kernel_bugs.md)
- [Tech talks and articles](docs/talks.md)
- [Research work based on syzkaller](docs/research.md)

另外,docs目录下有一些子目录,对应不同os,看linux目录下的文件即可。

我们暂时只关心“Linux的fuzz工作是如何运作的”,别的都可以先不看。

整体结构

docs/internals.md介绍了syzkaller的运行结构.

docs/linux/internals.md -> [docs/linux/external_fuzzing_network.md , docs/linux/external_fuzzing_usb.md] 介绍了linux下的特殊工作结构,即关于如何fuzz network模块和USB模块,这部分将在后文提及,暂时跳过。

syzkaller的结构如下图所示。

syzkaller process structure

syzkaller的主要组件是syz-managersyz-executor, 前者在host上运行,控制整个fuzz工作,后者在vm上运行,负责在vm中执行程序并向manager发送反馈信息(报错,覆盖率等)。

在启动一个vm时,syz-manager通过ssh发送静态编译的executor并运行,之后,manager和executor将通过rpc协议进行通信。

syzkaller对vm的每一个输入都是由一串系统调用构成的程序,即syscall序列。

为什么是syscall序列?我们fuzz的对象是内核,目标是由用户态输入引发内核异常,而用户态和内核态的主要交互方式就是系统调用,至于其他交互方式,比如硬件上的插拔,这也可以使用自定义的syscall模拟,我们将在network和usb的部分看到这样的伪syscall。

syz-manager运行时会维护workdir,workdir中包含崩溃用例(crashes)和语料库(corpus),可以做这样一个简单描述:如果一个fuzz输入(syscall序列)运行时产生了任何告警或错误,那么就将其算作crash;如果一个fuzz输入产生了新的覆盖,那么就将其加入语料库。语料库的作用就是提供fuzz变异的种子,即在一个“有趣的用例”的基础上进一步测试。

syzlang

我们已经知道syzkaller在做的事是不断生成syscall序列发到虚拟机里执行,那么这些序列是如何被构造的呢?

syzkaller使用syscall description language(简称syzlang) 来描述系统调用的参数约束,以下是文档中给出的示例:

1
2
3
4
open(file filename, flags flags[open_flags], mode flags[open_mode]) fd
read(fd fd, buf buffer[out], count len[buf])
close(fd fd)
open_mode = S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IWGRP, S_IXGRP, S_IROTH, S_IWOTH, S_IXOTH

这个示例本身是过于简单的,可以在sys/$os下看到syzkaller预置的syzlang,比如在sys/linux/dev_fb.txt中,可以看到这样的规约:

1
2
3
4
5
6
openat$fb0(fd const[AT_FDCWD], file ptr[in, string["/dev/fb0"]], flags flags[open_flags], mode const[0]) fd_fb
openat$fb1(fd const[AT_FDCWD], file ptr[in, string["/dev/fb1"]], flags flags[open_flags], mode const[0]) fd_fb

write$fb(fd fd_fb, data ptr[in, array[int8]], len bytesize[data])
read$fb(fd fd_fb, data ptr[out, array[int8]], len bytesize[data])
mmap$fb(addr vma, len len[addr], prot flags[mmap_prot], flags flags[mmap_flags], fd fd_fb, off intptr[0:0x100000, 0x1000])

注意到系统调用名后有$符号,这是表示使用不同参数约束的syscall,例如上面的两个openat。(syzlang的详细语法描述可以参见docs/syscall_descriptions_syntax.md

这样做的好处显而易见,因为有些syscall的处理逻辑非常多,在不同功能/模块下需要不同的参数约束,典型的例子是ioctl,其行为完全取决于传入的cmd参数以及绑定的设备文件,我们在kernel中可以看到ioctl的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)
{
CLASS(fd, f)(fd);
int error;

if (fd_empty(f))
return -EBADF;

error = security_file_ioctl(fd_file(f), cmd, arg);
if (error)
return error;

error = do_vfs_ioctl(fd_file(f), fd, cmd, arg);
if (error == -ENOIOCTLCMD)
error = vfs_ioctl(fd_file(f), cmd, arg);

return error;
}

如果我们按照这个定义在syslang中直接这样写而不添加其变种:

1
ioctl(fd int32, cmd int32, arg int64)

……那就完蛋了,fuzz跑到猴年马月都出不了成果。举个例子,usb bus上的控制消息传递,需要一个指向usb设备的fd,特定的cmd值,并且把arg构造成指向一个urb的指针才可以进到相关逻辑。

而在syzlang中把这样的syscall拆分成带有更具体语义的变种,fuzz时就可以产生更多有效的输入和变异,提高fuzz效率。

syzkaller中内置的规约基本是完善的,虽然针对大量协议可以进一步做细化,但这个话题不在本系列文章的讨论范围,我们直接拿着用就好了。

coverage

在fuzz linux内核时,syzkaller依靠KCOV机制来完成覆盖率信息收集,覆盖率信息将会在更新语料库时起到参考作用。

启用选项CONFIG_KCOV=y,内核编译时会进行基本块插桩,当代码运行到这些位置就会进行kcov相关调用,返回程序运行时命中的地址,进而实现覆盖率记录。

在得到kcov信息后,syzkaller使用binutils将地址转换为源代码位置,docs/linux/coverage.md解释了这一步具体的操作流,此处略过。

在web dashboard上可以直观地查看kernel每一个源码文件的coverage(/cover)。

coverage

crash

所有fuzz工具的目标都是产生crash,至于如何从crash到exploit,这不在fuzz的讨论范围内。

引用一下机器猫先生的博客

归根结底,一个漏洞到底有没有价值,需要很多外部知识——例如,假如某个输入能让程序额外运行一秒钟,这对于 nginx 来说是巨大问题;但对于 checkpng 之类的小程序,便是无关紧要的。一个缓冲区 over-read 漏洞,对于 openssl 是致命的,但对于 gimp 这类程序而言,只是多了个让程序崩溃的 bug,大家并不特别关心其安全方面的危险。由于一个漏洞的价值难以衡量,笔者决定不钻研这个问题,仍以 crash 数量为衡量 fuzzer 好坏的第一标准。

report

syzkaller在发现crash后将其保存至workdir。

crash info

其中,description作为crash的名称,logN是syzkaller执行该用例时的日志以及vm中的输出信息,machineInfoN是执行该用例的vm配置信息,reportN则是在前两者基础上经过处理的crash报告。

reproduce

fuzz过程中会分出一部分虚拟机用于crash的复现(reproduce)。复现的意义在于,将原始触发crash的syscall序列尽可能简化,并验证该crash是否稳定可重复。

syzkaller提供了两个复现器,C reproducerSyz reproducer,优先使用后者。

fuzz运行一段时间后,dashboard上就可以看到crash的信息了:

crash

has repro表示使用Syz reproducer复现成功,提供一个syscall序列:

Syz repro

has C repro则对应C reproducer,提供一个C编写的程序。

C repro

此外,还可以使用syzkaller提供的工具进行手动reproduce并执行复现程序,参考docs/reproducing_crashes.md

tools

syzkaller提供了很多实用小工具,都在tools目录下。

syz-cover可以使用kcov信息(从web dashboard的/rawcover获取)生成可视化的coverage报告,syz-benchcmp可以接收两个覆盖率记录生成对比图,syz-db可以添加/移除corpus.db中的语料,其他的在这里就不一一列举了,可以自行探索。

summary

本文梳理了syzkaller的启动流程和文档信息,大致描述了syzkaller的运行结构,从文档初窥syzkaller的系统性设计思路。下一篇将正式开始源码审计,分析syzkaller的架构逻辑和策略实现。