这是内核漏洞挖掘技术系列的第六篇。
第一篇:内核漏洞挖掘技术系列(1)——trinity
第二篇:内核漏洞挖掘技术系列(2)——bochspwn
第三篇:内核漏洞挖掘技术系列(3)——bochspwn-reloaded(1)
第四篇:内核漏洞挖掘技术系列(3)——bochspwn-reloaded(2)
第五篇:内核漏洞挖掘技术系列(4)——syzkaller(1)

前言

在上一篇文章中我们简单聊了聊syzkaller的整体架构和如何人工更新系统调用模板,这些系统调用模板通过syz-extractsyz-sysgen翻译成syzkaller使用的代码。在这篇文章中我们就来看看具体怎么实现的。系统调用模板大概是下面这样的形式。在继续阅读之前希望读者能自己实践掌握系统调用模板的语法,文章中就不再赘述。

syscallname "(" [arg ["," arg]*] ")" [type]
arg = argname type
argname = identifier
type = typename [ "[" type-options "]" ]
typename = "const" | "intN" | "intptr" | "flags" | "array" | "ptr" |
            "buffer" | "string" | "strconst" | "filename" | "len" |
            "bytesize" | "bytesizeN" | "bitsize" | "vma" | "proc"
type-options = [type-opt ["," type-opt]]

使用syz-extract得到常量和值一一对应的.const文件,然后使用syz-sysgen编译AST和常量值,并返回包含生成的prog对象的Prog。syz-sysgen具体又分为下面4步。
    1.assignSyscallNumbers:分配系统调用号,检测不受支持的系统调用并丢弃
    2.patchConsts:将AST中的常量patch成对应的值
    3.check:对AST进行语义检查
    4.genSyscalls:从AST生成prog对象

syz-extract

在syz-extract文件夹下除了extract.go还有对应不同操作系统的go文件,我们仍然以linux.go为例。在extract.go的main函数中,首先调用archFileList函数得到我们编写的系统调用模板文件和相应的架构的配置。


然后调用了对应extractor的prepare函数。linux.go中首先检查是否设置了sourcedir源代码目录,如果设置build参数表示重新生成特定架构的内核头文件于是make mrproper清理;如果没有设置build参数则不能指定多个架构。

然后分别对Arch和File调用processArch函数和processFile函数处理。

在processArch函数中首先调用ParseGlob函数,ParseGlob函数调用了Parse函数将编写的txt文件解析成AST(Abstract Syntax Tree,抽象语法树)。

在Parse函数中调用了parseTopRecover函数解析出节点加入到top中,并且会在struct前后加上空行,移除重复的空行。

parseTopRecover函数中主要调用了parseTop函数。

parseTop函数根据标识符的类型调用不同的函数处理。

返回到processArch函数,然后调用ExtractConsts函数提取出常量标识符,ExtractConsts函数主要调用了Compile函数。

Compile函数中首先调用的是createCompiler函数。在syscall_descriptions_syntax.md中可以看到syzkaller内建的一些别名和模板,在createCompiler函数中对它们进行了初始化。



返回到Compile函数,接下来调用typecheck函数分别调用checkDirectives,checkNames,checkFields,checkTypedefs,checkTypes这五个函数进行一些检查。对于可能出现的错误可以对照consts_errors.txt,errors.txt和errors2.txt中给出的例子。限于篇幅原因,不再展开。检查之后调用了extractConsts函数,extractConsts函数返回提取const值所需的文本常量和其它信息的列表。列表的定义如下,其中的内容分别为常量(consts),定义(defines),包含头文件数组(includeArray),包含目录数组(incdirArray)。

提取包含目录:

提取包含头文件:

提取定义之后把定义的name作为常量:

给系统调用名添加前缀之后作为常量(linux是__NR_):

对于call,struct,resource等等类型提取参数中的常量:

提取struct类型size中的常量:

最后提取flag和resource中的常量:

extractConsts函数返回之后Compile函数不会再继续执行。

返回到processArch函数,将文件名和对应的常量标识符信息发给Channel之后就可以调用processFile函数了。比如当指定架构为amd64时,输入文件名inname如果是sys/linux/binder.txt,那么输出文件名outname就是sys/linux/binder_amd64.const了。其中又调用了对应extractor的processFile函数。

linux.go中processFile函数主要调用了extract函数。

extract函数中首先调用了compile函数。compile函数中cc是gcc,args是传给gcc的参数,data主要是常量标识符信息。


在compile函数中,首先用指定的srcTemplate模板将数据解析成源代码。

例如ipc.txt解析之后的结果如下。

然后编译成二进制文件。

在extract函数中如果第一次调用compile函数出现错误,这可能是因为有些常量和系统调用编号在一些架构上没有定义。从输出中找到这些没有定义的常量,不把它们加入到data中,然后尝试再次调用compile函数。

我们可以看到在代码中将vals数组中的每个成员都以unsigned long long的格式输出,所以运行编译好的二进制文件就得到了每个常量对应的值。

在extract函数返回之后还要对mmap系统调用做下处理。i386/arm架构上的mmap被转换为old_mmap,我们将其修复为mmap2。

在pkg/csource中也做了同样的处理。

我们返回到extract.go中的processFile函数,在得到extractor的processFile函数返回的值之后,调用SerializeConsts函数将其序列化为常量=值的形式,最后写到文件。


syz-sysgen

syz-sysgen先创建文件夹gen用来存放生成的go文件,然后从.txt文件中提取出AST,从.const文件中提取出常量,调用Compile函数。与前面那次调用Compile函数不同,这一次调用Compile函数时因为consts不为nil,所以会继续执行下面的代码。

除了前面同样调用createCompiler函数和typecheck函数,接下来首先调用的是assignSyscallNumbers函数,assignSyscallNumbers函数分配系统调用号,检测不受支持的系统调用并丢弃。

接着调用patchConsts函数将AST中的常量patch成对应的值。

然后调用check函数对AST进行语义检查,最后调用genSyscalls函数。genSyscalls函数中主要是调用了genSyscall函数,然后按照系统调用名排序。

在genSyscall函数中调用genType函数生成返回值,调用genFieldArray函数生成每个参数。

在genType函数和genFieldArray函数中都主要是调用不同类型的Gen函数。

同时,返回的prog对象中调用genResources函数生成资源,genStructDescs函数生成结构体的描述。

回到sysgen.go之后再调用generate函数把它们输出到io.Writer中,然后写入gen文件夹下对应架构的.go文件。

在go语言中init函数会在每个包完成初始化后自动执行,并且执行优先级比main函数高,所以这里RegisterTarget函数在初始化后就执行了。

接下来在writeExecutorSyscalls函数中将一些配置写入executor\defs.h文件,将系统调用名和对应的系统调用号写入executor\syscalls.h文件。


总结

这篇文章我们大致分析了上一篇文章展示的编写的系统调用模板被编译过程的原理,可以理解成syzkaller实现了一种描述系统调用的小型的编程语言。接下来我们将分析fuzz的过程,crash复现,Generate和Mutate策略等话题。

点击收藏 | 1 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖