这是内核漏洞挖掘技术系列的第七篇。
第一篇:内核漏洞挖掘技术系列(1)——trinity
第二篇:内核漏洞挖掘技术系列(2)——bochspwn
第三篇:内核漏洞挖掘技术系列(3)——bochspwn-reloaded(1)
第四篇:内核漏洞挖掘技术系列(3)——bochspwn-reloaded(2)
第五篇:内核漏洞挖掘技术系列(4)——syzkaller(1)
第六篇:内核漏洞挖掘技术系列(4)——syzkaller(2)
在上一篇文章中我们聊了聊编写的系统调用模板被编译的过程,这篇文章从syz-manager入手分析,主要来看看crash复现的过程。syzkaller的使用是通过这条命令:./syz-manager -config=my.cfg
syzkaller\pkg\mgrconfig\testdata目录下提供的示例qemu.cfg如下。
{
"target": "linux/amd64",
"http": "myhost.com:56741",
"workdir": "/syzkaller/workdir",
"kernel_obj": "/linux/",
"image": "/linux_image/wheezy.img",
"sshkey": "/linux_image/ssh/id_rsa",
"syzkaller": "/syzkaller",
"disable_syscalls": ["keyctl", "add_key", "request_key"],
"suppressions": ["some known bug"],
"procs": 4,
"type": "qemu",
"vm": {
"count": 16,
"cpu": 2,
"mem": 2048,
"kernel": "/linux/arch/x86/boot/bzImage",
"initrd": "linux/initrd"
}
}
这些参数的含义如下(有一些上面的示例中没有)。
http:显示正在运行的syz-manager进程信息的URL
email_addrs:第一次出现bug时接收通知的电子邮件地址,只支持Mailx
workdir:syz-manager进程的工作目录的位置。产生的文件包括:
- workdir/crashes/*:crash输出文件
- workdir/corpus.db:包含一些程序的语料库
- workdir/instance-x:每个VM实例临时文件
syzkaller:syzkaller的位置,syz-manager将在bin子目录中查找二进制文件
kernel_obj:包含目标文件的目录,例如linux中的vmlinux
procs:每个VM中的并行测试进程数,一般是4或8
image:qemu实例的磁盘镜像文件的位置
sshkey:用于与虚拟机通信的SSH密钥的位置
sandbox:沙盒模式,支持以下模式:
- none:默认设置,不做任何特殊的事情
- setuid:冒充用户nobody(65534)
- namespace:使用命名空间删除权限(内核需要使用CONFIG_NAMESPACES,CONFIG_UTS_NS,CONFIG_USER_NS,CONFIG_PID_NS和CONFIG_NET_NS构建)
enable_syscalls:测试的系统调用列表
disable_syscalls:禁用的系统调用列表
suppressions:已知错误的正则表达式列表
type:要使用的虚拟机类型,例如qemu
vm:特定VM类型相关的参数,例如对于qemu来说参数包括:
- count:并行运行的VM数
- kernel:要测试的内核的bzImage文件的位置
- cmdline:启动内核的其它命令行选项,例如root=/dev/sda1
- cpu:要在VM中模拟的CPU数
- mem:VM的内存大小,以MB为单位
除了config参数以外,syz-manager还可以接受debug参数和bench参数。debug参数将VM所有输出打印到console帮助我们排查使用中出现的错误;bench参数定期将执行的统计信息写入我们指定的文件。
main函数中首先启用日志缓存的功能,缓存在内存中的日志不能超过1000行或者1^29个字节。然后加载config文件,获取操作系统和架构信息,检查是否支持。还记得syz-sysgen生成的.go文件中的RegisterTarget函数么?这里用GetTarget函数获取参数对应的target。
在qemu.cfg中可以看到通过disable_syscalls指定排除的syscall,同样可以通过enable_syscalls指定测试的syscall,如果没有这两个参数默认会fuzz所有的syscall。那么接下来就是通过ParseEnabledSyscalls解析这两个参数,之后就进入RunManager函数中了。
在RunManager函数中如果config文件中指定的type不为none则创建一个vmpool。将type指定为none是在调试/开发中用的,这样manager就不会启动VM而是需要手动启动。
一个vmPool可以用来创建多个独立的VM。前面在讲解整体架构的时候说过vm.go对不同的虚拟化方案提供了统一的接口,这里会调用到qemu.go的Ctor函数。其中主要检查了一些参数,所以这里不再展开。
接下来又经过一些初始化操作之后在一个线程中定期记录VM状态、crash数量等信息。如果设置了bench参数还要在指定的文件中记录一些信息。最后调用vmLoop函数。
crash被保存在reproQueue中,通过len(reproQueue) != 0判断当前是否有等待复现的crash。
vmIndexes := append([]int{}, instances[len(instances)-instancesPerRepro:]...)
instances = instances[:len(instances)-instancesPerRepro]
这两行代码把instances分成vmIndexes和instances两个部分,vmIndexes对crash进行复现,instances运行新的实例。我们先看Run函数复现部分的代码。在经过一些设置之后,主要是调用到了repro函数。
在repro函数中首先调用extractProg函数提取出触发crash的程序。Timeouts有三个取值:10s,1min和5min。10s是用来复现比较简单的crash用的,5min是用来复现条件竞争这样比较复杂的crash用的。如果单个程序无法复现,则采用二分查找的方法找出那些触发crash的程序。按照时间从短到长,从后向前(通常最后一个程序就是触发crash的程序),从单个到多个的顺序尝试复现出crash。
如果能够成功复现,则继续调用minimizeProg函数对其最小化。
minimizeProg函数主要调用了Minimize函数。
Minimize函数首先调用了SanitizeCall函数,因为有些系统调用需要做一些特殊的处理。
然后尝试逐个移除系统调用。
test中的例子如下,open系统调用被移除了。
之后再去除系统调用的无关参数。
在do函数中,根据不同的参数类型调用不同的minimize函数。
比如如果参数是指针类型的,把指针或者指针指向的内容置空。
如果参数是数组类型的,尝试一个一个移除数组中的元素。
之后,调用extractC函数提取出C程序。
extractC函数主要是调用了testCProg函数,后者调用csource中的Write函数生成C代码,csource中的Build函数编译出可执行文件。比较简单,所以不再详细分析。
接下来调用simplifyProg函数对之前的结果进行简化,simplifyProg函数再次调用extractC函数提取出C程序,然后调用simplifyC函数对提取出的C程序进行简化。这里简化的是复现crash时设置的一些选项,比如线程、并发、沙盒等等。
我们返回到manager.go中,这一部分的代码就分析完了。在下一篇文章中我们将介绍vmLoop函数中是怎么进行fuzz的。