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

多运行一段时间,确认vm没有爆出一些一看就不对的错误。
- lost connection: ssh 出问题了,建议在syzkaller启动前用你获得的内核镜像和用户空间镜像手动起一个qemu,看看能不能走ssh连进去
- no output from test machine: 多半是内核编译得不对,请确保采用了版本完全匹配的
.config
总之,fuzz顺利启动了,接下来我们去从头到尾看一遍文档。
docs
syzkaller根目录的readme给了一些指引:
1 | |
另外,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的主要组件是syz-manager和syz-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 | |
这个示例本身是过于简单的,可以在sys/$os下看到syzkaller预置的syzlang,比如在sys/linux/dev_fb.txt中,可以看到这样的规约:
1 | |
注意到系统调用名后有$符号,这是表示使用不同参数约束的syscall,例如上面的两个openat。(syzlang的详细语法描述可以参见docs/syscall_descriptions_syntax.md)
这样做的好处显而易见,因为有些syscall的处理逻辑非常多,在不同功能/模块下需要不同的参数约束,典型的例子是ioctl,其行为完全取决于传入的cmd参数以及绑定的设备文件,我们在kernel中可以看到ioctl的定义:
1 | |
如果我们按照这个定义在syslang中直接这样写而不添加其变种:
1 | |
……那就完蛋了,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)。

crash
所有fuzz工具的目标都是产生crash,至于如何从crash到exploit,这不在fuzz的讨论范围内。
引用一下机器猫先生的博客:
归根结底,一个漏洞到底有没有价值,需要很多外部知识——例如,假如某个输入能让程序额外运行一秒钟,这对于 nginx 来说是巨大问题;但对于 checkpng 之类的小程序,便是无关紧要的。一个缓冲区 over-read 漏洞,对于 openssl 是致命的,但对于 gimp 这类程序而言,只是多了个让程序崩溃的 bug,大家并不特别关心其安全方面的危险。由于一个漏洞的价值难以衡量,笔者决定不钻研这个问题,仍以 crash 数量为衡量 fuzzer 好坏的第一标准。
report
syzkaller在发现crash后将其保存至workdir。

其中,description作为crash的名称,logN是syzkaller执行该用例时的日志以及vm中的输出信息,machineInfoN是执行该用例的vm配置信息,reportN则是在前两者基础上经过处理的crash报告。
reproduce
fuzz过程中会分出一部分虚拟机用于crash的复现(reproduce)。复现的意义在于,将原始触发crash的syscall序列尽可能简化,并验证该crash是否稳定可重复。
syzkaller提供了两个复现器,C reproducer和Syz reproducer,优先使用后者。
fuzz运行一段时间后,dashboard上就可以看到crash的信息了:

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

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

此外,还可以使用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的架构逻辑和策略实现。